#!/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" # standard library import os import math import bezmisc import re import copy import sys import time import cmath import numpy import codecs import random # local library import inkex import simplestyle import simplepath import cubicsuperpath import simpletransform import bezmisc inkex.localize() ### 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]<dist[0] : dist = d+[1.] if dist[0] <= dist_bounds[0] : return dist d = csp_seg_to_point_distance(sp3,sp4,sp1[1],sample_points, tolerance) if d[0]<dist[0] : dist = [d[0],0.,d[1]] if dist[0] <= dist_bounds[0] : return dist d = csp_seg_to_point_distance(sp3,sp4,sp2[1],sample_points, tolerance) if d[0]<dist[0] : dist = [d[0],1.,d[1]] if dist[0] <= dist_bounds[0] : return dist sample_points -= 2 if sample_points < 1 : sample_points = 1 ax1,ay1,bx1,by1,cx1,cy1,dx1,dy1 = csp_parameterize(sp1,sp2) ax2,ay2,bx2,by2,cx2,cy2,dx2,dy2 = csp_parameterize(sp3,sp4) # try to find closes points using Newtons method for k in range(sample_points) : for j in range(sample_points) : t1,t2 = float(k+1)/(sample_points+1), float(j)/(sample_points+1) t12, t13, t22, t23 = t1*t1, t1*t1*t1, t2*t2, t2*t2*t2 i = 0 F1, F2, F = [0,0], [[0,0],[0,0]], 1e100 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) while i<2 or abs(F-Flast)>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<tolerance and i<10 : t = .5 f1x = 3*ax*t**2 + 2*bx*t + cx f1y = 3*ay*t**2 + 2*by*t + cy f2x = 6*ax*t + 2*bx f2y = 6*ay*t + 2*by f3x = 6*ax f3y = 6*ay d = pow(f1x**2+f1y**2,1.5) if d != 0 : Flast = F F = (f1x*f2y-f1y*f2x)/d F1 = ( ( d*(f1x*f3y-f3x*f1y) - (f1x*f2y-f2x*f1y)*3.*(f2x*f1x+f2y*f1y)*pow(f1x**2+f1y**2,.5) ) / (f1x**2+f1y**2)**3 ) i+=1 if F1!=0: t -= F/F1 else: break else: break return t def csp_curvature_at_t(sp1,sp2,t, depth = 3) : ax,ay,bx,by,cx,cy,dx,dy = bezmisc.bezierparameterize(csp_segment_to_bez(sp1,sp2)) #curvature = (x'y''-y'x'') / (x'^2+y'^2)^1.5 f1x = 3*ax*t**2 + 2*bx*t + cx f1y = 3*ay*t**2 + 2*by*t + cy f2x = 6*ax*t + 2*bx f2y = 6*ay*t + 2*by d = (f1x**2+f1y**2)**1.5 if d != 0 : return (f1x*f2y-f1y*f2x)/d else : t1 = f1x*f2y-f1y*f2x if t1 > 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]<s[j] : break if s[j]-s[j-1] != 0 : t = (segs[i] - s[j-1])/(s[j]-s[j-1]) sp1,sp2,sp3 = csp_split(csp[j-1],csp[j], t) csp = csp[:j-1] + [sp1,sp2,sp3] + csp[j+1:] s = s[:j] + [ s[j-1]*(1-t)+s[j]*t ] + s[j:] return csp, s def csp_slope(sp1,sp2,t): bez = (sp1[1][:],sp1[2][:],sp2[0][:],sp2[1][:]) return bezmisc.bezierslopeatt(bez,t) def csp_line_intersection(l1,l2,sp1,sp2): dd=l1[0] cc=l2[0]-l1[0] bb=l1[1] aa=l2[1]-l1[1] if aa==cc==0 : return [] if aa: coef1=cc/aa coef2=1 else: coef1=1 coef2=aa/cc bez = (sp1[1][:],sp1[2][:],sp2[0][:],sp2[1][:]) ax,ay,bx,by,cx,cy,x0,y0=bezmisc.bezierparameterize(bez) a=coef1*ay-coef2*ax b=coef1*by-coef2*bx c=coef1*cy-coef2*cx d=coef1*(y0-bb)-coef2*(x0-dd) roots = cubic_solver(a,b,c,d) retval = [] for i in roots : if type(i) is complex and abs(i.imag)<1e-7: i = i.real if type(i) is not complex and -1e-10<=i<=1.+1e-10: retval.append(i) return retval def csp_split_by_two_points(sp1,sp2,t1,t2) : if t1>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<t<1.-1e-10 : sp3,sp4,sp5 = csp_split(res[-2],res[-1], (t-last_t)/(1-last_t)) last_t = t res[-2:] = [sp3,sp4,sp5] return res def csp_subpath_split_by_points(subpath, points) : # points are [[i,t]...] where i-segment's number points.sort() points = [[1,0.]] + points + [[len(subpath)-1,1.]] parts = [] for int1,int2 in zip(points,points[1:]) : if int1==int2 : continue if int1[1] == 1. : int1[0] += 1 int1[1] = 0. if int1==int2 : continue if int2[1] == 0. : int2[0] -= 1 int2[1] = 1. if int1[0] == 0 and int2[0]==len(subpath)-1:# and small(int1[1]) and small(int2[1]-1) : continue if int1[0]==int2[0] : # same segment sp = csp_split_by_two_points(subpath[int1[0]-1],subpath[int1[0]],int1[1], int2[1]) if sp[1]!=sp[2] : parts += [ [sp[1],sp[2]] ] else : sp5,sp1,sp2 = csp_split(subpath[int1[0]-1],subpath[int1[0]],int1[1]) sp3,sp4,sp5 = csp_split(subpath[int2[0]-1],subpath[int2[0]],int2[1]) if int1[0]==int2[0]-1 : parts += [ [sp1, [sp2[0],sp2[1],sp3[2]], sp4] ] else : parts += [ [sp1,sp2]+subpath[int1[0]+1:int2[0]-1]+[sp3,sp4] ] return parts def arc_from_s_r_n_l(s,r,n,l) : if abs(n[0]**2+n[1]**2 - 1) > 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))<straight_tolerance : return (p-i).mag(), [i.x, i.y] else : d1, d2 = (p-P0).mag(), (p-P2).mag() if d1<d2 : return (d1, [P0.x,P0.y]) else : return (d2, [P2.x,P2.y]) def csp_to_arc_distance(sp1,sp2, arc1, arc2, tolerance = 0.01 ): # arc = [start,end,center,alpha] n, i = 10, 0 d, d1, dl = (0,(0,0)), (0,(0,0)), 0 while i<1 or (abs(d1[0]-dl[0])>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 j<len(joined_result) : if csp_subpaths_end_to_start_distance2(joined_result[j],s1) <0.000001 : joined_result[j] = csp_concat_subpaths(joined_result[j],s1) done_smf = True joined_smf = True break if csp_subpaths_end_to_start_distance2(s1,joined_result[j]) <0.000001 : joined_result[j] = csp_concat_subpaths(s1,joined_result[j]) done_smf = True joined_smf = True break j += 1 if not joined_smf : joined_result += [s1[:]] if done_smf : result = joined_result[:] joined_result = [] return joined_result def triangle_cross(a,b,c): return (a[0]-b[0])*(c[1]-b[1]) - (c[0]-b[0])*(a[1]-b[1]) def csp_segment_convex_hull(sp1,sp2): a,b,c,d = sp1[1][:], sp1[2][:], sp2[0][:], sp2[1][:] abc = triangle_cross(a,b,c) abd = triangle_cross(a,b,d) bcd = triangle_cross(b,c,d) cad = triangle_cross(c,a,d) if abc == 0 and abd == 0 : return [min(a,b,c,d), max(a,b,c,d)] if abc == 0 : return [d, min(a,b,c), max(a,b,c)] if abd == 0 : return [c, min(a,b,d), max(a,b,d)] if bcd == 0 : return [a, min(b,c,d), max(b,c,d)] if cad == 0 : return [b, min(c,a,d), max(c,a,d)] m1, m2, m3 = abc*abd>0, 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)<small_tolerance def atan2(*arg): if len(arg)==1 and ( type(arg[0]) == type([0.,0.]) or type(arg[0])==type((0.,0.)) ) : return (math.pi/2 - math.atan2(arg[0][0], arg[0][1]) ) % math.pi2 elif len(arg)==2 : return (math.pi/2 - math.atan2(arg[0],arg[1]) ) % math.pi2 else : raise ValueError, "Bad argumets for atan! (%s)" % arg def get_text(node) : value = None if node.text!=None : value = value +"\n" + node.text if value != None else node.text for k in node : if k.tag == inkex.addNS('tspan','svg'): if k.text!=None : value = value +"\n" + k.text if value != None else k.text return value def draw_text(text,x,y, group = None, style = None, font_size = 10, gcodetools_tag = None) : if style == None : style = "font-family:DejaVu Sans;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:DejaVu Sans;fill:#000000;fill-opacity:1;stroke:none;" style += "font-size:%fpx;"%font_size attributes = { 'x': str(x), inkex.addNS("space","xml"):"preserve", 'y': str(y), 'style' : style } if gcodetools_tag!=None : attributes["gcodetools"] = str(gcodetools_tag) if group == None: group = options.doc_root t = inkex.etree.SubElement( group, inkex.addNS('text','svg'), attributes) text = str(text).split("\n") for s in text : span = inkex.etree.SubElement( t, inkex.addNS('tspan','svg'), { 'x': str(x), 'y': str(y), inkex.addNS("role","sodipodi"):"line", }) y += font_size span.text = str(s) def draw_csp(csp, stroke = "#f00", fill = "none", comment = "", width = 0.354, group = None, style = None, gcodetools_tag = None) : if style == None : style = "fill:%s;fill-opacity:1;stroke:%s;stroke-width:%s"%(fill,stroke,width) attributes = { 'd': cubicsuperpath.formatPath(csp), 'style' : style } if comment != '': attributes['comment'] = comment if group == None : group = options.doc_root return inkex.etree.SubElement( group, inkex.addNS('path','svg'), attributes) def draw_pointer(x,color = "#f00", figure = "cross", group = None, comment = "", fill=None, width = .1, size = 10., text = None, font_size=None, pointer_type=None, attrib = None) : size = size/2 if attrib == None : attrib = {} if pointer_type == None: pointer_type = "Pointer" attrib["gcodetools"] = pointer_type if group == None: group = options.self.current_layer if text != None : if font_size == None : font_size = 7 group = inkex.etree.SubElement( group, inkex.addNS('g','svg'), {"gcodetools": pointer_type+" group"} ) draw_text(text,x[0]+size*2.2,x[1]-size, group = group, font_size = font_size) if figure == "line" : s = "" for i in range(1,len(x)/2) : s+= " %s, %s " %(x[i*2],x[i*2+1]) attrib.update({"d": "M %s,%s L %s"%(x[0],x[1],s), "style":"fill:none;stroke:%s;stroke-width:%f;"%(color,width),"comment":str(comment)}) inkex.etree.SubElement( group, inkex.addNS('path','svg'), attrib) elif figure == "arrow" : if fill == None : fill = "#12b3ff" fill_opacity = "0.8" d = "m %s,%s " % (x[0],x[1]) + re.sub("([0-9\-.e]+)",(lambda match: str(float(match.group(1))*size*2.)), "0.88464,-0.40404 c -0.0987,-0.0162 -0.186549,-0.0589 -0.26147,-0.1173 l 0.357342,-0.35625 c 0.04631,-0.039 0.0031,-0.13174 -0.05665,-0.12164 -0.0029,-1.4e-4 -0.0058,-1.4e-4 -0.0087,0 l -2.2e-5,2e-5 c -0.01189,0.004 -0.02257,0.0119 -0.0305,0.0217 l -0.357342,0.35625 c -0.05818,-0.0743 -0.102813,-0.16338 -0.117662,-0.26067 l -0.409636,0.88193 z") attrib.update({"d": d, "style":"fill:%s;stroke:none;fill-opacity:%s;"%(fill,fill_opacity),"comment":str(comment)}) inkex.etree.SubElement( group, inkex.addNS('path','svg'), attrib) else : attrib.update({"d": "m %s,%s l %f,%f %f,%f %f,%f %f,%f , %f,%f"%(x[0],x[1], size,size, -2*size,-2*size, size,size, size,-size, -2*size,2*size ), "style":"fill:none;stroke:%s;stroke-width:%f;"%(color,width),"comment":str(comment)}) inkex.etree.SubElement( group, inkex.addNS('path','svg'), attrib) def straight_segments_intersection(a,b, true_intersection = True) : # (True intersection means check ta and tb are in [0,1]) ax,bx,cx,dx, ay,by,cy,dy = a[0][0],a[1][0],b[0][0],b[1][0], a[0][1],a[1][1],b[0][1],b[1][1] if (ax==bx and ay==by) or (cx==dx and cy==dy) : return False, 0, 0 if (bx-ax)*(dy-cy)-(by-ay)*(dx-cx)==0 : # Lines are parallel ta = (ax-cx)/(dx-cx) if cx!=dx else (ay-cy)/(dy-cy) tb = (bx-cx)/(dx-cx) if cx!=dx else (by-cy)/(dy-cy) tc = (cx-ax)/(bx-ax) if ax!=bx else (cy-ay)/(by-ay) td = (dx-ax)/(bx-ax) if ax!=bx else (dy-ay)/(by-ay) return ("Overlap" if 0<=ta<=1 or 0<=tb<=1 or 0<=tc<=1 or 0<=td<=1 or not true_intersection else False), (ta,tb), (tc,td) else : ta = ( (ay-cy)*(dx-cx)-(ax-cx)*(dy-cy) ) / ( (bx-ax)*(dy-cy)-(by-ay)*(dx-cx) ) tb = ( ax-cx+ta*(bx-ax) ) / (dx-cx) if dx!=cx else ( ay-cy+ta*(by-ay) ) / (dy-cy) return (0<=ta<=1 and 0<=tb<=1 or not true_intersection), ta, tb def isnan(x): return type(x) is float and x != x def isinf(x): inf = 1e5000; return x == inf or x == -inf def between(c,x,y): return x-straight_tolerance<=c<=y+straight_tolerance or y-straight_tolerance<=c<=x+straight_tolerance def cubic_solver_real(a,b,c,d): # returns only real roots of a cubic equation. roots = cubic_solver(a,b,c,d) res = [] for root in roots : if type(root) is complex : if -1e-10<root.imag<1e-10 : res.append(root.real) else : res.append(root) return res def cubic_solver(a,b,c,d): if a!=0: # Monics formula see http://en.wikipedia.org/wiki/Cubic_function#Monic_formula_of_roots a,b,c = (b/a, c/a, d/a) m = 2*a**3 - 9*a*b + 27*c k = a**2 - 3*b n = m**2 - 4*k**3 w1 = -.5 + .5*cmath.sqrt(3)*1j w2 = -.5 - .5*cmath.sqrt(3)*1j if n>=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<options.biarc_max_split_depth: sp1,sp2,sp3 = csp_split(sp1,sp2) l1, l2 = cspseglength(sp1,sp2), cspseglength(sp2,sp3) if l1+l2 == 0 : zm = z1 else : zm = z1+(z2-z1)*l1/(l1+l2) return biarc(sp1,sp2,z1,zm,depth+1)+biarc(sp2,sp3,zm,z2,depth+1) else: return [ [sp1[1],'line', 0, 0, sp2[1], [z1,z2]] ] P0, P4 = P(sp1[1]), P(sp2[1]) TS, TE, v = (P(sp1[2])-P0), -(P(sp2[0])-P4), P0 - P4 tsa, tea, va = TS.angle(), TE.angle(), v.angle() if TE.mag()<straight_distance_tolerance and TS.mag()<straight_distance_tolerance: # Both tangents are zerro - line straight return [ [sp1[1],'line', 0, 0, sp2[1], [z1,z2]] ] if TE.mag() < straight_distance_tolerance: TE = -(TS+v).unit() r = TS.mag()/v.mag()*2 elif TS.mag() < straight_distance_tolerance: TS = -(TE+v).unit() r = 1/( TE.mag()/v.mag()*2 ) else: r=TS.mag()/TE.mag() TS, TE = TS.unit(), TE.unit() tang_are_parallel = ((tsa-tea)%math.pi<straight_tolerance or math.pi-(tsa-tea)%math.pi<straight_tolerance ) if ( tang_are_parallel and ((v.mag()<straight_distance_tolerance or TE.mag()<straight_distance_tolerance or TS.mag()<straight_distance_tolerance) or 1-abs(TS*v/(TS.mag()*v.mag()))<straight_tolerance) ): # Both tangents are parallel and start and end are the same - line straight # or one of tangents still smaller then tollerance # Both tangents and v are parallel - line straight return [ [sp1[1],'line', 0, 0, sp2[1], [z1,z2]] ] c,b,a = v*v, 2*v*(r*TS+TE), 2*r*(TS*TE-1) if v.mag()==0: return biarc_split(sp1, sp2, z1, z2, depth) asmall, bsmall, csmall = abs(a)<10**-10,abs(b)<10**-10,abs(c)<10**-10 if asmall and b!=0: beta = -c/b elif csmall and a!=0: beta = -b/a elif not asmall: discr = b*b-4*a*c if discr < 0: raise ValueError, (a,b,c,discr) disq = discr**.5 beta1 = (-b - disq) / 2 / a beta2 = (-b + disq) / 2 / a if beta1*beta2 > 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 (p0a<p2a and (p1a<p0a or p2a<p1a)) or (p2a<p1a<p0a) : alpha = -2*math.pi+alpha if abs(R.x)>1000000 or abs(R.y)>1000000 or (R-P0).mag<options.min_arc_radius**2 : return None, None else : return R, alpha R1,a1 = calculate_arc_params(P0,P1,P2) R2,a2 = calculate_arc_params(P2,P3,P4) if R1==None or R2==None or (R1-P0).mag()<straight_tolerance or (R2-P2).mag()<straight_tolerance : return [ [sp1[1],'line', 0, 0, sp2[1], [z1,z2]] ] d = csp_to_arc_distance(sp1,sp2, [P0,P2,R1,a1],[P2,P4,R2,a2]) if d > options.biarc_tolerance and depth<options.biarc_max_split_depth : return biarc_split(sp1, sp2, z1, z2, depth) else: if R2.mag()*a2 == 0 : zm = z2 else : zm = z1 + (z2-z1)*(abs(R1.mag()*a1))/(abs(R2.mag()*a2)+abs(R1.mag()*a1)) l = (P0-P2).l2() if l < EMC_TOLERANCE_EQUAL**2 or l<EMC_TOLERANCE_EQUAL**2 * R1.l2() /100 : # arc should be straight otherwise it could be threated as full circle arc1 = [ sp1[1], 'line', 0, 0, [P2.x,P2.y], [z1,zm] ] else : arc1 = [ sp1[1], 'arc', [R1.x,R1.y], a1, [P2.x,P2.y], [z1,zm] ] l = (P4-P2).l2() if l < EMC_TOLERANCE_EQUAL**2 or l<EMC_TOLERANCE_EQUAL**2 * R2.l2() /100 : # arc should be straight otherwise it could be threated as full circle arc2 = [ [P2.x,P2.y], 'line', 0, 0, [P4.x,P4.y], [zm,z2] ] else : arc2 = [ [P2.x,P2.y], 'arc', [R2.x,R2.y], a2, [P4.x,P4.y], [zm,z2] ] return [ arc1, arc2 ] def biarc_curve_segment_length(seg): if seg[1] == "arc" : return math.sqrt((seg[0][0]-seg[2][0])**2+(seg[0][1]-seg[2][1])**2)*seg[3] elif seg[1] == "line" : return math.sqrt((seg[0][0]-seg[4][0])**2+(seg[0][1]-seg[4][1])**2) else: return 0 def biarc_curve_clip_at_l(curve, l, clip_type = "strict") : # get first subcurve and ceck it's length subcurve, subcurve_l, moved = [], 0, False for seg in curve: if seg[1] == "move" and moved or seg[1] == "end" : break if seg[1] == "move" : moved = True subcurve_l += biarc_curve_segment_length(seg) if seg[1] == "arc" or seg[1] == "line" : subcurve += [seg] if subcurve_l < l and clip_type == "strict" : return [] lc = 0 if (subcurve[-1][4][0]-subcurve[0][0][0])**2 + (subcurve[-1][4][1]-subcurve[0][0][1])**2 < 10**-7 : subcurve_closed = True i = 0 reverse = False while lc<l : seg = subcurve[i] if reverse : if seg[1] == "line" : seg = [seg[4], "line", 0 , 0, seg[0], seg[5]] # Hmmm... Do we have to swap seg[5][0] and seg[5][1] (zstart and zend) or not? elif seg[1] == "arc" : seg = [seg[4], "arc", seg[2] , -seg[3], seg[0], seg[5]] # Hmmm... Do we have to swap seg[5][0] and seg[5][1] (zstart and zend) or not? ls = biarc_curve_segment_length(seg) if ls != 0 : if l-lc>ls : 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]<end[0] : if c<0 : inside = not inside elif c == 0 : return True # point is on the edge elif st[0]==end[0]==p[0] and (st[1]<=p[1]<=end[1] or end[1]<=p[1]<=st[1]) : # point is on the edge return True return inside def hull(self) : # Add vertices at all self intersection points. hull = [] for i1 in range(len(self.polygon)): poly1 = self.polygon[i1] poly_ = [] for j1 in range(len(poly1)): s, e = poly1[j1-1],poly1[j1] poly_ += [s] # Check self intersections for j2 in range(j1+1,len(poly1)): s1, e1 = poly1[j2-1],poly1[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] # 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)<angle_quadrant(sin1,cos1) : return True if sin>=0 and cos>0 : return sin<sin1 if sin>0 and cos<=0 : return sin>sin1 if sin<=0 and cos<0 : return sin>sin1 if sin<0 and cos>=0 : return sin<sin1 def get_closes_edge_by_angle(edges, last): # Last edge is normalized vector of the last edge. min_angle = [0,1] next = last last_edge = [(last[0][0]-last[1][0])/last[2], (last[0][1]-last[1][1])/last[2]] for p in edges: #draw_pointer(list(p[0])+[p[0][0]+last_edge[0]*40,p[0][1]+last_edge[1]*40], "Red", "line", width=1) #print_("len(edges)=",len(edges)) cur = [(p[1][0]-p[0][0])/p[2],(p[1][1]-p[0][1])/p[2]] cos, sin = dot(cur,last_edge), cross(cur,last_edge) #draw_pointer(list(p[0])+[p[0][0]+cur[0]*40,p[0][1]+cur[1]*40], "Orange", "line", width=1, comment = [sin,cos]) #print_("cos, sin=",cos,sin) #print_("min_angle_before=",min_angle) if angle_is_less(sin,cos,min_angle[0],min_angle[1]) : min_angle = [sin,cos] next = p #print_("min_angle=",min_angle) return next # Join edges together into new polygon cutting the vertexes inside new polygon self.polygon = [] len_edges = sum([len(edges[p]) for p in edges]) loops = 0 while len(edges)>0 : poly = [] if loops > len_edges : raise ValueError, "Hull error" loops+=1 # Find left most vertex. start = (1e100,1) for edge in edges : start = min(start, min(edges[edge])) last = [(start[0][0]-1,start[0][1]),start[0],1] first_run = True loops1 = 0 while (last[1]!=start[0] or first_run) : first_run = False if loops1 > 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<start_gene : end_gene, start_gene = start_gene, end_gene parent1, parent2 = parent2, parent1 for i in range(start_gene,end_gene) : #rotation_mutate_param = random.random()/100 #xposition_mutate_param = random.random()/100 tr = 1. #- rotation_mutate_param tp = 1. #- xposition_mutate_param specimen[i] = [parent1[i][0], parent1[i][1]*tr+parent2[i][1]*(1-tr),parent1[i][2]*tp+parent2[i][2]*(1-tp)] genes_order += [ parent1[i][0] ] for i in range(0,start_gene)+range(end_gene,self.genes_count) : tr = 0. #rotation_mutate_param tp = 0. #xposition_mutate_param j = i while parent2[j][0] in genes_order : j = (j+1)%self.genes_count specimen[i] = [parent2[j][0], parent1[i][1]*tr+parent2[i][1]*(1-tr),parent1[i][2]*tp+parent2[i][2]*(1-tp)] genes_order += [ parent2[j][0] ] for i in range(random.randint(self.mutation_genes_count[0],self.mutation_genes_count[0]*self.incest_mutation_count_multiplyer )) : if random.random() < self.order_mutate_factor * self.incest_mutation_multiplyer : i1,i2 = random.randint(0,self.genes_count-1),random.randint(0,self.genes_count-1) specimen[i1][0], specimen[i2][0] = specimen[i2][0], specimen[i1][0] if random.random() < self.move_mutation_factor * self.incest_mutation_multiplyer: i1 = random.randint(0,self.genes_count-1) specimen[i1][1] = (specimen[i1][1]+random.random()*math.pi2*self.move_mutation_multiplier)%1. specimen[i1][2] = (specimen[i1][2]+random.random()*self.move_mutation_multiplier)%1. self.population += [ [None,specimen] ] def test_spiece_drop_down(self,spiece) : surface = Polygon() for p in spiece : time_ = time.time() poly = Polygon(copy.deepcopy(self.polygons[p[0]].polygon)) poly.rotate(p[1]*math.pi2) w = poly.width() left = poly.bounds()[0] poly.move( -left + (self.width-w)*p[2],0) poly.drop_down(surface) surface.add(poly) return surface def test(self,test_function): time_ = time.time() for i in range(len(self.population)) : if self.population[i][0] == None : surface = test_function(self.population[i][1]) b = surface.bounds() self.population[i][0] = (b[3]-b[1])*(b[2]-b[0]) self.population.sort() def test_spiece_centroid(self,spiece) : poly = Polygon( self.polygons[spiece[0][0]].polygon[:]) poly.rotate(spiece[0][1]*math.pi2) surface = Polygon(poly.polygon) for p in spiece[1:] : poly = Polygon(self.polygons[p[0]].polygon[:]) c = surface.centroid() surface.move(-c[0],-c[1]) c1 = poly.centroid() poly.move(-c1[0],-c1[1]) poly.rotate(p[1]*math.pi2+p[2]*math.pi2) surface.rotate(p[2]*math.pi2) poly.drop_down(surface) surface.add(poly) surface.rotate(-p[2]*math.pi2) return surface def test_inline(self) : ### ### Fast test function using weave's from scipy inline function ### try : converters is None except : try: from scipy import weave from scipy.weave import converters except: options.self.error("For this function Scipy is needed. See http://www.cnc-club.ru/gcodetools for details.","error") # Prepare vars poly_, subpoly_, points_ = [], [], [] for poly in self.polygons : p = poly.polygon poly_ += [len(subpoly_), len(subpoly_)+len(p)*2] for subpoly in p : subpoly_ += [len(points_), len(points_)+len(subpoly)*2+2] for point in subpoly : points_ += point points_ += subpoly[0] # Close subpolygon test_ = [] population_ = [] for spiece in self.population: test_.append( spiece[0] if spiece[0] != None else -1) for sp in spiece[1]: population_ += sp lp_, ls_, l_, lt_ = len(poly_), len(subpoly_), len(points_), len(test_) f = open('inline_test.c', 'r') code = f.read() f.close() f = open('inline_test_functions.c', 'r') functions = f.read() f.close() stdout_ = sys.stdout s = '' sys.stdout = s test = weave.inline( code, ['points_','subpoly_','poly_', 'lp_', 'ls_', 'l_', 'lt_','test_', 'population_'], compiler='gcc', support_code = functions, ) if s!='' : options.self.error(s,"warning") sys.stdout = stdout_ for i in range(len(test_)): self.population[i][0] = test_[i] #surface.draw() ################################################################################ ### ### Gcodetools class ### ################################################################################ class Gcodetools(inkex.Effect): def export_gcode(self,gcode, no_headers = False) : if self.options.postprocessor != "" or self.options.postprocessor_custom != "" : postprocessor = Postprocessor(self.error) postprocessor.gcode = gcode if self.options.postprocessor != "" : postprocessor.process(self.options.postprocessor) if self.options.postprocessor_custom != "" : postprocessor.process(self.options.postprocessor_custom) if not no_headers : postprocessor.gcode = self.header + postprocessor.gcode + self.footer f = open(self.options.directory+self.options.file, "w") f.write(postprocessor.gcode) f.close() ################################################################################ ### In/out paths: ### TODO move it to the bottom ################################################################################ def plasma_prepare_path(self) : def add_arc(sp1,sp2,end = False,l=10.,r=10.) : if not end : n = csp_normalized_normal(sp1,sp2,0.) return csp_reverse([arc_from_s_r_n_l(sp1[1],r,n,-l)])[0] else: n = csp_normalized_normal(sp1,sp2,1.) return arc_from_s_r_n_l(sp2[1],r,n,l) def add_normal(sp1,sp2,end = False,l=10.,r=10.) : # r is needed only for be compatible with add_arc if not end : n = csp_normalized_normal(sp1,sp2,0.) p = [n[0]*l+sp1[1][0],n[1]*l+sp1[1][1]] return csp_subpath_line_to([], [p,sp1[1]]) else: n = csp_normalized_normal(sp1,sp2,1.) p = [n[0]*l+sp2[1][0],n[1]*l+sp2[1][1]] return csp_subpath_line_to([], [sp2[1],p]) def add_tangent(sp1,sp2,end = False,l=10.,r=10.) : # r is needed only for be compatible with add_arc if not end : n = csp_normalized_slope(sp1,sp2,0.) p = [-n[0]*l+sp1[1][0],-n[1]*l+sp1[1][1]] return csp_subpath_line_to([], [p,sp1[1]]) else: n = csp_normalized_slope(sp1,sp2,1.) p = [n[0]*l+sp2[1][0],n[1]*l+sp2[1][1]] return csp_subpath_line_to([], [sp2[1],p]) if not self.options.in_out_path and not self.options.plasma_prepare_corners and self.options.in_out_path_do_not_add_reference_point: self.error("Warning! Extenstion is not said to do anything! Enable one of Create in-out paths or Prepare corners checkboxes or disable Do not add in-out referense point!") return # Add in-out-reference point if there is no one yet. if ( (len(self.in_out_reference_points)==0 and self.options.in_out_path or not self.options.in_out_path and not self.options.plasma_prepare_corners ) and not self.options.in_out_path_do_not_add_reference_point) : self.options.orientation_points_count = "in-out reference point" self.orientation() if self.options.in_out_path or self.options.plasma_prepare_corners: self.set_markers() add_func = {"Round":add_arc, "Perpendicular": add_normal, "Tangent": add_tangent}[self.options.in_out_path_type] if self.options.in_out_path_type == "Round" and self.options.in_out_path_len > 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(_("Nothing 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_)<max_dist**2: s1,s2 = csp_normalized_slope(sp1,sp2,1.), csp_normalized_slope(sp2,sp3,0.) max_cross = max(max_cross,[cross(s1,s2),j-1]) # return back last point subpath.append(subpath[0]) if max_cross[1] !=None and max_cross[0]>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 i<len(c) else None for i in range(6)] if c[5] == 0 : c[5]=None s,s1 = [" X", " Y", " Z", " I", " J", " K"], ["","","","","",""] m,a = [1,1,self.options.Zscale*Zauto_scale,1,1,self.options.Zscale*Zauto_scale], [0,0,self.options.Zoffset,0,0,0] r = '' for i in range(6): if c[i]!=None: r += s[i] + ("%f" % (c[i]*m[i]+a[i])) + s1[i] return r def calculate_angle(a, current_a): return min( [abs(a-current_a%math.pi2+math.pi2), a+current_a-current_a%math.pi2+math.pi2], [abs(a-current_a%math.pi2-math.pi2), a+current_a-current_a%math.pi2-math.pi2], [abs(a-current_a%math.pi2), a+current_a-current_a%math.pi2])[1] if len(curve)==0 : return "" try : self.last_used_tool == None except : self.last_used_tool = None print_("working on curve") print_(curve) if tool != self.last_used_tool : g += ( "(Change tool to %s)\n" % re.sub("\"'\(\)\\\\"," ",tool["name"]) ) + tool["tool change gcode"] + "\n" lg, zs, f = 'G00', self.options.Zsafe, " F%f"%tool['feed'] current_a = 0 go_to_safe_distance = "G00" + c([None,None,zs]) + "\n" penetration_feed = " F%s"%tool['penetration feed'] for i in range(1,len(curve)): # Creating Gcode for curve between s=curve[i-1] and si=curve[i] start at s[0] end at s[4]=si[0] s, si = curve[i-1], curve[i] feed = f if lg not in ['G01','G02','G03'] else '' if s[1] == 'move': g += go_to_safe_distance + "G00" + c(si[0]) + "\n" + tool['gcode before path'] + "\n" lg = 'G00' elif s[1] == 'end': g += go_to_safe_distance + tool['gcode after path'] + "\n" lg = 'G00' elif s[1] == 'line': if tool['4th axis meaning'] == "tangent knife" : a = atan2(si[0][0]-s[0][0],si[0][1]-s[0][1]) a = calculate_angle(a, current_a) g+="G01 A%s\n" % (a*tool['4th axis scale']+tool['4th axis offset']) current_a = a if lg=="G00": g += "G01" + c([None,None,s[5][0]+depth]) + penetration_feed +"(Penetrate)\n" g += "G01" +c(si[0]+[s[5][1]+depth]) + feed + "\n" lg = 'G01' elif s[1] == 'arc': r = [(s[2][0]-s[0][0]), (s[2][1]-s[0][1])] if tool['4th axis meaning'] == "tangent knife" : if s[3]<0 : # CW a1 = atan2(s[2][1]-s[0][1],-s[2][0]+s[0][0]) + math.pi else: #CCW a1 = atan2(-s[2][1]+s[0][1],s[2][0]-s[0][0]) + math.pi a = calculate_angle(a1, current_a) g+="G01 A%s\n" % (a*tool['4th axis scale']+tool['4th axis offset']) current_a = a axis4 = " A%s"%((current_a+s[3])*tool['4th axis scale']+tool['4th axis offset']) current_a = current_a+s[3] else : axis4 = "" if lg=="G00": g += "G01" + c([None,None,s[5][0]+depth]) + penetration_feed + "(Penetrate)\n" if (r[0]**2 + r[1]**2)>self.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]<minx: minx=p[0] out[0]=[p] if miny==p[1]: out[1]+=[p] if miny==None or p[1]<miny: miny=p[1] out[1]=[p] if maxx==p[0]: out[2]+=[p] if maxx==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]<z : curves_.append(curve) z = zs - abs(self.tools[layer][0]["depth step"]*(step+1)) gcode += "\n(Pass at depth %s)\n"%z 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: gcode += gcode_comment_str("Start cutting path id: %s"%curves[key][0][0]) if curves[key][0][2] != "()" : gcode += curves[key][0][2] # add comment for subcurve in curves_[key][1]: gcode += self.generate_gcode(subcurve, layer, max(z,curves_[key][0][1])) gcode += gcode_comment_str("End cutting path id: %s\n\n"%curves[key][0][0]) self.export_gcode(gcode) ################################################################################ ### ### dxfpoints ### ################################################################################ def dxfpoints(self): if self.selected_paths == {}: self.error(_("Nothing is selected. Please select something to convert to drill point (dxfpoint) or clear point sign."),"warning") for layer in self.layers : if layer in self.selected_paths : for path in self.selected_paths[layer]: # print_(("processing path",path.get('d'))) if self.options.dxfpoints_action == 'replace': # print_("trying to set as dxfpoint") path.set("dxfpoint","1") r = re.match("^\s*.\s*(\S+)",path.get("d")) if r!=None: print_(("got path=",r.group(1))) path.set("d","m %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" % r.group(1)) path.set("style",styles["dxf_points"]) if self.options.dxfpoints_action == 'save': path.set("dxfpoint","1") if self.options.dxfpoints_action == 'clear' and path.get("dxfpoint") == "1": path.set("dxfpoint","0") # for id, node in self.selected.iteritems(): # print_((id,node,node.attrib)) ################################################################################ ### ### Artefacts ### ################################################################################ def area_artefacts(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 paths : # paths[layer].reverse() # Reverse list of paths to leave their order for path in paths[layer] : parent = path.getparent() style = path.get("style") if "style" in path.keys() else "" 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")) remove = [] for i in range(len(csp)) : subpath = [ [point[:] for point in points] for points in csp[i]] subpath = self.apply_transforms(path,[subpath])[0] bounds = csp_simple_bound([subpath]) if (bounds[2]-bounds[0])**2+(bounds[3]-bounds[1])**2 < self.options.area_find_artefacts_diameter**2: if self.options.area_find_artefacts_action == "mark with an arrow" : arrow = cubicsuperpath.parsePath( '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' % (subpath[0][1][0],subpath[0][1][1]) ) arrow = self.apply_transforms(path,arrow,True) inkex.etree.SubElement(parent, inkex.addNS('path','svg'), { 'd': cubicsuperpath.formatPath(arrow), 'style': styles["area artefact arrow"], 'gcodetools': 'area artefact arrow', }) elif self.options.area_find_artefacts_action == "mark with style" : inkex.etree.SubElement(parent, inkex.addNS('path','svg'), {'d': cubicsuperpath.formatPath(csp[i]), 'style': styles["area artefact"]}) remove.append(i) elif self.options.area_find_artefacts_action == "delete" : remove.append(i) print_("Deleted artefact %s" % subpath ) remove.reverse() for i in remove : del csp[i] if len(csp) == 0 : parent.remove(path) else : path.set("d", cubicsuperpath.formatPath(csp)) return ################################################################################ ### ### Calculate area curves ### ################################################################################ def area(self) : 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") 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<options.biarc_max_split_depth: sp1,sp2,sp3 = csp_split(sp1,sp2) l1, l2 = cspseglength(sp1,sp2), cspseglength(sp2,sp3) if l1+l2 == 0 : zm = z1 else : zm = z1+(z2-z1)*l1/(l1+l2) return biarc(sp1,sp2,z1,zm,depth+1)+biarc(sp2,sp3,zm,z2,depth+1) else: return [ [sp1[1],'line', 0, 0, sp2[1], [z1,z2]] ] P0, P4 = P(sp1[1]), P(sp2[1]) TS, TE, v = (P(sp1[2])-P0), -(P(sp2[0])-P4), P0 - P4 tsa, tea, va = TS.angle(), TE.angle(), v.angle() if TE.mag()<straight_distance_tolerance and TS.mag()<straight_distance_tolerance: # Both tangents are zerro - line straight return [ [sp1[1],'line', 0, 0, sp2[1], [z1,z2]] ] if TE.mag() < straight_distance_tolerance: TE = -(TS+v).unit() r = TS.mag()/v.mag()*2 elif TS.mag() < straight_distance_tolerance: TS = -(TE+v).unit() r = 1/( TE.mag()/v.mag()*2 ) else: r=TS.mag()/TE.mag() TS, TE = TS.unit(), TE.unit() tang_are_parallel = ((tsa-tea)%math.pi<straight_tolerance or math.pi-(tsa-tea)%math.pi<straight_tolerance ) if ( tang_are_parallel and ((v.mag()<straight_distance_tolerance or TE.mag()<straight_distance_tolerance or TS.mag()<straight_distance_tolerance) or 1-abs(TS*v/(TS.mag()*v.mag()))<straight_tolerance) ): # Both tangents are parallel and start and end are the same - line straight # or one of tangents still smaller then tollerance # Both tangents and v are parallel - line straight return [ [sp1[1],'line', 0, 0, sp2[1], [z1,z2]] ] c,b,a = v*v, 2*v*(r*TS+TE), 2*r*(TS*TE-1) if v.mag()==0: return biarc_split(sp1, sp2, z1, z2, depth) asmall, bsmall, csmall = abs(a)<10**-10,abs(b)<10**-10,abs(c)<10**-10 if asmall and b!=0: beta = -c/b elif csmall and a!=0: beta = -b/a elif not asmall: discr = b*b-4*a*c if discr < 0: raise ValueError, (a,b,c,discr) disq = discr**.5 beta1 = (-b - disq) / 2 / a beta2 = (-b + disq) / 2 / a if beta1*beta2 > 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 (p0a<p2a and (p1a<p0a or p2a<p1a)) or (p2a<p1a<p0a) : alpha = -2*math.pi+alpha if abs(R.x)>1000000 or abs(R.y)>1000000 or (R-P0).mag<options.min_arc_radius**2 : return None, None else : return R, alpha R1,a1 = calculate_arc_params(P0,P1,P2) R2,a2 = calculate_arc_params(P2,P3,P4) if R1==None or R2==None or (R1-P0).mag()<straight_tolerance or (R2-P2).mag()<straight_tolerance : return [ [sp1[1],'line', 0, 0, sp2[1], [z1,z2]] ] d = csp_to_arc_distance(sp1,sp2, [P0,P2,R1,a1],[P2,P4,R2,a2]) if d > options.biarc_tolerance and depth<options.biarc_max_split_depth : return biarc_split(sp1, sp2, z1, z2, depth) else: if R2.mag()*a2 == 0 : zm = z2 else : zm = z1 + (z2-z1)*(abs(R1.mag()*a1))/(abs(R2.mag()*a2)+abs(R1.mag()*a1)) l = (P0-P2).l2() if l < EMC_TOLERANCE_EQUAL**2 or l<EMC_TOLERANCE_EQUAL**2 * R1.l2() /100 : # arc should be straight otherwise it could be threated as full circle arc1 = [ sp1[1], 'line', 0, 0, [P2.x,P2.y], [z1,zm] ] else : arc1 = [ sp1[1], 'arc', [R1.x,R1.y], a1, [P2.x,P2.y], [z1,zm] ] l = (P4-P2).l2() if l < EMC_TOLERANCE_EQUAL**2 or l<EMC_TOLERANCE_EQUAL**2 * R2.l2() /100 : # arc should be straight otherwise it could be threated as full circle arc2 = [ [P2.x,P2.y], 'line', 0, 0, [P4.x,P4.y], [zm,z2] ] else : arc2 = [ [P2.x,P2.y], 'arc', [R2.x,R2.y], a2, [P4.x,P4.y], [zm,z2] ] return [ arc1, arc2 ] for layer in self.layers : if layer in self.selected_paths : for path in self.selected_paths[layer]: 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 = self.transform_csp(csp, layer) # lets pretend that csp is a polyline poly = [ [point[1] for point in subpath] for subpath in csp ] self.draw_csp([ [ [point,point,point] for point in subpoly] for subpoly in poly ],layer) # lets create biarcs for subpoly in poly : # lets split polyline into different smooth parths. if len(subpoly)>2 : 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] or last_one) : if 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 <svg:g id="layern" inkscape:label="layername"> # #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 <svg:g id="gnnnnn"> 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)<limit and abs(dx*cy-cx*dy)<limit : return [[a,b,c,d]] abx=(a[0]+b[0])/2.0 aby=(a[1]+b[1])/2.0 bcx=(b[0]+c[0])/2.0 bcy=(b[1]+c[1])/2.0 cdx=(c[0]+d[0])/2.0 cdy=(c[1]+d[1])/2.0 abcx=(abx+bcx)/2.0 abcy=(aby+bcy)/2.0 bcdx=(bcx+cdx)/2.0 bcdy=(bcy+cdy)/2.0 m=[(abcx+bcdx)/2.0,(abcy+bcdy)/2.0] return bez_divide(a,[abx,aby],[abcx,abcy],m) + bez_divide(m,[bcdx,bcdy],[cdx,cdy],d) #end of bez_divide def get_biggest((x1,y1),(nx,ny)): """LT Find biggest circle we can draw inside path at point x1,y1 normal nx,ny Parameters: point - either on a line or at a reflex corner normal - normalised to 1 if on a line, to 1/cos(a) at a corner Returns: tuple (j,i,r) ..where j and i are indices of limiting segment, r is radius """ global max_dist, nlLT, i, j n1 = nlLT[j][i-1] #current node jjmin = -1 iimin = -1 r = max_dist # set limits within which to look for lines xmin, xmax = x1+r*nx-r, x1+r*nx+r ymin, ymax = y1+r*ny-r, y1+r*ny+r for jj in xrange(0,len(nlLT)) : #for every subpath of this object for ii in xrange(0,len(nlLT[jj])) : #for every point and line if nlLT[jj][ii-1][2] : #if a point if jj==j : #except this one if abs(ii-i)<3 or abs(ii-i)>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 (x23max<xmin or x23min>xmax or y23max<ymin or y23min>ymax) : 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<=t1<r : r = t1 iimin = ii jjmin = jj xmin, xmax = x1+r*nx-r, x1+r*nx+r ymin, ymax = y1+r*ny-r, y1+r*ny+r #next ii #next jj return (jjmin,iimin,r) #end of get_biggest def line_divide((x0,y0),j0,i0,(x1,y1),j1,i1,(nx,ny),length): """LT recursively divide a line as much as necessary NOTE: This function is not currently used By noting which other path segment is touched by the circles at each end, we can see if anything is to be gained by a further subdivision, since if they touch the same bit of path we can move linearly between them. Also, we can handle points correctly. Parameters: end points and indices of limiting path, normal, length Returns: list of toolpath points each a list of 3 reals: x, y coordinates, radius """ global nlLT, i, j, lmin x2=(x0+x1)/2 y2=(y0+y1)/2 j2,i2,r2=get_biggest( (x2,y2), (nx,ny)) if length<lmin : return [ [x2, y2, r2] ] if j2==j0 and i2==i0 : #Same as left end. Don't subdivide this part any more return [ [x2, y2, r2], line_divide((x2,y2),j2,i2,(x1,y1),j1,i1,(nx,ny),length/2)] if j2==j1 and i2==i1 : #Same as right end. Don't subdivide this part any more return [ line_divide((x0,y0),j0,i0,(x2,y2),j2,i2,(nx,ny),length/2), [x2, y2, r2] ] return [ line_divide((x0,y0),j0,i0,(x2,y2),j2,i2,(nx,ny),length/2), line_divide((x2,y2),j2,i2,(x1,y1),j1,i1,(nx,ny),length/2)] #end of line_divide() def save_point((x,y),w,i,j,ii,jj): """LT Save this point and delete previous one if linear The point is, we generate tons of points but many may be in a straight 3D line. There is no benefit in saving the imtermediate points. """ global wl, cspm x=round(x,4) #round to 4 decimals y=round(y,4) #round to 4 decimals w=round(w,4) #round to 4 decimals if len(cspm)>1 : 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 xydist<engraving_tolerance : #so far so good wdist=w2+(w-w2)*length1/length2 -w1 if abs(wdist)<engraving_tolerance : #print_("pop",j,i,xy1) cspm.pop() wl.pop() cspm+=[ [ [x,y],[x,y],[x,y],i,j,ii,jj ] ] wl+=[w] #end of save_point def draw_point((x0,y0),(x,y),w,t): """LT Draw this point as a circle with a 1px dot in the middle (x,y) and a 3D line from (x0,y0) down to x,y. 3D line thickness should be t/2 Note that points that are subsequently erased as being unneeded do get displayed, but this helps the user see the total area covered. """ global gcode_3Dleft ,gcode_3Dright if self.options.engraving_draw_calculation_paths : inkex.etree.SubElement( engraving_group, inkex.addNS('path','svg'), {"gcodetools": "Engraving calculation toolpath", 'style': "fill:#ff00ff; fill-opacity:0.46; stroke:#000000; stroke-width:0.1;", inkex.addNS('cx','sodipodi'): str(x), inkex.addNS('cy','sodipodi'): str(y), inkex.addNS('rx','sodipodi'): str(1), inkex.addNS('ry','sodipodi'): str(1), inkex.addNS('type','sodipodi'): 'arc'}) #Don't draw zero radius circles if w: 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(x), inkex.addNS('cy','sodipodi'): str(y),inkex.addNS('rx','sodipodi'): str(w), inkex.addNS('ry','sodipodi'): str(w), inkex.addNS('type','sodipodi'): 'arc'}) # Find slope direction for shading s=math.atan2(y-y0,x-x0) #-pi to pi # convert to 2 hex digits as a shade of red s2="#{0:x}0000".format(int(101*(1.5-math.sin(s+0.5)))) inkex.etree.SubElement( gcode_3Dleft , inkex.addNS('path','svg'), { "d": "M %f,%f L %f,%f" %(x0-eye_dist,y0,x-eye_dist-0.14*w,y), 'style': "stroke:" + s2 + "; stroke-opacity:1; stroke-width:" + str(t/2) +" ; fill:none", "gcodetools": "Gcode G1R" }) inkex.etree.SubElement( gcode_3Dright , inkex.addNS('path','svg'), { "d": "M %f,%f L %f,%f" %(x0+eye_dist,y0,x+eye_dist+0.14*r,y), 'style': "stroke:" + s2 + "; stroke-opacity:1; stroke-width:" + str(t/2) +" ; fill:none", "gcodetools": "Gcode G1L" }) #end of draw_point #end of subfunction definitions. engraving() starts here: gcode = '' r,w, wmax = 0,0,0 #theoretical and tool-radius-limited radii in pixels x1,y1,nx,ny =0,0,0,0 cspe =[] we = [] if len(self.selected_paths)<=0: self.error(_("Please select at least one path to engrave and run again."),"warning") return if not self.check_dir() : return #Find what units the user uses unit=" mm" if self.options.unit == "G20 (All units in inches)" : unit=" inches" elif self.options.unit != "G21 (All units in mm)" : self.error(_("Unknown unit selected. mm assumed"),"warning") print_("engraving_max_dist mm/inch", self.options.engraving_max_dist ) #LT See if we can use this parameter for line and Bezier subdivision: bitlen=20/self.options.engraving_newton_iterations for layer in self.layers : if layer in self.selected_paths : #Calculate scale in pixels per user unit (mm or inch) p1=self.orientation_points[layer][0][0] p2=self.orientation_points[layer][0][1] ol=math.hypot(p1[0][0]-p2[0][0],p1[0][1]-p2[0][1]) oluu=math.hypot(p1[1][0]-p2[1][0],p1[1][1]-p2[1][1]) print_("Orientation2 p1 p2 ol oluu",p1,p2,ol,oluu) orientation_scale = ol/oluu self.set_tool(layer) shape = self.tools[layer][0]['shape'] if re.search('w', shape) : toolshape = eval('lambda w: ' + shape.strip('"')) else: self.error(_("Tool '%s' has no shape. 45 degree cone assumed!") % self.tools[layer][0]['name'],"Continue") toolshape = lambda w: w #Get tool radius in pixels toolr=self.tools[layer][0]['diameter'] * orientation_scale/2 print_("tool radius in pixels=", toolr) #max dist from path to engrave in user's units max_distuu = min(self.tools[layer][0]['diameter']/2, self.options.engraving_max_dist) max_dist=max_distuu*orientation_scale print_("max_dist pixels", max_dist ) engraving_group = inkex.etree.SubElement( self.selected_paths[layer][0].getparent(), inkex.addNS('g','svg') ) if self.options.engraving_draw_calculation_paths and (self.my3Dlayer == None) : self.my3Dlayer=inkex.etree.SubElement(self.document.getroot(), 'g') #Create a generic element at root level self.my3Dlayer.set(inkex.addNS('label', 'inkscape'), "3D") #Gives it a name self.my3Dlayer.set(inkex.addNS('groupmode', 'inkscape'), 'layer') #Tells Inkscape it's a layer #Create groups for left and right eyes if self.options.engraving_draw_calculation_paths : gcode_3Dleft = inkex.etree.SubElement(self.my3Dlayer, inkex.addNS('g','svg'), {"gcodetools":"Gcode 3D L"}) gcode_3Dright = inkex.etree.SubElement(self.my3Dlayer, inkex.addNS('g','svg'), {"gcodetools":"Gcode 3D R"}) for node in self.selected_paths[layer] : if node.tag == inkex.addNS('path','svg'): cspi = cubicsuperpath.parsePath(node.get('d')) #LT: Create my own list. n1LT[j] is for subpath j nlLT = [] for j in xrange(len(cspi)): #LT For each subpath... # Remove zero length segments, assume closed path i = 0 #LT was from i=1 while i<len(cspi[j]): if abs(cspi[j][i-1][1][0]-cspi[j][i][1][0])<engraving_tolerance and abs(cspi[j][i-1][1][1]-cspi[j][i][1][1])<engraving_tolerance: cspi[j][i-1][2] = cspi[j][i][2] del cspi[j][i] else: i += 1 for csp in cspi: #LT6a For each subpath... #Create copies in 3D layer print_("csp is zz ",csp) cspl=[] cspr=[] #create list containing lines and points, starting with a point # line members: [x,y],[nx,ny],False,i # x,y is start of line. Normal on engraved side. # Normal is normalised (unit length) #Note that Y axis increases down the page # corner members: [x,y],[nx,ny],True,sin(halfangle) # if halfangle>0: 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 l1<engraving_tolerance : continue nx1=nx1/l1 #normalise them ny1=ny1/l1 nlLT[-1]+=[[bLT[seg],[nx1,ny1], False,i]] first=False if seg<2 : #get outgoing bisector nx0=nx1 ny0=ny1 nx1=bLT[seg+1][1]-bLT[seg+2][1] ny1=bLT[seg+2][0]-bLT[seg+1][0] l1=math.hypot(nx1,ny1) if l1<engraving_tolerance : continue nx1=nx1/l1 #normalise them ny1=ny1/l1 #bisect bx,by,s=bisect((nx0,ny0),(nx1,ny1)) nlLT[-1] += [[bLT[seg+1],[bx,by], True, 0.]] #LT for each segment - ends here. print_(("engraving_draw_calculation_paths=",self.options.engraving_draw_calculation_paths)) if self.options.engraving_draw_calculation_paths: #Copy complete paths to 3D layer #print_("cspl",cspl) cspl+=[cspl[0]] #Close paths cspr+=[cspr[0]] #Close paths inkex.etree.SubElement( gcode_3Dleft , inkex.addNS('path','svg'), { "d": cubicsuperpath.formatPath([cspl]), 'style': "stroke:#808080; stroke-opacity:1; stroke-width:0.6; fill:none", "gcodetools": "G1L outline" }) inkex.etree.SubElement( gcode_3Dright , inkex.addNS('path','svg'), { "d": cubicsuperpath.formatPath([cspr]), 'style': "stroke:#808080; stroke-opacity:1; stroke-width:0.6; fill:none", "gcodetools": "G1L outline" }) for p in nlLT[-1]: #For last sub-path if p[2]: inkex.etree.SubElement( engraving_group, inkex.addNS('path','svg'), { "d": "M %f,%f L %f,%f" %(p[0][0],p[0][1],p[0][0]+p[1][0]*10,p[0][1]+p[1][1]*10), 'style': "stroke:#f000af; stroke-opacity:0.46; stroke-width:0.1; fill:none", "gcodetools": "Engraving normals" }) else: inkex.etree.SubElement( engraving_group, inkex.addNS('path','svg'), { "d": "M %f,%f L %f,%f" %(p[0][0],p[0][1],p[0][0]+p[1][0]*10,p[0][1]+p[1][1]*10), 'style': "stroke:#0000ff; stroke-opacity:0.46; stroke-width:0.1; fill:none", "gcodetools": "Engraving bisectors" }) #LT6a build nlLT[j] for each subpath - ends here #for nnn in nlLT : #print_("nlLT",nnn) #LT debug stuff # Calculate offset points reflex=False for j in xrange(len(nlLT)): #LT6b for each subpath cspm=[] #Will be my output. List of csps. wl=[] #Will be my w output list w = r = 0 #LT initial, as first point is an angle for i in xrange(len(nlLT[j])) : #LT for each node #LT Note: Python enables wrapping of array indices # backwards to -1, -2, but not forwards. Hence: n0 = nlLT[j][i-2] #previous node n1 = nlLT[j][i-1] #current node n2 = nlLT[j][i] #next node #if n1[2] == True and n1[3]==0 : # A straight angle #continue x1a,y1a = n1[0] #this point/start of this line nx,ny = n1[1] x1b,y1b = n2[0] #next point/end of this line if n1[2] == True : # We're at a corner bits=1 bit0=0 #lastr=r #Remember r from last line lastw=w #Remember w from last line w = max_dist if n1[3]>0 : #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 w<lastw : #need to adjust it draw_point((x1,y1),(n0[0][0]+n0[1][0]*w,n0[0][1]+n0[1][1]*w),w, (lastw-w)/2) save_point((n0[0][0]+n0[1][0]*w,n0[0][1]+n0[1][1]*w),w,i,j,iimin,jjmin) if n1[2] == True : # We're at a corner if n1[3]>0 : #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 = self.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 0<a<=math.pi/2 or -math.pi<a<-math.pi/2 else 1 if not orientation: o = 1-o # Add first horisontal straight line if needed if not first_seg and new_subpath==[] : new_subpath = [ [[subpath[0][i][0] - width*o ,subpath[0][i][1]] for i in range(3)] ] new_subpath = csp_concat_subpaths( new_subpath, [ [[sp1[i][0] - width*o ,sp1[i][1]] for i in range(3)], [[sp2[i][0] - width*o ,sp2[i][1]] for i in range(3)] ] ) first_seg = False # Add last horisontal straigth line if needed if a==0 or a==math.pi : new_subpath += [ [[subpath[-1][i][0] - width*o ,subpath[-1][i][1]] for i in range(3)] ] new_csp += [new_subpath] self.draw_csp(new_csp,layer) # # o = (1 if cross(n, [0,1])>0 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 y<len(self.graffiti_preview) and x*4<len(self.graffiti_preview[0]) : d = math.sqrt( (x1-x)**2 +(y1-y)**2 ) a = float(a_)*( max(0,(1-(d-radius))) if d>radius 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 y1<y2 else x2 y = min(y1,y2) x,y = graffiti_preview_transform(x,y) graffiti_preview_draw_point(x,y,color) def create_connector(p1,p2,t1,t2): P1,P2 = P(p1), P(p2) N1, N2 = P(rotate_ccw(t1)), P(rotate_ccw(t2)) r = self.options.graffiti_min_radius C1,C2 = P1+N1*r, P2+N2*r # Get closest possible centers of arcs, also we define that arcs are both ccw or both not. dc, N1, N2, m = ( ( (((P2-N1*r) - (P1-N2*r)).l2(),-N1,-N2, 1) if vectors_ccw(t1,t2) else (((P2+N1*r) - (P1+N2*r)).l2(), N1, N2,-1) ) if vectors_ccw((P1-C1).to_list(),t1) == vectors_ccw((P2-C2).to_list(),t2) else ( (((P2+N1*r) - (P1-N2*r)).l2(), N1,-N2, 1) if vectors_ccw(t1,t2) else (((P2-N1*r) - (P1+N2*r)).l2(),-N1, N2, 1) ) ) dc = math.sqrt(dc) C1,C2 = P1+N1*r, P2+N2*r Dc = C2-C1 if dc == 0 : # can be joined by one arc return csp_from_arc(p1, p2, C1.to_list(), r, t1) cos, sin = Dc.x/dc, Dc.y/dc #draw_csp(self.transform_csp([[ [[C1.x-r*sin,C1.y+r*cos]]*3,[[C2.x-r*sin,C2.y+r*cos]]*3 ]],layer,reverse=True), color = "#00ff00;" ) #draw_pointer(self.transform(C1.to_list(),layer,reverse=True)) #draw_pointer(self.transform(C2.to_list(),layer,reverse=True)) p1_end = [C1.x-r*sin*m,C1.y+r*cos*m] p2_st = [C2.x-r*sin*m,C2.y+r*cos*m] if point_to_point_d2(p1,p1_end)<0.0001 and point_to_point_d2(p2,p2_st)<0.0001 : return ([[p1,p1,p1],[p2,p2,p2]]) arc1 = csp_from_arc(p1, p1_end, C1.to_list(), r, t1) arc2 = csp_from_arc(p2_st, p2, C2.to_list(), r, [cos,sin]) return csp_concat_subpaths(arc1,arc2) if not self.check_dir() : return 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.tool = [] gcode = """(Header) (Generated by gcodetools from Inkscape.) (Using graffiti extension.) (Header end.)""" minx,miny,maxx,maxy = float("inf"),float("inf"),float("-inf"),float("-inf") # Get all reference points and path's bounds to make preview for layer in self.layers : if layer in paths : # Set reference points if layer not in self.graffiti_reference_points: reference_points = None for i in range(self.layers.index(layer),-1,-1): if self.layers[i] in self.graffiti_reference_points : reference_points = self.graffiti_reference_points[self.layers[i]] self.graffiti_reference_points[layer] = self.graffiti_reference_points[self.layers[i]] break if reference_points == None : self.error('There are no graffiti reference points for layer %s'%layer,"error") # Transform reference points for i in range(len(self.graffiti_reference_points[layer])): self.graffiti_reference_points[layer][i][0] = self.transform(self.graffiti_reference_points[layer][i][0], layer) point = self.graffiti_reference_points[layer][i] gcode += "(Reference point %f;%f for %s axis)\n"%(point[0][0],point[0][1],point[1]) if self.options.graffiti_create_preview : for point in self.graffiti_reference_points[layer]: minx,miny,maxx,maxy = min(minx,point[0][0]), min(miny,point[0][1]), max(maxx,point[0][0]), max(maxy,point[0][1]) for path in paths[layer]: csp = cubicsuperpath.parsePath(path.get("d")) csp = self.apply_transforms(path, csp) csp = self.transform_csp(csp, layer) bounds = csp_simple_bound(csp) minx,miny,maxx,maxy = min(minx,bounds[0]), min(miny,bounds[1]), max(maxx,bounds[2]), max(maxy,bounds[3]) if self.options.graffiti_create_preview : self.graffiti_preview = list([ [255]*(4*self.options.graffiti_preview_size) for i in range(self.options.graffiti_preview_size)]) self.graffiti_preview_transform = [minx,miny,maxx,maxy] for layer in self.layers : if layer in paths : r = re.match("\s*\(\s*([0-9\-,.]+)\s*;\s*([0-9\-,.]+)\s*\)\s*",self.options.graffiti_start_pos) if r : start_point = [float(r.group(1)),float(r.group(2))] else : start_point = [0.,0.] last_sp1 = [[start_point[0],start_point[1]-10] for i in range(3)] last_sp2 = [start_point for i in range(3)] self.set_tool(layer) self.tool = self.tools[layer][0] # Change tool every layer. (Probably layer = color so it'll be # better to change it even if the tool has not been changed) gcode += ( "(Change tool to %s)\n" % re.sub("\"'\(\)\\\\"," ",self.tool["name"]) ) + self.tool["tool change gcode"] + "\n" subpaths = [] for path in paths[layer]: # Rebuild the paths to polyline. csp = cubicsuperpath.parsePath(path.get("d")) csp = self.apply_transforms(path, csp) csp = self.transform_csp(csp, layer) subpaths += csp polylines = [] while len(subpaths)>0: 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<len(subpath)-1: if (cspseglength(subpath[i],subpath[i+1])<0.00000001 ) : subpath[i][2] = subpath[i+1][2] del subpath[i+1] else : i += 1 for sp1, sp2 in zip(subpath,subpath[1:]) : if spl != None and abs(cross( csp_normalized_slope(spl,sp1,1.),csp_normalized_slope(sp1,sp2,0.) )) > 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()