diff --git a/README.md b/README.md index ec30a1b..c6d7ee5 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,13 @@ # svg2gcode -Convert vector images (SVG) to gcode for usage with a laser plotter. \ No newline at end of file +Convert vector images (SVG) to gcode for usage with a laser plotter. + +Based on the vector to gcode implementation from [Vishal Patil](https://github.com/vishpat/svg2gcode) + +# Requirements + +`pip install inkex` + +# Usage + +`clear && cat test_data/10mmx10mm.svg | python3 svg2gcode.py > test_data/test.gcode` diff --git a/__init__.py b/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/bezmisc.py b/bezmisc.py new file mode 100755 index 0000000..348c360 --- /dev/null +++ b/bezmisc.py @@ -0,0 +1,290 @@ +#!/usr/bin/env python +''' +Copyright (C) 2010 Nick Drobchenko, nick@cnc-club.ru +Copyright (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 +''' + +from __future__ import absolute_import +from __future__ import print_function +import math, cmath + +def rootWrapper(a,b,c,d): + if a: + # 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.0*a**3 - 9.0*a*b + 27.0*c + k = a**2 - 3.0*b + n = m**2 - 4.0*k**3 + w1 = -.5 + .5*cmath.sqrt(-3.0) + w2 = -.5 - .5*cmath.sqrt(-3.0) + if n < 0: + m1 = pow(complex((m+cmath.sqrt(n))/2),1./3) + n1 = pow(complex((m-cmath.sqrt(n))/2),1./3) + else: + if m+math.sqrt(n) < 0: + m1 = -pow(-(m+math.sqrt(n))/2,1./3) + else: + m1 = pow((m+math.sqrt(n))/2,1./3) + if m-math.sqrt(n) < 0: + n1 = -pow(-(m-math.sqrt(n))/2,1./3) + else: + n1 = pow((m-math.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: + det=c**2.0-4.0*b*d + if det: + return (-c+cmath.sqrt(det))/(2.0*b),(-c-cmath.sqrt(det))/(2.0*b) + else: + return -c/(2.0*b), + elif c: + return 1.0*(-d/c), + return () + +def bezierparameterize(xxx_todo_changeme): + #parametric bezier + ((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3)) = xxx_todo_changeme + 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 + #ax,ay,bx,by,cx,cy,x0,y0=bezierparameterize(((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3))) + +def linebezierintersect(xxx_todo_changeme1, xxx_todo_changeme2): + #parametric line + ((lx1,ly1),(lx2,ly2)) = xxx_todo_changeme1 + ((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3)) = xxx_todo_changeme2 + dd=lx1 + cc=lx2-lx1 + bb=ly1 + aa=ly2-ly1 + + if aa: + coef1=cc/aa + coef2=1 + else: + coef1=1 + coef2=aa/cc + + ax,ay,bx,by,cx,cy,x0,y0=bezierparameterize(((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3))) + #cubic intersection coefficients + a=coef1*ay-coef2*ax + b=coef1*by-coef2*bx + c=coef1*cy-coef2*cx + d=coef1*(y0-bb)-coef2*(x0-dd) + + roots = rootWrapper(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(bezierpointatt(((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3)),i)) + return retval + +def bezierpointatt(xxx_todo_changeme3,t): + ((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3)) = xxx_todo_changeme3 + ax,ay,bx,by,cx,cy,x0,y0=bezierparameterize(((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3))) + x=ax*(t**3)+bx*(t**2)+cx*t+x0 + y=ay*(t**3)+by*(t**2)+cy*t+y0 + return x,y + +def bezierslopeatt(xxx_todo_changeme4,t): + ((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3)) = xxx_todo_changeme4 + ax,ay,bx,by,cx,cy,x0,y0=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 + return dx,dy + +def beziertatslope(xxx_todo_changeme5, xxx_todo_changeme6): + ((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3)) = xxx_todo_changeme5 + (dy,dx) = xxx_todo_changeme6 + ax,ay,bx,by,cx,cy,x0,y0=bezierparameterize(((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3))) + #quadratic coefficents of slope formula + if dx: + slope = 1.0*(dy/dx) + a=3*ay-3*ax*slope + b=2*by-2*bx*slope + c=cy-cx*slope + elif dy: + slope = 1.0*(dx/dy) + a=3*ax-3*ay*slope + b=2*bx-2*by*slope + c=cx-cy*slope + else: + return [] + + roots = rootWrapper(0,a,b,c) + 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 + +def tpoint(xxx_todo_changeme7, xxx_todo_changeme8,t): + (x1,y1) = xxx_todo_changeme7 + (x2,y2) = xxx_todo_changeme8 + return x1+t*(x2-x1),y1+t*(y2-y1) +def beziersplitatt(xxx_todo_changeme9,t): + ((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3)) = xxx_todo_changeme9 + m1=tpoint((bx0,by0),(bx1,by1),t) + m2=tpoint((bx1,by1),(bx2,by2),t) + m3=tpoint((bx2,by2),(bx3,by3),t) + m4=tpoint(m1,m2,t) + m5=tpoint(m2,m3,t) + m=tpoint(m4,m5,t) + + return ((bx0,by0),m1,m4,m),(m,m5,m3,(bx3,by3)) + +''' +Approximating the arc length of a bezier curve +according to + +if: + L1 = |P0 P1| +|P1 P2| +|P2 P3| + L0 = |P0 P3| +then: + L = 1/2*L0 + 1/2*L1 + ERR = L1-L0 +ERR approaches 0 as the number of subdivisions (m) increases + 2^-4m + +Reference: +Jens Gravesen +"Adaptive subdivision and the length of Bezier curves" +mat-report no. 1992-10, Mathematical Institute, The Technical +University of Denmark. +''' +def pointdistance(xxx_todo_changeme10, xxx_todo_changeme11): + (x1,y1) = xxx_todo_changeme10 + (x2,y2) = xxx_todo_changeme11 + return math.sqrt(((x2 - x1) ** 2) + ((y2 - y1) ** 2)) +def Gravesen_addifclose(b, len, error = 0.001): + box = 0 + for i in range(1,4): + box += pointdistance(b[i-1], b[i]) + chord = pointdistance(b[0], b[3]) + if (box - chord) > error: + first, second = beziersplitatt(b, 0.5) + Gravesen_addifclose(first, len, error) + Gravesen_addifclose(second, len, error) + else: + len[0] += (box / 2.0) + (chord / 2.0) +def bezierlengthGravesen(b, error = 0.001): + len = [0] + Gravesen_addifclose(b, len, error) + return len[0] + +# balf = Bezier Arc Length Function +balfax,balfbx,balfcx,balfay,balfby,balfcy = 0,0,0,0,0,0 +def balf(t): + retval = (balfax*(t**2) + balfbx*t + balfcx)**2 + (balfay*(t**2) + balfby*t + balfcy)**2 + return math.sqrt(retval) + +def Simpson(f, a, b, n_limit, tolerance): + n = 2 + multiplier = (b - a)/6.0 + endsum = f(a) + f(b) + interval = (b - a)/2.0 + asum = 0.0 + bsum = f(a + interval) + est1 = multiplier * (endsum + (2.0 * asum) + (4.0 * bsum)) + est0 = 2.0 * est1 + #print multiplier, endsum, interval, asum, bsum, est1, est0 + while n < n_limit and abs(est1 - est0) > tolerance: + n *= 2 + multiplier /= 2.0 + interval /= 2.0 + asum += bsum + bsum = 0.0 + est0 = est1 + for i in range(1, n, 2): + bsum += f(a + (i * interval)) + est1 = multiplier * (endsum + (2.0 * asum) + (4.0 * bsum)) + #print multiplier, endsum, interval, asum, bsum, est1, est0 + return est1 + +def bezierlengthSimpson(xxx_todo_changeme12, tolerance = 0.001): + ((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3)) = xxx_todo_changeme12 + global balfax,balfbx,balfcx,balfay,balfby,balfcy + ax,ay,bx,by,cx,cy,x0,y0=bezierparameterize(((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3))) + balfax,balfbx,balfcx,balfay,balfby,balfcy = 3*ax,2*bx,cx,3*ay,2*by,cy + return Simpson(balf, 0.0, 1.0, 4096, tolerance) + +def beziertatlength(xxx_todo_changeme13, l = 0.5, tolerance = 0.001): + ((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3)) = xxx_todo_changeme13 + global balfax,balfbx,balfcx,balfay,balfby,balfcy + ax,ay,bx,by,cx,cy,x0,y0=bezierparameterize(((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3))) + balfax,balfbx,balfcx,balfay,balfby,balfcy = 3*ax,2*bx,cx,3*ay,2*by,cy + t = 1.0 + tdiv = t + curlen = Simpson(balf, 0.0, t, 4096, tolerance) + targetlen = l * curlen + diff = curlen - targetlen + while abs(diff) > tolerance: + tdiv /= 2.0 + if diff < 0: + t += tdiv + else: + t -= tdiv + curlen = Simpson(balf, 0.0, t, 4096, tolerance) + diff = curlen - targetlen + return t + +#default bezier length method +bezierlength = bezierlengthSimpson + +if __name__ == '__main__': +# import timing + #print linebezierintersect(((,),(,)),((,),(,),(,),(,))) + #print linebezierintersect(((0,1),(0,-1)),((-1,0),(-.5,0),(.5,0),(1,0))) + tol = 0.00000001 + curves = [((0,0),(1,5),(4,5),(5,5)), + ((0,0),(0,0),(5,0),(10,0)), + ((0,0),(0,0),(5,1),(10,0)), + ((-10,0),(0,0),(10,0),(10,10)), + ((15,10),(0,0),(10,0),(-5,10))] + ''' + for curve in curves: + timing.start() + g = bezierlengthGravesen(curve,tol) + timing.finish() + gt = timing.micro() + + timing.start() + s = bezierlengthSimpson(curve,tol) + timing.finish() + st = timing.micro() + + print g, gt + print s, st + ''' + for curve in curves: + print(beziertatlength(curve,0.5)) + + +# vim: expandtab shiftwidth=4 tabstop=8 softtabstop=4 encoding=utf-8 textwidth=99 diff --git a/config.py b/config.py new file mode 100644 index 0000000..83c39b4 --- /dev/null +++ b/config.py @@ -0,0 +1,45 @@ + +"""Z position when tool is touching surface in mm""" +z_touching = 0.0 + +"""Z position where tool should travel in mm""" +z_travel = 0.0 + +"""Distance between pen and Y=0.0 in mm""" +y_offset = 0.0 + +"""Distance between pen and X=0.0 in mm""" +x_offset = 0.0 + +"""Print bed width in mm""" +bed_max_x = 300 + +"""Print bed height in mm""" +bed_max_y = 300 + +"""X/Y speed in mm/minute""" +xy_speed = 700 + +"""Wait time between phases in milliseconds""" +wait_time = 1000 + +"""G-code emitted at the start of processing the SVG file""" +preamble = "G90 ;Absolute programming\nG21 ;Programming in millimeters (mm)\nM5 ;Disable laser" + +"""G-code emitted at the end of processing the SVG file""" +postamble = "G1 X0.0 Y0.0; Display printbed\nM02 ;End of program" + +"""G-code emitted before processing a SVG shape""" +shape_preamble = "; ------\n; Draw shapes" + +"""G-code emitted after processing a SVG shape""" +shape_postamble = "; ------\n; Shape completed" + +""" +Used to control the smoothness/sharpness of the curves. +Smaller the value greater the sharpness. Make sure the +value is greater than 0.1 +""" +smoothness = 0.5 + + diff --git a/convert.py b/convert.py new file mode 100644 index 0000000..6b928e1 --- /dev/null +++ b/convert.py @@ -0,0 +1,8 @@ +from __future__ import absolute_import +from __future__ import print_function +import os + +for filename in os.listdir('.'): + if filename.endswith('.py'): + os.system("modernize -w " + filename + " --no-six") + print(("Converting", filename + "...")) \ No newline at end of file diff --git a/cspsubdiv.py b/cspsubdiv.py new file mode 100755 index 0000000..1b6905c --- /dev/null +++ b/cspsubdiv.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python +from __future__ import absolute_import +from bezmisc import * +from ffgeom import * + +def maxdist(xxx_todo_changeme): + ((p0x,p0y),(p1x,p1y),(p2x,p2y),(p3x,p3y)) = xxx_todo_changeme + p0 = Point(p0x,p0y) + p1 = Point(p1x,p1y) + p2 = Point(p2x,p2y) + p3 = Point(p3x,p3y) + + s1 = Segment(p0,p3) + return max(s1.distanceToPoint(p1),s1.distanceToPoint(p2)) + + +def cspsubdiv(csp,flat): + for sp in csp: + subdiv(sp,flat) + +def subdiv(sp,flat,i=1): + p0 = sp[i-1][1] + p1 = sp[i-1][2] + p2 = sp[i][0] + p3 = sp[i][1] + + b = (p0,p1,p2,p3) + m = maxdist(b) + if m <= flat: + try: + subdiv(sp,flat,i+1) + except IndexError: + pass + else: + one, two = beziersplitatt(b,0.5) + sp[i-1][2] = one[1] + sp[i][0] = two[2] + p = [one[2],one[3],two[1]] + sp[i:1] = [p] + subdiv(sp,flat,i) diff --git a/cubicsuperpath.py b/cubicsuperpath.py new file mode 100755 index 0000000..62d1a1c --- /dev/null +++ b/cubicsuperpath.py @@ -0,0 +1,170 @@ +#!/usr/bin/env python +""" +cubicsuperpath.py + +Copyright (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 + +""" +from __future__ import absolute_import +import simplepath +from math import * + +def matprod(mlist): + prod=mlist[0] + for m in mlist[1:]: + a00=prod[0][0]*m[0][0]+prod[0][1]*m[1][0] + a01=prod[0][0]*m[0][1]+prod[0][1]*m[1][1] + a10=prod[1][0]*m[0][0]+prod[1][1]*m[1][0] + a11=prod[1][0]*m[0][1]+prod[1][1]*m[1][1] + prod=[[a00,a01],[a10,a11]] + return prod +def rotmat(teta): + return [[cos(teta),-sin(teta)],[sin(teta),cos(teta)]] +def applymat(mat, pt): + x=mat[0][0]*pt[0]+mat[0][1]*pt[1] + y=mat[1][0]*pt[0]+mat[1][1]*pt[1] + pt[0]=x + pt[1]=y +def norm(pt): + return sqrt(pt[0]*pt[0]+pt[1]*pt[1]) + +def ArcToPath(p1,params): + A=p1[:] + rx,ry,teta,longflag,sweepflag,x2,y2=params[:] + teta = teta*pi/180.0 + B=[x2,y2] + if rx==0 or ry==0: + return([[A,A,A],[B,B,B]]) + mat=matprod((rotmat(teta),[[1/rx,0],[0,1/ry]],rotmat(-teta))) + applymat(mat, A) + applymat(mat, B) + k=[-(B[1]-A[1]),B[0]-A[0]] + d=k[0]*k[0]+k[1]*k[1] + k[0]/=sqrt(d) + k[1]/=sqrt(d) + d=sqrt(max(0,1-d/4)) + if longflag==sweepflag: + d*=-1 + O=[(B[0]+A[0])/2+d*k[0],(B[1]+A[1])/2+d*k[1]] + OA=[A[0]-O[0],A[1]-O[1]] + OB=[B[0]-O[0],B[1]-O[1]] + start=acos(OA[0]/norm(OA)) + if OA[1]<0: + start*=-1 + end=acos(OB[0]/norm(OB)) + if OB[1]<0: + end*=-1 + + if sweepflag and start>end: + end +=2*pi + if (not sweepflag) and start" + + def __str__(self): + return self.xml_node + +class path(svgshape): + def __init__(self, xml_node): + super(path, self).__init__(xml_node) + + if not self.xml_node == None: + path_el = self.xml_node + self.d = path_el.get('d') + else: + self.d = None + logging.error("path: Unable to get the attributes for %s", self.xml_node) + + def d_path(self): + return self.d + +class rect(svgshape): + + def __init__(self, xml_node): + super(rect, self).__init__(xml_node) + + if not self.xml_node == None: + rect_el = self.xml_node + self.x = float(rect_el.get('x')) if rect_el.get('x') else 0 + self.y = float(rect_el.get('y')) if rect_el.get('y') else 0 + self.rx = float(rect_el.get('rx')) if rect_el.get('rx') else 0 + self.ry = float(rect_el.get('ry')) if rect_el.get('ry') else 0 + self.width = float(rect_el.get('width')) if rect_el.get('width') else 0 + self.height = float(rect_el.get('height')) if rect_el.get('height') else 0 + else: + self.x = self.y = self.rx = self.ry = self.width = self.height = 0 + logging.error("rect: Unable to get the attributes for %s", self.xml_node) + + def d_path(self): + a = list() + a.append( ['M ', [self.x, self.y]] ) + a.append( [' l ', [self.width, 0]] ) + a.append( [' l ', [0, self.height]] ) + a.append( [' l ', [-self.width, 0]] ) + a.append( [' Z', []] ) + return simplepath.formatPath(a) + +class ellipse(svgshape): + + def __init__(self, xml_node): + super(ellipse, self).__init__(xml_node) + + if not self.xml_node == None: + ellipse_el = self.xml_node + self.cx = float(ellipse_el.get('cx')) if ellipse_el.get('cx') else 0 + self.cy = float(ellipse_el.get('cy')) if ellipse_el.get('cy') else 0 + self.rx = float(ellipse_el.get('rx')) if ellipse_el.get('rx') else 0 + self.ry = float(ellipse_el.get('ry')) if ellipse_el.get('ry') else 0 + else: + self.cx = self.cy = self.rx = self.ry = 0 + logging.error("ellipse: Unable to get the attributes for %s", self.xml_node) + + def d_path(self): + x1 = self.cx - self.rx + x2 = self.cx + self.rx + p = 'M %f,%f ' % ( x1, self.cy ) + \ + 'A %f,%f ' % ( self.rx, self.ry ) + \ + '0 1 0 %f,%f ' % ( x2, self.cy ) + \ + 'A %f,%f ' % ( self.rx, self.ry ) + \ + '0 1 0 %f,%f' % ( x1, self.cy ) + return p + +class circle(ellipse): + def __init__(self, xml_node): + super(ellipse, self).__init__(xml_node) + + if not self.xml_node == None: + circle_el = self.xml_node + self.cx = float(circle_el.get('cx')) if circle_el.get('cx') else 0 + self.cy = float(circle_el.get('cy')) if circle_el.get('cy') else 0 + self.rx = float(circle_el.get('r')) if circle_el.get('r') else 0 + self.ry = self.rx + else: + self.cx = self.cy = self.r = 0 + logging.error("Circle: Unable to get the attributes for %s", self.xml_node) + +class line(svgshape): + + def __init__(self, xml_node): + super(line, self).__init__(xml_node) + + if not self.xml_node == None: + line_el = self.xml_node + self.x1 = float(line_el.get('x1')) if line_el.get('x1') else 0 + self.y1 = float(line_el.get('y1')) if line_el.get('y1') else 0 + self.x2 = float(line_el.get('x2')) if line_el.get('x2') else 0 + self.y2 = float(line_el.get('y2')) if line_el.get('y2') else 0 + else: + self.x1 = self.y1 = self.x2 = self.y2 = 0 + logging.error("line: Unable to get the attributes for %s", self.xml_node) + + def d_path(self): + a = [] + a.append( ['M ', [self.x1, self.y1]] ) + a.append( ['L ', [self.x2, self.y2]] ) + return simplepath.formatPath(a) + +class polycommon(svgshape): + + def __init__(self, xml_node, polytype): + super(polycommon, self).__init__(xml_node) + self.points = list() + + if not self.xml_node == None: + polycommon_el = self.xml_node + points = polycommon_el.get('points') if polycommon_el.get('points') else list() + for pa in points.split(): + self.points.append(pa) + else: + logging.error("polycommon: Unable to get the attributes for %s", self.xml_node) + + +class polygon(polycommon): + + def __init__(self, xml_node): + super(polygon, self).__init__(xml_node, 'polygon') + + def d_path(self): + d = "M " + self.points[0] + for i in range( 1, len(self.points) ): + d += " L " + self.points[i] + d += " Z" + return d + +class polyline(polycommon): + + def __init__(self, xml_node): + super(polyline, self).__init__(xml_node, 'polyline') + + def d_path(self): + d = "M " + self.points[0] + for i in range( 1, len(self.points) ): + d += " L " + self.points[i] + return d + +def point_generator(path, mat, flatness): + + if len(simplepath.parsePath(path)) == 0: + return + + simple_path = simplepath.parsePath(path) + startX,startY = float(simple_path[0][1][0]), float(simple_path[0][1][1]) + yield startX, startY + + p = cubicsuperpath.parsePath(path) + + if mat: + simpletransform.applyTransformToPath(mat, p) + + for sp in p: + cspsubdiv.subdiv( sp, flatness) + for csp in sp: + ctrl_pt1 = csp[0] + ctrl_pt2 = csp[1] + end_pt = csp[2] + yield end_pt[0], end_pt[1], diff --git a/simplepath.py b/simplepath.py new file mode 100755 index 0000000..459b101 --- /dev/null +++ b/simplepath.py @@ -0,0 +1,203 @@ +#!/usr/bin/env python +""" +simplepath.py +functions for digesting paths into a simple list structure + +Copyright (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 + +""" +from __future__ import absolute_import +import re, math + +def lexPath(d): + """ + returns and iterator that breaks path data + identifies command and parameter tokens + """ + offset = 0 + length = len(d) + delim = re.compile(r'[ \t\r\n,]+') + command = re.compile(r'[MLHVCSQTAZmlhvcsqtaz]') + parameter = re.compile(r'(([-+]?[0-9]+(\.[0-9]*)?|[-+]?\.[0-9]+)([eE][-+]?[0-9]+)?)') + while 1: + m = delim.match(d, offset) + if m: + offset = m.end() + if offset >= length: + break + m = command.match(d, offset) + if m: + yield [d[offset:m.end()], True] + offset = m.end() + continue + m = parameter.match(d, offset) + if m: + yield [d[offset:m.end()], False] + offset = m.end() + continue + #TODO: create new exception + raise Exception('Invalid path data!') +''' +pathdefs = {commandfamily: + [ + implicitnext, + #params, + [casts,cast,cast], + [coord type,x,y,0] + ]} +''' +pathdefs = { + 'M':['L', 2, [float, float], ['x','y']], + 'L':['L', 2, [float, float], ['x','y']], + 'H':['H', 1, [float], ['x']], + 'V':['V', 1, [float], ['y']], + 'C':['C', 6, [float, float, float, float, float, float], ['x','y','x','y','x','y']], + 'S':['S', 4, [float, float, float, float], ['x','y','x','y']], + 'Q':['Q', 4, [float, float, float, float], ['x','y','x','y']], + 'T':['T', 2, [float, float], ['x','y']], + 'A':['A', 7, [float, float, float, int, int, float, float], [0,0,0,0,0,'x','y']], + 'Z':['L', 0, [], []] + } +def parsePath(d): + """ + Parse SVG path and return an array of segments. + Removes all shorthand notation. + Converts coordinates to absolute. + """ + retval = [] + lexer = lexPath(d) + + pen = (0.0,0.0) + subPathStart = pen + lastControl = pen + lastCommand = '' + + while 1: + try: + token, isCommand = next(lexer) + except StopIteration: + break + params = [] + needParam = True + if isCommand: + if not lastCommand and token.upper() != 'M': + raise Exception('Invalid path, must begin with moveto.') + else: + command = token + else: + #command was omited + #use last command's implicit next command + needParam = False + if lastCommand: + if lastCommand.isupper(): + command = pathdefs[lastCommand][0] + else: + command = pathdefs[lastCommand.upper()][0].lower() + else: + raise Exception('Invalid path, no initial command.') + numParams = pathdefs[command.upper()][1] + while numParams > 0: + if needParam: + try: + token, isCommand = next(lexer) + if isCommand: + raise Exception('Invalid number of parameters') + except StopIteration: + raise Exception('Unexpected end of path') + cast = pathdefs[command.upper()][2][-numParams] + param = cast(token) + if command.islower(): + if pathdefs[command.upper()][3][-numParams]=='x': + param += pen[0] + elif pathdefs[command.upper()][3][-numParams]=='y': + param += pen[1] + params.append(param) + needParam = True + numParams -= 1 + #segment is now absolute so + outputCommand = command.upper() + + #Flesh out shortcut notation + if outputCommand in ('H','V'): + if outputCommand == 'H': + params.append(pen[1]) + if outputCommand == 'V': + params.insert(0,pen[0]) + outputCommand = 'L' + if outputCommand in ('S','T'): + params.insert(0,pen[1]+(pen[1]-lastControl[1])) + params.insert(0,pen[0]+(pen[0]-lastControl[0])) + if outputCommand == 'S': + outputCommand = 'C' + if outputCommand == 'T': + outputCommand = 'Q' + + #current values become "last" values + if outputCommand == 'M': + subPathStart = tuple(params[0:2]) + pen = subPathStart + if outputCommand == 'Z': + pen = subPathStart + else: + pen = tuple(params[-2:]) + + if outputCommand in ('Q','C'): + lastControl = tuple(params[-4:-2]) + else: + lastControl = pen + lastCommand = command + + retval.append([outputCommand,params]) + return retval + +def formatPath(a): + """Format SVG path data from an array""" + return "".join([cmd + " ".join([str(p) for p in params]) for cmd, params in a]) + +def translatePath(p, x, y): + for cmd,params in p: + defs = pathdefs[cmd] + for i in range(defs[1]): + if defs[3][i] == 'x': + params[i] += x + elif defs[3][i] == 'y': + params[i] += y + +def scalePath(p, x, y): + for cmd,params in p: + defs = pathdefs[cmd] + for i in range(defs[1]): + if defs[3][i] == 'x': + params[i] *= x + elif defs[3][i] == 'y': + params[i] *= y + +def rotatePath(p, a, cx = 0, cy = 0): + if a == 0: + return p + for cmd,params in p: + defs = pathdefs[cmd] + for i in range(defs[1]): + if defs[3][i] == 'x': + x = params[i] - cx + y = params[i + 1] - cy + r = math.sqrt((x**2) + (y**2)) + if r != 0: + theta = math.atan2(y, x) + a + params[i] = (r * math.cos(theta)) + cx + params[i + 1] = (r * math.sin(theta)) + cy + diff --git a/simpletransform.py b/simpletransform.py new file mode 100755 index 0000000..7ef7772 --- /dev/null +++ b/simpletransform.py @@ -0,0 +1,242 @@ +#!/usr/bin/env python +''' +Copyright (C) 2006 Jean-Francois Barraud, barraud@math.univ-lille1.fr +Copyright (C) 2010 Alvin Penner, penner@vaxxine.com + +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 +barraud@math.univ-lille1.fr + +This code defines several functions to make handling of transform +attribute easier. +''' +from __future__ import absolute_import +import cubicsuperpath, bezmisc +import copy, math, re, inkex + +def parseTransform(transf,mat=[[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]): + if transf=="" or transf==None: + return(mat) + stransf = transf.strip() + result=re.match("(translate|scale|rotate|skewX|skewY|matrix)\s*\(([^)]*)\)\s*,?",stransf) +#-- translate -- + if result.group(1)=="translate": + args=result.group(2).replace(',',' ').split() + dx=float(args[0]) + if len(args)==1: + dy=0.0 + else: + dy=float(args[1]) + matrix=[[1,0,dx],[0,1,dy]] +#-- scale -- + if result.group(1)=="scale": + args=result.group(2).replace(',',' ').split() + sx=float(args[0]) + if len(args)==1: + sy=sx + else: + sy=float(args[1]) + matrix=[[sx,0,0],[0,sy,0]] +#-- rotate -- + if result.group(1)=="rotate": + args=result.group(2).replace(',',' ').split() + a=float(args[0])*math.pi/180 + if len(args)==1: + cx,cy=(0.0,0.0) + else: + cx,cy=map(float,args[1:]) + matrix=[[math.cos(a),-math.sin(a),cx],[math.sin(a),math.cos(a),cy]] + matrix=composeTransform(matrix,[[1,0,-cx],[0,1,-cy]]) +#-- skewX -- + if result.group(1)=="skewX": + a=float(result.group(2))*math.pi/180 + matrix=[[1,math.tan(a),0],[0,1,0]] +#-- skewY -- + if result.group(1)=="skewY": + a=float(result.group(2))*math.pi/180 + matrix=[[1,0,0],[math.tan(a),1,0]] +#-- matrix -- + if result.group(1)=="matrix": + a11,a21,a12,a22,v1,v2=result.group(2).replace(',',' ').split() + matrix=[[float(a11),float(a12),float(v1)], [float(a21),float(a22),float(v2)]] + + matrix=composeTransform(mat,matrix) + if result.end() < len(stransf): + return(parseTransform(stransf[result.end():], matrix)) + else: + return matrix + +def formatTransform(mat): + return ("matrix(%f,%f,%f,%f,%f,%f)" % (mat[0][0], mat[1][0], mat[0][1], mat[1][1], mat[0][2], mat[1][2])) + +def composeTransform(M1,M2): + a11 = M1[0][0]*M2[0][0] + M1[0][1]*M2[1][0] + a12 = M1[0][0]*M2[0][1] + M1[0][1]*M2[1][1] + a21 = M1[1][0]*M2[0][0] + M1[1][1]*M2[1][0] + a22 = M1[1][0]*M2[0][1] + M1[1][1]*M2[1][1] + + v1 = M1[0][0]*M2[0][2] + M1[0][1]*M2[1][2] + M1[0][2] + v2 = M1[1][0]*M2[0][2] + M1[1][1]*M2[1][2] + M1[1][2] + return [[a11,a12,v1],[a21,a22,v2]] + +def composeParents(node, mat): + trans = node.get('transform') + if trans: + mat = composeTransform(parseTransform(trans), mat) + if node.getparent().tag == inkex.addNS('g','svg'): + mat = composeParents(node.getparent(), mat) + return mat + +def applyTransformToNode(mat,node): + m=parseTransform(node.get("transform")) + newtransf=formatTransform(composeTransform(mat,m)) + node.set("transform", newtransf) + +def applyTransformToPoint(mat,pt): + x = mat[0][0]*pt[0] + mat[0][1]*pt[1] + mat[0][2] + y = mat[1][0]*pt[0] + mat[1][1]*pt[1] + mat[1][2] + pt[0]=x + pt[1]=y + +def applyTransformToPath(mat,path): + for comp in path: + for ctl in comp: + for pt in ctl: + applyTransformToPoint(mat,pt) + +def fuseTransform(node): + if node.get('d')==None: + #FIXME: how do you raise errors? + raise AssertionError('can not fuse "transform" of elements that have no "d" attribute') + t = node.get("transform") + if t == None: + return + m = parseTransform(t) + d = node.get('d') + p = cubicsuperpath.parsePath(d) + applyTransformToPath(m,p) + node.set('d', cubicsuperpath.formatPath(p)) + del node.attrib["transform"] + +#################################################################### +##-- Some functions to compute a rough bbox of a given list of objects. +##-- this should be shipped out in an separate file... + +def boxunion(b1,b2): + if b1 is None: + return b2 + elif b2 is None: + return b1 + else: + return((min(b1[0],b2[0]), max(b1[1],b2[1]), min(b1[2],b2[2]), max(b1[3],b2[3]))) + +def roughBBox(path): + xmin,xMax,ymin,yMax = path[0][0][0][0],path[0][0][0][0],path[0][0][0][1],path[0][0][0][1] + for pathcomp in path: + for ctl in pathcomp: + for pt in ctl: + xmin = min(xmin,pt[0]) + xMax = max(xMax,pt[0]) + ymin = min(ymin,pt[1]) + yMax = max(yMax,pt[1]) + return xmin,xMax,ymin,yMax + +def refinedBBox(path): + xmin,xMax,ymin,yMax = path[0][0][1][0],path[0][0][1][0],path[0][0][1][1],path[0][0][1][1] + for pathcomp in path: + for i in range(1, len(pathcomp)): + cmin, cmax = cubicExtrema(pathcomp[i-1][1][0], pathcomp[i-1][2][0], pathcomp[i][0][0], pathcomp[i][1][0]) + xmin = min(xmin, cmin) + xMax = max(xMax, cmax) + cmin, cmax = cubicExtrema(pathcomp[i-1][1][1], pathcomp[i-1][2][1], pathcomp[i][0][1], pathcomp[i][1][1]) + ymin = min(ymin, cmin) + yMax = max(yMax, cmax) + return xmin,xMax,ymin,yMax + +def cubicExtrema(y0, y1, y2, y3): + cmin = min(y0, y3) + cmax = max(y0, y3) + d1 = y1 - y0 + d2 = y2 - y1 + d3 = y3 - y2 + if (d1 - 2*d2 + d3): + if (d2*d2 > d1*d3): + t = (d1 - d2 + math.sqrt(d2*d2 - d1*d3))/(d1 - 2*d2 + d3) + if (t > 0) and (t < 1): + y = y0*(1-t)*(1-t)*(1-t) + 3*y1*t*(1-t)*(1-t) + 3*y2*t*t*(1-t) + y3*t*t*t + cmin = min(cmin, y) + cmax = max(cmax, y) + t = (d1 - d2 - math.sqrt(d2*d2 - d1*d3))/(d1 - 2*d2 + d3) + if (t > 0) and (t < 1): + y = y0*(1-t)*(1-t)*(1-t) + 3*y1*t*(1-t)*(1-t) + 3*y2*t*t*(1-t) + y3*t*t*t + cmin = min(cmin, y) + cmax = max(cmax, y) + elif (d3 - d1): + t = -d1/(d3 - d1) + if (t > 0) and (t < 1): + y = y0*(1-t)*(1-t)*(1-t) + 3*y1*t*(1-t)*(1-t) + 3*y2*t*t*(1-t) + y3*t*t*t + cmin = min(cmin, y) + cmax = max(cmax, y) + return cmin, cmax + +def computeBBox(aList,mat=[[1,0,0],[0,1,0]]): + bbox=None + for node in aList: + m = parseTransform(node.get('transform')) + m = composeTransform(mat,m) + #TODO: text not supported! + d = None + if node.get("d"): + d = node.get('d') + elif node.get('points'): + d = 'M' + node.get('points') + elif node.tag in [ inkex.addNS('rect','svg'), 'rect', inkex.addNS('image','svg'), 'image' ]: + d = 'M' + node.get('x', '0') + ',' + node.get('y', '0') + \ + 'h' + node.get('width') + 'v' + node.get('height') + \ + 'h-' + node.get('width') + elif node.tag in [ inkex.addNS('line','svg'), 'line' ]: + d = 'M' + node.get('x1') + ',' + node.get('y1') + \ + ' ' + node.get('x2') + ',' + node.get('y2') + elif node.tag in [ inkex.addNS('circle','svg'), 'circle', \ + inkex.addNS('ellipse','svg'), 'ellipse' ]: + rx = node.get('r') + if rx is not None: + ry = rx + else: + rx = node.get('rx') + ry = node.get('ry') + cx = float(node.get('cx', '0')) + cy = float(node.get('cy', '0')) + x1 = cx - float(rx) + x2 = cx + float(rx) + d = 'M %f %f ' % (x1, cy) + \ + 'A' + rx + ',' + ry + ' 0 1 0 %f,%f' % (x2, cy) + \ + 'A' + rx + ',' + ry + ' 0 1 0 %f,%f' % (x1, cy) + + if d is not None: + p = cubicsuperpath.parsePath(d) + applyTransformToPath(m,p) + bbox=boxunion(refinedBBox(p),bbox) + + elif node.tag == inkex.addNS('use','svg') or node.tag=='use': + refid=node.get(inkex.addNS('href','xlink')) + path = '//*[@id="%s"]' % refid[1:] + refnode = node.xpath(path) + bbox=boxunion(computeBBox(refnode,m),bbox) + + bbox=boxunion(computeBBox(node,m),bbox) + return bbox + + +# vim: expandtab shiftwidth=4 tabstop=8 softtabstop=4 fileencoding=utf-8 textwidth=99 diff --git a/svg2gcode.code-workspace b/svg2gcode.code-workspace new file mode 100644 index 0000000..876a149 --- /dev/null +++ b/svg2gcode.code-workspace @@ -0,0 +1,8 @@ +{ + "folders": [ + { + "path": "." + } + ], + "settings": {} +} \ No newline at end of file diff --git a/svg2gcode.py b/svg2gcode.py new file mode 100755 index 0000000..fb58966 --- /dev/null +++ b/svg2gcode.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python + +from __future__ import absolute_import +from __future__ import print_function +import sys +import xml.etree.ElementTree as ET +import shapes as shapes_pkg +from shapes import point_generator +from config import * + +def generate_gcode(): + svg_shapes = set(['rect', 'circle', 'ellipse', 'line', 'polyline', 'polygon', 'path']) + + tree = ET.parse(sys.stdin) + root = tree.getroot() + + width = root.get('width') + height = root.get('height') + if width == None or height == None: + viewbox = root.get('viewBox') + if viewbox: + _, _, width, height = viewbox.split() + + if width == None or height == None: + print("Unable to get width and height for the svg") + sys.exit(1) + width = float(width.split("mm")[0]) + height = float(height.split("mm")[0]) + print(";SVG: With:" + str(width) + " Height:" + str(height)) + # Must keep the ratio of the svg, so offset will be largest between x/y + offset = max(x_offset, y_offset) + corrected_bed_max_x = bed_max_x - offset + corrected_bed_max_y = bed_max_y - offset + scale_x = 1 #corrected_bed_max_x / max(width, height) + scale_y = 1 #corrected_bed_max_y / max(width, height) + + print(preamble) + # Iterator to lower printhead at first point + num_points = 0 + + for elem in root.iter(): + + try: + _, tag_suffix = elem.tag.split('}') + except ValueError: + continue + + if tag_suffix in svg_shapes: + shape_class = getattr(shapes_pkg, tag_suffix) + shape_obj = shape_class(elem) + d = shape_obj.d_path() + m = shape_obj.transformation_matrix() + + if d: + print(shape_preamble) + p = point_generator(d, m, smoothness) + for x,y in p: + print(";X: " + str(x) + " Y: " + str(y)) + if x > 0 and x < bed_max_x and y > 0 and y < bed_max_y: + print("G1 X%0.01f Y%0.01f" % ((scale_x*x)+x_offset, (scale_y*y)+y_offset)) + num_points += 1 + if num_points == 1: + print("M3 I S150 ;start laser") + else: + print("\n; Coordinates out of range:", "G1 X%0.01f Y%0.01f" % ((scale_x*x)+x_offset, (scale_y*y)+y_offset)) + print("; Raw:", str(x), str(y), "\nScaled:", str(scale_x*x), str(scale_y*y), "\nScale Factors:", scale_x, scale_y, "\n") + print("exit") + sys.exit(-1) + print("M5 ;stop laser") + num_points = 0 + print(shape_postamble) + + print(postamble) + print("; Generated", num_points, "points") + +if __name__ == "__main__": + print("; " + str(sys.setrecursionlimit(20000))) + generate_gcode() + + + diff --git a/test_data/10mmx10mm.svg b/test_data/10mmx10mm.svg new file mode 100644 index 0000000..795c6c7 --- /dev/null +++ b/test_data/10mmx10mm.svg @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/test_data/Pezi.svg b/test_data/Pezi.svg new file mode 100644 index 0000000..eb2b1a6 --- /dev/null +++ b/test_data/Pezi.svg @@ -0,0 +1,43 @@ + + + + diff --git a/test_data/Test2DDrawing-BodySketch.svg b/test_data/Test2DDrawing-BodySketch.svg new file mode 100644 index 0000000..a90353d --- /dev/null +++ b/test_data/Test2DDrawing-BodySketch.svg @@ -0,0 +1,8 @@ + + + + + +b'Sketch' + + \ No newline at end of file diff --git a/test_data/Test2DDrawing.FCStd b/test_data/Test2DDrawing.FCStd new file mode 100644 index 0000000..2361a99 Binary files /dev/null and b/test_data/Test2DDrawing.FCStd differ diff --git a/test_data/Test2DDrawing.FCStd1 b/test_data/Test2DDrawing.FCStd1 new file mode 100644 index 0000000..7105b10 Binary files /dev/null and b/test_data/Test2DDrawing.FCStd1 differ diff --git a/test_data/Test_H.svg b/test_data/Test_H.svg new file mode 100644 index 0000000..bf31fa6 --- /dev/null +++ b/test_data/Test_H.svg @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test_data/Zeichnung.svg b/test_data/Zeichnung.svg new file mode 100644 index 0000000..53d9475 --- /dev/null +++ b/test_data/Zeichnung.svg @@ -0,0 +1,43 @@ + + + + diff --git a/test_data/test.gcode b/test_data/test.gcode new file mode 100644 index 0000000..a99c393 --- /dev/null +++ b/test_data/test.gcode @@ -0,0 +1,26 @@ +; None +;SVG: With:10.0 Height:10.0 +G90 ;Absolute programming +G21 ;Programming in millimeters (mm) +M5 ;Disable laser +; ------ +; Draw shapes +;X: 0.053103756 Y: 0.053103756 +G1 X0.1 Y0.1 +M3 I S150 ;start laser +;X: 0.053103756 Y: 0.053103756 +G1 X0.1 Y0.1 +;X: 9.946895956 Y: 0.053103756 +G1 X9.9 Y0.1 +;X: 9.946895956 Y: 9.946895956 +G1 X9.9 Y9.9 +;X: 0.0531037560000005 Y: 9.946895956 +G1 X0.1 Y9.9 +;X: 0.053103756 Y: 0.053103756 +G1 X0.1 Y0.1 +M5 ;stop laser +; ------ +; Shape completed +G1 X0.0 Y0.0; Display printbed +M02 ;End of program +; Generated 0 points