#!/usr/bin/env python """ gright (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 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 """ ### ### Gcode tools v 1.2 ### import inkex, simplestyle, simplepath import cubicsuperpath, simpletransform, bezmisc import os import math import bezmisc import re import copy import sys import time import cmath import numpy _ = inkex._ def bezierparameterize(((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3))): #parametric bezier ax,ay,bx,by,cx,cy,x0,y0 = 0, 0, 0, 0, 0, 0, 0, 0 if ( (bx0,by0)==(bx1,by1) and (bx2,by2)==(bx3,by3) or (bx0,by0)==(bx1,by1)==(bx2,by2) or (bx1,by1)==(bx2,by2)==(bx3,by3) ): x0=bx0 cx=bx3-bx0 y0=by0 cy=by3-by0 elif (bx2,by2)==(bx3,by3) : x0=bx0 cx = (bx1-bx0)*2 bx = bx0-2*bx1+bx2 y0=by0 cy = (by1-by0)*2 by = by0-2*by1+by2 elif (bx0,by0)==(bx1,by1) : x0=bx1 cx = (bx2-bx1)*2 bx = bx1-2*bx2+bx3 y0=by1 cy = (by2-by1)*2 by = by1-2*by2+by3 else: x0=bx0 y0=by0 cx=3*(bx1-x0) bx=3*(bx2-bx1)-cx ax=bx3-x0-cx-bx cy=3*(by1-y0) by=3*(by2-by1)-cy ay=by3-y0-cy-by return ax,ay,bx,by,cx,cy,x0,y0 bezmisc.bezierparameterize = bezierparameterize ################################################################################ ### ### 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 options = {} defaults = { 'header': '%\n(Generated by gcode_tools from inkscape.) \nM3 \n', 'footer': 'M5 \nG00 X0.0000 Y0.0000 \nM2 \n(end)\n%' } 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', 'stroke-width':'1' }), 'biarc1': simplestyle.formatStyle({ 'stroke': '#8f8', 'fill': 'none', 'stroke-width':'1' }), 'line': simplestyle.formatStyle({ 'stroke': '#f88', 'fill': 'none', 'stroke-width':'1' }), 'area': simplestyle.formatStyle({ 'stroke': '#777', 'fill': 'none', 'stroke-width':'0.1' }), } biarc_style_dark = { 'biarc0': simplestyle.formatStyle({ 'stroke': '#33a', 'fill': 'none', 'stroke-width':'1' }), 'biarc1': simplestyle.formatStyle({ 'stroke': '#3a3', 'fill': 'none', 'stroke-width':'1' }), 'line': simplestyle.formatStyle({ 'stroke': '#a33', 'fill': 'none', 'stroke-width':'1' }), 'area': simplestyle.formatStyle({ 'stroke': '#222', 'fill': 'none', 'stroke-width':'0.3' }), } biarc_style_dark_area = { 'biarc0': simplestyle.formatStyle({ 'stroke': '#33a', 'fill': 'none', 'stroke-width':'0.1' }), 'biarc1': simplestyle.formatStyle({ 'stroke': '#3a3', 'fill': 'none', 'stroke-width':'0.1' }), 'line': simplestyle.formatStyle({ 'stroke': '#a33', 'fill': 'none', 'stroke-width':'0.1' }), 'area': simplestyle.formatStyle({ 'stroke': '#222', 'fill': 'none', 'stroke-width':'0.3' }), } biarc_style_i = { 'biarc0': simplestyle.formatStyle({ 'stroke': '#880', 'fill': 'none', 'stroke-width':'1' }), 'biarc1': simplestyle.formatStyle({ 'stroke': '#808', 'fill': 'none', 'stroke-width':'1' }), 'line': simplestyle.formatStyle({ 'stroke': '#088', 'fill': 'none', 'stroke-width':'1' }), 'area': simplestyle.formatStyle({ 'stroke': '#999', 'fill': 'none', 'stroke-width':'0.3' }), } biarc_style_dark_i = { 'biarc0': simplestyle.formatStyle({ 'stroke': '#dd5', 'fill': 'none', 'stroke-width':'1' }), 'biarc1': simplestyle.formatStyle({ 'stroke': '#d5d', 'fill': 'none', 'stroke-width':'1' }), 'line': simplestyle.formatStyle({ 'stroke': '#5dd', 'fill': 'none', 'stroke-width':'1' }), 'area': simplestyle.formatStyle({ 'stroke': '#aaa', 'fill': 'none', 'stroke-width':'0.3' }), } ################################################################################ ### ### Common functions ### ################################################################################ def isnan(x): return type(x) is float and x != x def isinf(x): inf = 1e5000; return x == inf or x == -inf ### ### Just simple output function for better debugging ### def print_(s=''): f = open(options.log_filename,"a") f.write(str(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] ### ### Functions to operate with CubicSuperPath ### def point_to_csp_simple_bound_dist(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 point_to_csp_bound_dist(p, sp1, sp2 , max_needed_distance): d = point_to_csp_simple_bound_dist(p, [[ sp1,sp2]] ) if max_needed_distance<=d : return d sp = sp1[1:]+sp2[:2] d = None int_count = 0 for i in xrange(4): x1, y1, dx, dy = sp[i-1][0],sp[i-1][1], sp[i][0]-sp[i-1][0], sp[i][1]-sp[i-1][1] if (dx**2+dy**2)>0 : d1 = min( (p[0]-x1)**2 + (p[1]-y1)**2, (p[0]-sp[i][0])**2 + (p[1]-sp[i][1])**2 ) if 0<=((p[1]-y1)*dy+(p[0]-x1)*dx)/(dx**2+dy**2)<=1: d1 = min( d1, ((p[0]-x1)*dy+(y1-p[1])*dx)**2/(dx**2+dy**2) ) else : d1 = (p[0]-x1)**2 + (p[1]-y1)**2 d = min(d,d1) if d!=None else d1 # Get intersections with horisontal or vertical lines that goes throught p and x if dx!=0 and 0<=(p[0]-x1)/dx<1 and (p[0]-x1)/dx*dy+y1>=p[1] : if p[0]-x1/dx*dy+y1==p[1]: return 0 int_count +=1 elif x1==p[0] : if y1<=p[1]<=sp[i][1] or sp[i][1]<=p[1]<=y1: return 0 elif y1<=p[1]: x2, y2, dx2, dy2 = sp[i-1][0],sp[i-1][1], sp[i][0]-sp[i-1][0], sp[i][1]-sp[i-1][1] if dx2!=0 and dy!=0: t1 = (x1-x2)/dx2 if not (0<=t1<=1 and 0<=(y2-y1+t1*dy2)/dy<=1): int_count +=1 elif x1!=x2: int_count +=1 if int_count%2 == 0 : return math.sqrt(d) else: return -math.sqrt(d) def csp_at_t(sp1,sp2,t): bez = (sp1[1][:],sp1[2][:],sp2[0][:],sp2[1][:]) return bezmisc.bezierpointatt(bez,t) def cspbezsplit(sp1, sp2, t = 0.5): s1,s2 = bezmisc.beziersplitatt((sp1[1],sp1[2],sp2[0],sp2[1]),t) return [ [sp1[0][:], sp1[1][:], list(s1[1])], [list(s1[2]), list(s1[3]), list(s2[1])], [list(s2[2]), sp2[1][:], sp2[2][:]] ] def cspbezsplitatlength(sp1, sp2, l = 0.5, tolerance = 0.01): bez = (sp1[1][:],sp1[2][:],sp2[0][:],sp2[1][:]) t = bezmisc.beziertatlength(bez, l, tolerance) return cspbezsplit(sp1, sp2, t) def cspseglength(sp1,sp2, tolerance = 0.001): 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 # rebuild_csp Adds to csp control points makin it's segments looks like segs def rebuild_csp (csp, segs, s=None): 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]0 : i = c + (p-c).unit()*r alpha = ((i-c).angle() - (P0-c).angle()) if a*alpha<0: if alpha>0: alpha = alpha-math.pi2 else: alpha = math.pi2+alpha if between(alpha,0,a) or min(abs(alpha),abs(alpha-a))tolerance and i<4): i += 1 dl = d1*1 for j in range(n+1): t = float(j)/n p = csp_at_t(sp1,sp2,t) d = min(distance_from_point_to_arc(p,arc1), distance_from_point_to_arc(p,arc2)) d1 = max(d1,d) n=n*2 return d1[0] def get_distance_from_point_to_csp(p,sp1,sp2, tolerance = 0.01 ): n, i = 10, 0 d, dl = [None,(0,0)],[0,(0,0)] while i<2 or (abs(d[0]-dl[0])>tolerance and i<4): i += 1 dl = d[:] for j in range(n+1): t = float(j)/n cp = csp_at_t(sp1,sp2,t) d = min( [(P(cp)-P(p)).mag(),t], d) if d[0]!=None else [(P(cp)-P(p)).mag(),t] n=n*2 return d def reverce_csp (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 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 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 [] 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 i.imag==0: i = i.real if type(i) is not complex and 0<=i<=1: retval.append(i) return retval ################################################################################ ### ### Biarc function ### ### Calculates biarc approximation of cubic super path segment ### splits segment if needed or approximates it with straight line ### ################################################################################ def biarc(sp1, sp2, z1, z2, depth=0): def biarc_split(sp1,sp2, z1, z2, depth): if depth 0 : raise ValueError, (a,b,c,disq,beta1,beta2) beta = max(beta1, beta2) elif asmall and bsmall: return biarc_split(sp1, sp2, z1, z2, depth) alpha = beta * r ab = alpha + beta P1 = P0 + alpha * TS P3 = P4 - beta * TE P2 = (beta / ab) * P1 + (alpha / ab) * P3 def calculate_arc_params(P0,P1,P2): D = (P0+P2)/2 if (D-P1).mag()==0: return None, None R = D - ( (D-P0).mag()**2/(D-P1).mag() )*(P1-D).unit() p0a, p1a, p2a = (P0-R).angle()%(2*math.pi), (P1-R).angle()%(2*math.pi), (P2-R).angle()%(2*math.pi) alpha = (p2a - p0a) % (2*math.pi) if (p0a1000000 or abs(R.y)>1000000 or (R-P0).mag options.biarc_tolerance and depth0: 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])) c += [ [ [subpath[-1][1][0],subpath[-1][1][1]] ,'end',0,0] ] return c def draw_curve(self, curve, group=None, style=biarc_style): if group==None: group = inkex.etree.SubElement( self.biarcGroup, inkex.addNS('g','svg') ) s, arcn = '', 0 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,True), self.transform(b,True), self.transform(c,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],True), (self.transform(si[2],True) if type(si[2])==type([]) and len(si[2])==2 else si[2]) if s!='': if s[1] == 'line': inkex.etree.SubElement( group, inkex.addNS('path','svg'), { 'style': style['line'], 'd':'M %s,%s L %s,%s' % (s[0][0], s[0][1], si[0][0], si[0][1]), 'comment': str(s) } ) 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) if a>0: a_end = a_st+a else: a_end = a_st*1 a_st = a_st+a inkex.etree.SubElement( group, inkex.addNS('path','svg'), { 'style': style['biarc%s' % (arcn%2)], 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', 'comment': str(s) }) s = si def check_dir(self): 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 + ( """ #4 = %f (Feed) #5 = 1 (Scale xy) #7 = %f (Scale z) #8 = 0 (Offset x) #9 = 0 (Offset y) #10 = %f (Offset z) #11 = %f (Safe distanse)\n""" % (self.options.feed, self.options.Zscale, self.options.Zoffset, self.options.Zsafe) if not self.options.generate_not_parametric_code else "" ) return True else: inkex.errormsg(_("Directory does not exist!")) return False def generate_gcode(self, curve, depth): def c(c): c = [c[i] if iself.options.min_arc_radius: r1, r2 = (P(s[0])-P(s[2])), (P(si[0])-P(s[2])) if abs(r1.mag()-r2.mag()) < 0.001 : if lg=="G00": g += "G01" + c([None,None,s[5][0]+depth]) + feed + "\n" 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 + "\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 + "\n" lg = 'G02' else: if lg=="G00": g += "G01" + c([None,None,s[5][0]+depth]) + "\n" g += "G01" +c(si[0]+[s[5][1]+depth]) + feed + "\n" lg = 'G01' if si[1] == 'end': g += "G00" + c([None,None,zs]) + "\n" return g def apply_transforms(self,g,csp): root = self.document.getroot() i=0 while (g.getparent()!=root) and i<10: if 'transform' in g.keys(): trans = g.get('transform') trans = simpletransform.parseTransform(trans) simpletransform.applyTransformToPath(trans, csp) g=g.getparent() return csp def transform(self,source_point, reverse=False): def search_in_group(g): p = [] for i in g: if i.tag == inkex.addNS("g",'svg') and i.get("comment") == "Gcode tools orientation point": p += [i] elif i.tag == inkex.addNS("g",'svg'): p1 = search_in_group(i) if len(p1)==3: return p1 if len(p)==3 : return p return [] if self.transform_matrix==None: # Search orientation points in current Layer firs then in whole painting for g in [self.current_layer, self.document.getroot()] if self.current_layer is not None else [self.document.getroot()]: p = search_in_group(g) if len(p)==3 : break if len(p)==3: points = [] for g in p: point = [[],[]] for node in g: if node.get('comment') == "Gcode tools orientation point arrow": point[0] = self.apply_transforms(node,cubicsuperpath.parsePath(node.get("d")))[0][0][1] if node.get('comment') == "Gcode tools orientation point text": r = re.match(r'(?i)\s*\(\s*(\d*(,|\.)*\d*)\s*;\s*(\d*(,|\.)*\d*)\)\s*',node.text) point[1] = [float(r.group(1)),float(r.group(3))] if point[0]!=[] and point[1]!=[]: points += [point] if len(points)==3: 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 = [[m[j*3+i][0] for i in range(3)] for j in range(3)] else : self.transform_matrix = [[1,0,0],[0,1,0],[0,0,1]] else : self.transform_matrix = [[1,0,0],[0,1,0],[0,0,1]] else : self.transform_matrix = [[1,0,0],[0,1,0],[0,0,1]] self.transform_matrix_reverse = numpy.linalg.inv(self.transform_matrix).tolist() print_("\n Transformation matrixes:") print_(self.transform_matrix) print_(self.transform_matrix_reverse) x,y = source_point[0], source_point[1] if not reverse : t = self.transform_matrix return [t[0][0]*x+t[0][1]*y+t[0][2], t[1][0]*x+t[1][1]*y+t[1][2]] else : t = self.transform_matrix_reverse 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): 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]) return csp ################################################################################ ### ### Effect ### ### Main function of Gcode tools class ### ################################################################################ def effect(self): global options options = self.options # 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("Gcode tools 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 self.transform_matrix = None ################################################################################ ### ### Curve to Gcode ### ################################################################################ if self.options.active_tab not in ['"path-to-gcode"', '"area"', '"engraving"', '"orientation"']: inkex.errormsg(_("Select one of the active tabs - Path to Gcode, Area, Engraving or Orientation.")) return elif self.options.active_tab == '"path-to-gcode"': if len(self.options.ids)<=0: inkex.errormsg(_("This extension requires at least one selected path.")) return if not self.check_dir() : return gcode = self.header # Set group self.biarcGroup = inkex.etree.SubElement( self.selected[self.options.ids[0]].getparent(), inkex.addNS('g','svg') ) options.Group = self.biarcGroup p = [] for id, node in self.selected.iteritems(): if node.tag == inkex.addNS('path','svg'): csp = cubicsuperpath.parsePath(node.get("d")) if 'transform' in node.keys(): trans = node.get('transform') trans = simpletransform.parseTransform(trans) simpletransform.applyTransformToPath(trans, csp) p += csp # gcode += '(Found path %s)\n' % node.get('id').replace('(','').replace(')','').replace('\n','') curve = self.parse_curve(p) self.draw_curve(curve) if self.options.Zstep == 0 : Zstep = 1 for step in range( 0, int(math.ceil( abs( (self.options.Zdepth-self.options.Zsurface)/self.options.Zstep )) ) ): Zpos = max( self.options.Zdepth, self.options.Zsurface - abs(self.options.Zstep*(step+1)) ) gcode += self.generate_gcode(curve,Zpos) gcode += self.footer try: f = open(self.options.directory+'/'+self.options.file, "w") f.write(gcode) f.close() except: inkex.errormsg(_("Can not write to specified file!")) return ################################################################################ ### ### Calculate area curves ### ################################################################################ elif self.options.active_tab == '"area"' : if self.options.tool_diameter<=0 : inkex.errormsg(_("Tool diameter must be > 0!")) return for id, node in self.selected.iteritems(): self.biarcGroup = inkex.etree.SubElement( node.getparent(), inkex.addNS('g','svg') ) break for id, node in self.selected.iteritems(): if node.tag == inkex.addNS('path','svg'): d = node.get('d') csp = cubicsuperpath.parsePath(d) # Finding top most point in path (min y value) my = (None, 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])) if ay == 0 : roots = [ -cy/(2*by) ] if by !=0 else [] else: det = (2.0*by)**2 - 4.0*(3*ay*cy) roots = [ (-2*by+math.sqrt(det))/(6*ay), (-2*by+math.sqrt(det))/(6*ay) ] if det>=0 else [] roots += [1,0] for t in roots : if 0<=t<=1: y = ay*(t**3)+by*(t**2)+cy*t+y0 x = ax*(t**3)+bx*(t**2)+cx*t+x0 if my[0]>y or my[0] == None : my = (y,i,j,t,x) elif my[0]==y and x>my[4] : my = (y,i,j,t,x) if my[0]!=None : subp = csp[my[1]] del csp[my[1]] j = my[2] if my[3] in [0,1]: if my[3] == 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 = cspbezsplit(subp[j-1],subp[j],my[3]) 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 # Reverce path if needed st,end = [],[] for i in range(1,len(subp)): pst = P( csp_at_t(subp[i-1],subp[i],.1) ) pend = P( csp_at_t(subp[-i-1],subp[-i],.9) ) if st==[] and (pst-P(subp[0][1])).mag()>straight_tolerance: st = pst - P(subp[0][1]) if end==[] and (pend-P(subp[0][1])).mag()>straight_tolerance: end = pend - P(subp[0][1]) if st!=[] and end!=[]: break if math.atan2(st.x,st.y) % math.pi2 < math.atan2(end.x,end.y) % math.pi2 : 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) 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) r = self.options.area_inkscape_radius sign=1 if r>0 else -1 a = self.transform([0,0],True) b = self.transform([self.options.tool_diameter,0],True) tool_d = math.sqrt( (b[0]-a[0])**2 + (b[1]-a[1])**2 ) c = self.transform([r,0],True) r = math.sqrt( (c[0]-a[0])**2 + (c[1]-a[1])**2 ) print_("tool_diameter=%10.3f, r=%10.3f" % (tool_d, r)) for i in range(self.options.max_area_curves): radius = - tool_d * (i+0.5) * sign if abs(radius)>abs(r): radius = -r inkex.etree.SubElement( self.biarcGroup, inkex.addNS('path','svg'), { inkex.addNS('type','sodipodi'): 'inkscape:offset', inkex.addNS('radius','inkscape'): str(radius), inkex.addNS('original','inkscape'): d, 'style': biarc_style_i['area'] }) if radius == -r : break ################################################################################ ### ### Engraving ### ################################################################################ elif self.options.active_tab == '"engraving"' : ################################################################################ ### ### To find center of cutter a system of non linear equations will be solved ### using Newton's method ### ################################################################################ if not self.check_dir() : return gcode = self.header def inv(a): # invert matrix 3x3 det = 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]) 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 find_cutter_center((x1,y1),(nx1,ny1), sp1,sp2,t3 = .5): bez = (sp1[1][:],sp1[2][:],sp2[0][:],sp2[1][:]) ax,ay,bx,by,cx,cy,dx,dy=bezmisc.bezierparameterize(bez) fx=ax*(t3*t3*t3)+bx*(t3*t3)+cx*t3+dx fy=ay*(t3*t3*t3)+by*(t3*t3)+cy*t3+dy f1x = -(3*ay*(t3*t3)+2*by*t3+cy) f1y = 3*ax*(t3*t3)+2*bx*t3+cx if (ny*f1x-nx*f1y) != 0 : t1 = ((fy-y1)*f1x - (fx-x1)*f1y)/(ny*f1x-nx*f1y) t2 = (x1-fx-t1*nx)/f1x if f1x != 0 else (y1-fy-t1*ny)/f1y if (ny*f1x-nx*f1y)==0 or t1<0 or t2<0 : t1 = self.options.tool_diameter t2 = self.options.tool_diameter/math.sqrt(f1x*f1x+f1y*f1y) t = [ t1, t2, t3 ] i = 0 F = [0.,0.,0.] F1 = [[0.,0.,0.],[0.,0.,0.],[0.,0.,0.]] while i==0 or abs(F[0])+abs(F[1])+math.sqrt(abs(F[2])) >engraving_tolerance and i<10: t1,t2,t3 = t[0],t[1],t[2] fx=ax*(t3*t3*t3)+bx*(t3*t3)+cx*t3+dx fy=ay*(t3*t3*t3)+by*(t3*t3)+cy*t3+dy f1x=3*ax*(t3*t3)+2*bx*t3+cx f1y=3*ay*(t3*t3)+2*by*t3+cy i+=1 tx = fx-x1-nx1*t1 ty = fy-y1-ny1*t1 F[0] = x1+nx1*t1-fx+t2*f1y F[1] = y1+ny1*t1-fy-t2*f1x F[2] = t1*t1 - tx*tx -ty*ty F1[0][0] = nx1 F1[0][1] = f1y F1[0][2] = -f1x+t2*(6*ay*t3+2*by) F1[1][0] = ny1 F1[1][1] = -f1x F1[1][2] = -f1y-t2*(6*ax*t3+2*bx) F1[2][0] = 2*t1+2*nx1*tx +2*ny1*ty F1[2][1] = 0 F1[2][2] = -2*f1x*tx -2*f1y*ty F1 = inv(F1) if ( isnan(F[0]) or isnan(F[1]) or isnan(F[2]) or isinf(F[0]) or isinf(F[1]) or isinf(F[2]) ): return t+[1e100,i] if F1!= None : t[0] -= F1[0][0]*F[0] + F1[0][1]*F[1] + F1[0][2]*F[2] t[1] -= F1[1][0]*F[0] + F1[1][1]*F[1] + F1[1][2]*F[2] t[2] -= F1[2][0]*F[0] + F1[2][1]*F[1] + F1[2][2]*F[2] else: break return t+[abs(F[0])+abs(F[1])+math.sqrt(abs(F[2])),i] def csp_simpe_bound(csp): minx,miny,maxx,maxy = 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 self.Group = inkex.etree.SubElement( self.selected[self.options.ids[0]].getparent(), inkex.addNS('g','svg') ) cspe =[] we = [] for id, node in self.selected.iteritems(): if node.tag == inkex.addNS('path','svg'): cspi = cubicsuperpath.parsePath(node.get('d')) for j in xrange(len(cspi)): # Remove zerro length segments i = 1 while i 0 and ang1 < self.options.engraving_sharp_angle_tollerance : # inner angle n[-1][2] = True elif ang < 0 and ang1 < self.options.engraving_sharp_angle_tollerance : # outer angle a = -math.acos(nx*nx2+ny*ny2) for t in [.0,.25,.75,1.]: n1 += [ [ [x1,y1], [nx*math.cos(a*t)-ny*math.sin(a*t),nx*math.sin(a*t)+ny*math.cos(a*t)], False, True, i ] ] nl += [ n ] + ([ n1 ] if n1!=[] else []) # Modify first/last points if curve is closed if abs(csp[-1][1][0]-csp[0][1][0]) 0 and 180-math.acos(nx*nx2+ny*ny2)*180/math.pi < self.options.engraving_sharp_angle_tollerance : # inner angle nl[-1][-1][2] = True elif ang < 0 and 180-math.acos(nx*nx2+ny*ny2)*180/math.pi < self.options.engraving_sharp_angle_tollerance : # outer angle a = -math.acos(nx*nx2+ny*ny2) n1 = [] for t in [.0,.25,.75,1.]: n1 += [ [ [x1,y1], [nx*math.cos(a*t)-ny*math.sin(a*t),nx*math.sin(a*t)+ny*math.cos(a*t)], False, True, i ] ] nl += [ n1 ] if self.options.engraving_draw_calculation_paths==True: for i in nl: for p in i: inkex.etree.SubElement( self.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", }) # Calculate offset points csp_points = [] for ki in xrange(len(nl)): p = [] for ti in xrange(3) if ki!=len(nl)-1 else xrange(4): n = nl[ki][ti] x1,y1 = n[0] nx,ny = n[1] d, r = 0, None if ti==0 and nl[ki-1][-1][2] == True or ti==3 and nl[ki][ti][2] == True: # Point is a sharp angle r=0p r = 0 else : for j in xrange(0,len(cspi)): for i in xrange(1,len(cspi[j])): d = point_to_csp_bound_dist([x1,y1], cspi[j][i-1], cspi[j][i], self.options.engraving_max_dist*2) if d>=self.options.engraving_max_dist*2 : r = min(d/2,r) if r!=None else d/2 continue for n1 in xrange(self.options.engraving_newton_iterations): t = find_cutter_center((x1,y1),(nx,ny), cspi[j][i-1], cspi[j][i], float(n1)/(self.options.engraving_newton_iterations-1)) if t[0] > engraving_tolerance and 0<=t[2]<=1 and abs(t[3])0: r = min( d,r) if r!=None else d else : r = min(r,self.options.engraving_max_dist) if r!=None else self.options.engraving_max_dist else: r = min(t[0],r) if r!=None else t[0] for j in xrange(0,len(cspi)): for i in xrange(0,len(cspi[j])): x2,y2 = cspi[j][i][1] if (abs(x1-x2)>engraving_tolerance or abs(y1-y2)>engraving_tolerance ) and (x2*nx - x1*nx + y2*ny - y1*ny) != 0: t1 = .5 * ( (x1-x2)**2+(y1-y2)**2 ) / (x2*nx - x1*nx + y2*ny - y1*ny) if t1>0 : r = min(t1,r) if r!=None else t1 if self.options.engraving_draw_calculation_paths==True: inkex.etree.SubElement( self.Group, inkex.addNS('path','svg'), {'style': "fill:#ff00ff; fill-opacity:0.46; stroke:#000000; stroke-width:0.1;", inkex.addNS('cx','sodipodi'): str(x1+nx*r), inkex.addNS('cy','sodipodi'): str(y1+ny*r), inkex.addNS('rx','sodipodi'): str(1), inkex.addNS('ry','sodipodi'): str(1), inkex.addNS('type','sodipodi'): 'arc'}) inkex.etree.SubElement( self.Group, inkex.addNS('path','svg'), {'style': "fill:none; fill-opacity:0.46; stroke:#000000; stroke-width:0.1;", inkex.addNS('cx','sodipodi'): str(x1+nx*r), inkex.addNS('cy','sodipodi'): str(y1+ny*r),inkex.addNS('rx','sodipodi'): str(r), inkex.addNS('ry','sodipodi'): str(r), inkex.addNS('type','sodipodi'): 'arc'}) r = min(r, self.options.engraving_max_dist) w = min(r, self.options.tool_diameter) p += [ [x1+nx*w,y1+ny*w,r,w] ] if len(csp_points)>0 : csp_points[-1] += [p[0]] csp_points += [ p ] # Splitting path to pieces each of them not further from path more than engraving_max_dist engraving_path = [ [] ] for p_ in csp_points : for p in p_: if p[2]0 : cspm[-1][2] = sp1[2] cspm += [sp2[:], sp3[:], sp4[:]] w += [p[i][3] for i in range(1,4)] else : cspm += [sp1[:], sp2[:], sp3[:], sp4[:]] w += [p[i][3] for i in range(4)] node = inkex.etree.SubElement( self.Group, inkex.addNS('path','svg'), { "d": cubicsuperpath.formatPath([cspm]), 'style': biarc_style_i['biarc1'] }) for i in xrange(len(cspm)): inkex.etree.SubElement( self.Group, inkex.addNS('path','svg'), {'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(w[i]), inkex.addNS('ry','sodipodi'): str(w[i]), inkex.addNS('type','sodipodi'): 'arc'}) cspe += [cspm] we += [w] if self.options.engraving_cutter_shape_function != "": f = eval('lambda w: ' + self.options.engraving_cutter_shape_function.strip('"')) else: f = lambda w: w if cspe!=[]: curve = self.parse_curve(cspe,we,f) self.draw_curve(curve,self.Group) gcode += self.generate_gcode(curve,self.options.Zsurface) gcode += self.footer try: f = open(self.options.directory+'/'+self.options.file, "w") f.write(gcode) f.close() except: inkex.errormsg(_("Can not write to specified file!")) return else : inkex.errormsg(_("No need to engrave sharp angles.")) ################################################################################ ### ### Orientation ### ################################################################################ elif self.options.active_tab == '"orientation"' : points = [[self.options.orientation_point1x, self.options.orientation_point1y], [self.options.orientation_point2x, self.options.orientation_point2y], [self.options.orientation_point3x, self.options.orientation_point3y]] orientation_group = inkex.etree.SubElement(self.current_layer if self.current_layer is not None else self.document.getroot(), inkex.addNS('g','svg')) for i in points : si = [i[0]*self.options.orientation_scale, i[1]*self.options.orientation_scale] g = inkex.etree.SubElement(orientation_group, inkex.addNS('g','svg'), {'comment': "Gcode tools orientation point"}) 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]), 'comment': "Gcode tools orientation point arrow" }) t = inkex.etree.SubElement( g, inkex.addNS('text','svg'), { 'style': "font-size:10px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;fill:#000000;fill-opacity:1;stroke:none;", 'x': str(si[0]+10), 'y': str(-si[1]-10), 'comment': "Gcode tools orientation point text" }) t.text = "(%s; %s)" % (i[0],i[1]) e = Gcode_tools() e.affect()