From eff60aa3ee87a96a4256f094b0984f0918021d53 Mon Sep 17 00:00:00 2001 From: localhorst Date: Mon, 13 Jun 2022 12:45:47 +0200 Subject: [PATCH] inital default --- README.md | 12 +- __init__.py | 0 bezmisc.py | 290 +++++++++++++++++++++++++ config.py | 45 ++++ convert.py | 8 + cspsubdiv.py | 40 ++++ cubicsuperpath.py | 170 +++++++++++++++ ffgeom.py | 142 ++++++++++++ shapes.py | 190 ++++++++++++++++ simplepath.py | 203 +++++++++++++++++ simpletransform.py | 242 +++++++++++++++++++++ svg2gcode.code-workspace | 8 + svg2gcode.py | 81 +++++++ test_data/10mmx10mm.svg | 86 ++++++++ test_data/Pezi.svg | 43 ++++ test_data/Test2DDrawing-BodySketch.svg | 8 + test_data/Test2DDrawing.FCStd | Bin 0 -> 6263 bytes test_data/Test2DDrawing.FCStd1 | Bin 0 -> 6293 bytes test_data/Test_H.svg | 108 +++++++++ test_data/Zeichnung.svg | 43 ++++ test_data/test.gcode | 26 +++ 21 files changed, 1744 insertions(+), 1 deletion(-) create mode 100755 __init__.py create mode 100755 bezmisc.py create mode 100644 config.py create mode 100644 convert.py create mode 100755 cspsubdiv.py create mode 100755 cubicsuperpath.py create mode 100644 ffgeom.py create mode 100644 shapes.py create mode 100755 simplepath.py create mode 100755 simpletransform.py create mode 100644 svg2gcode.code-workspace create mode 100755 svg2gcode.py create mode 100644 test_data/10mmx10mm.svg create mode 100644 test_data/Pezi.svg create mode 100644 test_data/Test2DDrawing-BodySketch.svg create mode 100644 test_data/Test2DDrawing.FCStd create mode 100644 test_data/Test2DDrawing.FCStd1 create mode 100644 test_data/Test_H.svg create mode 100644 test_data/Zeichnung.svg create mode 100644 test_data/test.gcode 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 0000000000000000000000000000000000000000..2361a990b699a70c49222b3ef1c351a128d7cc33 GIT binary patch literal 6263 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 literal 0 HcmV?d00001 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