#!/usr/bin/env python """ Comments starting "#LT" or "#CLT" are by Chris Lusby Taylor who rewrote the engraving function in 2011. History of CLT changes to engraving and other functions it uses: 9 May 2011 Changed test of tool diameter to square it 10 May Note that there are many unused functions, including: bound_to_bound_distance, csp_curvature_radius_at_t, csp_special_points, csplength, rebuild_csp, csp_slope, csp_simple_bound_to_point_distance, csp_bound_to_point_distance, bez_at_t, bez_to_point_distance, bez_normalized_slope, matrix_mul, transpose Fixed csp_point_inside_bound() to work if x outside bounds 20 May Now encoding the bisectors of angles. 23 May Using r/cos(a) instead of normalised normals for bisectors of angles. 23 May Note that Z values generated for engraving are in pixels, not mm. Removed the biarc curves - straight lines are better. 24 May Changed Bezier slope calculation to be less sensitive to tiny differences in points. Added use of self.options.engraving_newton_iterations to control accuracy 25 May Big restructure and new recursive function. Changed the way I treat corners - I now find if the centre of a proposed circle is within the area bounded by the line being tested and the two angle bisectors at its ends. See get_radius_to_line(). 29 May Eliminating redundant points. If A,B,C colinear, drop B 30 May Eliminating redundant lines in divided Beziers. Changed subdivision of lines 7Jun Try to show engraving in 3D 8 Jun Displaying in stereo 3D. Fixed a bug in bisect - it could go wrong due to rounding errors if 1+x1.x2+y1.y2<0 which should never happen. BTW, I spotted a non-normalised normal returned by csp_normalized_normal. Need to check for that. 9 Jun Corrected spelling of 'definition' but still match previous 'defention' and 'defenition' if found in file Changed get_tool to find 1.6.04 tools or new tools with corrected spelling 10 Jun Put 3D into a separate layer called 3D, created unless it already exists Changed csp_normalized_slope to reject lines shorter than 1e-9. 10 Jun Changed all dimensions seen by user to be mm/inch, not pixels. This includes tool diameter, maximum engraving distance, tool shape and all Z values. 12 Jun ver 208 Now scales correctly if orientation points moved or stretched. 12 Jun ver 209. Now detect if engraving toolshape not a function of radius Graphics now indicate Gcode toolpath, limited by min(tool diameter/2,max-dist) TODO Change line division to be recursive, depending on what line is touched. See line_divide engraving() functions (c) 2011 Chris Lusby Taylor, clusbytaylor@enterprise.net Copyright (C) 2009 Nick Drobchenko, nick@cnc-club.ru based on gcode.py (C) 2007 hugomatic... based on addnodes.py (C) 2005,2007 Aaron Spike, aaron@ekips.org based on dots.py (C) 2005 Aaron Spike, aaron@ekips.org based on interp.py (C) 2005 Aaron Spike, aaron@ekips.org based on bezmisc.py (C) 2005 Aaron Spike, aaron@ekips.org based on cubicsuperpath.py (C) 2005 Aaron Spike, aaron@ekips.org This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ ### ### Gcodetools v 1.7 ### gcodetools_current_version = "1.7" import inkex, simplestyle, simplepath import cubicsuperpath, simpletransform, bezmisc import os import math import bezmisc import re import copy import sys import time import cmath import numpy import codecs import random import gettext _ = gettext.gettext ### Check if inkex has errormsg (0.46 version does not have one.) Could be removed later. if "errormsg" not in dir(inkex): inkex.errormsg = lambda msg: sys.stderr.write((unicode(msg) + "\n").encode("UTF-8")) def bezierslopeatt(((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3)),t): ax,ay,bx,by,cx,cy,x0,y0=bezmisc.bezierparameterize(((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3))) dx=3*ax*(t**2)+2*bx*t+cx dy=3*ay*(t**2)+2*by*t+cy if dx==dy==0 : dx = 6*ax*t+2*bx dy = 6*ay*t+2*by if dx==dy==0 : dx = 6*ax dy = 6*ay if dx==dy==0 : print_("Slope error x = %s*t^3+%s*t^2+%s*t+%s, y = %s*t^3+%s*t^2+%s*t+%s, t = %s, dx==dy==0" % (ax,bx,cx,dx,ay,by,cy,dy,t)) print_(((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3))) dx, dy = 1, 1 return dx,dy bezmisc.bezierslopeatt = bezierslopeatt def ireplace(self,old,new,count=0): pattern = re.compile(re.escape(old),re.I) return re.sub(pattern,new,self,count) def isset(variable): # VARIABLE NAME SHOULD BE A STRING! Like isset("foobar") return variable in locals() or variable in globals() ################################################################################ ### ### Styles and additional parameters ### ################################################################################ math.pi2 = math.pi*2 straight_tolerance = 0.0001 straight_distance_tolerance = 0.0001 engraving_tolerance = 0.0001 loft_lengths_tolerance = 0.0000001 EMC_TOLERANCE_EQUAL = 0.00001 options = {} defaults = { 'header': """% (Header) (Generated by gcodetools from Inkscape.) (Using default header. To add your own header create file "header" in the output dir.) M3 (Header end.) """, 'footer': """ (Footer) M5 G00 X0.0000 Y0.0000 M2 (Using default footer. To add your own footer create file "footer" in the output dir.) (end) %""" } intersection_recursion_depth = 10 intersection_tolerance = 0.00001 styles = { "in_out_path_style" : simplestyle.formatStyle({ 'stroke': '#0072a7', 'fill': 'none', 'stroke-width':'1', 'marker-mid':'url(#InOutPathMarker)' }), "loft_style" : { 'main curve': simplestyle.formatStyle({ 'stroke': '#88f', 'fill': 'none', 'stroke-width':'1', 'marker-end':'url(#Arrow2Mend)' }), }, "biarc_style" : { 'biarc0': simplestyle.formatStyle({ 'stroke': '#88f', 'fill': 'none', "marker-end":"url(#DrawCurveMarker)", 'stroke-width':'1' }), 'biarc1': simplestyle.formatStyle({ 'stroke': '#8f8', 'fill': 'none', "marker-end":"url(#DrawCurveMarker)", 'stroke-width':'1' }), 'line': simplestyle.formatStyle({ 'stroke': '#f88', 'fill': 'none', "marker-end":"url(#DrawCurveMarker)", 'stroke-width':'1' }), 'area': simplestyle.formatStyle({ 'stroke': '#777', 'fill': 'none', "marker-end":"url(#DrawCurveMarker)", 'stroke-width':'0.1' }), }, "biarc_style_dark" : { 'biarc0': simplestyle.formatStyle({ 'stroke': '#33a', 'fill': 'none', "marker-end":"url(#DrawCurveMarker)", 'stroke-width':'1' }), 'biarc1': simplestyle.formatStyle({ 'stroke': '#3a3', 'fill': 'none', "marker-end":"url(#DrawCurveMarker)", 'stroke-width':'1' }), 'line': simplestyle.formatStyle({ 'stroke': '#a33', 'fill': 'none', "marker-end":"url(#DrawCurveMarker)", 'stroke-width':'1' }), 'area': simplestyle.formatStyle({ 'stroke': '#222', 'fill': 'none', "marker-end":"url(#DrawCurveMarker)", 'stroke-width':'0.3' }), }, "biarc_style_dark_area" : { 'biarc0': simplestyle.formatStyle({ 'stroke': '#33a', 'fill': 'none', "marker-end":"url(#DrawCurveMarker)", 'stroke-width':'0.1' }), 'biarc1': simplestyle.formatStyle({ 'stroke': '#3a3', 'fill': 'none', "marker-end":"url(#DrawCurveMarker)", 'stroke-width':'0.1' }), 'line': simplestyle.formatStyle({ 'stroke': '#a33', 'fill': 'none', "marker-end":"url(#DrawCurveMarker)", 'stroke-width':'0.1' }), 'area': simplestyle.formatStyle({ 'stroke': '#222', 'fill': 'none', "marker-end":"url(#DrawCurveMarker)", 'stroke-width':'0.3' }), }, "biarc_style_i" : { 'biarc0': simplestyle.formatStyle({ 'stroke': '#880', 'fill': 'none', "marker-end":"url(#DrawCurveMarker)", 'stroke-width':'1' }), 'biarc1': simplestyle.formatStyle({ 'stroke': '#808', 'fill': 'none', "marker-end":"url(#DrawCurveMarker)", 'stroke-width':'1' }), 'line': simplestyle.formatStyle({ 'stroke': '#088', 'fill': 'none', "marker-end":"url(#DrawCurveMarker)", 'stroke-width':'1' }), 'area': simplestyle.formatStyle({ 'stroke': '#999', 'fill': 'none', "marker-end":"url(#DrawCurveMarker)", 'stroke-width':'0.3' }), }, "biarc_style_dark_i" : { 'biarc0': simplestyle.formatStyle({ 'stroke': '#dd5', 'fill': 'none', "marker-end":"url(#DrawCurveMarker)", 'stroke-width':'1' }), 'biarc1': simplestyle.formatStyle({ 'stroke': '#d5d', 'fill': 'none', "marker-end":"url(#DrawCurveMarker)", 'stroke-width':'1' }), 'line': simplestyle.formatStyle({ 'stroke': '#5dd', 'fill': 'none', "marker-end":"url(#DrawCurveMarker)", 'stroke-width':'1' }), 'area': simplestyle.formatStyle({ 'stroke': '#aaa', 'fill': 'none', "marker-end":"url(#DrawCurveMarker)", 'stroke-width':'0.3' }), }, "biarc_style_lathe_feed" : { 'biarc0': simplestyle.formatStyle({ 'stroke': '#07f', 'fill': 'none', "marker-end":"url(#DrawCurveMarker)", 'stroke-width':'.4' }), 'biarc1': simplestyle.formatStyle({ 'stroke': '#0f7', 'fill': 'none', "marker-end":"url(#DrawCurveMarker)", 'stroke-width':'.4' }), 'line': simplestyle.formatStyle({ 'stroke': '#f44', 'fill': 'none', "marker-end":"url(#DrawCurveMarker)", 'stroke-width':'.4' }), 'area': simplestyle.formatStyle({ 'stroke': '#aaa', 'fill': 'none', "marker-end":"url(#DrawCurveMarker)", 'stroke-width':'0.3' }), }, "biarc_style_lathe_passing feed" : { 'biarc0': simplestyle.formatStyle({ 'stroke': '#07f', 'fill': 'none', "marker-end":"url(#DrawCurveMarker)", 'stroke-width':'.4' }), 'biarc1': simplestyle.formatStyle({ 'stroke': '#0f7', 'fill': 'none', "marker-end":"url(#DrawCurveMarker)", 'stroke-width':'.4' }), 'line': simplestyle.formatStyle({ 'stroke': '#f44', 'fill': 'none', "marker-end":"url(#DrawCurveMarker)", 'stroke-width':'.4' }), 'area': simplestyle.formatStyle({ 'stroke': '#aaa', 'fill': 'none', "marker-end":"url(#DrawCurveMarker)", 'stroke-width':'0.3' }), }, "biarc_style_lathe_fine feed" : { 'biarc0': simplestyle.formatStyle({ 'stroke': '#7f0', 'fill': 'none', "marker-end":"url(#DrawCurveMarker)", 'stroke-width':'.4' }), 'biarc1': simplestyle.formatStyle({ 'stroke': '#f70', 'fill': 'none', "marker-end":"url(#DrawCurveMarker)", 'stroke-width':'.4' }), 'line': simplestyle.formatStyle({ 'stroke': '#744', 'fill': 'none', "marker-end":"url(#DrawCurveMarker)", 'stroke-width':'.4' }), 'area': simplestyle.formatStyle({ 'stroke': '#aaa', 'fill': 'none', "marker-end":"url(#DrawCurveMarker)", 'stroke-width':'0.3' }), }, "area artefact": simplestyle.formatStyle({ 'stroke': '#ff0000', 'fill': '#ffff00', 'stroke-width':'1' }), "area artefact arrow": simplestyle.formatStyle({ 'stroke': '#ff0000', 'fill': '#ffff00', 'stroke-width':'1' }), "dxf_points": simplestyle.formatStyle({ "stroke": "#ff0000", "fill": "#ff0000"}), } ################################################################################ ### Gcode additional functions ################################################################################ def gcode_comment_str(s, replace_new_line = False): if replace_new_line : s = re.sub(r"[\n\r]+", ".", s) res = "" if s[-1] == "\n" : s = s[:-1] for a in s.split("\n") : if a != "" : res += "(" + re.sub(r"[\(\)\\\n\r]", ".", a) + ")\n" else : res += "\n" return res ################################################################################ ### Cubic Super Path additional functions ################################################################################ def csp_from_polyline(line) : return [ [ [point[:] for k in range(3) ] for point in subline ] for subline in line ] def csp_remove_zerro_segments(csp, tolerance = 1e-7): res = [] for subpath in csp: if len(subpath) > 0 : res.append([subpath[0]]) for sp1,sp2 in zip(subpath,subpath[1:]) : if point_to_point_d2(sp1[1],sp2[1])<=tolerance and point_to_point_d2(sp1[2],sp2[1])<=tolerance and point_to_point_d2(sp1[1],sp2[0])<=tolerance : res[-1][-1][2] = sp2[2] else : res[-1].append(sp2) return res def point_inside_csp(p,csp, on_the_path = True) : # we'll do the raytracing and see how many intersections are there on the ray's way. # if number of intersections is even then point is outside. # ray will be x=p.x and y=>p.y # you can assing any value to on_the_path, by dfault if point is on the path # function will return thai it's inside the path. x,y = p ray_intersections_count = 0 for subpath in csp : for i in range(1, len(subpath)) : sp1, sp2 = subpath[i-1], subpath[i] ax,ay,bx,by,cx,cy,dx,dy = csp_parameterize(sp1,sp2) if ax==0 and bx==0 and cx==0 and dx==x : #we've got a special case here b = csp_true_bounds( [[sp1,sp2]]) if b[1][1]<=y<=b[3][1] : # points is on the path return on_the_path else : # we can skip this segment because it wont influence the answer. pass else: for t in csp_line_intersection([x,y],[x,y+5],sp1,sp2) : if t == 0 or t == 1 : #we've got another special case here x1,y1 = csp_at_t(sp1,sp2,t) if y1==y : # the point is on the path return on_the_path # if t == 0 we sould have considered this case previously. if t == 1 : # we have to check the next segmant if it is on the same side of the ray st_d = csp_normalized_slope(sp1,sp2,1)[0] if st_d == 0 : st_d = csp_normalized_slope(sp1,sp2,0.99)[0] for j in range(1, len(subpath)+1): if (i+j) % len(subpath) == 0 : continue # skip the closing segment sp11,sp22 = subpath[(i-1+j) % len(subpath)], subpath[(i+j) % len(subpath)] ax1,ay1,bx1,by1,cx1,cy1,dx1,dy1 = csp_parameterize(sp1,sp2) if ax1==0 and bx1==0 and cx1==0 and dx1==x : continue # this segment parallel to the ray, so skip it en_d = csp_normalized_slope(sp11,sp22,0)[0] if en_d == 0 : en_d = csp_normalized_slope(sp11,sp22,0.01)[0] if st_d*en_d <=0 : ray_intersections_count += 1 break else : x1,y1 = csp_at_t(sp1,sp2,t) if y1==y : # the point is on the path return on_the_path else : if y1>y and 3*ax*t**2 + 2*bx*t + cx !=0 : # if it's 0 the path only touches the ray ray_intersections_count += 1 return ray_intersections_count%2 == 1 def csp_close_all_subpaths(csp, tolerance = 0.000001): for i in range(len(csp)): if point_to_point_d2(csp[i][0][1] , csp[i][-1][1])> tolerance**2 : csp[i][-1][2] = csp[i][-1][1][:] csp[i] += [ [csp[i][0][1][:] for j in range(3)] ] else: if csp[i][0][1] != csp[i][-1][1] : csp[i][-1][1] = csp[i][0][1][:] return csp def csp_simple_bound(csp): minx,miny,maxx,maxy = None,None,None,None for subpath in csp: for sp in subpath : for p in sp: minx = min(minx,p[0]) if minx!=None else p[0] miny = min(miny,p[1]) if miny!=None else p[1] maxx = max(maxx,p[0]) if maxx!=None else p[0] maxy = max(maxy,p[1]) if maxy!=None else p[1] return minx,miny,maxx,maxy def csp_segment_to_bez(sp1,sp2) : return sp1[1:]+sp2[:2] def bound_to_bound_distance(sp1,sp2,sp3,sp4) : min_dist = 1e100 max_dist = 0 points1 = csp_segment_to_bez(sp1,sp2) points2 = csp_segment_to_bez(sp3,sp4) for i in range(4) : for j in range(4) : min_, max_ = line_to_line_min_max_distance_2(points1[i-1], points1[i], points2[j-1], points2[j]) min_dist = min(min_dist,min_) max_dist = max(max_dist,max_) print_("bound_to_bound", min_dist, max_dist) return min_dist, max_dist def csp_to_point_distance(csp, p, dist_bounds = [0,1e100], tolerance=.01) : min_dist = [1e100,0,0,0] for j in range(len(csp)) : for i in range(1,len(csp[j])) : d = csp_seg_to_point_distance(csp[j][i-1],csp[j][i],p,sample_points = 5, tolerance = .01) if d[0] < dist_bounds[0] : # draw_pointer( list(csp_at_t(subpath[dist[2]-1],subpath[dist[2]],dist[3])) # +list(csp_at_t(csp[dist[4]][dist[5]-1],csp[dist[4]][dist[5]],dist[6])),"red","line", comment = math.sqrt(dist[0])) return [d[0],j,i,d[1]] else : if d[0] < min_dist[0] : min_dist = [d[0],j,i,d[1]] return min_dist def csp_seg_to_point_distance(sp1,sp2,p,sample_points = 5, tolerance = .01) : ax,ay,bx,by,cx,cy,dx,dy = csp_parameterize(sp1,sp2) dx, dy = dx-p[0], dy-p[1] if sample_points < 2 : sample_points = 2 d = min( [(p[0]-sp1[1][0])**2 + (p[1]-sp1[1][1])**2,0.], [(p[0]-sp2[1][0])**2 + (p[1]-sp2[1][1])**2,1.] ) for k in range(sample_points) : t = float(k)/(sample_points-1) i = 0 while i==0 or abs(f)>0.000001 and i<20 : t2,t3 = t**2,t**3 f = (ax*t3+bx*t2+cx*t+dx)*(3*ax*t2+2*bx*t+cx) + (ay*t3+by*t2+cy*t+dy)*(3*ay*t2+2*by*t+cy) df = (6*ax*t+2*bx)*(ax*t3+bx*t2+cx*t+dx) + (3*ax*t2+2*bx*t+cx)**2 + (6*ay*t+2*by)*(ay*t3+by*t2+cy*t+dy) + (3*ay*t2+2*by*t+cy)**2 if df!=0 : t = t - f/df else : break i += 1 if 0<=t<=1 : p1 = csp_at_t(sp1,sp2,t) d1 = (p1[0]-p[0])**2 + (p1[1]-p[1])**2 if d1 < d[0] : d = [d1,t] return d def csp_seg_to_csp_seg_distance(sp1,sp2,sp3,sp4, dist_bounds = [0,1e100], sample_points = 5, tolerance=.01) : # check the ending points first dist = csp_seg_to_point_distance(sp1,sp2,sp3[1],sample_points, tolerance) dist += [0.] if dist[0] <= dist_bounds[0] : return dist d = csp_seg_to_point_distance(sp1,sp2,sp4[1],sample_points, tolerance) if d[0]tolerance and i<30 : #draw_pointer(csp_at_t(sp1,sp2,t1)) f1x = 3*ax1*t12+2*bx1*t1+cx1 f1y = 3*ay1*t12+2*by1*t1+cy1 f2x = 3*ax2*t22+2*bx2*t2+cx2 f2y = 3*ay2*t22+2*by2*t2+cy2 F1[0] = 2*f1x*x + 2*f1y*y F1[1] = -2*f2x*x - 2*f2y*y F2[0][0] = 2*(6*ax1*t1+2*bx1)*x + 2*f1x*f1x + 2*(6*ay1*t1+2*by1)*y +2*f1y*f1y F2[0][1] = -2*f1x*f2x - 2*f1y*f2y F2[1][0] = -2*f2x*f1x - 2*f2y*f1y F2[1][1] = -2*(6*ax2*t2+2*bx2)*x + 2*f2x*f2x - 2*(6*ay2*t2+2*by2)*y + 2*f2y*f2y F2 = inv_2x2(F2) if F2!=None : t1 -= ( F2[0][0]*F1[0] + F2[0][1]*F1[1] ) t2 -= ( F2[1][0]*F1[0] + F2[1][1]*F1[1] ) t12, t13, t22, t23 = t1*t1, t1*t1*t1, t2*t2, t2*t2*t2 x,y = ax1*t13+bx1*t12+cx1*t1+dx1 - (ax2*t23+bx2*t22+cx2*t2+dx2), ay1*t13+by1*t12+cy1*t1+dy1 - (ay2*t23+by2*t22+cy2*t2+dy2) Flast = F F = x*x+y*y else : break i += 1 if F < dist[0] and 0<=t1<=1 and 0<=t2<=1: dist = [F,t1,t2] if dist[0] <= dist_bounds[0] : return dist return dist def csp_to_csp_distance(csp1,csp2, dist_bounds = [0,1e100], tolerance=.01) : dist = [1e100,0,0,0,0,0,0] for i1 in range(len(csp1)) : for j1 in range(1,len(csp1[i1])) : for i2 in range(len(csp2)) : for j2 in range(1,len(csp2[i2])) : d = csp_seg_bound_to_csp_seg_bound_max_min_distance(csp1[i1][j1-1],csp1[i1][j1],csp2[i2][j2-1],csp2[i2][j2]) if d[0] >= dist_bounds[1] : continue if d[1] < dist_bounds[0] : return [d[1],i1,j1,1,i2,j2,1] d = csp_seg_to_csp_seg_distance(csp1[i1][j1-1],csp1[i1][j1],csp2[i2][j2-1],csp2[i2][j2], dist_bounds, tolerance=tolerance) if d[0] < dist[0] : dist = [d[0], i1,j1,d[1], i2,j2,d[2]] if dist[0] <= dist_bounds[0] : return dist if dist[0] >= dist_bounds[1] : return dist return dist # draw_pointer( list(csp_at_t(csp1[dist[1]][dist[2]-1],csp1[dist[1]][dist[2]],dist[3])) # + list(csp_at_t(csp2[dist[4]][dist[5]-1],csp2[dist[4]][dist[5]],dist[6])), "#507","line") def csp_split(sp1,sp2,t=.5) : [x1,y1],[x2,y2],[x3,y3],[x4,y4] = sp1[1], sp1[2], sp2[0], sp2[1] x12 = x1+(x2-x1)*t y12 = y1+(y2-y1)*t x23 = x2+(x3-x2)*t y23 = y2+(y3-y2)*t x34 = x3+(x4-x3)*t y34 = y3+(y4-y3)*t x1223 = x12+(x23-x12)*t y1223 = y12+(y23-y12)*t x2334 = x23+(x34-x23)*t y2334 = y23+(y34-y23)*t x = x1223+(x2334-x1223)*t y = y1223+(y2334-y1223)*t return [sp1[0],sp1[1],[x12,y12]], [[x1223,y1223],[x,y],[x2334,y2334]], [[x34,y34],sp2[1],sp2[2]] def csp_true_bounds(csp) : # Finds minx,miny,maxx,maxy of the csp and return their (x,y,i,j,t) minx = [float("inf"), 0, 0, 0] maxx = [float("-inf"), 0, 0, 0] miny = [float("inf"), 0, 0, 0] maxy = [float("-inf"), 0, 0, 0] for i in range(len(csp)): for j in range(1,len(csp[i])): ax,ay,bx,by,cx,cy,x0,y0 = bezmisc.bezierparameterize((csp[i][j-1][1],csp[i][j-1][2],csp[i][j][0],csp[i][j][1])) roots = cubic_solver(0, 3*ax, 2*bx, cx) + [0,1] for root in roots : if type(root) is complex and abs(root.imag)<1e-10: root = root.real if type(root) is not complex and 0<=root<=1: y = ay*(root**3)+by*(root**2)+cy*root+y0 x = ax*(root**3)+bx*(root**2)+cx*root+x0 maxx = max([x,y,i,j,root],maxx) minx = min([x,y,i,j,root],minx) roots = cubic_solver(0, 3*ay, 2*by, cy) + [0,1] for root in roots : if type(root) is complex and root.imag==0: root = root.real if type(root) is not complex and 0<=root<=1: y = ay*(root**3)+by*(root**2)+cy*root+y0 x = ax*(root**3)+bx*(root**2)+cx*root+x0 maxy = max([y,x,i,j,root],maxy) miny = min([y,x,i,j,root],miny) maxy[0],maxy[1] = maxy[1],maxy[0] miny[0],miny[1] = miny[1],miny[0] return minx,miny,maxx,maxy ############################################################################ ### csp_segments_intersection(sp1,sp2,sp3,sp4) ### ### Returns array containig all intersections between two segmets of cubic ### super path. Results are [ta,tb], or [ta0, ta1, tb0, tb1, "Overlap"] ### where ta, tb are values of t for the intersection point. ############################################################################ def csp_segments_intersection(sp1,sp2,sp3,sp4) : a, b = csp_segment_to_bez(sp1,sp2), csp_segment_to_bez(sp3,sp4) def polish_intersection(a,b,ta,tb, tolerance = intersection_tolerance) : ax,ay,bx,by,cx,cy,dx,dy = bezmisc.bezierparameterize(a) ax1,ay1,bx1,by1,cx1,cy1,dx1,dy1 = bezmisc.bezierparameterize(b) i = 0 F, F1 = [.0,.0], [[.0,.0],[.0,.0]] while i==0 or (abs(F[0])**2+abs(F[1])**2 > tolerance and i<10): ta3, ta2, tb3, tb2 = ta**3, ta**2, tb**3, tb**2 F[0] = ax*ta3+bx*ta2+cx*ta+dx-ax1*tb3-bx1*tb2-cx1*tb-dx1 F[1] = ay*ta3+by*ta2+cy*ta+dy-ay1*tb3-by1*tb2-cy1*tb-dy1 F1[0][0] = 3*ax *ta2 + 2*bx *ta + cx F1[0][1] = -3*ax1*tb2 - 2*bx1*tb - cx1 F1[1][0] = 3*ay *ta2 + 2*by *ta + cy F1[1][1] = -3*ay1*tb2 - 2*by1*tb - cy1 det = F1[0][0]*F1[1][1] - F1[0][1]*F1[1][0] if det!=0 : F1 = [ [ F1[1][1]/det, -F1[0][1]/det], [-F1[1][0]/det, F1[0][0]/det] ] ta = ta - ( F1[0][0]*F[0] + F1[0][1]*F[1] ) tb = tb - ( F1[1][0]*F[0] + F1[1][1]*F[1] ) else: break i += 1 return ta, tb def recursion(a,b, ta0,ta1,tb0,tb1, depth_a,depth_b) : global bezier_intersection_recursive_result if a==b : bezier_intersection_recursive_result += [[ta0,tb0,ta1,tb1,"Overlap"]] return tam, tbm = (ta0+ta1)/2, (tb0+tb1)/2 if depth_a>0 and depth_b>0 : a1,a2 = bez_split(a,0.5) b1,b2 = bez_split(b,0.5) if bez_bounds_intersect(a1,b1) : recursion(a1,b1, ta0,tam,tb0,tbm, depth_a-1,depth_b-1) if bez_bounds_intersect(a2,b1) : recursion(a2,b1, tam,ta1,tb0,tbm, depth_a-1,depth_b-1) if bez_bounds_intersect(a1,b2) : recursion(a1,b2, ta0,tam,tbm,tb1, depth_a-1,depth_b-1) if bez_bounds_intersect(a2,b2) : recursion(a2,b2, tam,ta1,tbm,tb1, depth_a-1,depth_b-1) elif depth_a>0 : a1,a2 = bez_split(a,0.5) if bez_bounds_intersect(a1,b) : recursion(a1,b, ta0,tam,tb0,tb1, depth_a-1,depth_b) if bez_bounds_intersect(a2,b) : recursion(a2,b, tam,ta1,tb0,tb1, depth_a-1,depth_b) elif depth_b>0 : b1,b2 = bez_split(b,0.5) if bez_bounds_intersect(a,b1) : recursion(a,b1, ta0,ta1,tb0,tbm, depth_a,depth_b-1) if bez_bounds_intersect(a,b2) : recursion(a,b2, ta0,ta1,tbm,tb1, depth_a,depth_b-1) else : # Both segments have been subdevided enougth. Let's get some intersections :). intersection, t1, t2 = straight_segments_intersection([a[0]]+[a[3]],[b[0]]+[b[3]]) if intersection : if intersection == "Overlap" : t1 = ( max(0,min(1,t1[0]))+max(0,min(1,t1[1])) )/2 t2 = ( max(0,min(1,t2[0]))+max(0,min(1,t2[1])) )/2 bezier_intersection_recursive_result += [[ta0+t1*(ta1-ta0),tb0+t2*(tb1-tb0)]] global bezier_intersection_recursive_result bezier_intersection_recursive_result = [] recursion(a,b,0.,1.,0.,1.,intersection_recursion_depth,intersection_recursion_depth) intersections = bezier_intersection_recursive_result for i in range(len(intersections)) : if len(intersections[i])<5 or intersections[i][4] != "Overlap" : intersections[i] = polish_intersection(a,b,intersections[i][0],intersections[i][1]) return intersections def csp_segments_true_intersection(sp1,sp2,sp3,sp4) : intersections = csp_segments_intersection(sp1,sp2,sp3,sp4) res = [] for intersection in intersections : if ( (len(intersection)==5 and intersection[4] == "Overlap" and (0<=intersection[0]<=1 or 0<=intersection[1]<=1) and (0<=intersection[2]<=1 or 0<=intersection[3]<=1) ) or ( 0<=intersection[0]<=1 and 0<=intersection[1]<=1 ) ) : res += [intersection] return res def csp_get_t_at_curvature(sp1,sp2,c, sample_points = 16): # returns a list containning [t1,t2,t3,...,tn], 0<=ti<=1... if sample_points < 2 : sample_points = 2 tolerance = .0000000001 res = [] ax,ay,bx,by,cx,cy,dx,dy = csp_parameterize(sp1,sp2) for k in range(sample_points) : t = float(k)/(sample_points-1) i, F = 0, 1e100 while i<2 or abs(F)>tolerance and i<17 : try : # some numerical calculation could exceed the limits t2 = t*t #slopes... f1x = 3*ax*t2+2*bx*t+cx f1y = 3*ay*t2+2*by*t+cy f2x = 6*ax*t+2*bx f2y = 6*ay*t+2*by f3x = 6*ax f3y = 6*ay d = (f1x**2+f1y**2)**1.5 F1 = ( ( (f1x*f3y-f3x*f1y)*d - (f1x*f2y-f2x*f1y)*3.*(f2x*f1x+f2y*f1y)*((f1x**2+f1y**2)**.5) ) / ((f1x**2+f1y**2)**3) ) F = (f1x*f2y-f1y*f2x)/d - c t -= F/F1 except: break i += 1 if 0<=t<=1 and F<=tolerance: if len(res) == 0 : res.append(t) for i in res : if abs(t-i)<=0.001 : break if not abs(t-i)<=0.001 : res.append(t) return res def csp_max_curvature(sp1,sp2): ax,ay,bx,by,cx,cy,dx,dy = csp_parameterize(sp1,sp2) tolerance = .0001 F = 0. i = 0 while i<2 or F-Flast 0 : return 1e100 if t1 < 0 : return -1e100 # Use the Lapitals rule to solve 0/0 problem for 2 times... t1 = 2*(bx*ay-ax*by)*t+(ay*cx-ax*cy) if t1 > 0 : return 1e100 if t1 < 0 : return -1e100 t1 = bx*ay-ax*by if t1 > 0 : return 1e100 if t1 < 0 : return -1e100 if depth>0 : # little hack ;^) hope it wont influence anything... return csp_curvature_at_t(sp1,sp2,t*1.004, depth-1) return 1e100 def csp_curvature_radius_at_t(sp1,sp2,t) : c = csp_curvature_at_t(sp1,sp2,t) if c == 0 : return 1e100 else: return 1/c def csp_special_points(sp1,sp2) : # special points = curvature == 0 ax,ay,bx,by,cx,cy,dx,dy = bezmisc.bezierparameterize((sp1[1],sp1[2],sp2[0],sp2[1])) a = 3*ax*by-3*ay*bx b = 3*ax*cy-3*cx*ay c = bx*cy-cx*by roots = cubic_solver(0, a, b, c) res = [] for i in roots : if type(i) is complex and i.imag==0: i = i.real if type(i) is not complex and 0<=i<=1: res.append(i) return res def csp_subpath_ccw(subpath): # Remove all zerro length segments s = 0 #subpath = subpath[:] if (P(subpath[-1][1])-P(subpath[0][1])).l2() > 1e-10 : subpath[-1][2] = subpath[-1][1] subpath[0][0] = subpath[0][1] subpath += [ [subpath[0][1],subpath[0][1],subpath[0][1]] ] pl = subpath[-1][2] for sp1 in subpath: for p in sp1 : s += (p[0]-pl[0])*(p[1]+pl[1]) pl = p return s<0 def csp_at_t(sp1,sp2,t): ax,bx,cx,dx = sp1[1][0], sp1[2][0], sp2[0][0], sp2[1][0] ay,by,cy,dy = sp1[1][1], sp1[2][1], sp2[0][1], sp2[1][1] x1, y1 = ax+(bx-ax)*t, ay+(by-ay)*t x2, y2 = bx+(cx-bx)*t, by+(cy-by)*t x3, y3 = cx+(dx-cx)*t, cy+(dy-cy)*t x4,y4 = x1+(x2-x1)*t, y1+(y2-y1)*t x5,y5 = x2+(x3-x2)*t, y2+(y3-y2)*t x,y = x4+(x5-x4)*t, y4+(y5-y4)*t return [x,y] def csp_at_length(sp1,sp2,l=0.5, tolerance = 0.01): bez = (sp1[1][:],sp1[2][:],sp2[0][:],sp2[1][:]) t = bezmisc.beziertatlength(bez, l, tolerance) return csp_at_t(sp1,sp2,t) def csp_splitatlength(sp1, sp2, l = 0.5, tolerance = 0.01): bez = (sp1[1][:],sp1[2][:],sp2[0][:],sp2[1][:]) t = bezmisc.beziertatlength(bez, l, tolerance) return csp_split(sp1, sp2, t) def cspseglength(sp1,sp2, tolerance = 0.01): bez = (sp1[1][:],sp1[2][:],sp2[0][:],sp2[1][:]) return bezmisc.bezierlength(bez, tolerance) def csplength(csp): total = 0 lengths = [] for sp in csp: for i in xrange(1,len(sp)): l = cspseglength(sp[i-1],sp[i]) lengths.append(l) total += l return lengths, total def csp_segments(csp): l, seg = 0, [0] for sp in csp: for i in xrange(1,len(sp)): l += cspseglength(sp[i-1],sp[i]) seg += [ l ] if l>0 : seg = [seg[i]/l for i in xrange(len(seg))] return seg,l def rebuild_csp (csp, segs, s=None): # rebuild_csp() adds to csp control points making it's segments looks like segs if s==None : s, l = csp_segments(csp) if len(s)>len(segs) : return None segs = segs[:] segs.sort() for i in xrange(len(s)): d = None for j in xrange(len(segs)): d = min( [abs(s[i]-segs[j]),j], d) if d!=None else [abs(s[i]-segs[j]),j] del segs[d[1]] for i in xrange(len(segs)): for j in xrange(0,len(s)): if segs[i]t2 : t1, t2 = t2, t1 if t1 == t2 : sp1,sp2,sp3 = csp_split(sp1,sp2,t) return [sp1,sp2,sp2,sp3] elif t1 <= 1e-10 and t2 >= 1.-1e-10 : return [sp1,sp1,sp2,sp2] elif t1 <= 1e-10: sp1,sp2,sp3 = csp_split(sp1,sp2,t2) return [sp1,sp1,sp2,sp3] elif t2 >= 1.-1e-10 : sp1,sp2,sp3 = csp_split(sp1,sp2,t1) return [sp1,sp2,sp3,sp3] else: sp1,sp2,sp3 = csp_split(sp1,sp2,t1) sp2,sp3,sp4 = csp_split(sp2,sp3,(t2-t1)/(1-t1) ) return [sp1,sp2,sp3,sp4] def csp_seg_split(sp1,sp2, points): # points is float=t or list [t1, t2, ..., tn] if type(points) is float : points = [points] points.sort() res = [sp1,sp2] last_t = 0 for t in points: if 1e-10 1e-10 : n = normalize(n) return arc_from_c_s_l([s[0]+n[0]*r, s[1]+n[1]*r],s,l) def arc_from_c_s_l(c,s,l) : r = point_to_point_d(c,s) if r == 0 : return [] alpha = l/r cos_, sin_ = math.cos(alpha), math.sin(alpha) e = [ c[0] + (s[0]-c[0])*cos_ - (s[1]-c[1])*sin_, c[1] + (s[0]-c[0])*sin_ + (s[1]-c[1])*cos_] n = [c[0]-s[0],c[1]-s[1]] slope = rotate_cw(n) if l>0 else rotate_ccw(n) return csp_from_arc(s, e, c, r, slope) def csp_from_arc(start, end, center, r, slope_st) : # Creates csp that approximise specified arc r = abs(r) alpha = (atan2(end[0]-center[0],end[1]-center[1]) - atan2(start[0]-center[0],start[1]-center[1])) % math.pi2 sectors = int(abs(alpha)*2/math.pi)+1 alpha_start = atan2(start[0]-center[0],start[1]-center[1]) cos_,sin_ = math.cos(alpha_start), math.sin(alpha_start) k = (4.*math.tan(alpha/sectors/4.)/3.) if dot(slope_st , [- sin_*k*r, cos_*k*r]) < 0 : if alpha>0 : alpha -= math.pi2 else: alpha += math.pi2 if abs(alpha*r)<0.001 : return [] sectors = int(abs(alpha)*2/math.pi)+1 k = (4.*math.tan(alpha/sectors/4.)/3.) result = [] for i in range(sectors+1) : cos_,sin_ = math.cos(alpha_start + alpha*i/sectors), math.sin(alpha_start + alpha*i/sectors) sp = [ [], [center[0] + cos_*r, center[1] + sin_*r], [] ] sp[0] = [sp[1][0] + sin_*k*r, sp[1][1] - cos_*k*r ] sp[2] = [sp[1][0] - sin_*k*r, sp[1][1] + cos_*k*r ] result += [sp] result[0][0] = result[0][1][:] result[-1][2] = result[-1][1] return result def point_to_arc_distance(p, arc): ### Distance calculattion from point to arc P0,P2,c,a = arc dist = None p = P(p) r = (P0-c).mag() if r>0 : i = c + (p-c).unit()*r alpha = ((i-c).angle() - (P0-c).angle()) if a*alpha<0: if alpha>0: alpha = alpha-math.pi2 else: alpha = math.pi2+alpha if between(alpha,0,a) or min(abs(alpha),abs(alpha-a))tolerance and i<4): i += 1 dl = d1*1 for j in range(n+1): t = float(j)/n p = csp_at_t(sp1,sp2,t) d = min(point_to_arc_distance(p,arc1), point_to_arc_distance(p,arc2)) d1 = max(d1,d) n=n*2 return d1[0] def csp_simple_bound_to_point_distance(p, csp): minx,miny,maxx,maxy = None,None,None,None for subpath in csp: for sp in subpath: for p_ in sp: minx = min(minx,p_[0]) if minx!=None else p_[0] miny = min(miny,p_[1]) if miny!=None else p_[1] maxx = max(maxx,p_[0]) if maxx!=None else p_[0] maxy = max(maxy,p_[1]) if maxy!=None else p_[1] return math.sqrt(max(minx-p[0],p[0]-maxx,0)**2+max(miny-p[1],p[1]-maxy,0)**2) def csp_point_inside_bound(sp1, sp2, p): bez = [sp1[1],sp1[2],sp2[0],sp2[1]] x,y = p c = 0 #CLT added test of x in range xmin=1e100 xmax=-1e100 for i in range(4): [x0,y0], [x1,y1] = bez[i-1], bez[i] xmin=min(xmin,x0) xmax=max(xmax,x0) if x0-x1!=0 and (y-y0)*(x1-x0)>=(x-x0)*(y1-y0) and x>min(x0,x1) and x<=max(x0,x1) : c +=1 return xmin<=x<=xmax and c%2==0 def csp_bound_to_point_distance(sp1, sp2, p): if csp_point_inside_bound(sp1, sp2, p) : return 0. bez = csp_segment_to_bez(sp1,sp2) min_dist = 1e100 for i in range(0,4): d = point_to_line_segment_distance_2(p, bez[i-1],bez[i]) if d <= min_dist : min_dist = d return min_dist def line_line_intersect(p1,p2,p3,p4) : # Return only true intersection. if (p1[0]==p2[0] and p1[1]==p2[1]) or (p3[0]==p4[0] and p3[1]==p4[1]) : return False x = (p2[0]-p1[0])*(p4[1]-p3[1]) - (p2[1]-p1[1])*(p4[0]-p3[0]) if x==0 : # Lines are parallel if (p3[0]-p1[0])*(p2[1]-p1[1]) == (p3[1]-p1[1])*(p2[0]-p1[0]) : if p3[0]!=p4[0] : t11 = (p1[0]-p3[0])/(p4[0]-p3[0]) t12 = (p2[0]-p3[0])/(p4[0]-p3[0]) t21 = (p3[0]-p1[0])/(p2[0]-p1[0]) t22 = (p4[0]-p1[0])/(p2[0]-p1[0]) else: t11 = (p1[1]-p3[1])/(p4[1]-p3[1]) t12 = (p2[1]-p3[1])/(p4[1]-p3[1]) t21 = (p3[1]-p1[1])/(p2[1]-p1[1]) t22 = (p4[1]-p1[1])/(p2[1]-p1[1]) return ("Overlap" if (0<=t11<=1 or 0<=t12<=1) and (0<=t21<=1 or 0<=t22<=1) else False) else: return False else : return ( 0<=((p4[0]-p3[0])*(p1[1]-p3[1]) - (p4[1]-p3[1])*(p1[0]-p3[0]))/x<=1 and 0<=((p2[0]-p1[0])*(p1[1]-p3[1]) - (p2[1]-p1[1])*(p1[0]-p3[0]))/x<=1 ) def line_line_intersection_points(p1,p2,p3,p4) : # Return only points [ (x,y) ] if (p1[0]==p2[0] and p1[1]==p2[1]) or (p3[0]==p4[0] and p3[1]==p4[1]) : return [] x = (p2[0]-p1[0])*(p4[1]-p3[1]) - (p2[1]-p1[1])*(p4[0]-p3[0]) if x==0 : # Lines are parallel if (p3[0]-p1[0])*(p2[1]-p1[1]) == (p3[1]-p1[1])*(p2[0]-p1[0]) : if p3[0]!=p4[0] : t11 = (p1[0]-p3[0])/(p4[0]-p3[0]) t12 = (p2[0]-p3[0])/(p4[0]-p3[0]) t21 = (p3[0]-p1[0])/(p2[0]-p1[0]) t22 = (p4[0]-p1[0])/(p2[0]-p1[0]) else: t11 = (p1[1]-p3[1])/(p4[1]-p3[1]) t12 = (p2[1]-p3[1])/(p4[1]-p3[1]) t21 = (p3[1]-p1[1])/(p2[1]-p1[1]) t22 = (p4[1]-p1[1])/(p2[1]-p1[1]) res = [] if (0<=t11<=1 or 0<=t12<=1) and (0<=t21<=1 or 0<=t22<=1) : if 0<=t11<=1 : res += [p1] if 0<=t12<=1 : res += [p2] if 0<=t21<=1 : res += [p3] if 0<=t22<=1 : res += [p4] return res else: return [] else : t1 = ((p4[0]-p3[0])*(p1[1]-p3[1]) - (p4[1]-p3[1])*(p1[0]-p3[0]))/x t2 = ((p2[0]-p1[0])*(p1[1]-p3[1]) - (p2[1]-p1[1])*(p1[0]-p3[0]))/x if 0<=t1<=1 and 0<=t2<=1 : return [ [p1[0]*(1-t1)+p2[0]*t1, p1[1]*(1-t1)+p2[1]*t1] ] else : return [] def point_to_point_d2(a,b): return (a[0]-b[0])**2 + (a[1]-b[1])**2 def point_to_point_d(a,b): return math.sqrt((a[0]-b[0])**2 + (a[1]-b[1])**2) def point_to_line_segment_distance_2(p1, p2,p3) : # p1 - point, p2,p3 - line segment #draw_pointer(p1) w0 = [p1[0]-p2[0], p1[1]-p2[1]] v = [p3[0]-p2[0], p3[1]-p2[1]] c1 = w0[0]*v[0] + w0[1]*v[1] if c1 <= 0 : return w0[0]*w0[0]+w0[1]*w0[1] c2 = v[0]*v[0] + v[1]*v[1] if c2 <= c1 : return (p1[0]-p3[0])**2 + (p1[1]-p3[1])**2 return (p1[0]- p2[0]-v[0]*c1/c2)**2 + (p1[1]- p2[1]-v[1]*c1/c2) def line_to_line_distance_2(p1,p2,p3,p4): if line_line_intersect(p1,p2,p3,p4) : return 0 return min( point_to_line_segment_distance_2(p1,p3,p4), point_to_line_segment_distance_2(p2,p3,p4), point_to_line_segment_distance_2(p3,p1,p2), point_to_line_segment_distance_2(p4,p1,p2)) def csp_seg_bound_to_csp_seg_bound_max_min_distance(sp1,sp2,sp3,sp4) : bez1 = csp_segment_to_bez(sp1,sp2) bez2 = csp_segment_to_bez(sp3,sp4) min_dist = 1e100 max_dist = 0. for i in range(4) : if csp_point_inside_bound(sp1, sp2, bez2[i]) or csp_point_inside_bound(sp3, sp4, bez1[i]) : min_dist = 0. break for i in range(4) : for j in range(4) : d = line_to_line_distance_2(bez1[i-1],bez1[i],bez2[j-1],bez2[j]) if d < min_dist : min_dist = d d = (bez2[j][0]-bez1[i][0])**2 + (bez2[j][1]-bez1[i][1])**2 if max_dist < d : max_dist = d return min_dist, max_dist def csp_reverse(csp) : for i in range(len(csp)) : n = [] for j in csp[i] : n = [ [j[2][:],j[1][:],j[0][:]] ] + n csp[i] = n[:] return csp def csp_normalized_slope(sp1,sp2,t) : ax,ay,bx,by,cx,cy,dx,dy=bezmisc.bezierparameterize((sp1[1][:],sp1[2][:],sp2[0][:],sp2[1][:])) if sp1[1]==sp2[1]==sp1[2]==sp2[0] : return [1.,0.] f1x = 3*ax*t*t+2*bx*t+cx f1y = 3*ay*t*t+2*by*t+cy if abs(f1x*f1x+f1y*f1y) > 1e-9 : #LT changed this from 1e-20, which caused problems l = math.sqrt(f1x*f1x+f1y*f1y) return [f1x/l, f1y/l] if t == 0 : f1x = sp2[0][0]-sp1[1][0] f1y = sp2[0][1]-sp1[1][1] if abs(f1x*f1x+f1y*f1y) > 1e-9 : #LT changed this from 1e-20, which caused problems l = math.sqrt(f1x*f1x+f1y*f1y) return [f1x/l, f1y/l] else : f1x = sp2[1][0]-sp1[1][0] f1y = sp2[1][1]-sp1[1][1] if f1x*f1x+f1y*f1y != 0 : l = math.sqrt(f1x*f1x+f1y*f1y) return [f1x/l, f1y/l] elif t == 1 : f1x = sp2[1][0]-sp1[2][0] f1y = sp2[1][1]-sp1[2][1] if abs(f1x*f1x+f1y*f1y) > 1e-9 : l = math.sqrt(f1x*f1x+f1y*f1y) return [f1x/l, f1y/l] else : f1x = sp2[1][0]-sp1[1][0] f1y = sp2[1][1]-sp1[1][1] if f1x*f1x+f1y*f1y != 0 : l = math.sqrt(f1x*f1x+f1y*f1y) return [f1x/l, f1y/l] else : return [1.,0.] def csp_normalized_normal(sp1,sp2,t) : nx,ny = csp_normalized_slope(sp1,sp2,t) return [-ny, nx] def csp_parameterize(sp1,sp2): return bezmisc.bezierparameterize(csp_segment_to_bez(sp1,sp2)) def csp_concat_subpaths(*s): def concat(s1,s2) : if s1 == [] : return s2 if s2 == [] : return s1 if (s1[-1][1][0]-s2[0][1][0])**2 + (s1[-1][1][1]-s2[0][1][1])**2 > 0.00001 : return s1[:-1]+[ [s1[-1][0],s1[-1][1],s1[-1][1]], [s2[0][1],s2[0][1],s2[0][2]] ] + s2[1:] else : return s1[:-1]+[ [s1[-1][0],s2[0][1],s2[0][2]] ] + s2[1:] if len(s) == 0 : return [] if len(s) ==1 : return s[0] result = s[0] for s1 in s[1:]: result = concat(result,s1) return result def csp_subpaths_end_to_start_distance2(s1,s2): return (s1[-1][1][0]-s2[0][1][0])**2 + (s1[-1][1][1]-s2[0][1][1])**2 def csp_clip_by_line(csp,l1,l2) : result = [] for i in range(len(csp)): s = csp[i] intersections = [] for j in range(1,len(s)) : intersections += [ [j,int_] for int_ in csp_line_intersection(l1,l2,s[j-1],s[j])] splitted_s = csp_subpath_split_by_points(s, intersections) for s in splitted_s[:] : clip = False for p in csp_true_bounds([s]) : if (l1[1]-l2[1])*p[0] + (l2[0]-l1[0])*p[1] + (l1[0]*l2[1]-l2[0]*l1[1])<-0.01 : clip = True break if clip : splitted_s.remove(s) result += splitted_s return result def csp_subpath_line_to(subpath, points, prepend = False) : # Appends subpath with line or polyline. if len(points)>0 : if not prepend : if len(subpath)>0: subpath[-1][2] = subpath[-1][1][:] if type(points[0]) == type([1,1]) : for p in points : subpath += [ [p[:],p[:],p[:]] ] else: subpath += [ [points,points,points] ] else : if len(subpath)>0: subpath[0][0] = subpath[0][1][:] if type(points[0]) == type([1,1]) : for p in points : subpath = [ [p[:],p[:],p[:]] ] + subpath else: subpath = [ [points,points,points] ] + subpath return subpath def csp_join_subpaths(csp) : result = csp[:] done_smf = True joined_result = [] while done_smf : done_smf = False while len(result)>0: s1 = result[-1][:] del(result[-1]) j = 0 joined_smf = False while j0, abc*bcd>0, abc*cad>0 if m1 and m2 and m3 : return [a,b,c] if m1 and m2 and not m3 : return [a,b,c,d] if m1 and not m2 and m3 : return [a,b,d,c] if not m1 and m2 and m3 : return [a,d,b,c] if m1 and not (m2 and m3) : return [a,b,d] if not (m1 and m2) and m3 : return [c,a,d] if not (m1 and m3) and m2 : return [b,c,d] raise ValueError, "csp_segment_convex_hull happend something that shouldnot happen!" ################################################################################ ### Bezier additional functions ################################################################################ def bez_bounds_intersect(bez1, bez2) : return bounds_intersect(bez_bound(bez2), bez_bound(bez1)) def bez_bound(bez) : return [ min(bez[0][0], bez[1][0], bez[2][0], bez[3][0]), min(bez[0][1], bez[1][1], bez[2][1], bez[3][1]), max(bez[0][0], bez[1][0], bez[2][0], bez[3][0]), max(bez[0][1], bez[1][1], bez[2][1], bez[3][1]), ] def bounds_intersect(a, b) : return not ( (a[0]>b[2]) or (b[0]>a[2]) or (a[1]>b[3]) or (b[1]>a[3]) ) def tpoint((x1,y1),(x2,y2),t): return [x1+t*(x2-x1),y1+t*(y2-y1)] def bez_to_csp_segment(bez) : return [bez[0],bez[0],bez[1]], [bez[2],bez[3],bez[3]] def bez_split(a,t=0.5) : a1 = tpoint(a[0],a[1],t) at = tpoint(a[1],a[2],t) b2 = tpoint(a[2],a[3],t) a2 = tpoint(a1,at,t) b1 = tpoint(b2,at,t) a3 = tpoint(a2,b1,t) return [a[0],a1,a2,a3], [a3,b1,b2,a[3]] def bez_at_t(bez,t) : return csp_at_t([bez[0],bez[0],bez[1]],[bez[2],bez[3],bez[3]],t) def bez_to_point_distance(bez,p,needed_dist=[0.,1e100]): # returns [d^2,t] return csp_seg_to_point_distance(bez_to_csp_segment(bez),p,needed_dist) def bez_normalized_slope(bez,t): return csp_normalized_slope([bez[0],bez[0],bez[1]], [bez[2],bez[3],bez[3]],t) ################################################################################ ### Some vector functions ################################################################################ def normalize((x,y)) : l = math.sqrt(x**2+y**2) if l == 0 : return [0.,0.] else : return [x/l, y/l] def cross(a,b) : return a[1] * b[0] - a[0] * b[1] def dot(a,b) : return a[0] * b[0] + a[1] * b[1] def rotate_ccw(d) : return [-d[1],d[0]] def rotate_cw(d) : return [d[1],-d[0]] def vectors_ccw(a,b): return a[0]*b[1]-b[0]*a[1] < 0 def vector_add(a,b) : return [a[0]+b[0],a[1]+b[1]] def vector_mul(a,b) : return [a[0]*b,a[1]*b] def vector_from_to_length(a,b): return math.sqrt((a[0]-b[0])*(a[0]-b[0]) + (a[1]-b[1])*(a[1]-b[1])) ################################################################################ ### Common functions ################################################################################ def matrix_mul(a,b) : return [ [ sum([a[i][k]*b[k][j] for k in range(len(a[0])) ]) for j in range(len(b[0]))] for i in range(len(a))] try : return [ [ sum([a[i][k]*b[k][j] for k in range(len(a[0])) ]) for j in range(len(b[0]))] for i in range(len(a))] except : return None def transpose(a) : try : return [ [ a[i][j] for i in range(len(a)) ] for j in range(len(a[0])) ] except : return None def det_3x3(a): return float( a[0][0]*a[1][1]*a[2][2] + a[0][1]*a[1][2]*a[2][0] + a[1][0]*a[2][1]*a[0][2] - a[0][2]*a[1][1]*a[2][0] - a[0][0]*a[2][1]*a[1][2] - a[0][1]*a[2][2]*a[1][0] ) def inv_3x3(a): # invert matrix 3x3 det = det_3x3(a) if det==0: return None return [ [ (a[1][1]*a[2][2] - a[2][1]*a[1][2])/det, -(a[0][1]*a[2][2] - a[2][1]*a[0][2])/det, (a[0][1]*a[1][2] - a[1][1]*a[0][2])/det ], [ -(a[1][0]*a[2][2] - a[2][0]*a[1][2])/det, (a[0][0]*a[2][2] - a[2][0]*a[0][2])/det, -(a[0][0]*a[1][2] - a[1][0]*a[0][2])/det ], [ (a[1][0]*a[2][1] - a[2][0]*a[1][1])/det, -(a[0][0]*a[2][1] - a[2][0]*a[0][1])/det, (a[0][0]*a[1][1] - a[1][0]*a[0][1])/det ] ] def inv_2x2(a): # invert matrix 2x2 det = a[0][0]*a[1][1] - a[1][0]*a[0][1] if det==0: return None return [ [a[1][1]/det, -a[0][1]/det], [-a[1][0]/det, a[0][0]/det] ] def small(a) : global small_tolerance return abs(a)=0 : t = m+math.sqrt(n) m1 = pow(t/2,1./3) if t>=0 else -pow(-t/2,1./3) t = m-math.sqrt(n) n1 = pow(t/2,1./3) if t>=0 else -pow(-t/2,1./3) else : m1 = pow(complex((m+cmath.sqrt(n))/2),1./3) n1 = pow(complex((m-cmath.sqrt(n))/2),1./3) x1 = -1./3 * (a + m1 + n1) x2 = -1./3 * (a + w1*m1 + w2*n1) x3 = -1./3 * (a + w2*m1 + w1*n1) return [x1,x2,x3] elif b!=0: det = c**2-4*b*d if det>0 : return [(-c+math.sqrt(det))/(2*b),(-c-math.sqrt(det))/(2*b)] elif d == 0 : return [-c/(b*b)] else : return [(-c+cmath.sqrt(det))/(2*b),(-c-cmath.sqrt(det))/(2*b)] elif c!=0 : return [-d/c] else : return [] ################################################################################ ### print_ prints any arguments into specified log file ################################################################################ def print_(*arg): f = open(options.log_filename,"a") for s in arg : s = str(unicode(s).encode('unicode_escape'))+" " f.write( s ) f.write("\n") f.close() ################################################################################ ### Point (x,y) operations ################################################################################ class P: def __init__(self, x, y=None): if not y==None: self.x, self.y = float(x), float(y) else: self.x, self.y = float(x[0]), float(x[1]) def __add__(self, other): return P(self.x + other.x, self.y + other.y) def __sub__(self, other): return P(self.x - other.x, self.y - other.y) def __neg__(self): return P(-self.x, -self.y) def __mul__(self, other): if isinstance(other, P): return self.x * other.x + self.y * other.y return P(self.x * other, self.y * other) __rmul__ = __mul__ def __div__(self, other): return P(self.x / other, self.y / other) def mag(self): return math.hypot(self.x, self.y) def unit(self): h = self.mag() if h: return self / h else: return P(0,0) def dot(self, other): return self.x * other.x + self.y * other.y def rot(self, theta): c = math.cos(theta) s = math.sin(theta) return P(self.x * c - self.y * s, self.x * s + self.y * c) def angle(self): return math.atan2(self.y, self.x) def __repr__(self): return '%f,%f' % (self.x, self.y) def pr(self): return "%.2f,%.2f" % (self.x, self.y) def to_list(self): return [self.x, self.y] def ccw(self): return P(-self.y,self.x) def l2(self): return self.x*self.x + self.y*self.y class Arc(): def __init__(self,st,end,c,a): self.st = P(st) self.end = P(end) self.c = P(c) self.r = (P(st)-P(c)).mag() self.a = ( (self.st-self.c).angle() - (self.end-self.c).angle() ) % math.pi2 if a<0 : self.a -= math.pi2 def offset(self, r): if self.a>0 : r += self.r else : r = self.r - r if self.r != 0 : self.st = self.c + (self.st-self.c)*r/self.r self.end = self.c + (self.end-self.c)*r/self.r self.r = r def length(self): return abs(self.a*self.r) def draw(self, group, style, layer, transform, num = 0, reverse_angle = 1): st = P(gcodetools.transform(self.st.to_list(), layer, True)) c = P(gcodetools.transform(self.c.to_list(), layer, True)) a = self.a * reverse_angle r = (st-c) a_st = (math.atan2(r.x,-r.y) - math.pi/2) % (math.pi*2) r = r.mag() if a<0: a_end = a_st+a style = style['biarc%s'%(num%2)] else: a_end = a_st a_st = a_st+a style = style['biarc%s_r'%(num%2)] attr = { 'style': style, inkex.addNS('cx','sodipodi'): str(c.x), inkex.addNS('cy','sodipodi'): str(c.y), inkex.addNS('rx','sodipodi'): str(r), inkex.addNS('ry','sodipodi'): str(r), inkex.addNS('start','sodipodi'): str(a_st), inkex.addNS('end','sodipodi'): str(a_end), inkex.addNS('open','sodipodi'): 'true', inkex.addNS('type','sodipodi'): 'arc', "gcodetools": "Preview", } if transform != [] : attr["transform"] = transform inkex.etree.SubElement( group, inkex.addNS('path','svg'), attr) def intersect(self,b) : return [] class Line(): def __init__(self,st,end): if st.__class__ == P : st = st.to_list() if end.__class__ == P : end = end.to_list() self.st = P(st) self.end = P(end) self.l = self.length() if self.l != 0 : self.n = ((self.end-self.st)/self.l).ccw() else: self.n = [0,1] def offset(self, r): self.st -= self.n*r self.end -= self.n*r def l2(self): return (self.st-self.end).l2() def length(self): return (self.st-self.end).mag() def draw(self, group, style, layer, transform, num = 0, reverse_angle = 1): st = gcodetools.transform(self.st.to_list(), layer, True) end = gcodetools.transform(self.end.to_list(), layer, True) attr = { 'style': style['line'], 'd':'M %s,%s L %s,%s' % (st[0],st[1],end[0],end[1]), "gcodetools": "Preview", } if transform != [] : attr["transform"] = transform inkex.etree.SubElement( group, inkex.addNS('path','svg'), attr ) def intersect(self,b) : if b.__class__ == Line : if self.l < 10e-8 or b.l < 10e-8 : return [] v1 = self.end - self.st v2 = b.end - b.st x = v1.x*v2.y - v2.x*v1.y if x == 0 : # lines are parallel res = [] if (self.st.x-b.st.x)*v1.y - (self.st.y-b.st.y)*v1.x == 0: # lines are the same if v1.x != 0 : if 0<=(self.st.x-b.st.x)/v2.x<=1 : res.append(self.st) if 0<=(self.end.x-b.st.x)/v2.x<=1 : res.append(self.end) if 0<=(b.st.x-self.st.x)/v1.x<=1 : res.append(b.st) if 0<=(b.end.x-b.st.x)/v1.x<=1 : res.append(b.end) else : if 0<=(self.st.y-b.st.y)/v2.y<=1 : res.append(self.st) if 0<=(self.end.y-b.st.y)/v2.y<=1 : res.append(self.end) if 0<=(b.st.y-self.st.y)/v1.y<=1 : res.append(b.st) if 0<=(b.end.y-b.st.y)/v1.y<=1 : res.append(b.end) return res else : t1 = ( -v1.x*(b.end.y-self.end.y) + v1.y*(b.end.x-self.end.x) ) / x t2 = ( -v1.y*(self.st.x-b.st.x) + v1.x*(self.st.y-b.st.y) ) / x gcodetools.error((x,t1,t2), "warning") if 0<=t1<=1 and 0<=t2<=1 : return [ self.st+v1*t1 ] else : return [] else: return [] class Biarc: def __init__(self, items=None): if items == None : self.items = [] else: self.items = items def l(self) : return sum([i.length() for i in items]) def close(self) : for subitems in self.items: if (subitems[0].st-subitems[-1].end).l2()>10e-16 : subitems.append(Line(subitems[-1].end,subitems[0].st)) def offset(self,r) : # offset each element self.close() for subitems in self.items : for item in subitems : item.offset(r) self.connect(r) def connect(self, r) : for subitems in self.items : for a,b in zip(subitems, subitems[1:]) : i = a.intersect(b) for p in i : draw_pointer(p.to_list()) def clip_offset(self): pass def draw(self, layer, group=None, style=styles["biarc_style"]): global gcodetools gcodetools.set_markers() for i in [0,1]: style['biarc%s_r'%i] = simplestyle.parseStyle(style['biarc%s'%i]) style['biarc%s_r'%i]["marker-start"] = "url(#DrawCurveMarker_r)" del(style['biarc%s_r'%i]["marker-end"]) style['biarc%s_r'%i] = simplestyle.formatStyle(style['biarc%s_r'%i]) if group==None: if "preview_groups" not in dir(options.self) : gcodetools.preview_groups = { layer: inkex.etree.SubElement( gcodetools.layers[min(1,len(gcodetools.layers)-1)], inkex.addNS('g','svg'), {"gcodetools": "Preview group"} ) } elif layer not in gcodetools.preview_groups : gcodetools.preview_groups[layer] = inkex.etree.SubElement( gcodetools.layers[min(1,len(gcodetools.layers)-1)], inkex.addNS('g','svg'), {"gcodetools": "Preview group"} ) group = gcodetools.preview_groups[layer] transform = gcodetools.get_transforms(group) if transform != [] : transform = gcodetools.reverse_transform(transform) transform = simpletransform.formatTransform(transform) a,b,c = [0.,0.], [1.,0.], [0.,1.] k = (b[0]-a[0])*(c[1]-a[1])-(c[0]-a[0])*(b[1]-a[1]) a,b,c = gcodetools.transform(a, layer, True), gcodetools.transform(b, layer, True), gcodetools.transform(c, layer, True) if ((b[0]-a[0])*(c[1]-a[1])-(c[0]-a[0])*(b[1]-a[1]))*k > 0 : reverse_angle = -1 else : reverse_angle = 1 num = 0 for subitems in self.items : for item in subitems : num += 1 #if num>1 : break item.draw(group, style, layer, transform, num, reverse_angle) def from_old_style(self, curve) : #Crve defenitnion [start point, type = {'arc','line','move','end'}, arc center, arc angle, end point, [zstart, zend]] self.items = [] for sp in curve: print_(sp) if sp[1] == 'move': self.items.append([]) if sp[1] == 'arc': self.items[-1].append(Arc(sp[0],sp[4],sp[2],sp[3])) if sp[1] == 'line': self.items[-1].append(Line(sp[0],sp[4])) ################################################################################ ### ### Offset function ### ### This function offsets given cubic super path. ### It's based on src/livarot/PathOutline.cpp from Inkscape's source code. ### ### ################################################################################ def csp_offset(csp, r) : offset_tolerance = 0.05 offset_subdivision_depth = 10 time_ = time.time() time_start = time_ print_("Offset start at %s"% time_) print_("Offset radius %s"% r) def csp_offset_segment(sp1,sp2,r) : result = [] t = csp_get_t_at_curvature(sp1,sp2,1/r) if len(t) == 0 : t =[0.,1.] t.sort() if t[0]>.00000001 : t = [0.]+t if t[-1]<.99999999 : t.append(1.) for st,end in zip(t,t[1:]) : c = csp_curvature_at_t(sp1,sp2,(st+end)/2) sp = csp_split_by_two_points(sp1,sp2,st,end) if sp[1]!=sp[2]: if (c>1/r and r<0 or c<1/r and r>0) : offset = offset_segment_recursion(sp[1],sp[2],r, offset_subdivision_depth, offset_tolerance) else : # This part will be clipped for sure... TODO Optimize it... offset = offset_segment_recursion(sp[1],sp[2],r, offset_subdivision_depth, offset_tolerance) if result==[] : result = offset[:] else: if csp_subpaths_end_to_start_distance2(result,offset)<0.0001 : result = csp_concat_subpaths(result,offset) else: intersection = csp_get_subapths_last_first_intersection(result,offset) if intersection != [] : i,t1,j,t2 = intersection sp1_,sp2_,sp3_ = csp_split(result[i-1],result[i],t1) result = result[:i-1] + [ sp1_, sp2_ ] sp1_,sp2_,sp3_ = csp_split(offset[j-1],offset[j],t2) result = csp_concat_subpaths( result, [sp2_,sp3_] + offset[j+1:] ) else : pass # ??? #raise ValueError, "Offset curvature clipping error" #draw_csp([result]) return result def create_offset_segment(sp1,sp2,r) : # See Gernot Hoffmann "Bezier Curves" p.34 -> 7.1 Bezier Offset Curves p0,p1,p2,p3 = P(sp1[1]),P(sp1[2]),P(sp2[0]),P(sp2[1]) s0,s1,s3 = p1-p0,p2-p1,p3-p2 n0 = s0.ccw().unit() if s0.l2()!=0 else P(csp_normalized_normal(sp1,sp2,0)) n3 = s3.ccw().unit() if s3.l2()!=0 else P(csp_normalized_normal(sp1,sp2,1)) n1 = s1.ccw().unit() if s1.l2()!=0 else (n0.unit()+n3.unit()).unit() q0,q3 = p0+r*n0, p3+r*n3 c = csp_curvature_at_t(sp1,sp2,0) q1 = q0 + (p1-p0)*(1- (r*c if abs(c)<100 else 0) ) c = csp_curvature_at_t(sp1,sp2,1) q2 = q3 + (p2-p3)*(1- (r*c if abs(c)<100 else 0) ) return [[q0.to_list(), q0.to_list(), q1.to_list()],[q2.to_list(), q3.to_list(), q3.to_list()]] def csp_get_subapths_last_first_intersection(s1,s2): _break = False for i in range(1,len(s1)) : sp11, sp12 = s1[-i-1], s1[-i] for j in range(1,len(s2)) : sp21,sp22 = s2[j-1], s2[j] intersection = csp_segments_true_intersection(sp11,sp12,sp21,sp22) if intersection != [] : _break = True break if _break:break if _break : intersection = max(intersection) return [len(s1)-i,intersection[0], j,intersection[1]] else : return [] def csp_join_offsets(prev,next,sp1,sp2,sp1_l,sp2_l,r): if len(next)>1 : if (P(prev[-1][1])-P(next[0][1])).l2()<0.001 : return prev,[],next intersection = csp_get_subapths_last_first_intersection(prev,next) if intersection != [] : i,t1,j,t2 = intersection sp1_,sp2_,sp3_ = csp_split(prev[i-1],prev[i],t1) sp3_,sp4_,sp5_ = csp_split(next[j-1], next[j],t2) return prev[:i-1] + [ sp1_, sp2_ ], [], [sp4_,sp5_] + next[j+1:] # Offsets do not intersect... will add an arc... start = (P(csp_at_t(sp1_l,sp2_l,1.)) + r*P(csp_normalized_normal(sp1_l,sp2_l,1.))).to_list() end = (P(csp_at_t(sp1,sp2,0.)) + r*P(csp_normalized_normal(sp1,sp2,0.))).to_list() arc = csp_from_arc(start, end, sp1[1], r, csp_normalized_slope(sp1_l,sp2_l,1.) ) if arc == [] : return prev,[],next else: # Clip prev by arc if csp_subpaths_end_to_start_distance2(prev,arc)>0.00001 : intersection = csp_get_subapths_last_first_intersection(prev,arc) if intersection != [] : i,t1,j,t2 = intersection sp1_,sp2_,sp3_ = csp_split(prev[i-1],prev[i],t1) sp3_,sp4_,sp5_ = csp_split(arc[j-1],arc[j],t2) prev = prev[:i-1] + [ sp1_, sp2_ ] arc = [sp4_,sp5_] + arc[j+1:] #else : raise ValueError, "Offset curvature clipping error" # Clip next by arc if next == [] : return prev,[],arc if csp_subpaths_end_to_start_distance2(arc,next)>0.00001 : intersection = csp_get_subapths_last_first_intersection(arc,next) if intersection != [] : i,t1,j,t2 = intersection sp1_,sp2_,sp3_ = csp_split(arc[i-1],arc[i],t1) sp3_,sp4_,sp5_ = csp_split(next[j-1],next[j],t2) arc = arc[:i-1] + [ sp1_, sp2_ ] next = [sp4_,sp5_] + next[j+1:] #else : raise ValueError, "Offset curvature clipping error" return prev,arc,next def offset_segment_recursion(sp1,sp2,r, depth, tolerance) : sp1_r,sp2_r = create_offset_segment(sp1,sp2,r) err = max( csp_seg_to_point_distance(sp1_r,sp2_r, (P(csp_at_t(sp1,sp2,.25)) + P(csp_normalized_normal(sp1,sp2,.25))*r).to_list())[0], csp_seg_to_point_distance(sp1_r,sp2_r, (P(csp_at_t(sp1,sp2,.50)) + P(csp_normalized_normal(sp1,sp2,.50))*r).to_list())[0], csp_seg_to_point_distance(sp1_r,sp2_r, (P(csp_at_t(sp1,sp2,.75)) + P(csp_normalized_normal(sp1,sp2,.75))*r).to_list())[0], ) if err>tolerance**2 and depth>0: #print_(csp_seg_to_point_distance(sp1_r,sp2_r, (P(csp_at_t(sp1,sp2,.25)) + P(csp_normalized_normal(sp1,sp2,.25))*r).to_list())[0], tolerance) if depth > offset_subdivision_depth-2 : t = csp_max_curvature(sp1,sp2) t = max(.1,min(.9 ,t)) else : t = .5 sp3,sp4,sp5 = csp_split(sp1,sp2,t) r1 = offset_segment_recursion(sp3,sp4,r, depth-1, tolerance) r2 = offset_segment_recursion(sp4,sp5,r, depth-1, tolerance) return r1[:-1]+ [[r1[-1][0],r1[-1][1],r2[0][2]]] + r2[1:] else : #draw_csp([[sp1_r,sp2_r]]) #draw_pointer(sp1[1]+sp1_r[1], "#057", "line") #draw_pointer(sp2[1]+sp2_r[1], "#705", "line") return [sp1_r,sp2_r] ############################################################################ # Some small definitions ############################################################################ csp_len = len(csp) ############################################################################ # Prepare the path ############################################################################ # Remove all small segments (segment length < 0.001) for i in xrange(len(csp)) : for j in xrange(len(csp[i])) : sp = csp[i][j] if (P(sp[1])-P(sp[0])).mag() < 0.001 : csp[i][j][0] = sp[1] if (P(sp[2])-P(sp[0])).mag() < 0.001 : csp[i][j][2] = sp[1] for i in xrange(len(csp)) : for j in xrange(1,len(csp[i])) : if cspseglength(csp[i][j-1], csp[i][j])<0.001 : csp[i] = csp[i][:j] + csp[i][j+1:] if cspseglength(csp[i][-1],csp[i][0])>0.001 : csp[i][-1][2] = csp[i][-1][1] csp[i]+= [ [csp[i][0][1],csp[i][0][1],csp[i][0][1]] ] # TODO Get rid of self intersections. original_csp = csp[:] # Clip segments which has curvature>1/r. Because their offset will be selfintersecting and very nasty. print_("Offset prepared the path in %s"%(time.time()-time_)) print_("Path length = %s"% sum([len(i)for i in csp] ) ) time_ = time.time() ############################################################################ # Offset ############################################################################ # Create offsets for all segments in the path. And join them together inside each subpath. unclipped_offset = [[] for i in xrange(csp_len)] offsets_original = [[] for i in xrange(csp_len)] join_points = [[] for i in xrange(csp_len)] intersection = [[] for i in xrange(csp_len)] for i in xrange(csp_len) : subpath = csp[i] subpath_offset = [] last_offset_len = 0 for sp1,sp2 in zip(subpath, subpath[1:]) : segment_offset = csp_offset_segment(sp1,sp2,r) if subpath_offset == [] : subpath_offset = segment_offset prev_l = len(subpath_offset) else : prev, arc, next = csp_join_offsets(subpath_offset[-prev_l:],segment_offset,sp1,sp2,sp1_l,sp2_l,r) #draw_csp([prev],"Blue") #draw_csp([arc],"Magenta") subpath_offset = csp_concat_subpaths(subpath_offset[:-prev_l+1],prev,arc,next) prev_l = len(next) sp1_l, sp2_l = sp1[:], sp2[:] # Join last and first offsets togother to close the curve prev, arc, next = csp_join_offsets(subpath_offset[-prev_l:], subpath_offset[:2], subpath[0], subpath[1], sp1_l,sp2_l, r) subpath_offset[:2] = next[:] subpath_offset = csp_concat_subpaths(subpath_offset[:-prev_l+1],prev,arc) #draw_csp([prev],"Blue") #draw_csp([arc],"Red") #draw_csp([next],"Red") # Collect subpath's offset and save it to unclipped offset list. unclipped_offset[i] = subpath_offset[:] #for k,t in intersection[i]: # draw_pointer(csp_at_t(subpath_offset[k-1], subpath_offset[k], t)) #inkex.etree.SubElement( options.doc_root, inkex.addNS('path','svg'), {"d": cubicsuperpath.formatPath(unclipped_offset), "style":"fill:none;stroke:#0f0;"} ) print_("Offsetted path in %s"%(time.time()-time_)) time_ = time.time() #for i in range(len(unclipped_offset)): # draw_csp([unclipped_offset[i]], color = ["Green","Red","Blue"][i%3], width = .1) #return [] ############################################################################ # Now to the clipping. ############################################################################ # First of all find all intersection's between all segments of all offseted subpaths, including self intersections. #TODO define offset tolerance here global small_tolerance small_tolerance = 0.01 summ = 0 summ1 = 0 for subpath_i in xrange(csp_len) : for subpath_j in xrange(subpath_i,csp_len) : subpath = unclipped_offset[subpath_i] subpath1 = unclipped_offset[subpath_j] for i in xrange(1,len(subpath)) : # If subpath_i==subpath_j we are looking for self intersections, so # we'll need search intersections only for xrange(i,len(subpath1)) for j in ( xrange(i,len(subpath1)) if subpath_i==subpath_j else xrange(len(subpath1))) : if subpath_i==subpath_j and j==i : # Find self intersections of a segment sp1,sp2,sp3 = csp_split(subpath[i-1],subpath[i],.5) intersections = csp_segments_intersection(sp1,sp2,sp2,sp3) summ +=1 for t in intersections : summ1 += 1 if not ( small(t[0]-1) and small(t[1]) ) and 0<=t[0]<=1 and 0<=t[1]<=1 : intersection[subpath_i] += [ [i,t[0]/2],[j,t[1]/2+.5] ] else : intersections = csp_segments_intersection(subpath[i-1],subpath[i],subpath1[j-1],subpath1[j]) summ +=1 for t in intersections : summ1 += 1 #TODO tolerance dependence to cpsp_length(t) if len(t) == 2 and 0<=t[0]<=1 and 0<=t[1]<=1 and not ( subpath_i==subpath_j and ( (j-i-1) % (len(subpath)-1) == 0 and small(t[0]-1) and small(t[1]) or (i-j-1) % (len(subpath)-1) == 0 and small(t[1]-1) and small(t[0]) ) ) : intersection[subpath_i] += [ [i,t[0]] ] intersection[subpath_j] += [ [j,t[1]] ] #draw_pointer(csp_at_t(subpath[i-1],subpath[i],t[0]),"#f00") #print_(t) #print_(i,j) elif len(t)==5 and t[4]=="Overlap": intersection[subpath_i] += [ [i,t[0]], [i,t[1]] ] intersection[subpath_j] += [ [j,t[1]], [j,t[3]] ] print_("Intersections found in %s"%(time.time()-time_)) print_("Examined %s segments"%(summ)) print_("found %s intersections"%(summ1)) time_ = time.time() ######################################################################## # Split unclipped offset by intersection points into splitted_offset ######################################################################## splitted_offset = [] for i in xrange(csp_len) : subpath = unclipped_offset[i] if len(intersection[i]) > 0 : parts = csp_subpath_split_by_points(subpath, intersection[i]) # Close parts list to close path (The first and the last parts are joined together) if [1,0.] not in intersection[i] : parts[0][0][0] = parts[-1][-1][0] parts[0] = csp_concat_subpaths(parts[-1], parts[0]) splitted_offset += parts[:-1] else: splitted_offset += parts[:] else : splitted_offset += [subpath[:]] #for i in range(len(splitted_offset)): # draw_csp([splitted_offset[i]], color = ["Green","Red","Blue"][i%3]) print_("Splitted in %s"%(time.time()-time_)) time_ = time.time() ######################################################################## # Clipping ######################################################################## result = [] for subpath_i in range(len(splitted_offset)): clip = False s1 = splitted_offset[subpath_i] for subpath_j in range(len(splitted_offset)): s2 = splitted_offset[subpath_j] if (P(s1[0][1])-P(s2[-1][1])).l2()<0.0001 and ( (subpath_i+1) % len(splitted_offset) != subpath_j ): if dot(csp_normalized_normal(s2[-2],s2[-1],1.),csp_normalized_slope(s1[0],s1[1],0.))*r<-0.0001 : clip = True break if (P(s2[0][1])-P(s1[-1][1])).l2()<0.0001 and ( (subpath_j+1) % len(splitted_offset) != subpath_i ): if dot(csp_normalized_normal(s2[0],s2[1],0.),csp_normalized_slope(s1[-2],s1[-1],1.))*r>0.0001 : clip = True break if not clip : result += [s1[:]] elif options.offset_draw_clippend_path : draw_csp([s1],color="Red",width=.1) draw_pointer( csp_at_t(s2[-2],s2[-1],1.)+ (P(csp_at_t(s2[-2],s2[-1],1.))+ P(csp_normalized_normal(s2[-2],s2[-1],1.))*10).to_list(),"Green", "line" ) draw_pointer( csp_at_t(s1[0],s1[1],0.)+ (P(csp_at_t(s1[0],s1[1],0.))+ P(csp_normalized_slope(s1[0],s1[1],0.))*10).to_list(),"Red", "line" ) # Now join all together and check closure and orientation of result joined_result = csp_join_subpaths(result) # Check if each subpath from joined_result is closed #draw_csp(joined_result,color="Green",width=1) for s in joined_result[:] : if csp_subpaths_end_to_start_distance2(s,s) > 0.001 : # Remove open parts if options.offset_draw_clippend_path: draw_csp([s],color="Orange",width=1) draw_pointer(s[0][1], comment= csp_subpaths_end_to_start_distance2(s,s)) draw_pointer(s[-1][1], comment = csp_subpaths_end_to_start_distance2(s,s)) joined_result.remove(s) else : # Remove small parts minx,miny,maxx,maxy = csp_true_bounds([s]) if (minx[0]-maxx[0])**2 + (miny[1]-maxy[1])**2 < 0.1 : joined_result.remove(s) print_("Clipped and joined path in %s"%(time.time()-time_)) time_ = time.time() ######################################################################## # Now to the Dummy cliping: remove parts from splitted offset if their # centers are closer to the original path than offset radius. ######################################################################## r1,r2 = ( (0.99*r)**2, (1.01*r)**2 ) if abs(r*.01)<1 else ((abs(r)-1)**2, (abs(r)+1)**2) for s in joined_result[:]: dist = csp_to_point_distance(original_csp, s[int(len(s)/2)][1], dist_bounds = [r1,r2], tolerance = .000001) if not r1 < dist[0] < r2 : joined_result.remove(s) if options.offset_draw_clippend_path: draw_csp([s], comment = math.sqrt(dist[0])) draw_pointer(csp_at_t(csp[dist[1]][dist[2]-1],csp[dist[1]][dist[2]],dist[3])+s[int(len(s)/2)][1],"blue", "line", comment = [math.sqrt(dist[0]),i,j,sp] ) print_("-----------------------------") print_("Total offset time %s"%(time.time()-time_start)) print_() return joined_result ################################################################################ ### ### Biarc function ### ### Calculates biarc approximation of cubic super path segment ### splits segment if needed or approximates it with straight line ### ################################################################################ def biarc(sp1, sp2, z1, z2, depth=0): def biarc_split(sp1,sp2, z1, z2, depth): if depth 0 : raise ValueError, (a,b,c,disq,beta1,beta2) beta = max(beta1, beta2) elif asmall and bsmall: return biarc_split(sp1, sp2, z1, z2, depth) alpha = beta * r ab = alpha + beta P1 = P0 + alpha * TS P3 = P4 - beta * TE P2 = (beta / ab) * P1 + (alpha / ab) * P3 def calculate_arc_params(P0,P1,P2): D = (P0+P2)/2 if (D-P1).mag()==0: return None, None R = D - ( (D-P0).mag()**2/(D-P1).mag() )*(P1-D).unit() p0a, p1a, p2a = (P0-R).angle()%(2*math.pi), (P1-R).angle()%(2*math.pi), (P2-R).angle()%(2*math.pi) alpha = (p2a - p0a) % (2*math.pi) if (p0a1000000 or abs(R.y)>1000000 or (R-P0).mag options.biarc_tolerance and depthls : res += [seg] else : if seg[1] == "arc" : r = math.sqrt((seg[0][0]-seg[2][0])**2+(seg[0][1]-seg[2][1])**2) x,y = seg[0][0]-seg[2][0], seg[0][1]-seg[2][1] a = seg[3]/ls*(l-lc) x,y = x*math.cos(a) - y*math.sin(a), x*math.sin(a) + y*math.cos(a) x,y = x+seg[2][0], y+seg[2][1] res += [[ seg[0], "arc", seg[2], a, [x,y], [seg[5][0],seg[5][1]/ls*(l-lc)] ]] if seg[1] == "line" : res += [[ seg[0], "line", 0, 0, [(seg[4][0]-seg[0][0])/ls*(l-lc),(seg[4][1]-seg[0][1])/ls*(l-lc)], [seg[5][0],seg[5][1]/ls*(l-lc)] ]] i += 1 if i >= len(subcurve) and not subcurve_closed: reverse = not reverse i = i%len(subcurve) return res class Postprocessor(): def __init__(self, error_function_handler): self.error = error_function_handler self.functions = { "remap" : self.remap, "remapi" : self.remapi , "scale" : self.scale, "move" : self.move, "flip" : self.flip_axis, "flip_axis" : self.flip_axis, "round" : self.round_coordinates, "parameterize" : self.parameterize, "regex" : self.re_sub_on_gcode_lines } def process(self,command): command = re.sub(r"\\\\",":#:#:slash:#:#:",command) command = re.sub(r"\\;",":#:#:semicolon:#:#:",command) command = command.split(";") for s in command: s = re.sub(":#:#:slash:#:#:","\\\\",s) s = re.sub(":#:#:semicolon:#:#:","\\;",s) s = s.strip() if s!="" : self.parse_command(s) def parse_command(self,command): r = re.match(r"([A-Za-z0-9_]+)\s*\(\s*(.*)\)",command) if not r: self.error("Parse error while postprocessing.\n(Command: '%s')"%(command), "error") function, parameters = r.group(1).lower(),r.group(2) if function in self.functions : print_("Postprocessor: executing function %s(%s)"%(function,parameters)) self.functions[function](parameters) else : self.error("Unrecognized function '%s' while postprocessing.\n(Command: '%s')"%(function,command), "error") def re_sub_on_gcode_lines(self, parameters): gcode = self.gcode.split("\n") self.gcode = "" try : for line in gcode : self.gcode += eval( "re.sub(%s,line)"%parameters) +"\n" except Exception as ex : self.error("Bad parameters for regexp. They should be as re.sub pattern and replacement parameters! For example: r\"G0(\d)\", r\"G\\1\" \n(Parameters: '%s')\n %s"%(parameters, ex), "error") def remapi(self,parameters): self.remap(parameters, case_sensitive = True) def remap(self,parameters, case_sensitive = False): # remap parameters should be like "x->y,y->x" parameters = parameters.replace("\,",":#:#:coma:#:#:") parameters = parameters.split(",") pattern, remap = [], [] for s in parameters: s = s.replace(":#:#:coma:#:#:","\,") r = re.match("""\s*(\'|\")(.*)\\1\s*->\s*(\'|\")(.*)\\3\s*""",s) if not r : self.error("Bad parameters for remap.\n(Parameters: '%s')"%(parameters), "error") pattern +=[r.group(2)] remap +=[r.group(4)] for i in range(len(pattern)) : if case_sensitive : self.gcode = ireplace(self.gcode, pattern[i], ":#:#:remap_pattern%s:#:#:"%i ) else : self.gcode = self.gcode.replace(pattern[i], ":#:#:remap_pattern%s:#:#:"%i) for i in range(len(remap)) : self.gcode = self.gcode.replace(":#:#:remap_pattern%s:#:#:"%i, remap[i]) def transform(self, move, scale): axis = ["xi","yj","zk","a"] flip = scale[0]*scale[1]*scale[2] < 0 gcode = "" warned = [] r_scale = scale[0] plane = "g17" for s in self.gcode.split("\n"): # get plane selection: s_wo_comments = re.sub(r"\([^\)]*\)","",s) r = re.search(r"(?i)(G17|G18|G19)", s_wo_comments) if r : plane = r.group(1).lower() if plane == "g17" : r_scale = scale[0] # plane XY -> scale x if plane == "g18" : r_scale = scale[0] # plane XZ -> scale x if plane == "g19" : r_scale = scale[1] # plane YZ -> scale y # Raise warning if scale factors are not the game for G02 and G03 if plane not in warned: r = re.search(r"(?i)(G02|G03)", s_wo_comments) if r : if plane == "g17" and scale[0]!=scale[1]: self.error("Post-processor: Scale factors for X and Y axis are not the same. G02 and G03 codes will be corrupted.","warning") if plane == "g18" and scale[0]!=scale[2]: self.error("Post-processor: Scale factors for X and Z axis are not the same. G02 and G03 codes will be corrupted.","warning") if plane == "g19" and scale[1]!=scale[2]: self.error("Post-processor: Scale factors for Y and Z axis are not the same. G02 and G03 codes will be corrupted.","warning") warned += [plane] # Transform for i in range(len(axis)) : if move[i] != 0 or scale[i] != 1: for a in axis[i] : r = re.search(r"(?i)("+a+r")\s*(-?)\s*(\d*\.?\d*)", s) if r and r.group(3)!="": s = re.sub(r"(?i)("+a+r")\s*(-?)\s*(\d*\.?\d*)", r"\1 %f"%(float(r.group(2)+r.group(3))*scale[i]+(move[i] if a not in ["i","j","k"] else 0) ), s) #scale radius R if r_scale != 1 : r = re.search(r"(?i)(r)\s*(-?\s*(\d*\.?\d*))", s) if r and r.group(3)!="": try: s = re.sub(r"(?i)(r)\s*(-?)\s*(\d*\.?\d*)", r"\1 %f"%( float(r.group(2)+r.group(3))*r_scale ), s) except: pass gcode += s + "\n" self.gcode = gcode if flip : self.remapi("'G02'->'G03', 'G03'->'G02'") def parameterize(self,parameters) : planes = [] feeds = {} coords = [] gcode = "" coords_def = {"x":"x","y":"y","z":"z","i":"x","j":"y","k":"z","a":"a"} for s in self.gcode.split("\n"): s_wo_comments = re.sub(r"\([^\)]*\)","",s) # get Planes r = re.search(r"(?i)(G17|G18|G19)", s_wo_comments) if r : plane = r.group(1).lower() if plane not in planes : planes += [plane] # get Feeds r = re.search(r"(?i)(F)\s*(-?)\s*(\d*\.?\d*)", s_wo_comments) if r : feed = float (r.group(2)+r.group(3)) if feed not in feeds : feeds[feed] = "#"+str(len(feeds)+20) #Coordinates for c in "xyzijka" : r = re.search(r"(?i)("+c+r")\s*(-?)\s*(\d*\.?\d*)", s_wo_comments) if r : c = coords_def[r.group(1).lower()] if c not in coords : coords += [c] # Add offset parametrization offset = {"x":"#6","y":"#7","z":"#8","a":"#9"} for c in coords: gcode += "%s = 0 (%s axis offset)\n" % (offset[c],c.upper()) # Add scale parametrization if planes == [] : planes = ["g17"] if len(planes)>1 : # have G02 and G03 in several planes scale_x = scale_y = scale_z required gcode += "#10 = 1 (Scale factor)\n" scale = {"x":"#10","i":"#10","y":"#10","j":"#10","z":"#10","k":"#10","r":"#10"} else : gcode += "#10 = 1 (%s Scale factor)\n" % ({"g17":"XY","g18":"XZ","g19":"YZ"}[planes[0]]) gcode += "#11 = 1 (%s Scale factor)\n" % ({"g17":"Z","g18":"Y","g19":"X"}[planes[0]]) scale = {"x":"#10","i":"#10","y":"#10","j":"#10","z":"#10","k":"#10","r":"#10"} if "g17" in planes : scale["z"] = "#11" scale["k"] = "#11" if "g18" in planes : scale["y"] = "#11" scale["j"] = "#11" if "g19" in planes : scale["x"] = "#11" scale["i"] = "#11" # Add a scale if "a" in coords: gcode += "#12 = 1 (A axis scale)\n" scale["a"] = "#12" # Add feed parametrization for f in feeds : gcode += "%s = %f (Feed definition)\n" % (feeds[f],f) # Parameterize Gcode for s in self.gcode.split("\n"): #feed replace : r = re.search(r"(?i)(F)\s*(-?)\s*(\d*\.?\d*)", s) if r and len(r.group(3))>0: s = re.sub(r"(?i)(F)\s*(-?)\s*(\d*\.?\d*)", "F [%s]"%feeds[float(r.group(2)+r.group(3))], s) #Coords XYZA replace for c in "xyza" : r = re.search(r"(?i)(("+c+r")\s*(-?)\s*(\d*\.?\d*))", s) if r and len(r.group(4))>0: s = re.sub(r"(?i)("+c+r")\s*((-?)\s*(\d*\.?\d*))", r"\1[\2*%s+%s]"%(scale[c],offset[c]), s) #Coords IJKR replace for c in "ijkr" : r = re.search(r"(?i)(("+c+r")\s*(-?)\s*(\d*\.?\d*))", s) if r and len(r.group(4))>0: s = re.sub(r"(?i)("+c+r")\s*((-?)\s*(\d*\.?\d*))", r"\1[\2*%s]"%scale[c], s) gcode += s + "\n" self.gcode = gcode def round_coordinates(self,parameters) : try: round_ = int(parameters) except : self.error("Bad parameters for round. Round should be an integer! \n(Parameters: '%s')"%(parameters), "error") gcode = "" for s in self.gcode.split("\n"): for a in "xyzijkaf" : r = re.search(r"(?i)("+a+r")\s*(-?\s*(\d*\.?\d*))", s) if r : if r.group(2)!="": s = re.sub( r"(?i)("+a+r")\s*(-?)\s*(\d*\.?\d*)", (r"\1 %0."+str(round_)+"f" if round_>0 else r"\1 %d")%round(float(r.group(2)),round_), s) gcode += s + "\n" self.gcode = gcode def scale(self, parameters): parameters = parameters.split(",") scale = [1.,1.,1.,1.] try : for i in range(len(parameters)) : if float(parameters[i])==0 : self.error("Bad parameters for scale. Scale should not be 0 at any axis! \n(Parameters: '%s')"%(parameters), "error") scale[i] = float(parameters[i]) except : self.error("Bad parameters for scale.\n(Parameters: '%s')"%(parameters), "error") self.transform([0,0,0,0],scale) def move(self, parameters): parameters = parameters.split(",") move = [0.,0.,0.,0.] try : for i in range(len(parameters)) : move[i] = float(parameters[i]) except : self.error("Bad parameters for move.\n(Parameters: '%s')"%(parameters), "error") self.transform(move,[1.,1.,1.,1.]) def flip_axis(self, parameters): parameters = parameters.lower() axis = {"x":1.,"y":1.,"z":1.,"a":1.} for p in parameters: if p in [","," "," ","\r","'",'"'] : continue if p not in ["x","y","z","a"] : self.error("Bad parameters for flip_axis. Parameter should be string consists of 'xyza' \n(Parameters: '%s')"%(parameters), "error") axis[p] = -axis[p] self.scale("%f,%f,%f,%f"%(axis["x"],axis["y"],axis["z"],axis["a"])) ################################################################################ ### Polygon class ################################################################################ class Polygon: def __init__(self, polygon=None): self.polygon = [] if polygon==None else polygon[:] def move(self, x, y) : for i in range(len(self.polygon)) : for j in range(len(self.polygon[i])) : self.polygon[i][j][0] += x self.polygon[i][j][1] += y def bounds(self) : minx,miny,maxx,maxy = 1e400, 1e400, -1e400, -1e400 for poly in self.polygon : for p in poly : if minx > p[0] : minx = p[0] if miny > p[1] : miny = p[1] if maxx < p[0] : maxx = p[0] if maxy < p[1] : maxy = p[1] return minx*1,miny*1,maxx*1,maxy*1 def width(self): b = self.bounds() return b[2]-b[0] def rotate_(self,sin,cos) : self.polygon = [ [ [point[0]*cos - point[1]*sin,point[0]*sin + point[1]*cos] for point in subpoly ] for subpoly in self.polygon ] def rotate(self, a): cos, sin = math.cos(a), math.sin(a) self.rotate_(sin,cos) def drop_into_direction(self, direction, surface) : # Polygon is a list of simple polygons # Surface is a polygon + line y = 0 # Direction is [dx,dy] if len(self.polygon) == 0 or len(self.polygon[0])==0 : return if direction[0]**2 + direction[1]**2 <1e-10 : return direction = normalize(direction) sin,cos = direction[0], -direction[1] self.rotate_(-sin,cos) surface.rotate_(-sin,cos) self.drop_down(surface, zerro_plane = False) self.rotate_(sin,cos) surface.rotate_(sin,cos) def centroid(self): centroids = [] sa = 0 for poly in self.polygon: cx,cy,a = 0,0,0 for i in range(len(poly)): [x1,y1],[x2,y2] = poly[i-1],poly[i] cx += (x1+x2)*(x1*y2-x2*y1) cy += (y1+y2)*(x1*y2-x2*y1) a += (x1*y2-x2*y1) a *= 3. if abs(a)>0 : cx /= a cy /= a sa += abs(a) centroids += [ [cx,cy,a] ] if sa == 0 : return [0.,0.] cx,cy = 0.,0. for c in centroids : cx += c[0]*c[2] cy += c[1]*c[2] cx /= sa cy /= sa return [cx,cy] def drop_down(self, surface, zerro_plane = True) : # Polygon is a list of simple polygons # Surface is a polygon + line y = 0 # Down means min y (0,-1) if len(self.polygon) == 0 or len(self.polygon[0])==0 : return # Get surface top point top = surface.bounds()[3] if zerro_plane : top = max(0, top) # Get polygon bottom point bottom = self.bounds()[1] self.move(0, top - bottom + 10) # Now get shortest distance from surface to polygon in positive x=0 direction # Such distance = min(distance(vertex, edge)...) where edge from surface and # vertex from polygon and vice versa... dist = 1e300 for poly in surface.polygon : for i in range(len(poly)) : for poly1 in self.polygon : for i1 in range(len(poly1)) : st,end = poly[i-1], poly[i] vertex = poly1[i1] if st[0]<=vertex[0]<= end[0] or end[0]<=vertex[0]<=st[0] : if st[0]==end[0] : d = min(vertex[1]-st[1],vertex[1]-end[1]) else : d = vertex[1] - st[1] - (end[1]-st[1])*(vertex[0]-st[0])/(end[0]-st[0]) if dist > d : dist = d # and vice versa just change the sign because vertex now under the edge st,end = poly1[i1-1], poly1[i1] vertex = poly[i] if st[0]<=vertex[0]<=end[0] or end[0]<=vertex[0]<=st[0] : if st[0]==end[0] : d = min(- vertex[1]+st[1],-vertex[1]+end[1]) else : d = - vertex[1] + st[1] + (end[1]-st[1])*(vertex[0]-st[0])/(end[0]-st[0]) if dist > d : dist = d if zerro_plane and dist > 10 + top : dist = 10 + top #print_(dist, top, bottom) #self.draw() self.move(0, -dist) def draw(self,color="#075",width=.1, group = None) : csp = [csp_subpath_line_to([],poly+[poly[0]]) for poly in self.polygon] draw_csp( csp, color=color,width=width, group = group) def add(self, add) : if type(add) == type([]) : self.polygon += add[:] else : self.polygon += add.polygon[:] def point_inside(self,p) : inside = False for poly in self.polygon : for i in range(len(poly)): st,end = poly[i-1], poly[i] if p==st or p==end : return True # point is a vertex = point is on the edge if st[0]>end[0] : st, end = end, st # This will be needed to check that edge if open only at rigth end c = (p[1]-st[1])*(end[0]-st[0])-(end[1]-st[1])*(p[0]-st[0]) #print_(c) if st[0]<=p[0]0.000001 and point_to_point_d2(p,e)>0.000001 : poly_ += [p] # Check self intersections with other polys for i2 in range(len(self.polygon)): if i1==i2 : continue poly2 = self.polygon[i2] for j2 in range(len(poly2)): s1, e1 = poly2[j2-1],poly2[j2] int_ = line_line_intersection_points(s,e,s1,e1) for p in int_ : if point_to_point_d2(p,s)>0.000001 and point_to_point_d2(p,e)>0.000001 : poly_ += [p] hull += [poly_] # Create the dictionary containing all edges in both directions edges = {} for poly in self.polygon : for i in range(len(poly)): s,e = tuple(poly[i-1]), tuple(poly[i]) if (point_to_point_d2(e,s)<0.000001) : continue break_s, break_e = False, False for p in edges : if point_to_point_d2(p,s)<0.000001 : break_s = True s = p if point_to_point_d2(p,e)<0.000001 : break_e = True e = p if break_s and break_e : break l = point_to_point_d(s,e) if not break_s and not break_e : edges[s] = [ [s,e,l] ] edges[e] = [ [e,s,l] ] #draw_pointer(s+e,"red","line") #draw_pointer(s+e,"red","line") else : if e in edges : for edge in edges[e] : if point_to_point_d2(edge[1],s)<0.000001 : break if point_to_point_d2(edge[1],s)>0.000001 : edges[e] += [ [e,s,l] ] #draw_pointer(s+e,"red","line") else : edges[e] = [ [e,s,l] ] #draw_pointer(s+e,"green","line") if s in edges : for edge in edges[s] : if point_to_point_d2(edge[1],e)<0.000001 : break if point_to_point_d2(edge[1],e)>0.000001 : edges[s] += [ [s,e, l] ] #draw_pointer(s+e,"red","line") else : edges[s] = [ [s,e,l] ] #draw_pointer(s+e,"green","line") def angle_quadrant(sin,cos): # quadrants are (0,pi/2], (pi/2,pi], (pi,3*pi/2], (3*pi/2, 2*pi], i.e. 0 is in the 4-th quadrant if sin>0 and cos>=0 : return 1 if sin>=0 and cos<0 : return 2 if sin<0 and cos<=0 : return 3 if sin<=0 and cos>0 : return 4 def angle_is_less(sin,cos,sin1,cos1): # 0 = 2*pi is the largest angle if [sin1, cos1] == [0,1] : return True if [sin, cos] == [0,1] : return False if angle_quadrant(sin,cos)>angle_quadrant(sin1,cos1) : return False if angle_quadrant(sin,cos)=0 and cos>0 : return sin0 and cos<=0 : return sin>sin1 if sin<=0 and cos<0 : return sin>sin1 if sin<0 and cos>=0 : return sin len_edges : raise ValueError, "Hull error" loops1 += 1 next = get_closes_edge_by_angle(edges[last[1]],last) #draw_pointer(next[0]+next[1],"Green","line", comment=i, width= 1) #print_(next[0],"-",next[1]) last = next poly += [ list(last[0]) ] self.polygon += [ poly ] # Remove all edges that are intersects new poly (any vertex inside new poly) poly_ = Polygon([poly]) for p in edges.keys()[:] : if poly_.point_inside(list(p)) : del edges[p] self.draw(color="Green", width=1) class Arangement_Genetic: # gene = [fittness, order, rotation, xposition] # spieces = [gene]*shapes count # population = [spieces] def __init__(self, polygons, material_width): self.population = [] self.genes_count = len(polygons) self.polygons = polygons self.width = material_width self.mutation_factor = 0.1 self.order_mutate_factor = 1. self.move_mutate_factor = 1. def add_random_species(self,count): for i in range(count): specimen = [] order = range(self.genes_count) random.shuffle(order) for j in order: specimen += [ [j, random.random(), random.random()] ] self.population += [ [None,specimen] ] def species_distance2(self,sp1,sp2) : # retun distance, each component is normalized s = 0 for j in range(self.genes_count) : s += ((sp1[j][0]-sp2[j][0])/self.genes_count)**2 + (( sp1[j][1]-sp2[j][1]))**2 + ((sp1[j][2]-sp2[j][2]))**2 return s def similarity(self,sp1,top) : # Define similarity as a simple distance between two points in len(gene)*len(spiece) -th dimentions # for sp2 in top_spieces sum(|sp1-sp2|)/top_count sim = 0 for sp2 in top : sim += math.sqrt(species_distance2(sp1,sp2[1])) return sim/len(top) def leave_top_species(self,count): self.population.sort() res = [ copy.deepcopy(self.population[0]) ] del self.population[0] for i in range(count-1) : t = [] for j in range(20) : i1 = random.randint(0,len(self.population)-1) t += [ [self.population[i1][0],i1] ] t.sort() res += [ copy.deepcopy(self.population[t[0][1]]) ] del self.population[t[0][1]] self.population = res #del self.population[0] #for c in range(count-1) : # rank = [] # for i in range(len(self.population)) : # sim = self.similarity(self.population[i][1],res) # rank += [ [self.population[i][0] / sim if sim>0 else 1e100,i] ] # rank.sort() # res += [ copy.deepcopy(self.population[rank[0][1]]) ] # print_(rank[0],self.population[rank[0][1]][0]) # print_(res[-1]) # del self.population[rank[0][1]] self.population = res def populate_species(self,count, parent_count): self.population.sort() self.inc = 0 for c in range(count): parent1 = random.randint(0,parent_count-1) parent2 = random.randint(0,parent_count-1) if parent1==parent2 : parent2 = (parent2+1) % parent_count parent1, parent2 = self.population[parent1][1], self.population[parent2][1] i1,i2 = 0, 0 genes_order = [] specimen = [ [0,0.,0.] for i in range(self.genes_count) ] self.incest_mutation_multiplyer = 1. self.incest_mutation_count_multiplyer = 1. if self.species_distance2(parent1, parent2) <= .01/self.genes_count : # OMG it's a incest :O!!! # Damn you bastards! self.inc +=1 self.incest_mutation_multiplyer = 2. self.incest_mutation_count_multiplyer = 2. else : pass # if random.random()<.01 : print_(self.species_distance2(parent1, parent2)) start_gene = random.randint(0,self.genes_count) end_gene = (max(1,random.randint(0,self.genes_count),int(self.genes_count/4))+start_gene) % self.genes_count if end_gene self.options.in_out_path_radius*3/2*math.pi : self.error("In-out len is to big for in-out radius will cropp it to be r*3/2*pi!", "warning") if self.selected_paths == {} and self.options.auto_select_paths: self.selected_paths = self.paths self.error(_("No paths are selected! Trying to work on all available paths."),"warning") if self.selected_paths == {}: self.error(_("Noting is selected. Please select something."),"warning") a = self.options.plasma_prepare_corners_tolerance corner_tolerance = cross([1.,0.], [math.cos(a),math.sin(a)]) for layer in self.layers : if layer in self.selected_paths : max_dist = self.transform_scalar(self.options.in_out_path_point_max_dist, layer, reverse=True) l = self.transform_scalar(self.options.in_out_path_len, layer, reverse=True) plasma_l = self.transform_scalar(self.options.plasma_prepare_corners_distance, layer, reverse=True) r = self.transform_scalar(self.options.in_out_path_radius, layer, reverse=True) l = min(l,r*3/2*math.pi) for path in self.selected_paths[layer]: csp = self.apply_transforms( path, cubicsuperpath.parsePath(path.get("d")) ) csp = csp_remove_zerro_segments(csp) res = [] for subpath in csp : # Find closes point to in-out reference point # If subpath is open skip this step if self.options.in_out_path : # split and reverse path for further add in-out points if point_to_point_d2(subpath[0][1], subpath[-1][1]) < 1.e-10 : d = [1e100,1,1,1.] for p in self.in_out_reference_points : d1 = csp_to_point_distance([subpath], p, dist_bounds = [0,max_dist], tolerance=.01) if d1[0] < d[0] : d = d1[:] p_ = p if d[0] < max_dist**2 : # Lets find is there any angles near this point to put in-out path in # the angle if it's possible # remove last node to make iterations easier subpath[0][0] = subpath[-1][0] del subpath[-1] max_cross = [-1e100, None] for j in range(len(subpath)) : sp1,sp2,sp3 = subpath[j-2],subpath[j-1],subpath[j] if point_to_point_d2(sp2[1],p_)corner_tolerance : # there's an angle near the point j = max_cross[1] if j<0 : j -= 1 if j!=0 : subpath = csp_concat_subpaths(subpath[j:],subpath[:j+1]) else : # have to cut path's segment d,i,j,t = d sp1,sp2,sp3 = csp_split(subpath[j-1],subpath[j],t) subpath = csp_concat_subpaths([sp2,sp3], subpath[j:], subpath[:j], [sp1,sp2]) if self.options.plasma_prepare_corners : # prepare corners # find corners and add some nodes # corner at path's start/end is ignored res_ = [subpath[0]] for sp2, sp3 in zip(subpath[1:],subpath[2:]) : sp1 = res_[-1] s1,s2 = csp_normalized_slope(sp1,sp2,1.), csp_normalized_slope(sp2,sp3,0.) if cross(s1,s2) > corner_tolerance : # got a corner to process S1,S2 = P(s1),P(s2) N = (S1-S2).unit()*plasma_l SP2= P(sp2[1]) P1 = (SP2 + N) res_ += [ [sp2[0],sp2[1], (SP2+S1*plasma_l).to_list() ], [ (P1-N.ccw()/2 ).to_list(), P1.to_list(), (P1+N.ccw()/2).to_list()], [(SP2-S2*plasma_l).to_list(), sp2[1],sp2[2]] ] else: res_ += [sp2] res_ += [sp3] subpath = res_ if self.options.in_out_path : # finally add let's add in-out paths... subpath = csp_concat_subpaths( add_func(subpath[0],subpath[1],False,l,r), subpath, add_func(subpath[-2],subpath[-1],True,l,r) ) res += [ subpath ] if self.options.in_out_path_replace_original_path : path.set("d", cubicsuperpath.formatPath( self.apply_transforms(path,res,True) )) else: draw_csp(res, width=1, style=styles["in_out_path_style"] ) ################################################################################ ### Arrangement: arranges paths by givven params ### TODO move it to the bottom ################################################################################ def arrangement(self) : paths = self.selected_paths surface = Polygon() polygons = [] time_ = time.time() print_("Arrangement start at %s"%(time_)) original_paths = [] for layer in self.layers : if layer in paths : for path in paths[layer] : csp = cubicsuperpath.parsePath(path.get("d")) polygon = Polygon() for subpath in csp : for sp1, sp2 in zip(subpath,subpath[1:]) : polygon.add([csp_segment_convex_hull(sp1,sp2)]) #print_("Redused edges count from", sum([len(poly) for poly in polygon.polygon ]) ) polygon.hull() original_paths += [path] polygons += [polygon] print_("Paths hull computed in %s sec."%(time.time()-time_)) print_("Got %s polygons having average %s edges each."% ( len(polygons), float(sum([ sum([len(poly) for poly in polygon.polygon]) for polygon in polygons ])) / len(polygons) ) ) time_ = time.time() # material_width = self.options.arrangement_material_width # population = Arangement_Genetic(polygons, material_width) # population.add_random_species(1) # population.test_population_centroid() ## return material_width = self.options.arrangement_material_width population = Arangement_Genetic(polygons, material_width) print_("Genetic algorithm start at %s"%(time_)) start_time = time.time() time_ = time.time() population.add_random_species(50) #population.test(population.test_spiece_centroid) print_("Initial population done in %s"%(time.time()-time_)) time_ = time.time() pop = copy.deepcopy(population) population_count = self.options.arrangement_population_count last_champ = -1 champions_count = 0 for i in range(population_count): population.leave_top_species(20) population.move_mutation_multiplier = random.random()/2 population.order_mutation_factor = .2 population.move_mutation_factor = 1. population.mutation_genes_count = [1,2] population.populate_species(250, 20) print_("Populate done at %s"%(time.time()-time_)) """ randomize = i%100 < 40 if i%100 < 40 : population.add_random_species(250) if 40<= i%100 < 100 : population.mutation_genes_count = [1,max(2,int(population.genes_count/4))] #[1,max(2,int(population.genes_count/2))] if 40<=i%100<60 else [1,max(2,int(population.genes_count/10))] population.move_mutation_multiplier = 1. if 40<=i%100<80 else .1 population.move_mutation_factor = (-(i%100)/30+10/3) if 50<=i%100<100 else .5 population.order_mutation_factor = 1./(i%100-79) if 80<=i%100<100 else 1. population.populate_species(250, 10) """ if self.options.arrangement_inline_test : population.test_inline() else: population.test(population.test_spiece_centroid) print_("Test done at %s"%(time.time()-time_)) draw_new_champ = False print_() if population.population[0][0]!= last_champ : draw_new_champ = True improve = last_champ-population.population[0][0] last_champ = population.population[0][0]*1 print_("Cicle %s done in %s"%(i,time.time()-time_)) time_ = time.time() print_("%s incests been found"%population.inc) print_() if i == 0 or i == population_count-1 or draw_new_champ : colors = ["blue"] surface = population.test_spiece_centroid(population.population[0][1]) b = surface.bounds() x,y = 400* (champions_count%10), 700*int(champions_count/10) surface.move(x-b[0],y-b[1]) surface.draw(width=2, color=colors[0]) draw_text("Step = %s\nSquare = %f\nSquare improvement = %f\nTime from start = %f"%(i,(b[2]-b[0])*(b[3]-b[1]),improve,time.time()-start_time),x,y-50) champions_count += 1 """ spiece = population.population[0][1] poly = Polygon(copy.deepcopy(population.polygons[spiece[0][0]].polygon)) poly.rotate(spiece[0][2]*math.pi2) surface = Polygon(poly.polygon) poly.draw(width = 2, color= "Violet") for p in spiece[1:] : poly = Polygon(copy.deepcopy(population.polygons[p[0]].polygon)) poly.rotate(p[2]*math.pi2) direction = [math.cos(p[1]*math.pi2), -math.sin(p[1]*math.pi2)] normalize(direction) c = surface.centroid() c1 = poly.centroid() poly.move(c[0]-c1[0]-direction[0]*400,c[1]-c1[1]-direction[1]*400) c = surface.centroid() c1 = poly.centroid() poly.draw(width = 5, color= "Violet") draw_pointer(c+c1,"Green","line") direction = normalize(direction) sin,cos = direction[0], direction[1] poly.rotate_(-sin,cos) surface.rotate_(-sin,cos) # poly.draw(color = "Violet",width=4) surface.draw(color = "Orange",width=4) poly.rotate_(sin,cos) surface.rotate_(sin,cos) poly.drop_into_direction(direction,surface) surface.add(poly) """ # Now we'll need apply transforms to original paths def __init__(self): inkex.Effect.__init__(self) self.OptionParser.add_option("-d", "--directory", action="store", type="string", dest="directory", default="/home/", help="Directory for gcode file") self.OptionParser.add_option("-f", "--filename", action="store", type="string", dest="file", default="-1.0", help="File name") self.OptionParser.add_option("", "--add-numeric-suffix-to-filename", action="store", type="inkbool", dest="add_numeric_suffix_to_filename", default=True,help="Add numeric suffix to filename") self.OptionParser.add_option("", "--Zscale", action="store", type="float", dest="Zscale", default="1.0", help="Scale factor Z") self.OptionParser.add_option("", "--Zoffset", action="store", type="float", dest="Zoffset", default="0.0", help="Offset along Z") self.OptionParser.add_option("-s", "--Zsafe", action="store", type="float", dest="Zsafe", default="0.5", help="Z above all obstacles") self.OptionParser.add_option("-z", "--Zsurface", action="store", type="float", dest="Zsurface", default="0.0", help="Z of the surface") self.OptionParser.add_option("-c", "--Zdepth", action="store", type="float", dest="Zdepth", default="-0.125", help="Z depth of cut") self.OptionParser.add_option("", "--Zstep", action="store", type="float", dest="Zstep", default="-0.125", help="Z step of cutting") self.OptionParser.add_option("-p", "--feed", action="store", type="float", dest="feed", default="4.0", help="Feed rate in unit/min") self.OptionParser.add_option("", "--biarc-tolerance", action="store", type="float", dest="biarc_tolerance", default="1", help="Tolerance used when calculating biarc interpolation.") self.OptionParser.add_option("", "--biarc-max-split-depth", action="store", type="int", dest="biarc_max_split_depth", default="4", help="Defines maximum depth of splitting while approximating using biarcs.") self.OptionParser.add_option("", "--path-to-gcode-order", action="store", type="string", dest="path_to_gcode_order", default="path by path", help="Defines cutting order path by path or layer by layer.") self.OptionParser.add_option("", "--path-to-gcode-depth-function",action="store", type="string", dest="path_to_gcode_depth_function", default="zd", help="Path to gcode depth function.") self.OptionParser.add_option("", "--path-to-gcode-sort-paths", action="store", type="inkbool", dest="path_to_gcode_sort_paths", default=True, help="Sort paths to reduse rapid distance.") self.OptionParser.add_option("", "--comment-gcode", action="store", type="string", dest="comment_gcode", default="", help="Comment Gcode") self.OptionParser.add_option("", "--comment-gcode-from-properties",action="store", type="inkbool", dest="comment_gcode_from_properties", default=False,help="Get additional comments from Object Properties") self.OptionParser.add_option("", "--tool-diameter", action="store", type="float", dest="tool_diameter", default="3", help="Tool diameter used for area cutting") self.OptionParser.add_option("", "--max-area-curves", action="store", type="int", dest="max_area_curves", default="100", help="Maximum area curves for each area") self.OptionParser.add_option("", "--area-inkscape-radius", action="store", type="float", dest="area_inkscape_radius", default="0", help="Area curves overlaping (depends on tool diameter [0,0.9])") self.OptionParser.add_option("", "--area-tool-overlap", action="store", type="float", dest="area_tool_overlap", default="-10", help="Radius for preparing curves using inkscape") self.OptionParser.add_option("", "--unit", action="store", type="string", dest="unit", default="G21 (All units in mm)", help="Units") self.OptionParser.add_option("", "--active-tab", action="store", type="string", dest="active_tab", default="", help="Defines which tab is active") self.OptionParser.add_option("", "--area-fill-angle", action="store", type="float", dest="area_fill_angle", default="0", help="Fill area with lines heading this angle") self.OptionParser.add_option("", "--area-fill-shift", action="store", type="float", dest="area_fill_shift", default="0", help="Shift the lines by tool d * shift") self.OptionParser.add_option("", "--area-fill-method", action="store", type="string", dest="area_fill_method", default="zig-zag", help="Filling method either zig-zag or spiral") self.OptionParser.add_option("", "--area-find-artefacts-diameter",action="store", type="float", dest="area_find_artefacts_diameter", default="1", help="Artefacts seeking radius") self.OptionParser.add_option("", "--area-find-artefacts-action", action="store", type="string", dest="area_find_artefacts_action", default="mark with an arrow", help="Artefacts action type") self.OptionParser.add_option("", "--auto_select_paths", action="store", type="inkbool", dest="auto_select_paths", default=True, help="Select all paths if nothing is selected.") self.OptionParser.add_option("", "--loft-distances", action="store", type="string", dest="loft_distances", default="10", help="Distances between paths.") self.OptionParser.add_option("", "--loft-direction", action="store", type="string", dest="loft_direction", default="crosswise", help="Direction of loft's interpolation.") self.OptionParser.add_option("", "--loft-interpolation-degree", action="store", type="float", dest="loft_interpolation_degree", default="2", help="Which interpolation use to loft the paths smooth interpolation or staright.") self.OptionParser.add_option("", "--min-arc-radius", action="store", type="float", dest="min_arc_radius", default=".1", help="All arc having radius less than minimum will be considered as straight line") self.OptionParser.add_option("", "--engraving-sharp-angle-tollerance",action="store", type="float", dest="engraving_sharp_angle_tollerance", default="150", help="All angles thar are less than engraving-sharp-angle-tollerance will be thought sharp") self.OptionParser.add_option("", "--engraving-max-dist", action="store", type="float", dest="engraving_max_dist", default="10", help="Distanse from original path where engraving is not needed (usualy it's cutting tool diameter)") self.OptionParser.add_option("", "--engraving-newton-iterations", action="store", type="int", dest="engraving_newton_iterations", default="4", help="Number of sample points used to calculate distance") self.OptionParser.add_option("", "--engraving-draw-calculation-paths",action="store", type="inkbool", dest="engraving_draw_calculation_paths", default=False, help="Draw additional graphics to debug engraving path") self.OptionParser.add_option("", "--engraving-cutter-shape-function",action="store", type="string", dest="engraving_cutter_shape_function", default="w", help="Cutter shape function z(w). Ex. cone: w. ") self.OptionParser.add_option("", "--lathe-width", action="store", type="float", dest="lathe_width", default=10., help="Lathe width") self.OptionParser.add_option("", "--lathe-fine-cut-width", action="store", type="float", dest="lathe_fine_cut_width", default=1., help="Fine cut width") self.OptionParser.add_option("", "--lathe-fine-cut-count", action="store", type="int", dest="lathe_fine_cut_count", default=1., help="Fine cut count") self.OptionParser.add_option("", "--lathe-create-fine-cut-using", action="store", type="string", dest="lathe_create_fine_cut_using", default="Move path", help="Create fine cut using") self.OptionParser.add_option("", "--lathe-x-axis-remap", action="store", type="string", dest="lathe_x_axis_remap", default="X", help="Lathe X axis remap") self.OptionParser.add_option("", "--lathe-z-axis-remap", action="store", type="string", dest="lathe_z_axis_remap", default="Z", help="Lathe Z axis remap") self.OptionParser.add_option("", "--lathe-rectangular-cutter-width",action="store", type="float", dest="lathe_rectangular_cutter_width", default="4", help="Rectangular cutter width") self.OptionParser.add_option("", "--create-log", action="store", type="inkbool", dest="log_create_log", default=False, help="Create log files") self.OptionParser.add_option("", "--log-filename", action="store", type="string", dest="log_filename", default='', help="Create log files") self.OptionParser.add_option("", "--orientation-points-count", action="store", type="string", dest="orientation_points_count", default="2", help="Orientation points count") self.OptionParser.add_option("", "--tools-library-type", action="store", type="string", dest="tools_library_type", default='cylinder cutter', help="Create tools definition") self.OptionParser.add_option("", "--dxfpoints-action", action="store", type="string", dest="dxfpoints_action", default='replace', help="dxfpoint sign toggle") self.OptionParser.add_option("", "--help-language", action="store", type="string", dest="help_language", default='http://www.cnc-club.ru/forum/viewtopic.php?f=33&t=35', help="Open help page in webbrowser.") self.OptionParser.add_option("", "--offset-radius", action="store", type="float", dest="offset_radius", default=10., help="Offset radius") self.OptionParser.add_option("", "--offset-step", action="store", type="float", dest="offset_step", default=10., help="Offset step") self.OptionParser.add_option("", "--offset-draw-clippend-path", action="store", type="inkbool", dest="offset_draw_clippend_path", default=False, help="Draw clipped path") self.OptionParser.add_option("", "--offset-just-get-distance", action="store", type="inkbool", dest="offset_just_get_distance", default=False, help="Don't do offset just get distance") self.OptionParser.add_option("", "--arrangement-material-width", action="store", type="float", dest="arrangement_material_width", default=500, help="Materials width for arrangement") self.OptionParser.add_option("", "--arrangement-population-count",action="store", type="int", dest="arrangement_population_count", default=100, help="Genetic algorithm populations count") self.OptionParser.add_option("", "--arrangement-inline-test", action="store", type="inkbool", dest="arrangement_inline_test", default=False, help="Use C-inline test (some additional packets will be needed)") self.OptionParser.add_option("", "--postprocessor", action="store", type="string", dest="postprocessor", default='', help="Postprocessor command.") self.OptionParser.add_option("", "--postprocessor-custom", action="store", type="string", dest="postprocessor_custom", default='', help="Postprocessor custom command.") self.OptionParser.add_option("", "--graffiti-max-seg-length", action="store", type="float", dest="graffiti_max_seg_length", default=1., help="Graffiti maximum segment length.") self.OptionParser.add_option("", "--graffiti-min-radius", action="store", type="float", dest="graffiti_min_radius", default=10., help="Graffiti minimal connector's radius.") self.OptionParser.add_option("", "--graffiti-start-pos", action="store", type="string", dest="graffiti_start_pos", default="(0;0)", help="Graffiti Start position (x;y).") self.OptionParser.add_option("", "--graffiti-create-linearization-preview", action="store", type="inkbool", dest="graffiti_create_linearization_preview", default=True, help="Graffiti create linearization preview.") self.OptionParser.add_option("", "--graffiti-create-preview", action="store", type="inkbool", dest="graffiti_create_preview", default=True, help="Graffiti create preview.") self.OptionParser.add_option("", "--graffiti-preview-size", action="store", type="int", dest="graffiti_preview_size", default=800, help="Graffiti preview's size.") self.OptionParser.add_option("", "--graffiti-preview-emmit", action="store", type="int", dest="graffiti_preview_emmit", default=800, help="Preview's paint emmit (pts/s).") self.OptionParser.add_option("", "--in-out-path", action="store", type="inkbool", dest="in_out_path", default=True, help="Create in-out paths") self.OptionParser.add_option("", "--in-out-path-do-not-add-reference-point", action="store", type="inkbool", dest="in_out_path_do_not_add_reference_point", default=False, help="Just add reference in-out point") self.OptionParser.add_option("", "--in-out-path-point-max-dist", action="store", type="float", dest="in_out_path_point_max_dist", default=10., help="In-out path max distance to reference point") self.OptionParser.add_option("", "--in-out-path-type", action="store", type="string", dest="in_out_path_type", default="Round", help="In-out path type") self.OptionParser.add_option("", "--in-out-path-len", action="store", type="float", dest="in_out_path_len", default=10., help="In-out path length") self.OptionParser.add_option("", "--in-out-path-replace-original-path",action="store", type="inkbool", dest="in_out_path_replace_original_path", default=False, help="Replace original path") self.OptionParser.add_option("", "--in-out-path-radius", action="store", type="float", dest="in_out_path_radius", default=10., help="In-out path radius for round path") self.OptionParser.add_option("", "--plasma-prepare-corners", action="store", type="inkbool", dest="plasma_prepare_corners", default=True, help="Prepare corners") self.OptionParser.add_option("", "--plasma-prepare-corners-distance", action="store", type="float", dest="plasma_prepare_corners_distance", default=10.,help="Stepout distance for corners") self.OptionParser.add_option("", "--plasma-prepare-corners-tolerance", action="store", type="float", dest="plasma_prepare_corners_tolerance", default=10.,help="Maximum angle for corner (0-180 deg)") self.default_tool = { "name": "Default tool", "id": "default tool", "diameter":10., "shape": "10", "penetration angle":90., "penetration feed":100., "depth step":1., "feed":400., "in trajectotry":"", "out trajectotry":"", "gcode before path":"", "gcode after path":"", "sog":"", "spinlde rpm":"", "CW or CCW":"", "tool change gcode":" ", "4th axis meaning": " ", "4th axis scale": 1., "4th axis offset": 0., "passing feed":"800", "fine feed":"800", } self.tools_field_order = [ 'name', 'id', 'diameter', 'feed', 'shape', 'penetration angle', 'penetration feed', "passing feed", 'depth step', "in trajectotry", "out trajectotry", "gcode before path", "gcode after path", "sog", "spinlde rpm", "CW or CCW", "tool change gcode", ] def parse_curve(self, p, layer, w = None, f = None): c = [] if len(p)==0 : return [] p = self.transform_csp(p, layer) ### Sort to reduce Rapid distance k = range(1,len(p)) keys = [0] while len(k)>0: end = p[keys[-1]][-1][1] dist = None for i in range(len(k)): start = p[k[i]][0][1] dist = max( ( -( ( end[0]-start[0])**2+(end[1]-start[1])**2 ) ,i) , dist ) keys += [k[dist[1]]] del k[dist[1]] for k in keys: subpath = p[k] c += [ [ [subpath[0][1][0],subpath[0][1][1]] , 'move', 0, 0] ] for i in range(1,len(subpath)): sp1 = [ [subpath[i-1][j][0], subpath[i-1][j][1]] for j in range(3)] sp2 = [ [subpath[i ][j][0], subpath[i ][j][1]] for j in range(3)] c += biarc(sp1,sp2,0,0) if w==None else biarc(sp1,sp2,-f(w[k][i-1]),-f(w[k][i])) # l1 = biarc(sp1,sp2,0,0) if w==None else biarc(sp1,sp2,-f(w[k][i-1]),-f(w[k][i])) # print_((-f(w[k][i-1]),-f(w[k][i]), [i1[5] for i1 in l1]) ) c += [ [ [subpath[-1][1][0],subpath[-1][1][1]] ,'end',0,0] ] return c ################################################################################ ### Draw csp ################################################################################ def draw_csp(self, csp, layer=None, group=None, fill='none', stroke='#178ade', width=0.354, style=None): if layer!=None : csp = self.transform_csp(csp,layer,reverse=True) if group==None and layer==None: group = self.document.getroot() elif group==None and layer!=None : group = layer csp = self.apply_transforms(group,csp, reverse=True) if style!=None : return draw_csp(csp, group=group, style=style) else : return draw_csp(csp, group=group, fill=fill, stroke=stroke, width=width) def draw_curve(self, curve, layer, group=None, style=styles["biarc_style"]): self.set_markers() for i in [0,1]: style['biarc%s_r'%i] = simplestyle.parseStyle(style['biarc%s'%i]) style['biarc%s_r'%i]["marker-start"] = "url(#DrawCurveMarker_r)" del(style['biarc%s_r'%i]["marker-end"]) style['biarc%s_r'%i] = simplestyle.formatStyle(style['biarc%s_r'%i]) if group==None: if "preview_groups" not in dir(self) : self.preview_groups = { layer: inkex.etree.SubElement( self.layers[min(1,len(self.layers)-1)], inkex.addNS('g','svg'), {"gcodetools": "Preview group"} ) } elif layer not in self.preview_groups : self.preview_groups[layer] = inkex.etree.SubElement( self.layers[min(1,len(self.layers)-1)], inkex.addNS('g','svg'), {"gcodetools": "Preview group"} ) group = self.preview_groups[layer] s, arcn = '', 0 transform = self.get_transforms(group) if transform != [] : transform = self.reverse_transform(transform) transform = simpletransform.formatTransform(transform) a,b,c = [0.,0.], [1.,0.], [0.,1.] k = (b[0]-a[0])*(c[1]-a[1])-(c[0]-a[0])*(b[1]-a[1]) a,b,c = self.transform(a, layer, True), self.transform(b, layer, True), self.transform(c, layer, True) if ((b[0]-a[0])*(c[1]-a[1])-(c[0]-a[0])*(b[1]-a[1]))*k > 0 : reverse_angle = 1 else : reverse_angle = -1 for sk in curve: si = sk[:] si[0], si[2] = self.transform(si[0], layer, True), (self.transform(si[2], layer, True) if type(si[2])==type([]) and len(si[2])==2 else si[2]) if s!='': if s[1] == 'line': attr = { 'style': style['line'], 'd':'M %s,%s L %s,%s' % (s[0][0], s[0][1], si[0][0], si[0][1]), "gcodetools": "Preview", } if transform != [] : attr["transform"] = transform inkex.etree.SubElement( group, inkex.addNS('path','svg'), attr ) elif s[1] == 'arc': arcn += 1 sp = s[0] c = s[2] s[3] = s[3]*reverse_angle a = ( (P(si[0])-P(c)).angle() - (P(s[0])-P(c)).angle() )%math.pi2 #s[3] if s[3]*a<0: if a>0: a = a-math.pi2 else: a = math.pi2+a r = math.sqrt( (sp[0]-c[0])**2 + (sp[1]-c[1])**2 ) a_st = ( math.atan2(sp[0]-c[0],- (sp[1]-c[1])) - math.pi/2 ) % (math.pi*2) st = style['biarc%s' % (arcn%2)][:] if a>0: a_end = a_st+a st = style['biarc%s'%(arcn%2)] else: a_end = a_st*1 a_st = a_st+a st = style['biarc%s_r'%(arcn%2)] attr = { 'style': st, inkex.addNS('cx','sodipodi'): str(c[0]), inkex.addNS('cy','sodipodi'): str(c[1]), inkex.addNS('rx','sodipodi'): str(r), inkex.addNS('ry','sodipodi'): str(r), inkex.addNS('start','sodipodi'): str(a_st), inkex.addNS('end','sodipodi'): str(a_end), inkex.addNS('open','sodipodi'): 'true', inkex.addNS('type','sodipodi'): 'arc', "gcodetools": "Preview", } if transform != [] : attr["transform"] = transform inkex.etree.SubElement( group, inkex.addNS('path','svg'), attr) s = si def check_dir(self): if self.options.directory[-1] not in ["/","\\"]: if "\\" in self.options.directory : self.options.directory += "\\" else : self.options.directory += "/" print_("Checking directory: '%s'"%self.options.directory) if (os.path.isdir(self.options.directory)): if (os.path.isfile(self.options.directory+'header')): f = open(self.options.directory+'header', 'r') self.header = f.read() f.close() else: self.header = defaults['header'] if (os.path.isfile(self.options.directory+'footer')): f = open(self.options.directory+'footer','r') self.footer = f.read() f.close() else: self.footer = defaults['footer'] self.header += self.options.unit + "\n" else: self.error(_("Directory does not exist! Please specify existing directory at Preferences tab!"),"error") return False if self.options.add_numeric_suffix_to_filename : dir_list = os.listdir(self.options.directory) if "." in self.options.file : r = re.match(r"^(.*)(\..*)$",self.options.file) ext = r.group(2) name = r.group(1) else: ext = "" name = self.options.file max_n = 0 for s in dir_list : r = re.match(r"^%s_0*(\d+)%s$"%(re.escape(name),re.escape(ext) ), s) if r : max_n = max(max_n,int(r.group(1))) filename = name + "_" + ( "0"*(4-len(str(max_n+1))) + str(max_n+1) ) + ext self.options.file = filename if self.options.directory[-1] not in ["/","\\"]: if "\\" in self.options.directory : self.options.directory += "\\" else : self.options.directory += "/" try: f = open(self.options.directory+self.options.file, "w") f.close() except: self.error(_("Can not write to specified file!\n%s"%(self.options.directory+self.options.file)),"error") return False return True ################################################################################ ### ### Generate Gcode ### Generates Gcode on given curve. ### ### Curve definition [start point, type = {'arc','line','move','end'}, arc center, arc angle, end point, [zstart, zend]] ### ################################################################################ def generate_gcode(self, curve, layer, depth): Zauto_scale = self.Zauto_scale[layer] tool = self.tools[layer][0] g = "" def c(c): c = [c[i] if iself.options.min_arc_radius**2: r1, r2 = (P(s[0])-P(s[2])), (P(si[0])-P(s[2])) if abs(r1.mag()-r2.mag()) < 0.001 : g += ("G02" if s[3]<0 else "G03") + c(si[0]+[ s[5][1]+depth, (s[2][0]-s[0][0]),(s[2][1]-s[0][1]) ]) + feed + axis4 + "\n" else: r = (r1.mag()+r2.mag())/2 g += ("G02" if s[3]<0 else "G03") + c(si[0]+[s[5][1]+depth]) + " R%f" % (r) + feed + axis4 + "\n" lg = 'G02' else: if tool['4th axis meaning'] == "tangent knife" : a = atan2(si[0][0]-s[0][0],si[0][1]-s[0][1]) + math.pi a = calculate_angle(a, current_a) g+="G01 A%s\n" % (a*tool['4th axis scale']+tool['4th axis offset']) current_a = a g += "G01" +c(si[0]+[s[5][1]+depth]) + feed + "\n" lg = 'G01' if si[1] == 'end': g += go_to_safe_distance + tool['gcode after path'] + "\n" return g def get_transforms(self,g): root = self.document.getroot() trans = [] while (g!=root): if 'transform' in g.keys(): t = g.get('transform') t = simpletransform.parseTransform(t) trans = simpletransform.composeTransform(t,trans) if trans != [] else t print_(trans) g=g.getparent() return trans def reverse_transform(self,transform): trans = numpy.array(transform + [[0,0,1]]) if numpy.linalg.det(trans)!=0 : trans = numpy.linalg.inv(trans).tolist()[:2] return trans else : return transform def apply_transforms(self,g,csp, reverse=False): trans = self.get_transforms(g) if trans != []: if not reverse : simpletransform.applyTransformToPath(trans, csp) else : simpletransform.applyTransformToPath(self.reverse_transform(trans), csp) return csp def transform_scalar(self,x,layer,reverse=False): return self.transform([x,0],layer,reverse)[0] - self.transform([0,0],layer,reverse)[0] def transform(self,source_point, layer, reverse=False): if layer not in self.transform_matrix: for i in range(self.layers.index(layer),-1,-1): if self.layers[i] in self.orientation_points : break if self.layers[i] not in self.orientation_points : self.error(_("Orientation points for '%s' layer have not been found! Please add orientation points using Orientation tab!") % layer.get(inkex.addNS('label','inkscape')),"no_orientation_points") elif self.layers[i] in self.transform_matrix : self.transform_matrix[layer] = self.transform_matrix[self.layers[i]] self.Zcoordinates[layer] = self.Zcoordinates[self.layers[i]] else : orientation_layer = self.layers[i] if len(self.orientation_points[orientation_layer])>1 : self.error(_("There are more than one orientation point groups in '%s' layer") % orientation_layer.get(inkex.addNS('label','inkscape')),"more_than_one_orientation_point_groups") points = self.orientation_points[orientation_layer][0] if len(points)==2: points += [ [ [(points[1][0][1]-points[0][0][1])+points[0][0][0], -(points[1][0][0]-points[0][0][0])+points[0][0][1]], [-(points[1][1][1]-points[0][1][1])+points[0][1][0], points[1][1][0]-points[0][1][0]+points[0][1][1]] ] ] if len(points)==3: print_("Layer '%s' Orientation points: " % orientation_layer.get(inkex.addNS('label','inkscape'))) for point in points: print_(point) # Zcoordinates definition taken from Orientatnion point 1 and 2 self.Zcoordinates[layer] = [max(points[0][1][2],points[1][1][2]), min(points[0][1][2],points[1][1][2])] matrix = numpy.array([ [points[0][0][0], points[0][0][1], 1, 0, 0, 0, 0, 0, 0], [0, 0, 0, points[0][0][0], points[0][0][1], 1, 0, 0, 0], [0, 0, 0, 0, 0, 0, points[0][0][0], points[0][0][1], 1], [points[1][0][0], points[1][0][1], 1, 0, 0, 0, 0, 0, 0], [0, 0, 0, points[1][0][0], points[1][0][1], 1, 0, 0, 0], [0, 0, 0, 0, 0, 0, points[1][0][0], points[1][0][1], 1], [points[2][0][0], points[2][0][1], 1, 0, 0, 0, 0, 0, 0], [0, 0, 0, points[2][0][0], points[2][0][1], 1, 0, 0, 0], [0, 0, 0, 0, 0, 0, points[2][0][0], points[2][0][1], 1] ]) if numpy.linalg.det(matrix)!=0 : m = numpy.linalg.solve(matrix, numpy.array( [[points[0][1][0]], [points[0][1][1]], [1], [points[1][1][0]], [points[1][1][1]], [1], [points[2][1][0]], [points[2][1][1]], [1]] ) ).tolist() self.transform_matrix[layer] = [[m[j*3+i][0] for i in range(3)] for j in range(3)] else : self.error(_("Orientation points are wrong! (if there are two orientation points they should not be the same. If there are three orientation points they should not be in a straight line.)"),"wrong_orientation_points") else : self.error(_("Orientation points are wrong! (if there are two orientation points they should not be the same. If there are three orientation points they should not be in a straight line.)"),"wrong_orientation_points") self.transform_matrix_reverse[layer] = numpy.linalg.inv(self.transform_matrix[layer]).tolist() print_("\n Layer '%s' transformation matrixes:" % layer.get(inkex.addNS('label','inkscape')) ) print_(self.transform_matrix) print_(self.transform_matrix_reverse) ###self.Zauto_scale[layer] = math.sqrt( (self.transform_matrix[layer][0][0]**2 + self.transform_matrix[layer][1][1]**2)/2 ) ### Zautoscale is absolete self.Zauto_scale[layer] = 1 print_("Z automatic scale = %s (computed according orientation points)" % self.Zauto_scale[layer]) x,y = source_point[0], source_point[1] if not reverse : t = self.transform_matrix[layer] else : t = self.transform_matrix_reverse[layer] return [t[0][0]*x+t[0][1]*y+t[0][2], t[1][0]*x+t[1][1]*y+t[1][2]] def transform_csp(self, csp_, layer, reverse = False): csp = [ [ [csp_[i][j][0][:],csp_[i][j][1][:],csp_[i][j][2][:]] for j in range(len(csp_[i])) ] for i in range(len(csp_)) ] for i in xrange(len(csp)): for j in xrange(len(csp[i])): for k in xrange(len(csp[i][j])): csp[i][j][k] = self.transform(csp[i][j][k],layer, reverse) return csp ################################################################################ ### Errors handling function, notes are just printed into Logfile, ### warnings are printed into log file and warning message is displayed but ### extension continues working, errors causes log and execution is halted ### Notes, warnings adn errors could be assigned to space or comma or dot ### sepparated strings (case is ignoreg). ################################################################################ def error(self, s, type_= "Warning"): notes = "Note " warnings = """ Warning tools_warning orientation_warning bad_orientation_points_in_some_layers more_than_one_orientation_point_groups more_than_one_tool orientation_have_not_been_defined tool_have_not_been_defined selection_does_not_contain_paths selection_does_not_contain_paths_will_take_all selection_is_empty_will_comupe_drawing selection_contains_objects_that_are_not_paths Continue """ errors = """ Error wrong_orientation_points area_tools_diameter_error no_tool_error active_layer_already_has_tool active_layer_already_has_orientation_points """ s = str(s) if type_.lower() in re.split("[\s\n,\.]+", errors.lower()) : print_(s) inkex.errormsg(s+"\n") sys.exit() elif type_.lower() in re.split("[\s\n,\.]+", warnings.lower()) : print_(s) inkex.errormsg(s+"\n") elif type_.lower() in re.split("[\s\n,\.]+", notes.lower()) : print_(s) else : print_(s) inkex.errormsg(s) sys.exit() ################################################################################ ### Set markers ################################################################################ def set_markers(self) : self.get_defs() # Add marker to defs if it doesnot exists if "CheckToolsAndOPMarker" not in self.defs : defs = inkex.etree.SubElement( self.document.getroot(), inkex.addNS("defs","svg")) marker = inkex.etree.SubElement( defs, inkex.addNS("marker","svg"), {"id":"CheckToolsAndOPMarker","orient":"auto","refX":"-4","refY":"-1.687441","style":"overflow:visible"}) inkex.etree.SubElement( marker, inkex.addNS("path","svg"), { "d":" m -4.588864,-1.687441 0.0,0.0 L -9.177728,0.0 c 0.73311,-0.996261 0.728882,-2.359329 0.0,-3.374882", "style": "fill:#000044; fill-rule:evenodd;stroke:none;" } ) if "DrawCurveMarker" not in self.defs : defs = inkex.etree.SubElement( self.document.getroot(), inkex.addNS("defs","svg")) marker = inkex.etree.SubElement( defs, inkex.addNS("marker","svg"), {"id":"DrawCurveMarker","orient":"auto","refX":"-4","refY":"-1.687441","style":"overflow:visible"}) inkex.etree.SubElement( marker, inkex.addNS("path","svg"), { "d":"m -4.588864,-1.687441 0.0,0.0 L -9.177728,0.0 c 0.73311,-0.996261 0.728882,-2.359329 0.0,-3.374882", "style": "fill:#000044; fill-rule:evenodd;stroke:none;" } ) if "DrawCurveMarker_r" not in self.defs : defs = inkex.etree.SubElement( self.document.getroot(), inkex.addNS("defs","svg")) marker = inkex.etree.SubElement( defs, inkex.addNS("marker","svg"), {"id":"DrawCurveMarker_r","orient":"auto","refX":"4","refY":"-1.687441","style":"overflow:visible"}) inkex.etree.SubElement( marker, inkex.addNS("path","svg"), { "d":"m 4.588864,-1.687441 0.0,0.0 L 9.177728,0.0 c -0.73311,-0.996261 -0.728882,-2.359329 0.0,-3.374882", "style": "fill:#000044; fill-rule:evenodd;stroke:none;" } ) if "InOutPathMarker" not in self.defs : defs = inkex.etree.SubElement( self.document.getroot(), inkex.addNS("defs","svg")) marker = inkex.etree.SubElement( defs, inkex.addNS("marker","svg"), {"id":"InOutPathMarker","orient":"auto","refX":"-4","refY":"-1.687441","style":"overflow:visible"}) inkex.etree.SubElement( marker, inkex.addNS("path","svg"), { "d":"m -4.588864,-1.687441 0.0,0.0 L -9.177728,0.0 c 0.73311,-0.996261 0.728882,-2.359329 0.0,-3.374882", "style": "fill:#0072a7; fill-rule:evenodd;stroke:none;" } ) ################################################################################ ### Get defs from svg ################################################################################ def get_defs(self) : self.defs = {} def recursive(g) : for i in g: if i.tag == inkex.addNS("defs","svg") : for j in i: self.defs[j.get("id")] = i if i.tag ==inkex.addNS("g",'svg') : recursive(i) recursive(self.document.getroot()) ################################################################################ ### ### Get Gcodetools info from the svg ### ################################################################################ def get_info(self): self.selected_paths = {} self.paths = {} self.tools = {} self.orientation_points = {} self.graffiti_reference_points = {} self.layers = [self.document.getroot()] self.Zcoordinates = {} self.transform_matrix = {} self.transform_matrix_reverse = {} self.Zauto_scale = {} self.in_out_reference_points = [] self.my3Dlayer = None def recursive_search(g, layer, selected=False): items = g.getchildren() items.reverse() for i in items: if selected: self.selected[i.get("id")] = i if i.tag == inkex.addNS("g",'svg') and i.get(inkex.addNS('groupmode','inkscape')) == 'layer': if i.get(inkex.addNS('label','inkscape')) == '3D' : self.my3Dlayer=i else : self.layers += [i] recursive_search(i,i) elif i.get('gcodetools') == "Gcodetools orientation group" : points = self.get_orientation_points(i) if points != None : self.orientation_points[layer] = self.orientation_points[layer]+[points[:]] if layer in self.orientation_points else [points[:]] print_("Found orientation points in '%s' layer: %s" % (layer.get(inkex.addNS('label','inkscape')), points)) else : self.error(_("Warning! Found bad orientation points in '%s' layer. Resulting Gcode could be corrupt!") % layer.get(inkex.addNS('label','inkscape')), "bad_orientation_points_in_some_layers") #Need to recognise old files ver 1.6.04 and earlier elif i.get("gcodetools") == "Gcodetools tool definition" or i.get("gcodetools") == "Gcodetools tool defenition" : tool = self.get_tool(i) self.tools[layer] = self.tools[layer] + [tool.copy()] if layer in self.tools else [tool.copy()] print_("Found tool in '%s' layer: %s" % (layer.get(inkex.addNS('label','inkscape')), tool)) elif i.get("gcodetools") == "Gcodetools graffiti reference point" : point = self.get_graffiti_reference_points(i) if point != [] : self.graffiti_reference_points[layer] = self.graffiti_reference_points[layer]+[point[:]] if layer in self.graffiti_reference_points else [point] else : self.error(_("Warning! Found bad graffiti reference point in '%s' layer. Resulting Gcode could be corrupt!") % layer.get(inkex.addNS('label','inkscape')), "bad_orientation_points_in_some_layers") elif i.tag == inkex.addNS('path','svg'): if "gcodetools" not in i.keys() : self.paths[layer] = self.paths[layer] + [i] if layer in self.paths else [i] if i.get("id") in self.selected : self.selected_paths[layer] = self.selected_paths[layer] + [i] if layer in self.selected_paths else [i] elif i.get("gcodetools") == "In-out reference point group" : items_ = i.getchildren() items_.reverse() for j in items_ : if j.get("gcodetools") == "In-out reference point" : self.in_out_reference_points.append( self.apply_transforms(j,cubicsuperpath.parsePath(j.get("d")))[0][0][1] ) elif i.tag == inkex.addNS("g",'svg'): recursive_search(i,layer, (i.get("id") in self.selected) ) elif i.get("id") in self.selected : # xgettext:no-pango-format self.error(_("This extension works with Paths and Dynamic Offsets and groups of them only! All other objects will be ignored!\nSolution 1: press Path->Object to path or Shift+Ctrl+C.\nSolution 2: Path->Dynamic offset or Ctrl+J.\nSolution 3: export all contours to PostScript level 2 (File->Save As->.ps) and File->Import this file."),"selection_contains_objects_that_are_not_paths") recursive_search(self.document.getroot(),self.document.getroot()) if len(self.layers) == 1 : self.error(_("Document has no layers! Add at least one layer using layers panel (Ctrl+Shift+L)"),"Error") root = self.document.getroot() if root in self.selected_paths or root in self.paths : self.error(_("Warning! There are some paths in the root of the document, but not in any layer! Using bottom-most layer for them."), "tools_warning" ) if root in self.selected_paths : if self.layers[-1] in self.selected_paths : self.selected_paths[self.layers[-1]] += self.selected_paths[root][:] else : self.selected_paths[self.layers[-1]] = self.selected_paths[root][:] del self.selected_paths[root] if root in self.paths : if self.layers[-1] in self.paths : self.paths[self.layers[-1]] += self.paths[root][:] else : self.paths[self.layers[-1]] = self.paths[root][:] del self.paths[root] def get_orientation_points(self,g): items = g.getchildren() items.reverse() p2, p3 = [], [] p = None for i in items: if i.tag == inkex.addNS("g",'svg') and i.get("gcodetools") == "Gcodetools orientation point (2 points)": p2 += [i] if i.tag == inkex.addNS("g",'svg') and i.get("gcodetools") == "Gcodetools orientation point (3 points)": p3 += [i] if len(p2)==2 : p=p2 elif len(p3)==3 : p=p3 if p==None : return None points = [] for i in p : point = [[],[]] for node in i : if node.get('gcodetools') == "Gcodetools orientation point arrow": point[0] = self.apply_transforms(node,cubicsuperpath.parsePath(node.get("d")))[0][0][1] if node.get('gcodetools') == "Gcodetools orientation point text": r = re.match(r'(?i)\s*\(\s*(-?\s*\d*(?:,|\.)*\d*)\s*;\s*(-?\s*\d*(?:,|\.)*\d*)\s*;\s*(-?\s*\d*(?:,|\.)*\d*)\s*\)\s*',get_text(node)) point[1] = [float(r.group(1)),float(r.group(2)),float(r.group(3))] if point[0]!=[] and point[1]!=[]: points += [point] if len(points)==len(p2)==2 or len(points)==len(p3)==3 : return points else : return None def get_graffiti_reference_points(self,g): point = [[], ''] for node in g : if node.get('gcodetools') == "Gcodetools graffiti reference point arrow": point[0] = self.apply_transforms(node,cubicsuperpath.parsePath(node.get("d")))[0][0][1] if node.get('gcodetools') == "Gcodetools graffiti reference point text": point[1] = get_text(node) if point[0]!=[] and point[1]!='' : return point else : return [] def get_tool(self, g): tool = self.default_tool.copy() tool["self_group"] = g for i in g: # Get parameters if i.get("gcodetools") == "Gcodetools tool background" : tool["style"] = simplestyle.parseStyle(i.get("style")) elif i.get("gcodetools") == "Gcodetools tool parameter" : key = None value = None for j in i: #need to recognise old tools from ver 1.6.04 if j.get("gcodetools") == "Gcodetools tool definition field name" or j.get("gcodetools") == "Gcodetools tool defention field name": key = get_text(j) if j.get("gcodetools") == "Gcodetools tool definition field value" or j.get("gcodetools") == "Gcodetools tool defention field value": value = get_text(j) if value == "(None)": value = "" if value == None or key == None: continue #print_("Found tool parameter '%s':'%s'" % (key,value)) if key in self.default_tool.keys() : try : tool[key] = type(self.default_tool[key])(value) except : tool[key] = self.default_tool[key] self.error(_("Warning! Tool's and default tool's parameter's (%s) types are not the same ( type('%s') != type('%s') ).") % (key, value, self.default_tool[key]), "tools_warning") else : tool[key] = value self.error(_("Warning! Tool has parameter that default tool has not ( '%s': '%s' ).") % (key, value), "tools_warning" ) return tool def set_tool(self,layer): # print_(("index(layer)=",self.layers.index(layer),"set_tool():layer=",layer,"self.tools=",self.tools)) # for l in self.layers: # print_(("l=",l)) for i in range(self.layers.index(layer),-1,-1): # print_(("processing layer",i)) if self.layers[i] in self.tools : break if self.layers[i] in self.tools : if self.layers[i] != layer : self.tools[layer] = self.tools[self.layers[i]] if len(self.tools[layer])>1 : self.error(_("Layer '%s' contains more than one tool!") % self.layers[i].get(inkex.addNS('label','inkscape')), "more_than_one_tool") return self.tools[layer] else : self.error(_("Can not find tool for '%s' layer! Please add one with Tools library tab!") % layer.get(inkex.addNS('label','inkscape')), "no_tool_error") ################################################################################ ### ### Path to Gcode ### ################################################################################ def path_to_gcode(self) : from functools import partial def get_boundaries(points): minx,miny,maxx,maxy=None,None,None,None out=[[],[],[],[]] for p in points: if minx==p[0]: out[0]+=[p] if minx==None or p[0]maxx: maxx=p[0] out[2]=[p] if maxy==p[1]: out[3]+=[p] if maxy==None or p[1]>maxy: maxy=p[1] out[3]=[p] return out def remove_duplicates(points): i=0 out=[] for p in points: for j in xrange(i,len(points)): if p==points[j]: points[j]=[None,None] if p!=[None,None]: out+=[p] i+=1 return(out) def get_way_len(points): l=0 for i in xrange(1,len(points)): l+=math.sqrt((points[i][0]-points[i-1][0])**2 + (points[i][1]-points[i-1][1])**2) return l def sort_dxfpoints(points): points=remove_duplicates(points) # print_(get_boundaries(get_boundaries(points)[2])[1]) ways=[ # l=0, d=1, r=2, u=3 [3,0], # ul [3,2], # ur [1,0], # dl [1,2], # dr [0,3], # lu [0,1], # ld [2,3], # ru [2,1], # rd ] # print_(("points=",points)) minimal_way=[] minimal_len=None minimal_way_type=None for w in ways: tpoints=points[:] cw=[] # print_(("tpoints=",tpoints)) for j in xrange(0,len(points)): p=get_boundaries(get_boundaries(tpoints)[w[0]])[w[1]] # print_(p) tpoints.remove(p[0]) cw+=p curlen = get_way_len(cw) if minimal_len==None or curlen < minimal_len: minimal_len=curlen minimal_way=cw minimal_way_type=w return minimal_way def sort_lines(lines): if len(lines) == 0 : return [] lines = [ [key]+lines[key] for key in range(len(lines))] keys = [0] end_point = lines[0][3:] print_("!!!",lines,"\n",end_point) del lines[0] while len(lines)>0: dist = [ [point_to_point_d2(end_point,lines[i][1:3]),i] for i in range(len(lines))] i = min(dist)[1] keys.append(lines[i][0]) end_point = lines[i][3:] del lines[i] return keys def sort_curves(curves): lines = [] for curve in curves: lines += [curve[0][0][0] + curve[-1][-1][0]] return sort_lines(lines) def print_dxfpoints(points): gcode="" for point in points: gcode +="(drilling dxfpoint)\nG00 Z%f\nG00 X%f Y%f\nG01 Z%f F%f\nG04 P%f\nG00 Z%f\n" % (self.options.Zsafe,point[0],point[1],self.Zcoordinates[layer][1],self.tools[layer][0]["penetration feed"],0.2,self.options.Zsafe) # print_(("got dxfpoints array=",points)) return gcode def get_path_properties(node, recursive=True, tags={inkex.addNS('desc','svg'):"Description",inkex.addNS('title','svg'):"Title"} ) : res = {} done = False root = self.document.getroot() while not done and node != root : for i in node.getchildren(): if i.tag in tags: res[tags[i.tag]] = i.text done = True node = node.getparent() return res if self.selected_paths == {} and self.options.auto_select_paths: paths=self.paths self.error(_("No paths are selected! Trying to work on all available paths."),"warning") else : paths = self.selected_paths self.check_dir() gcode = "" biarc_group = inkex.etree.SubElement( self.selected_paths.keys()[0] if len(self.selected_paths.keys())>0 else self.layers[0], inkex.addNS('g','svg') ) print_(("self.layers=",self.layers)) print_(("paths=",paths)) colors = {} for layer in self.layers : if layer in paths : print_(("layer",layer)) # transform simple path to get all var about orientation self.transform_csp([ [ [[0,0],[0,0],[0,0]], [[0,0],[0,0],[0,0]] ] ], layer) self.set_tool(layer) curves = [] dxfpoints = [] try : depth_func = eval('lambda c,d,s: ' + self.options.path_to_gcode_depth_function.strip('"')) except: self.error("Bad depth function! Enter correct function at Path to Gcode tab!") for path in paths[layer] : if "d" not in path.keys() : self.error(_("Warning: One or more paths do not have 'd' parameter, try to Ungroup (Ctrl+Shift+G) and Object to Path (Ctrl+Shift+C)!"),"selection_contains_objects_that_are_not_paths") continue csp = cubicsuperpath.parsePath(path.get("d")) csp = self.apply_transforms(path, csp) id_ = path.get("id") def set_comment(match, path): if match.group(1) in path.keys() : return path.get(match.group(1)) else: return "None" if self.options.comment_gcode != "" : comment = re.sub("\[([A-Za-z_\-\:]+)\]", partial(set_comment, path=path), self.options.comment_gcode) comment = comment.replace(":newline:","\n") comment = gcode_comment_str(comment) else: comment = "" if self.options.comment_gcode_from_properties : tags = get_path_properties(path) for tag in tags : comment += gcode_comment_str("%s: %s"%(tag,tags[tag])) style = simplestyle.parseStyle(path.get("style")) colors[id_] = simplestyle.parseColor(style['stroke'] if "stroke" in style and style['stroke']!='none' else "#000") if path.get("dxfpoint") == "1": tmp_curve=self.transform_csp(csp, layer) x=tmp_curve[0][0][0][0] y=tmp_curve[0][0][0][1] print_("got dxfpoint (scaled) at (%f,%f)" % (x,y)) dxfpoints += [[x,y]] else: zd,zs = self.Zcoordinates[layer][1], self.Zcoordinates[layer][0] c = 1. - float(sum(colors[id_]))/255/3 curves += [ [ [id_, depth_func(c,zd,zs), comment], [ self.parse_curve([subpath], layer) for subpath in csp ] ] ] # for c in curves : # print_(c) dxfpoints=sort_dxfpoints(dxfpoints) gcode+=print_dxfpoints(dxfpoints) for curve in curves : for subcurve in curve[1] : self.draw_curve(subcurve, layer) if self.options.path_to_gcode_order == 'subpath by subpath': curves_ = [] for curve in curves : curves_ += [ [curve[0],[subcurve]] for subcurve in curve[1] ] curves = curves_ self.options.path_to_gcode_order = 'path by path' if self.options.path_to_gcode_order == 'path by path': if self.options.path_to_gcode_sort_paths : keys = sort_curves( [curve[1] for curve in curves] ) else : keys = range(len(curves)) for key in keys: d = curves[key][0][1] for step in range( 0, int(math.ceil( abs((zs-d)/self.tools[layer][0]["depth step"] )) ) ): z = max(d, zs - abs(self.tools[layer][0]["depth step"]*(step+1))) gcode += gcode_comment_str("\nStart cutting path id: %s"%curves[key][0][0]) if curves[key][0][2] != "()" : gcode += curves[key][0][2] # add comment for curve in curves[key][1]: gcode += self.generate_gcode(curve, layer, z) gcode += gcode_comment_str("End cutting path id: %s\n\n"%curves[key][0][0]) else: # pass by pass mind = min( [curve[0][1] for curve in curves] ) for step in range( 0, int(math.ceil( abs((zs-mind)/self.tools[layer][0]["depth step"] )) ) ): z = zs - abs(self.tools[layer][0]["depth step"]*(step)) curves_ = [] for curve in curves: if curve[0][1] 0 but tool's diameter on '%s' layer is not!") % layer.get(inkex.addNS('label','inkscape')),"area_tools_diameter_error") for path in self.selected_paths[layer]: print_(("doing path", path.get("style"), path.get("d"))) area_group = inkex.etree.SubElement( path.getparent(), inkex.addNS('g','svg') ) d = path.get('d') print_(d) if d==None: print_("omitting non-path") self.error(_("Warning: omitting non-path"),"selection_contains_objects_that_are_not_paths") continue csp = cubicsuperpath.parsePath(d) if path.get(inkex.addNS('type','sodipodi'))!="inkscape:offset": print_("Path %s is not an offset. Preparation started." % path.get("id")) # Path is not offset. Preparation will be needed. # Finding top most point in path (min y value) min_x,min_y,min_i,min_j,min_t = csp_true_bounds(csp)[1] # Reverse path if needed. if min_y!=float("-inf") : # Move outline subpath to the begining of csp subp = csp[min_i] del csp[min_i] j = min_j # Split by the topmost point and join again if min_t in [0,1]: if min_t == 0: j=j-1 subp[-1][2], subp[0][0] = subp[-1][1], subp[0][1] subp = [ [subp[j][1], subp[j][1], subp[j][2]] ] + subp[j+1:] + subp[:j] + [ [subp[j][0], subp[j][1], subp[j][1]] ] else: sp1,sp2,sp3 = csp_split(subp[j-1],subp[j],min_t) subp[-1][2], subp[0][0] = subp[-1][1], subp[0][1] subp = [ [ sp2[1], sp2[1],sp2[2] ] ] + [sp3] + subp[j+1:] + subp[:j-1] + [sp1] + [[ sp2[0], sp2[1],sp2[1] ]] csp = [subp] + csp # reverse path if needed if csp_subpath_ccw(csp[0]) : for i in range(len(csp)): n = [] for j in csp[i]: n = [ [j[2][:],j[1][:],j[0][:]] ] + n csp[i] = n[:] d = cubicsuperpath.formatPath(csp) print_(("original d=",d)) d = re.sub(r'(?i)(m[^mz]+)',r'\1 Z ',d) d = re.sub(r'(?i)\s*z\s*z\s*',r' Z ',d) d = re.sub(r'(?i)\s*([A-Za-z])\s*',r' \1 ',d) print_(("formatted d=",d)) # scale = sqrt(Xscale**2 + Yscale**2) / sqrt(1**2 + 1**2) p0 = self.transform([0,0],layer) p1 = self.transform([0,1],layer) scale = (P(p0)-P(p1)).mag() if scale == 0 : scale = 1. else : scale = 1./scale print_(scale) tool_d = self.tools[layer][0]['diameter']*scale r = self.options.area_inkscape_radius * scale sign=1 if r>0 else -1 print_("Tool diameter = %s, r = %s" % (tool_d, r)) # avoiding infinite loops if self.options.area_tool_overlap>0.9 : self.options.area_tool_overlap = .9 for i in range(self.options.max_area_curves): radius = - tool_d * (i*(1-self.options.area_tool_overlap)+0.5) * sign if abs(radius)>abs(r): radius = -r inkex.etree.SubElement( area_group, inkex.addNS('path','svg'), { inkex.addNS('type','sodipodi'): 'inkscape:offset', inkex.addNS('radius','inkscape'): str(radius), inkex.addNS('original','inkscape'): d, 'style': styles["biarc_style_i"]['area'] }) print_(("adding curve",area_group,d,styles["biarc_style_i"]['area'])) if radius == -r : break ################################################################################ ### ### Polyline to biarc ### ### Converts Polyline to Biarc ################################################################################ def polyline_to_biarc(self): def biarc(sm, depth=0): def biarc_split(sp1,sp2, z1, z2, depth): if depth 0 : raise ValueError, (a,b,c,disq,beta1,beta2) beta = max(beta1, beta2) elif asmall and bsmall: return biarc_split(sp1, sp2, z1, z2, depth) alpha = beta * r ab = alpha + beta P1 = P0 + alpha * TS P3 = P4 - beta * TE P2 = (beta / ab) * P1 + (alpha / ab) * P3 def calculate_arc_params(P0,P1,P2): D = (P0+P2)/2 if (D-P1).mag()==0: return None, None R = D - ( (D-P0).mag()**2/(D-P1).mag() )*(P1-D).unit() p0a, p1a, p2a = (P0-R).angle()%(2*math.pi), (P1-R).angle()%(2*math.pi), (P2-R).angle()%(2*math.pi) alpha = (p2a - p0a) % (2*math.pi) if (p0a1000000 or abs(R.y)>1000000 or (R-P0).mag options.biarc_tolerance and depth2 : smooth = [ [subpoly[0],subpoly[1]] ] for p1,p2,p3 in zip(subpoly,subpoly[1:],subpoly[2:]) : # normalize p1p2 and p2p3 to get angle s1,s2 = normalize( p1[0]-p2[0], p1[1]-p2[1]), normalize( p3[0]-p2[0], p3[1]-p2[1]) if cross(s1,s2) > corner_tolerance : #it's an angle smooth += [ [p2,p3] ] else: smooth[-1].append(p3) for sm in smooth : smooth_polyline_to_biarc(sm) ################################################################################ ### ### Area fill ### ### Fills area with lines ################################################################################ def area_fill(self): # convert degrees into rad self.options.area_fill_angle = self.options.area_fill_angle * math.pi / 180 if len(self.selected_paths)<=0: self.error(_("This extension requires at least one selected path."),"warning") return for layer in self.layers : if layer in self.selected_paths : self.set_tool(layer) if self.tools[layer][0]['diameter']<=0 : self.error(_("Tool diameter must be > 0 but tool's diameter on '%s' layer is not!") % layer.get(inkex.addNS('label','inkscape')),"area_tools_diameter_error") tool = self.tools[layer][0] for path in self.selected_paths[layer]: lines = [] print_(("doing path", path.get("style"), path.get("d"))) area_group = inkex.etree.SubElement( path.getparent(), inkex.addNS('g','svg') ) d = path.get('d') if d==None: print_("omitting non-path") self.error(_("Warning: omitting non-path"),"selection_contains_objects_that_are_not_paths") continue csp = cubicsuperpath.parsePath(d) csp = self.apply_transforms(path, csp) csp = csp_close_all_subpaths(csp) csp = self.transform_csp(csp, layer) #maxx = max([x,y,i,j,root],maxx) # rotate the path to get bounds in defined direction. a = - self.options.area_fill_angle rotated_path = [ [ [ [point[0]*math.cos(a) - point[1]*math.sin(a), point[0]*math.sin(a)+point[1]*math.cos(a)] for point in sp] for sp in subpath] for subpath in csp ] bounds = csp_true_bounds(rotated_path) # Draw the lines # Get path's bounds b = [0.0, 0.0, 0.0, 0.0] # [minx,miny,maxx,maxy] for k in range(4): i, j, t = bounds[k][2], bounds[k][3], bounds[k][4] b[k] = csp_at_t(rotated_path[i][j-1],rotated_path[i][j],t)[k%2] # Zig-zag r = tool['diameter']*(1-self.options.area_tool_overlap) if r<=0 : self.error('Tools diameter must be greater than 0!', 'error') return lines += [ [] ] if self.options.area_fill_method == 'zig-zag' : i = b[0] - self.options.area_fill_shift*r top = True last_one = True while (i=b[2] : last_one = False if lines[-1] == [] : lines[-1] += [ [i,b[3]] ] if top : lines[-1] += [ [i,b[1]],[i+r,b[1]] ] else : lines[-1] += [ [i,b[3]], [i+r,b[3]] ] top = not top i += r else : w, h = b[2]-b[0] + self.options.area_fill_shift*r , b[3]-b[1] + self.options.area_fill_shift*r x,y = b[0] - self.options.area_fill_shift*r, b[1] - self.options.area_fill_shift*r lines[-1] += [ [x,y] ] stage = 0 start = True while w>0 and h>0 : stage = (stage+1)%4 if stage == 0 : y -= h h -= r elif stage == 1: x += w if not start: w -= r start = False elif stage == 2 : y += h h -= r elif stage == 3: x -= w w -=r lines[-1] += [ [x,y] ] stage = (stage+1)%4 if w <= 0 and h>0 : y = y-h if stage == 0 else y+h if h <= 0 and w>0 : x = x-w if stage == 3 else x+w lines[-1] += [ [x,y] ] # Rotate created paths back a = self.options.area_fill_angle lines = [ [ [point[0]*math.cos(a) - point[1]*math.sin(a), point[0]*math.sin(a)+point[1]*math.cos(a)] for point in subpath] for subpath in lines ] # get the intersection points splitted_line = [ [lines[0][0]] ] intersections = {} for l1,l2, in zip(lines[0],lines[0][1:]): ints = [] if l1[0]==l2[0] and l1[1]==l2[1] : continue for i in range(len(csp)) : for j in range(1,len(csp[i])) : sp1,sp2 = csp[i][j-1], csp[i][j] roots = csp_line_intersection(l1,l2,sp1,sp2) for t in roots : p = tuple(csp_at_t(sp1,sp2,t)) if l1[0]==l2[0] : t1 = (p[1]-l1[1])/(l2[1]-l1[1]) else : t1 = (p[0]-l1[0])/(l2[0]-l1[0]) if 0<=t1<=1 : ints += [[t1, p[0],p[1], i,j,t]] if p in intersections : intersections[p] += [ [i,j,t] ] else : intersections[p] = [ [i,j,t] ] #p = self.transform(p,layer,True) #draw_pointer(p) ints.sort() for i in ints: splitted_line[-1] +=[ [ i[1], i[2]] ] splitted_line += [ [ [ i[1], i[2]] ] ] splitted_line[-1] += [ l2 ] i = 0 print_(splitted_line) while i < len(splitted_line) : # check if the middle point of the first lines segment is inside the path. # and remove the subline if not. l1,l2 = splitted_line[i][0],splitted_line[i][1] p = [(l1[0]+l2[0])/2, (l1[1]+l2[1])/2] if not point_inside_csp(p, csp): #i +=1 del splitted_line[i] else : i += 1 # if we've used spiral method we'll try to save the order of cutting do_not_change_order = self.options.area_fill_method == 'spiral' # now let's try connect splitted lines #while len(splitted_line)>0 : #TODO # and apply back transrormations to draw them csp_line = csp_from_polyline(splitted_line) csp_line = self.transform_csp(csp_line, layer, True) self.draw_csp(csp_line, group = area_group) # draw_csp(lines) ################################################################################ ### ### Engraving ### #LT Notes to self: See wiki.inkscape.org/wiki/index.php/PythonEffectTutorial # To create anything in the Inkscape document, look at the XML editor for # details of how such an element looks in XML, then follow this model. #layer number n appears in XML as # #to create it, use #Mylayer=inkex.etree.SubElement(self.document.getroot(), 'g') #Create a generic element #Mylayer.set(inkex.addNS('label', 'inkscape'), "layername") #Gives it a name #Mylayer.set(inkex.addNS('groupmode', 'inkscape'), 'layer') #Tells Inkscape it's a layer # #group appears in XML as where nnnnn is a number # #to create it, use #Mygroup=inkex.etree.SubElement(parent, inkex.addNS('g','svg'), {"gcodetools":"My group label"}) # where parent may be the layer or a parent group. To get the parent group, you can use #parent = self.selected_paths[layer][0].getparent() ################################################################################ def engraving(self) : #global x1,y1,rx,ry global cspm, wl global nlLT, i, j global gcode_3Dleft ,gcode_3Dright global max_dist #minimum of tool radius and user's requested maximum distance global eye_dist eye_dist = 100 #3D constant. Try varying it for your eyes def bisect((nx1,ny1),(nx2,ny2)) : """LT Find angle bisecting the normals n1 and n2 Parameters: Normalised normals Returns: nx - Normal of bisector, normalised to 1/cos(a) ny - sinBis2 - sin(angle turned/2): positive if turning in Note that bisect(n1,n2) and bisect(n2,n1) give opposite sinBis2 results If sinturn is less than the user's requested angle tolerance, I return 0 """ #We can get absolute value of cos(bisector vector) #Note: Need to use max in case of rounding errors cosBis = math.sqrt(max(0,(1.0+nx1*nx2-ny1*ny2)/2.0)) #We can get correct sign of the sin, assuming cos is positive if (abs(ny1-ny2)< engraving_tolerance) or (abs(cosBis) < engraving_tolerance) : if (abs(nx1-nx2)< engraving_tolerance): return(nx1,ny1,0.0) sinBis = math.copysign(1,ny1) else : sinBis = cosBis*(nx2-nx1)/(ny1-ny2) #We can correct signs by noting that the dot product # of bisector and either normal must be >0 costurn=cosBis*nx1+sinBis*ny1 if costurn == 0 : return (ny1*100,-nx1*100,1) #Path doubles back on itself sinturn=sinBis*nx1-cosBis*ny1 if costurn<0 : sinturn=-sinturn if 0 < sinturn*114.6 < (180-self.options.engraving_sharp_angle_tollerance) : sinturn=0 #set to zero if less than the user wants to see. return (cosBis/costurn,sinBis/costurn, sinturn) #end bisect def get_radius_to_line((x1,y1),(nx1,ny1), (nx2,ny2),(x2,y2),(nx23,ny23),(x3,y3),(nx3,ny3)): """LT find biggest circle we can engrave here, if constrained by line 2-3 Parameters: x1,y1,nx1,ny1 coordinates and normal of the line we're currently engraving nx2,ny2 angle bisector at point 2 x2,y2 coordinates of first point of line 2-3 nx23,ny23 normal to the line 2-3 x3,y3 coordinates of the other end nx3,ny3 angle bisector at point 3 Returns: radius or self.options.engraving_max_dist if line doesn't limit radius This function can be used in three ways: - With nx1=ny1=0 it finds circle centred at x1,y1 - with nx1,ny1 normalised, it finds circle tangential at x1,y1 - with nx1,ny1 scaled by 1/cos(a) it finds circle centred on an angle bisector where a is the angle between the bisector and the previous/next normals If the centre of the circle tangential to the line 2-3 is outside the angle bisectors at its ends, ignore this line. # Note that it handles corners in the conventional manner of letter cutting # by mitering, not rounding. # Algorithm uses dot products of normals to find radius # and hence coordinates of centre """ global max_dist #Start by converting coordinates to be relative to x1,y1 x2,y2= x2-x1, y2-y1 x3,y3= x3-x1, y3-y1 #The logic uses vector arithmetic. #The dot product of two vectors gives the product of their lengths #multiplied by the cos of the angle between them. # So, the perpendicular distance from x1y1 to the line 2-3 # is equal to the dot product of its normal and x2y2 or x3y3 #It is also equal to the projection of x1y1-xcyc on the line's normal # plus the radius. But, as the normal faces inside the path we must negate it. #Make sure the line in question is facing x1,y1 and vice versa dist=-x2*nx23-y2*ny23 if dist<0 : return max_dist denom=1.-nx23*nx1-ny23*ny1 if denom < engraving_tolerance : return max_dist #radius and centre are: r=dist/denom cx=r*nx1 cy=r*ny1 #if c is not between the angle bisectors at the ends of the line, ignore #Use vector cross products. Not sure if I need the .0001 safety margins: if (x2-cx)*ny2 > (y2-cy)*nx2 +0.0001 : return max_dist if (x3-cx)*ny3 < (y3-cy)*nx3 -0.0001 : return max_dist return min(r, max_dist) #end of get_radius_to_line def get_radius_to_point((x1,y1),(nx,ny), (x2,y2)): """LT find biggest circle we can engrave here, constrained by point x2,y2 This function can be used in three ways: - With nx=ny=0 it finds circle centred at x1,y1 - with nx,ny normalised, it finds circle tangential at x1,y1 - with nx,ny scaled by 1/cos(a) it finds circle centred on an angle bisector where a is the angle between the bisector and the previous/next normals Note that I wrote this to replace find_cutter_centre. It is far less sophisticated but, I hope, far faster. It turns out that finding a circle touching a point is harder than a circle touching a line. """ global max_dist #Start by converting coordinates to be relative to x1,y1 x2,y2= x2-x1, y2-y1 denom=nx**2+ny**2-1 if denom<=engraving_tolerance : #Not a corner bisector if denom==-1 : #Find circle centre x1,y1 return math.sqrt(x2**2+y2**2) #if x2,y2 not in front of the normal... if x2*nx+y2*ny <=0 : return max_dist #print_("Straight",x1,y1,nx,ny,x2,y2) return (x2**2+y2**2)/(2*(x2*nx+y2*ny) ) #It is a corner bisector, so.. discriminator = (x2*nx+y2*ny)**2 - denom*(x2**2+y2**2) if discriminator < 0 : return max_dist #this part irrelevant r=(x2*nx+y2*ny -math.sqrt(discriminator))/denom #print_("Corner",x1,y1,nx,ny,x1+x2,y1+y2,discriminator,r) return min(r, max_dist) #end of get_radius_to_point def bez_divide(a,b,c,d): """LT recursively divide a Bezier. Divides until difference between each part and a straight line is less than some limit Note that, as simple as this code is, it is mathematically correct. Parameters: a,b,c and d are each a list of x,y real values Bezier end points a and d, control points b and c Returns: a list of Beziers. Each Bezier is a list with four members, each a list holding a coordinate pair Note that the final point of one member is the same as the first point of the next, and the control points there are smooth and symmetrical. I use this fact later. """ bx=b[0]-a[0] by=b[1]-a[1] cx=c[0]-a[0] cy=c[1]-a[1] dx=d[0]-a[0] dy=d[1]-a[1] limit=8*math.hypot(dx,dy)/self.options.engraving_newton_iterations #LT This is the only limit we get from the user currently if abs(dx*by-bx*dy)len(nlLT[j])-3 : continue t1=get_radius_to_point((x1,y1),(nx,ny),nlLT[jj][ii-1][0] ) #print_("Try pt i,ii,t1,x1,y1",i,ii,t1,x1,y1) else: #doing a line if jj==j : #except this one if abs(ii-i)<2 or abs(ii-i)==len(nlLT[j])-1 : continue if abs(ii-i)==2 and nlLT[j][(ii+i)/2-1][3]<=0 : continue if (abs(ii-i)==len(nlLT[j])-2) and nlLT[j][-1][3]<=0 : continue nx2,ny2 = nlLT[jj][ii-2][1] x2,y2 = nlLT[jj][ii-1][0] nx23,ny23 = nlLT[jj][ii-1][1] x3,y3 = nlLT[jj][ii][0] nx3,ny3 = nlLT[jj][ii][1] if nlLT[jj][ii-2][3]>0 : #acute, so use normal, not bisector nx2=nx23 ny2=ny23 if nlLT[jj][ii][3]>0 : #acute, so use normal, not bisector nx3=nx23 ny3=ny23 x23min,x23max=min(x2,x3),max(x2,x3) y23min,y23max=min(y2,y3),max(y2,y3) #see if line in range if n1[2]==False and (x23maxxmax or y23maxymax) : continue t1=get_radius_to_line((x1,y1),(nx,ny), (nx2,ny2),(x2,y2),(nx23,ny23), (x3,y3),(nx3,ny3)) #print_("Try line i,ii,t1,x1,y1",i,ii,t1,x1,y1) if 0<=t11 : xy1a,xy1,xy1b,i1,j1,ii1,jj1=cspm[-1] w1=wl[-1] if i==i1 and j==j1 and ii==ii1 and jj==jj1 : #one match xy1a,xy2,xy1b,i1,j1,ii1,jj1=cspm[-2] w2=wl[-2] if i==i1 and j==j1 and ii==ii1 and jj==jj1 : #two matches. Now test linearity length1=math.hypot(xy1[0]-x,xy1[1]-y) length2=math.hypot(xy2[0]-x,xy2[1]-y) length12=math.hypot(xy2[0]-xy1[0],xy2[1]-xy1[1]) #get the xy distance of point 1 from the line 0-2 if length2>length1 and length2>length12 : #point 1 between them xydist=abs( (xy2[0]-x)*(xy1[1]-y)-(xy1[0]-x)*(xy2[1]-y) )/length2 if xydist0: radius 0 here. normal is bisector # if halfangle<0. reflex angle. normal is bisector # corner normals are divided by cos(halfangle) #so that they will engrave correctly print_("csp is",csp) nlLT.append ([]) for i in range(0,len(csp)): #LT for each point #n = [] sp0, sp1, sp2 = csp[i-2], csp[i-1], csp[i] if self.options.engraving_draw_calculation_paths: #Copy it to 3D layer objects spl=[] spr=[] for j in range(0,3) : pl=[sp2[j][0]-eye_dist,sp2[j][1]] pr=[sp2[j][0]+eye_dist,sp2[j][1]] spl+=[pl] spr+=[pr] cspl+=[spl] cspr+=[spr] #LT find angle between this and previous segment x0,y0 = sp1[1] nx1,ny1 = csp_normalized_normal(sp1,sp2,0) #I don't trust this function, so test result if abs(1-math.hypot(nx1,ny1))> 0.00001 : print_("csp_normalised_normal error t=0",nx1,ny1,sp1,sp2) self.error(_("csp_normalised_normal error. See log."),"warning") nx0, ny0 = csp_normalized_normal(sp0,sp1,1) if abs(1-math.hypot(nx0,ny0))> 0.00001 : print_("csp_normalised_normal error t=1",nx0,ny0,sp1,sp2) self.error(_("csp_normalised_normal error. See log."),"warning") bx,by,s=bisect((nx0,ny0),(nx1,ny1)) #record x,y,normal,ifCorner, sin(angle-turned/2) nlLT[-1] += [[ [x0,y0],[bx,by], True, s]] #LT now do the line if sp1[1]==sp1[2] and sp2[0]==sp2[1] : #straightline nlLT[-1]+=[[sp1[1],[nx1,ny1],False,i]] else : #Bezier. First, recursively cut it up: nn=bez_divide(sp1[1],sp1[2],sp2[0],sp2[1]) first=True #Flag entry to divided Bezier for bLT in nn : #save as two line segments for seg in range(3) : if seg>0 or first : nx1=bLT[seg][1]-bLT[seg+1][1] ny1=bLT[seg+1][0]-bLT[seg][0] l1=math.hypot(nx1,ny1) if l10 : #acute. Limit radius len1=math.hypot( (n0[0][0]-n1[0][0]),( n0[0][1]-n1[0][1]) ) if i<(len(nlLT[j])-1) : len2=math.hypot( (nlLT[j][i+1][0][0]-n1[0][0]),(nlLT[j][i+1][0][1]-n1[0][1]) ) else: len2=math.hypot( (nlLT[j][0][0][0]-n1[0][0]),(nlLT[j][0][0][1]-n1[0][1]) ) #set initial r value, not to be exceeded w = math.sqrt(min(len1,len2))/n1[3] else: #line. Cut it up if long. if n0[3]>0 and not self.options.engraving_draw_calculation_paths : bit0=r*n0[3] #after acute corner else : bit0=0.0 length=math.hypot((x1b-x1a),(y1a-y1b)) bit0=(min(length,bit0)) bits=int((length-bit0)/bitlen) #split excess evenly at both ends bit0+=(length-bit0-bitlen*bits)/2 #print_("j,i,r,bit0,bits",j,i,w,bit0,bits) for b in xrange(bits) : #divide line into bits x1=x1a+ny*(b*bitlen+bit0) y1=y1a-nx*(b*bitlen+bit0) jjmin,iimin,w=get_biggest( (x1,y1), (nx,ny)) print_("i,j,jjmin,iimin,w",i,j,jjmin,iimin,w) #w = min(r, toolr) wmax=max(wmax,w) if reflex : #just after a reflex corner reflex = False if w0 : #acute save_point((x1+nx*w,y1+ny*w),w,i,j,iimin,jjmin) draw_point((x1,y1),(x1,y1),0,0) save_point((x1,y1),0,i,j,iimin,jjmin) elif n1[3]<0 : #reflex if w>lastw : draw_point((x1,y1),(x1+nx*lastw,y1+ny*lastw),w, (w-lastw)/2) wmax=max(wmax,w) save_point((x1+nx*w,y1+ny*w),w,i,j,iimin,jjmin) elif b>0 and n2[3]>0 and not self.options.engraving_draw_calculation_paths : #acute corner coming up if jjmin==j and iimin==i+2 : break draw_point((x1,y1),(x1+nx*w,y1+ny*w),w, bitlen) save_point((x1+nx*w,y1+ny*w),w,i,j,iimin,jjmin) #LT end of for each bit of this line if n1[2] == True and n1[3]<0 : #reflex angle reflex=True lastw = w #remember this w #LT next i cspm+=[cspm[0]] print_("cspm",cspm) wl+=[wl[0]] print_("wl",wl) #Note: Original csp_points was a list, each element #being 4 points, with the first being the same as the #last of the previous set. #Each point is a list of [cx,cy,r,w] #I have flattened it to a flat list of points. if self.options.engraving_draw_calculation_paths==True: node = inkex.etree.SubElement( engraving_group, inkex.addNS('path','svg'), { "d": cubicsuperpath.formatPath([cspm]), 'style': styles["biarc_style_i"]['biarc1'], "gcodetools": "Engraving calculation paths", }) for i in xrange(len(cspm)): inkex.etree.SubElement( engraving_group, inkex.addNS('path','svg'), {"gcodetools": "Engraving calculation paths", 'style': "fill:none; fill-opacity:0.46; stroke:#000000; stroke-width:0.1;", inkex.addNS('cx','sodipodi'): str(cspm[i][1][0]), inkex.addNS('cy','sodipodi'): str(cspm[i][1][1]),inkex.addNS('rx','sodipodi'): str(wl[i]), inkex.addNS('ry','sodipodi'): str(wl[i]), inkex.addNS('type','sodipodi'): 'arc'}) cspe += [cspm] wluu = [] #width list in user units: mm/inches for w in wl : wluu+=[ w / orientation_scale ] print_("wl in pixels",wl) print_("wl in user units",wluu) #LT previously, we was in pixels so gave wrong depth we += [wluu] #LT6b For each subpath - ends here #LT5 if it is a path - ends here #print_("cspe",cspe) #print_("we",we) #LT4 for each selected object in this layer - ends here if cspe!=[]: curve = self.parse_curve(cspe, layer, we, toolshape) #convert to lines self.draw_curve(curve, layer, engraving_group) gcode += self.generate_gcode(curve, layer, self.options.Zsurface) #LT3 for layers loop ends here if gcode!='' : self.header+="(Tool diameter should be at least "+str(2*wmax/orientation_scale)+unit+ ")\n" self.header+="(Depth, as a function of radius w, must be "+ self.tools[layer][0]['shape']+ ")\n" self.header+="(Rapid feeds use safe Z="+ str(self.options.Zsafe) + unit + ")\n" self.header+="(Material surface at Z="+ str(self.options.Zsurface) + unit + ")\n" self.export_gcode(gcode) else : self.error(_("No need to engrave sharp angles."),"warning") ################################################################################ ### ### Orientation ### ################################################################################ def orientation(self, layer=None) : if layer == None : layer = self.current_layer if self.current_layer is not None else self.document.getroot() transform = self.get_transforms(layer) if transform != [] : transform = self.reverse_transform(transform) transform = simpletransform.formatTransform(transform) if self.options.orientation_points_count == "graffiti" : print_(self.graffiti_reference_points) print_("Inserting graffiti points") if layer in self.graffiti_reference_points: graffiti_reference_points_count = len(self.graffiti_reference_points[layer]) else: graffiti_reference_points_count = 0 axis = ["X","Y","Z","A"][graffiti_reference_points_count%4] attr = {'gcodetools': "Gcodetools graffiti reference point"} if transform != [] : attr["transform"] = transform g = inkex.etree.SubElement(layer, inkex.addNS('g','svg'), attr) inkex.etree.SubElement( g, inkex.addNS('path','svg'), { 'style': "stroke:none;fill:#00ff00;", 'd':'m %s,%s 2.9375,-6.343750000001 0.8125,1.90625 6.843748640396,-6.84374864039 0,0 0.6875,0.6875 -6.84375,6.84375 1.90625,0.812500000001 z z' % (graffiti_reference_points_count*100, 0), 'gcodetools': "Gcodetools graffiti reference point arrow" }) draw_text(axis,graffiti_reference_points_count*100+10,-10, group = g, gcodetools_tag = "Gcodetools graffiti reference point text") elif self.options.orientation_points_count == "in-out reference point" : draw_pointer(group = self.current_layer, x = self.view_center, figure="arrow", pointer_type = "In-out reference point", text = "In-out point") else : print_("Inserting orientation points") if layer in self.orientation_points: self.error(_("Active layer already has orientation points! Remove them or select another layer!"),"active_layer_already_has_orientation_points") attr = {"gcodetools":"Gcodetools orientation group"} if transform != [] : attr["transform"] = transform orientation_group = inkex.etree.SubElement(layer, inkex.addNS('g','svg'), attr) doc_height = inkex.unittouu(self.document.getroot().get('height')) if self.document.getroot().get('height') == "100%" : doc_height = 1052.3622047 print_("Overruding height from 100 percents to %s" % doc_height) if self.options.unit == "G21 (All units in mm)" : points = [[0.,0.,self.options.Zsurface],[100.,0.,self.options.Zdepth],[0.,100.,0.]] orientation_scale = 3.5433070660 print_("orientation_scale < 0 ===> switching to mm units=%0.10f"%orientation_scale ) elif self.options.unit == "G20 (All units in inches)" : points = [[0.,0.,self.options.Zsurface],[5.,0.,self.options.Zdepth],[0.,5.,0.]] orientation_scale = 90 print_("orientation_scale < 0 ===> switching to inches units=%0.10f"%orientation_scale ) if self.options.orientation_points_count == "2" : points = points[:2] print_(("using orientation scale",orientation_scale,"i=",points)) for i in points : si = [i[0]*orientation_scale, i[1]*orientation_scale] g = inkex.etree.SubElement(orientation_group, inkex.addNS('g','svg'), {'gcodetools': "Gcodetools orientation point (%s points)" % self.options.orientation_points_count}) inkex.etree.SubElement( g, inkex.addNS('path','svg'), { 'style': "stroke:none;fill:#000000;", 'd':'m %s,%s 2.9375,-6.343750000001 0.8125,1.90625 6.843748640396,-6.84374864039 0,0 0.6875,0.6875 -6.84375,6.84375 1.90625,0.812500000001 z z' % (si[0], -si[1]+doc_height), 'gcodetools': "Gcodetools orientation point arrow" }) draw_text("(%s; %s; %s)" % (i[0],i[1],i[2]), (si[0]+10), (-si[1]-10+doc_height), group = g, gcodetools_tag = "Gcodetools orientation point text") ################################################################################ ### ### Tools library ### ################################################################################ def tools_library(self, layer=None) : # Add a tool to the drawing if layer == None : layer = self.current_layer if self.current_layer is not None else self.document.getroot() if layer in self.tools: self.error(_("Active layer already has a tool! Remove it or select another layer!"),"active_layer_already_has_tool") if self.options.tools_library_type == "cylinder cutter" : tool = { "name": "Cylindrical cutter", "id": "Cylindrical cutter 0001", "diameter":10, "penetration angle":90, "feed":"400", "penetration feed":"100", "depth step":"1", "tool change gcode":" " } elif self.options.tools_library_type == "lathe cutter" : tool = { "name": "Lathe cutter", "id": "Lathe cutter 0001", "diameter":10, "penetration angle":90, "feed":"400", "passing feed":"800", "fine feed":"100", "penetration feed":"100", "depth step":"1", "tool change gcode":" " } elif self.options.tools_library_type == "cone cutter": tool = { "name": "Cone cutter", "id": "Cone cutter 0001", "diameter":10, "shape":"w", "feed":"400", "penetration feed":"100", "depth step":"1", "tool change gcode":" " } elif self.options.tools_library_type == "tangent knife": tool = { "name": "Tangent knife", "id": "Tangent knife 0001", "feed":"400", "penetration feed":"100", "depth step":"100", "4th axis meaning": "tangent knife", "4th axis scale": 1., "4th axis offset": 0, "tool change gcode":" " } elif self.options.tools_library_type == "plasma cutter": tool = { "name": "Plasma cutter", "id": "Plasma cutter 0001", "diameter":10, "penetration feed":100, "feed":400, "gcode before path":"""G31 Z-100 F500 (find metal) G92 Z0 (zero z) G00 Z10 F500 (going up) M03 (turn on plasma) G04 P0.2 (pause) G01 Z1 (going to cutting z)\n""", "gcode after path":"M05 (turn off plasma)\n", } elif self.options.tools_library_type == "graffiti": tool = { "name": "Graffiti", "id": "Graffiti 0001", "diameter":10, "penetration feed":100, "feed":400, "gcode before path":"""M03 S1(Turn spray on)\n """, "gcode after path":"M05 (Turn spray off)\n ", "tool change gcode":"(Add G00 here to change sprayer if needed)\n", } else : tool = self.default_tool tool_num = sum([len(self.tools[i]) for i in self.tools]) colors = ["00ff00","0000ff","ff0000","fefe00","00fefe", "fe00fe", "fe7e00", "7efe00", "00fe7e", "007efe", "7e00fe", "fe007e"] tools_group = inkex.etree.SubElement(layer, inkex.addNS('g','svg'), {'gcodetools': "Gcodetools tool definition"}) bg = inkex.etree.SubElement( tools_group, inkex.addNS('path','svg'), {'style': "fill:#%s;fill-opacity:0.5;stroke:#444444; stroke-width:1px;"%colors[tool_num%len(colors)], "gcodetools":"Gcodetools tool background"}) y = 0 keys = [] for key in self.tools_field_order: if key in tool: keys += [key] for key in tool: if key not in keys: keys += [key] for key in keys : g = inkex.etree.SubElement(tools_group, inkex.addNS('g','svg'), {'gcodetools': "Gcodetools tool parameter"}) draw_text(key, 0, y, group = g, gcodetools_tag = "Gcodetools tool definition field name", font_size = 10 if key!='name' else 20) param = tool[key] if type(param)==str and re.match("^\s*$",param) : param = "(None)" draw_text(param, 150, y, group = g, gcodetools_tag = "Gcodetools tool definition field value", font_size = 10 if key!='name' else 20) v = str(param).split("\n") y += 15*len(v) if key!='name' else 20*len(v) bg.set('d',"m -20,-20 l 400,0 0,%f -400,0 z " % (y+50)) tool = [] tools_group.set("transform", simpletransform.formatTransform([ [1,0,self.view_center[0]-150 ], [0,1,self.view_center[1]] ] )) ################################################################################ ### ### Check tools and OP asignment ### ################################################################################ def check_tools_and_op(self): if len(self.selected)<=0 : self.error(_("Selection is empty! Will compute whole drawing."),"selection_is_empty_will_comupe_drawing") paths = self.paths else : paths = self.selected_paths # Set group group = inkex.etree.SubElement( self.selected_paths.keys()[0] if len(self.selected_paths.keys())>0 else self.layers[0], inkex.addNS('g','svg') ) trans_ = [[1,0.3,0],[0,0.5,0]] self.set_markers() bounds = [float('inf'),float('inf'),float('-inf'),float('-inf')] tools_bounds = {} for layer in self.layers : if layer in paths : self.set_tool(layer) tool = self.tools[layer][0] tools_bounds[layer] = tools_bounds[layer] if layer in tools_bounds else [float("inf"),float("-inf")] style = simplestyle.formatStyle(tool["style"]) for path in paths[layer] : style = "fill:%s; fill-opacity:%s; stroke:#000044; stroke-width:1; marker-mid:url(#CheckToolsAndOPMarker);" % ( tool["style"]["fill"] if "fill" in tool["style"] else "#00ff00", tool["style"]["fill-opacity"] if "fill-opacity" in tool["style"] else "0.5") group.insert( 0, inkex.etree.Element(path.tag, path.attrib)) new = group.getchildren()[0] new.set("style", style) trans = self.get_transforms(path) trans = simpletransform.composeTransform( trans_, trans if trans != [] else [[1.,0.,0.],[0.,1.,0.]]) csp = cubicsuperpath.parsePath(path.get("d")) simpletransform.applyTransformToPath(trans,csp) path_bounds = csp_simple_bound(csp) trans = simpletransform.formatTransform(trans) bounds = [min(bounds[0],path_bounds[0]), min(bounds[1],path_bounds[1]), max(bounds[2],path_bounds[2]), max(bounds[3],path_bounds[3])] tools_bounds[layer] = [min(tools_bounds[layer][0], path_bounds[1]), max(tools_bounds[layer][1], path_bounds[3])] new.set("transform", trans) trans_[1][2] += 20 trans_[1][2] += 100 for layer in self.layers : if layer in self.tools : if layer in tools_bounds : tool = self.tools[layer][0] g = copy.deepcopy(tool["self_group"]) g.attrib["gcodetools"] = "Check tools and OP asignment" trans = [[1,0.3,bounds[2]],[0,0.5,tools_bounds[layer][0]]] g.set("transform",simpletransform.formatTransform(trans)) group.insert( 0, g ) ################################################################################ ### TODO Launch browser on help tab ################################################################################ def help(self): self.error(_("""Tutorials, manuals and support can be found at\nEnglish support forum:\n http://www.cnc-club.ru/gcodetools\nand Russian support forum:\n http://www.cnc-club.ru/gcodetoolsru"""),"warning") return ################################################################################ ### Lathe ################################################################################ def generate_lathe_gcode(self, subpath, layer, feed_type) : if len(subpath) <2 : return "" feed = " F %f" % self.tool[feed_type] x,z = self.options.lathe_x_axis_remap, self.options.lathe_z_axis_remap flip_angle = -1 if x.lower()+z.lower() in ["xz", "yx", "zy"] else 1 alias = {"X":"I", "Y":"J", "Z":"K", "x":"i", "y":"j", "z":"k"} i_, k_ = alias[x], alias[z] c = [ [subpath[0][1], "move", 0, 0, 0] ] #draw_csp(self.transform_csp([subpath],layer,True), color = "Orange", width = .1) for sp1,sp2 in zip(subpath,subpath[1:]) : c += biarc(sp1,sp2,0,0) for i in range(1,len(c)) : # Just in case check end point of each segment c[i-1][4] = c[i][0][:] c += [ [subpath[-1][1], "end", 0, 0, 0] ] self.draw_curve(c, layer, style = styles["biarc_style_lathe_%s" % feed_type]) gcode = ("G01 %s %f %s %f" % (x, c[0][4][0], z, c[0][4][1]) ) + feed + "\n" # Just in case move to the start... for s in c : if s[1] == 'line': gcode += ("G01 %s %f %s %f" % (x, s[4][0], z, s[4][1]) ) + feed + "\n" elif s[1] == 'arc': r = [(s[2][0]-s[0][0]), (s[2][1]-s[0][1])] if (r[0]**2 + r[1]**2)>self.options.min_arc_radius**2: r1, r2 = (P(s[0])-P(s[2])), (P(s[4])-P(s[2])) if abs(r1.mag()-r2.mag()) < 0.001 : gcode += ("G02" if s[3]*flip_angle<0 else "G03") + (" %s %f %s %f %s %f %s %f" % (x,s[4][0],z,s[4][1],i_,(s[2][0]-s[0][0]), k_, (s[2][1]-s[0][1]) ) ) + feed + "\n" else: r = (r1.mag()+r2.mag())/2 gcode += ("G02" if s[3]*flip_angle<0 else "G03") + (" %s %f %s %f" % (x,s[4][0],z,y[4][1]) ) + " R%f"%r + feed + "\n" return gcode def lathe(self): if not self.check_dir() : return x,z = self.options.lathe_x_axis_remap, self.options.lathe_z_axis_remap x = re.sub("^\s*([XYZxyz])\s*$",r"\1",x) z = re.sub("^\s*([XYZxyz])\s*$",r"\1",z) if x not in ["X", "Y", "Z", "x", "y", "z"] or z not in ["X", "Y", "Z", "x", "y", "z"] : self.error(_("Lathe X and Z axis remap should be 'X', 'Y' or 'Z'. Exiting..."),"warning") return if x.lower() == z.lower() : self.error(_("Lathe X and Z axis remap should be the same. Exiting..."),"warning") return if x.lower()+z.lower() in ["xy","yx"] : gcode_plane_selection = "G17 (Using XY plane)\n" if x.lower()+z.lower() in ["xz","zx"] : gcode_plane_selection = "G18 (Using XZ plane)\n" if x.lower()+z.lower() in ["zy","yz"] : gcode_plane_selection = "G19 (Using YZ plane)\n" self.options.lathe_x_axis_remap, self.options.lathe_z_axis_remap = x, z paths = self.selected_paths self.tool = [] gcode = "" for layer in self.layers : if layer in paths : self.set_tool(layer) if self.tool != self.tools[layer][0] : self.tool = self.tools[layer][0] self.tool["passing feed"] = float(self.tool["passing feed"] if "passing feed" in self.tool else self.tool["feed"]) self.tool["feed"] = float(self.tool["feed"]) self.tool["fine feed"] = float(self.tool["fine feed"] if "fine feed" in self.tool else self.tool["feed"]) gcode += ( "(Change tool to %s)\n" % re.sub("\"'\(\)\\\\"," ",self.tool["name"]) ) + self.tool["tool change gcode"] + "\n" for path in paths[layer]: csp = self.transform_csp(cubicsuperpath.parsePath(path.get("d")),layer) for subpath in csp : # Offset the path if fine cut is defined. fine_cut = subpath[:] if self.options.lathe_fine_cut_width>0 : r = self.options.lathe_fine_cut_width if self.options.lathe_create_fine_cut_using == "Move path" : subpath = [ [ [i2[0],i2[1]+r] for i2 in i1] for i1 in subpath] else : # Close the path to make offset correct bound = csp_simple_bound([subpath]) minx,miny,maxx,maxy = csp_true_bounds([subpath]) offsetted_subpath = csp_subpath_line_to(subpath[:], [ [subpath[-1][1][0], miny[1]-r*10 ], [subpath[0][1][0], miny[1]-r*10 ], [subpath[0][1][0], subpath[0][1][1] ] ]) left,right = subpath[-1][1][0], subpath[0][1][0] if left>right : left, right = right,left offsetted_subpath = csp_offset([offsetted_subpath], r if not csp_subpath_ccw(offsetted_subpath) else -r ) offsetted_subpath = csp_clip_by_line(offsetted_subpath, [left,10], [left,0] ) offsetted_subpath = csp_clip_by_line(offsetted_subpath, [right,0], [right,10] ) offsetted_subpath = csp_clip_by_line(offsetted_subpath, [0, miny[1]-r], [10, miny[1]-r] ) #draw_csp(self.transform_csp(offsetted_subpath,layer,True), color = "Green", width = 1) # Join offsetted_subpath together # Hope there wont be any cicles subpath = csp_join_subpaths(offsetted_subpath)[0] # Create solid object from path and lathe_width bound = csp_simple_bound([subpath]) top_start, top_end = [subpath[0][1][0], self.options.lathe_width+self.options.Zsafe+self.options.lathe_fine_cut_width], [subpath[-1][1][0], self.options.lathe_width+self.options.Zsafe+self.options.lathe_fine_cut_width] gcode += ("G01 %s %f F %f \n" % (z, top_start[1], self.tool["passing feed"]) ) gcode += ("G01 %s %f %s %f F %f \n" % (x, top_start[0], z, top_start[1], self.tool["passing feed"]) ) subpath = csp_concat_subpaths(csp_subpath_line_to([],[top_start,subpath[0][1]]), subpath) subpath = csp_subpath_line_to(subpath,[top_end,top_start]) width = max(0, self.options.lathe_width - max(0, bound[1]) ) step = self.tool['depth step'] steps = int(math.ceil(width/step)) for i in range(steps+1): current_width = self.options.lathe_width - step*i intersections = [] for j in range(1,len(subpath)) : sp1,sp2 = subpath[j-1], subpath[j] intersections += [[j,k] for k in csp_line_intersection([bound[0]-10,current_width], [bound[2]+10,current_width], sp1, sp2)] intersections += [[j,k] for k in csp_line_intersection([bound[0]-10,current_width+step], [bound[2]+10,current_width+step], sp1, sp2)] parts = csp_subpath_split_by_points(subpath,intersections) for part in parts : minx,miny,maxx,maxy = csp_true_bounds([part]) y = (maxy[1]+miny[1])/2 if y > current_width+step : gcode += self.generate_lathe_gcode(part,layer,"passing feed") elif current_width <= y <= current_width+step : gcode += self.generate_lathe_gcode(part,layer,"feed") else : # full step cut part = csp_subpath_line_to([], [part[0][1], part[-1][1]] ) gcode += self.generate_lathe_gcode(part,layer,"feed") top_start, top_end = [fine_cut[0][1][0], self.options.lathe_width+self.options.Zsafe+self.options.lathe_fine_cut_width], [fine_cut[-1][1][0], self.options.lathe_width+self.options.Zsafe+self.options.lathe_fine_cut_width] gcode += "\n(Fine cutting start)\n(Calculating fine cut using %s)\n"%self.options.lathe_create_fine_cut_using for i in range(self.options.lathe_fine_cut_count) : width = self.options.lathe_fine_cut_width*(1-float(i+1)/self.options.lathe_fine_cut_count ) if width == 0 : current_pass = fine_cut else : if self.options.lathe_create_fine_cut_using == "Move path" : current_pass = [ [ [i2[0],i2[1]+width] for i2 in i1] for i1 in fine_cut] else : minx,miny,maxx,maxy = csp_true_bounds([fine_cut]) offsetted_subpath = csp_subpath_line_to(fine_cut[:], [ [fine_cut[-1][1][0], miny[1]-r*10 ], [fine_cut[0][1][0], miny[1]-r*10 ], [fine_cut[0][1][0], fine_cut[0][1][1] ] ]) left,right = fine_cut[-1][1][0], fine_cut[0][1][0] if left>right : left, right = right,left offsetted_subpath = csp_offset([offsetted_subpath], width if not csp_subpath_ccw(offsetted_subpath) else -width ) offsetted_subpath = csp_clip_by_line(offsetted_subpath, [left,10], [left,0] ) offsetted_subpath = csp_clip_by_line(offsetted_subpath, [right,0], [right,10] ) offsetted_subpath = csp_clip_by_line(offsetted_subpath, [0, miny[1]-r], [10, miny[1]-r] ) current_pass = csp_join_subpaths(offsetted_subpath)[0] gcode += "\n(Fine cut %i-th cicle start)\n"%(i+1) gcode += ("G01 %s %f %s %f F %f \n" % (x, top_start[0], z, top_start[1], self.tool["passing feed"]) ) gcode += ("G01 %s %f %s %f F %f \n" % (x, current_pass[0][1][0], z, current_pass[0][1][1]+self.options.lathe_fine_cut_width, self.tool["passing feed"]) ) gcode += ("G01 %s %f %s %f F %f \n" % (x, current_pass[0][1][0], z, current_pass[0][1][1], self.tool["fine feed"]) ) gcode += self.generate_lathe_gcode(current_pass,layer,"fine feed") gcode += ("G01 %s %f F %f \n" % (z, top_start[1], self.tool["passing feed"]) ) gcode += ("G01 %s %f %s %f F %f \n" % (x, top_start[0], z, top_start[1], self.tool["passing feed"]) ) self.export_gcode(gcode) ################################################################################ ### ### Lathe modify path ### Modifies path to fit current cutter. As for now straight rect cutter. ### ################################################################################ def lathe_modify_path(self): if self.selected_paths == {} and self.options.auto_select_paths: paths=self.paths self.error(_("No paths are selected! Trying to work on all available paths."),"warning") else : paths = self.selected_paths for layer in self.layers : if layer in paths : width = self.options.lathe_rectangular_cutter_width #self.set_tool(layer) for path in paths[layer]: csp = self.transform_csp(cubicsuperpath.parsePath(path.get("d")),layer) new_csp = [] for subpath in csp: orientation = subpath[-1][1][0]>subpath[0][1][0] last_n = None last_o = 0 new_subpath = [] # Split segment at x' and y' == 0 for sp1, sp2 in zip(subpath[:],subpath[1:]): ax,ay,bx,by,cx,cy,dx,dy = csp_parameterize(sp1,sp2) roots = cubic_solver_real(0, 3*ax, 2*bx, cx) roots += cubic_solver_real(0, 3*ay, 2*by, cy) new_subpath = csp_concat_subpaths(new_subpath, csp_seg_split(sp1,sp2,roots)) subpath = new_subpath new_subpath = [] first_seg = True for sp1, sp2 in zip(subpath[:],subpath[1:]): n = csp_normalized_normal(sp1,sp2,0) a = math.atan2(n[0],n[1]) if a == 0 or a == math.pi : n = csp_normalized_normal(sp1,sp2,1) a = math.atan2(n[0],n[1]) if a!=0 and a!=math.pi: o = 0 if 00 else -1)*orientation # new_subpath += [ [sp1[i][0] - width*o,sp1[i][1]] for i in range(3) ] # n = csp_normalized_normal(sp1,sp2,1) # o = (1 if cross(n, [0,1])>0 else -1)*orientation # new_subpath += [ [sp2[i][0] - width*o,sp2[i][1]] for i in range(3) ] ################################################################################ ### ### Update function ### ### Gets file containing version information from the web and compaares it with. ### current version. ################################################################################ def update(self) : try : import urllib f = urllib.urlopen("http://www.cnc-club.ru/gcodetools_latest_version", proxies = urllib.getproxies()) a = f.read() for s in a.split("\n") : r = re.search(r"Gcodetools\s+latest\s+version\s*=\s*(.*)",s) if r : ver = r.group(1).strip() if ver != gcodetools_current_version : self.error("There is a newer version of Gcodetools you can get it at: \nhttp://www.cnc-club.ru/gcodetools (English version). \nhttp://www.cnc-club.ru/gcodetools_ru (Russian version). ","Warning") else : self.error("You are currently using latest stable version of Gcodetools.","Warning") return self.error("Can not check the latest version. You can check it manualy at \nhttp://www.cnc-club.ru/gcodetools (English version). \nhttp://www.cnc-club.ru/gcodetools_ru (Russian version). \nCurrent version is Gcodetools %s"%gcodetools_current_version,"Warning") except : self.error("Can not check the latest version. You can check it manualy at \nhttp://www.cnc-club.ru/gcodetools (English version). \nhttp://www.cnc-club.ru/gcodetools_ru (Russian version). \nCurrent version is Gcodetools %s"%gcodetools_current_version,"Warning") ################################################################################ ### Graffiti function generates Gcode for graffiti drawer ################################################################################ def graffiti(self) : # Get reference points. def get_gcode_coordinates(point,layer): gcode = '' pos = [] for ref_point in self.graffiti_reference_points[layer] : c = math.sqrt((point[0]-ref_point[0][0])**2 + (point[1]-ref_point[0][1])**2) gcode += " %s %f"%(ref_point[1], c) pos += [c] return pos, gcode def graffiti_preview_draw_point(x1,y1,color,radius=.5): self.graffiti_preview = self.graffiti_preview r,g,b,a_ = color for x in range(int(x1-1-math.ceil(radius)), int(x1+1+math.ceil(radius)+1)): for y in range(int(y1-1-math.ceil(radius)), int(y1+1+math.ceil(radius)+1)): if x>=0 and y>=0 and yradius else 1 )/256 self.graffiti_preview[y][x*4] = int(r*a + (1-a)*self.graffiti_preview[y][x*4]) self.graffiti_preview[y][x*4+1] = int(g*a + (1-a)*self.graffiti_preview[y][x*4+1]) self.graffiti_preview[y][x*4+2] = int(g*b + (1-a)*self.graffiti_preview[y][x*4+2]) self.graffiti_preview[y][x*4+3] = min(255,int(self.graffiti_preview[y][x*4+3]+a*256)) def graffiti_preview_transform(x,y): tr = self.graffiti_preview_transform d = max(tr[2]-tr[0]+2,tr[3]-tr[1]+2) return [(x-tr[0]+1)*self.options.graffiti_preview_size/d, self.options.graffiti_preview_size - (y-tr[1]+1)*self.options.graffiti_preview_size/d] def draw_graffiti_segment(layer,start,end,feed,color=(0,255,0,40),emmit=1000): # Emit = dots per second l = math.sqrt(sum([(start[i]-end[i])**2 for i in range(len(start))])) time_ = l/feed c1,c2 = self.graffiti_reference_points[layer][0][0],self.graffiti_reference_points[layer][1][0] d = math.sqrt( (c1[0]-c2[0])**2 + (c1[1]-c2[1])**2 ) if d == 0 : raise ValueError, "Error! Reference points should not be the same!" for i in range(int(time_*emmit+1)) : t = i/(time_*emmit) r1,r2 = start[0]*(1-t) + end[0]*t, start[1]*(1-t) + end[1]*t a = (r1**2-r2**2+d**2)/(2*d) h = math.sqrt(r1**2 - a**2) xa = c1[0] + a*(c2[0]-c1[0])/d ya = c1[1] + a*(c2[1]-c1[1])/d x1 = xa + h*(c2[1]-c1[1])/d x2 = xa - h*(c2[1]-c1[1])/d y1 = ya - h*(c2[0]-c1[0])/d y2 = ya + h*(c2[0]-c1[0])/d x = x1 if y10: i = min( [( point_to_point_d2(last_sp2[1],subpaths[i][0][1]),i) for i in range(len(subpaths))] )[1] subpath = subpaths[i][:] del subpaths[i] polylines += [ ['connector', create_connector( last_sp2[1], subpath[0][1], csp_normalized_slope(last_sp1,last_sp2,1.), csp_normalized_slope(subpath[0],subpath[1],0.), )] ] polyline = [] spl = None # remove zerro length segments i = 0 while i 0.1 : # TODO add coefficient into inx # We've got sharp angle at sp1. polyline += [sp1] polylines += [['draw',polyline[:]]] polylines += [ ['connector', create_connector( sp1[1], sp1[1], csp_normalized_slope(spl,sp1,1.), csp_normalized_slope(sp1,sp2,0.), )] ] polyline = [] # max_segment_length polyline += [ sp1 ] print_(polyline) print_(sp1) spl = sp1 polyline += [ sp2 ] polylines += [ ['draw',polyline[:]] ] last_sp1, last_sp2 = sp1,sp2 # Add return to start_point if polylines == [] : continue polylines += [ ["connect1", [ [polylines[-1][1][-1][1] for i in range(3)],[start_point for i in range(3)] ] ] ] # Make polilynes from polylines. They are still csp. for i in range(len(polylines)) : polyline = [] l = 0 print_("polylines",polylines) print_(polylines[i]) for sp1,sp2 in zip(polylines[i][1],polylines[i][1][1:]) : print_(sp1,sp2) l = cspseglength(sp1,sp2) if l>0.00000001 : polyline += [sp1[1]] parts = int(math.ceil(l/self.options.graffiti_max_seg_length)) for j in range(1,parts): polyline += [csp_at_length(sp1,sp2,float(j)/parts) ] if l>0.00000001 : polyline += [sp2[1]] print_(i) polylines[i][1] = polyline t = 0 last_state = None for polyline_ in polylines: polyline = polyline_[1] # Draw linearization if self.options.graffiti_create_linearization_preview : t += 1 csp = [ [polyline[i],polyline[i],polyline[i]] for i in range(len(polyline))] draw_csp(self.transform_csp([csp],layer,reverse=True), color = "#00cc00;" if polyline_[0]=='draw' else "#ff5555;") # Export polyline to gcode # we are making trnsform from XYZA coordinates to R1...Rn # where R1...Rn are radius vectors from grafiti reference points # to current (x,y) point. Also we need to assign custom feed rate # for each segment. And we'll use only G01 gcode. last_real_pos, g = get_gcode_coordinates(polyline[0],layer) last_pos = polyline[0] if polyline_[0] == "draw" and last_state!="draw": gcode += self.tool['gcode before path']+"\n" for point in polyline : real_pos, g = get_gcode_coordinates(point,layer) real_l = sum([(real_pos[i]-last_real_pos[i])**2 for i in range(len(last_real_pos))]) l = (last_pos[0]-point[0])**2 + (last_pos[1]-point[1])**2 if l!=0: feed = self.tool['feed']*math.sqrt(real_l/l) gcode += "G01 " + g + " F %f\n"%feed if self.options.graffiti_create_preview : draw_graffiti_segment(layer,real_pos,last_real_pos,feed,color=(0,0,255,200) if polyline_[0] == "draw" else (255,0,0,200),emmit=self.options.graffiti_preview_emmit) last_real_pos = real_pos last_pos = point[:] if polyline_[0] == "draw" and last_state!="draw" : gcode += self.tool['gcode after path']+"\n" last_state = polyline_[0] self.export_gcode(gcode, no_headers=True) if self.options.graffiti_create_preview : try : # Draw reference points for layer in self.graffiti_reference_points: for point in self.graffiti_reference_points[layer] : x, y = graffiti_preview_transform(point[0][0],point[0][1]) graffiti_preview_draw_point(x,y,(0,255,0,255),radius=5) import png writer = png.Writer(width=self.options.graffiti_preview_size, height=self.options.graffiti_preview_size, size=None, greyscale=False, alpha=True, bitdepth=8, palette=None, transparent=None, background=None, gamma=None, compression=None, interlace=False, bytes_per_sample=None, planes=None, colormap=None, maxval=None, chunk_limit=1048576) f = open(self.options.directory+self.options.file+".png", 'wb') writer.write(f,self.graffiti_preview) f.close() except : self.error("Png module have not been found!","warning") ################################################################################ ### ### Effect ### ### Main function of Gcodetools class ### ################################################################################ def effect(self) : start_time = time.time() global options options = self.options options.self = self options.doc_root = self.document.getroot() # define print_ function global print_ if self.options.log_create_log : try : if os.path.isfile(self.options.log_filename) : os.remove(self.options.log_filename) f = open(self.options.log_filename,"a") f.write("Gcodetools log file.\nStarted at %s.\n%s\n" % (time.strftime("%d.%m.%Y %H:%M:%S"),options.log_filename)) f.write("%s tab is active.\n" % self.options.active_tab) f.close() except : print_ = lambda *x : None else : print_ = lambda *x : None if self.options.active_tab == '"help"' : self.help() return elif self.options.active_tab == '"about"' : self.help() return elif self.options.active_tab == '"test"' : self.test() elif self.options.active_tab not in ['"dxfpoints"','"path-to-gcode"', '"area_fill"', '"area"', '"area_artefacts"', '"engraving"', '"orientation"', '"tools_library"', '"lathe"', '"offset"', '"arrangement"', '"update"', '"graffiti"', '"lathe_modify_path"', '"plasma-prepare-path"']: self.error(_("Select one of the action tabs - Path to Gcode, Area, Engraving, DXF points, Orientation, Offset, Lathe or Tools library.\n Current active tab id is %s" % self.options.active_tab),"error") else: # Get all Gcodetools data from the scene. self.get_info() if self.options.active_tab in ['"dxfpoints"','"path-to-gcode"', '"area_fill"', '"area"', '"area_artefacts"', '"engraving"', '"lathe"', '"graffiti"', '"plasma-prepare-path"']: if self.orientation_points == {} : self.error(_("Orientation points have not been defined! A default set of orientation points has been automatically added."),"warning") self.orientation( self.layers[min(1,len(self.layers)-1)] ) self.get_info() if self.tools == {} : self.error(_("Cutting tool has not been defined! A default tool has been automatically added."),"warning") self.options.tools_library_type = "default" self.tools_library( self.layers[min(1,len(self.layers)-1)] ) self.get_info() if self.options.active_tab == '"path-to-gcode"': self.path_to_gcode() elif self.options.active_tab == '"area_fill"': self.area_fill() elif self.options.active_tab == '"area"': self.area() elif self.options.active_tab == '"area_artefacts"': self.area_artefacts() elif self.options.active_tab == '"dxfpoints"': self.dxfpoints() elif self.options.active_tab == '"engraving"': self.engraving() elif self.options.active_tab == '"orientation"': self.orientation() elif self.options.active_tab == '"graffiti"': self.graffiti() elif self.options.active_tab == '"tools_library"': if self.options.tools_library_type != "check": self.tools_library() else : self.check_tools_and_op() elif self.options.active_tab == '"lathe"': self.lathe() elif self.options.active_tab == '"lathe_modify_path"': self.lathe_modify_path() elif self.options.active_tab == '"update"': self.update() elif self.options.active_tab == '"offset"': if self.options.offset_just_get_distance : for layer in self.selected_paths : if len(self.selected_paths[layer]) == 2 : csp1, csp2 = cubicsuperpath.parsePath(self.selected_paths[layer][0].get("d")), cubicsuperpath.parsePath(self.selected_paths[layer][1].get("d")) dist = csp_to_csp_distance(csp1,csp2) print_(dist) draw_pointer( list(csp_at_t(csp1[dist[1]][dist[2]-1],csp1[dist[1]][dist[2]],dist[3])) +list(csp_at_t(csp2[dist[4]][dist[5]-1],csp2[dist[4]][dist[5]],dist[6])),"red","line", comment = math.sqrt(dist[0])) return if self.options.offset_step == 0 : self.options.offset_step = self.options.offset_radius if self.options.offset_step*self.options.offset_radius <0 : self.options.offset_step *= -1 time_ = time.time() offsets_count = 0 for layer in self.selected_paths : for path in self.selected_paths[layer] : offset = self.options.offset_step/2 while abs(offset) <= abs(self.options.offset_radius) : offset_ = csp_offset(cubicsuperpath.parsePath(path.get("d")), offset) offsets_count += 1 if offset_ != [] : for iii in offset_ : draw_csp([iii], color="Green", width=1) #print_(offset_) else : print_("------------Reached empty offset at radius %s"% offset ) break offset += self.options.offset_step print_() print_("-----------------------------------------------------------------------------------") print_("-----------------------------------------------------------------------------------") print_("-----------------------------------------------------------------------------------") print_() print_("Done in %s"%(time.time()-time_)) print_("Total offsets count %s"%offsets_count) elif self.options.active_tab == '"arrangement"': self.arrangement() elif self.options.active_tab == '"plasma-prepare-path"': self.plasma_prepare_path() print_("------------------------------------------") print_("Done in %f seconds"%(time.time()-start_time)) print_("End at %s."%time.strftime("%d.%m.%Y %H:%M:%S")) # gcodetools = Gcodetools() gcodetools.affect()