First version of the template project.

This commit is contained in:
Michael Schwarz 2014-11-26 16:24:08 +01:00
commit 16d3e4a963
17 changed files with 2626 additions and 0 deletions

4
.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
/config.mk
*.pyc
*.dxf
*.stl

47
Makefile Normal file
View file

@ -0,0 +1,47 @@
INKSCAPE ?= inkscape
OPENSCAD ?= openscad
# Used by dxf_export/main.sh
export INKSCAPE
# Run generate_scad.sh to get the names of all OpenSCAD files that should be generated using that same script.
GENERATED_FILES := $(addsuffix .scad,$(basename $(shell ./generate_sources.sh)))
GENERATED_SVG_FILES := $(filter %.svg, $(GENERATED_FILES))
GENERATED_SCAD_FILES := $(filter %.scad, $(GENERATED_FILES))
# Source SVG files.
SVG_FILES := $(shell find src -name '*.svg') $(GENERATED_SVG_FILES)
# Only OpenSCAD files whose names do not start with `_' are compiled to STL.
SCAD_FILES := $(shell find src -name '*.scad') $(GENERATED_SCAD_FILES)
LIBRARY_SCAD_FILES := $(foreach i,$(SCAD_FILES),$(if $(filter _%,$(notdir $(i))),$(i)))
COMPILED_SCAD_FILES := $(filter-out $(LIBRARY_SCAD_FILES),$(SCAD_FILES))
# All files that may be generated from the source files.
STL_FILES := $(patsubst %.scad,%.stl,$(COMPILED_SCAD_FILES))
DXF_FILES := $(patsubst %.svg,%.dxf,$(SVG_FILES))
# Everything.
all: $(STL_FILES)
# Everything^-1.
clean:
rm -rf $(DXF_FILES) $(STL_FILES) $(GENERATED_SCAD_FILES)
# Needs to be included after target all has been defined.
-include config.mk
# Assume that any compiled OpenSCAD file may depend on any non-compiled OpenSCAD file in the same directory.
$(foreach i,$(COMPILED_SCAD_FILES),$(eval $(i): $(filter $(dir $(i))%,$(LIBRARY_SCAD_FILES) $(DXF_FILES))))
# Rule to convert an SVG file to a DXF file.
%.dxf: %.svg
dxf_export/main.sh $< $@
# Rule to compile an OpenSCAD file to an STL file.
%.stl: %.scad
$(OPENSCAD) -o $@ $<
# Rule for automaticlaly generated OpenSCAD files.
$(GENERATED_FILES): generate_sources.sh
./generate_sources.sh $@ > $@

127
dxf_export/better_dxf_outlines.py Executable file
View file

@ -0,0 +1,127 @@
#!/usr/bin/env python
'''
Copyright (C) 2005,2007 Aaron Spike, aaron@ekips.org
- template dxf_outlines.dxf added Feb 2008 by Alvin Penner, penner@vaxxine.com
- layers, transformation, flattening added April 2008 by Bob Cook, bob@bobcookdev.com
- bug fix for xpath() calls added February 2009 by Bob Cook, bob@bobcookdev.com
- max value of 10 on path flattening, August 2011 by Bob Cook, bob@bobcookdev.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
'''
import inkex, simplepath, simpletransform, cubicsuperpath, cspsubdiv, dxf_templates, re
class MyEffect(inkex.Effect):
def __init__(self):
inkex.Effect.__init__(self)
self.dxf = ''
self.handle = 255
self.flatness = 0.1
def output(self):
print self.dxf
def dxf_add(self, str):
self.dxf += str
def dxf_insert_code(self, code, value):
self.dxf += code + "\n" + value + "\n"
def dxf_line(self,layer,csp):
self.dxf_insert_code( '0', 'LINE' )
self.dxf_insert_code( '8', layer )
self.dxf_insert_code( '62', '4' )
self.dxf_insert_code( '5', '%x' % self.handle )
self.dxf_insert_code( '100', 'AcDbEntity' )
self.dxf_insert_code( '100', 'AcDbLine' )
self.dxf_insert_code( '10', '%f' % csp[0][0] )
self.dxf_insert_code( '20', '%f' % csp[0][1] )
self.dxf_insert_code( '30', '0.0' )
self.dxf_insert_code( '11', '%f' % csp[1][0] )
self.dxf_insert_code( '21', '%f' % csp[1][1] )
self.dxf_insert_code( '31', '0.0' )
def dxf_point(self,layer,x,y):
self.dxf_insert_code( '0', 'POINT' )
self.dxf_insert_code( '8', layer )
self.dxf_insert_code( '62', '4' )
self.dxf_insert_code( '5', '%x' % self.handle )
self.dxf_insert_code( '100', 'AcDbEntity' )
self.dxf_insert_code( '100', 'AcDbPoint' )
self.dxf_insert_code( '10', '%f' % x )
self.dxf_insert_code( '20', '%f' % y )
self.dxf_insert_code( '30', '0.0' )
def dxf_path_to_lines(self,layer,p):
f = self.flatness
is_flat = 0
while is_flat < 1:
if f > 10:
break
try:
cspsubdiv.cspsubdiv(p, f)
is_flat = 1
except:
f += 0.1
for sub in p:
for i in range(len(sub)-1):
self.handle += 1
s = sub[i]
e = sub[i+1]
self.dxf_line(layer,[s[1],e[1]])
def dxf_path_to_point(self,layer,p):
bbox = simpletransform.roughBBox(p)
x = (bbox[0] + bbox[1]) / 2
y = (bbox[2] + bbox[3]) / 2
self.dxf_point(layer,x,y)
def effect(self):
self.dxf_insert_code( '999', 'Inkscape export via "Better DXF Output" (bob@bobcookdev.com)' )
self.dxf_add( dxf_templates.r14_header )
scale = 25.4/90.0
h = inkex.unittouu(self.document.getroot().xpath('@height',namespaces=inkex.NSS)[0])
path = '//svg:path'
for node in self.document.getroot().xpath(path,namespaces=inkex.NSS):
layer = node.getparent().get(inkex.addNS('label','inkscape'))
if layer == None:
layer = 'Layer 1'
d = node.get('d')
p = cubicsuperpath.parsePath(d)
t = node.get('transform')
if t != None:
m = simpletransform.parseTransform(t)
simpletransform.applyTransformToPath(m,p)
m = [[scale,0,0],[0,-scale,h*scale]]
simpletransform.applyTransformToPath(m,p)
if re.search('drill$',layer,re.I) == None:
self.dxf_path_to_lines(layer,p)
else:
self.dxf_path_to_point(layer,p)
self.dxf_add( dxf_templates.r14_footer )
e = MyEffect()
e.affect()

274
dxf_export/bezmisc.py Executable file
View file

@ -0,0 +1,274 @@
#!/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
'''
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(((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3))):
#parametric bezier
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(((lx1,ly1),(lx2,ly2)),((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3))):
#parametric line
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(((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3)),t):
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(((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3)),t):
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(((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3)),(dy,dx)):
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((x1,y1),(x2,y2),t):
return x1+t*(x2-x1),y1+t*(y2-y1)
def beziersplitatt(((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3)),t):
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 <http://www.cit.gu.edu.au/~anthony/info/graphics/bezier.curves>
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 <gravesen@mat.dth.dk>
"Adaptive subdivision and the length of Bezier curves"
mat-report no. 1992-10, Mathematical Institute, The Technical
University of Denmark.
'''
def pointdistance((x1,y1),(x2,y2)):
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 xrange(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(((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3)), tolerance = 0.001):
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(((bx0,by0),(bx1,by1),(bx2,by2),(bx3,by3)), l = 0.5, tolerance = 0.001):
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

37
dxf_export/cspsubdiv.py Executable file
View file

@ -0,0 +1,37 @@
#!/usr/bin/env python
from bezmisc import *
from ffgeom import *
def maxdist(((p0x,p0y),(p1x,p1y),(p2x,p2y),(p3x,p3y))):
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):
while i < len(sp):
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:
i += 1
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]
# vim: expandtab shiftwidth=4 tabstop=8 softtabstop=4 fileencoding=utf-8 textwidth=99

169
dxf_export/cubicsuperpath.py Executable file
View file

@ -0,0 +1,169 @@
#!/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
"""
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<end:
end -=2*pi
NbSectors=int(abs(start-end)*2/pi)+1
dTeta=(end-start)/NbSectors
#v=dTeta*2/pi*0.552
#v=dTeta*2/pi*4*(sqrt(2)-1)/3
v = 4*tan(dTeta/4)/3
#if not sweepflag:
# v*=-1
p=[]
for i in range(0,NbSectors+1,1):
angle=start+i*dTeta
v1=[O[0]+cos(angle)-(-v)*sin(angle),O[1]+sin(angle)+(-v)*cos(angle)]
pt=[O[0]+cos(angle) ,O[1]+sin(angle) ]
v2=[O[0]+cos(angle)- v *sin(angle),O[1]+sin(angle)+ v *cos(angle)]
p.append([v1,pt,v2])
p[ 0][0]=p[ 0][1][:]
p[-1][2]=p[-1][1][:]
mat=matprod((rotmat(teta),[[rx,0],[0,ry]],rotmat(-teta)))
for pts in p:
applymat(mat, pts[0])
applymat(mat, pts[1])
applymat(mat, pts[2])
return(p)
def CubicSuperPath(simplepath):
csp = []
subpath = -1
subpathstart = []
last = []
lastctrl = []
for s in simplepath:
cmd, params = s
if cmd == 'M':
if last:
csp[subpath].append([lastctrl[:],last[:],last[:]])
subpath += 1
csp.append([])
subpathstart = params[:]
last = params[:]
lastctrl = params[:]
elif cmd == 'L':
csp[subpath].append([lastctrl[:],last[:],last[:]])
last = params[:]
lastctrl = params[:]
elif cmd == 'C':
csp[subpath].append([lastctrl[:],last[:],params[:2]])
last = params[-2:]
lastctrl = params[2:4]
elif cmd == 'Q':
q0=last[:]
q1=params[0:2]
q2=params[2:4]
x0= q0[0]
x1=1./3*q0[0]+2./3*q1[0]
x2= 2./3*q1[0]+1./3*q2[0]
x3= q2[0]
y0= q0[1]
y1=1./3*q0[1]+2./3*q1[1]
y2= 2./3*q1[1]+1./3*q2[1]
y3= q2[1]
csp[subpath].append([lastctrl[:],[x0,y0],[x1,y1]])
last = [x3,y3]
lastctrl = [x2,y2]
elif cmd == 'A':
arcp=ArcToPath(last[:],params[:])
arcp[ 0][0]=lastctrl[:]
last=arcp[-1][1]
lastctrl = arcp[-1][0]
csp[subpath]+=arcp[:-1]
elif cmd == 'Z':
csp[subpath].append([lastctrl[:],last[:],last[:]])
last = subpathstart[:]
lastctrl = subpathstart[:]
#append final superpoint
csp[subpath].append([lastctrl[:],last[:],last[:]])
return csp
def unCubicSuperPath(csp):
a = []
for subpath in csp:
if subpath:
a.append(['M',subpath[0][1][:]])
for i in range(1,len(subpath)):
a.append(['C',subpath[i-1][2][:] + subpath[i][0][:] + subpath[i][1][:]])
return a
def parsePath(d):
return CubicSuperPath(simplepath.parsePath(d))
def formatPath(p):
return simplepath.formatPath(unCubicSuperPath(p))
# vim: expandtab shiftwidth=4 tabstop=8 softtabstop=4 encoding=utf-8 textwidth=99

645
dxf_export/dxf_templates.py Executable file
View file

@ -0,0 +1,645 @@
r14_header = ''' 0
SECTION
2
HEADER
9
$ACADVER
1
AC1014
9
$HANDSEED
5
FFFF
0
ENDSEC
0
SECTION
2
TABLES
0
TABLE
2
VPORT
5
8
330
0
100
AcDbSymbolTable
70
4
0
VPORT
5
2E
330
8
100
AcDbSymbolTableRecord
100
AcDbViewportTableRecord
2
*ACTIVE
70
0
10
0.0
20
0.0
11
1.0
21
1.0
12
4.25
22
5.5
13
0.0
23
0.0
14
10.0
24
10.0
15
10.0
25
10.0
16
0.0
26
0.0
36
1.0
17
0.0
27
0.0
37
0.0
40
11
41
1.24
42
50.0
43
0.0
44
0.0
50
0.0
51
0.0
71
0
72
100
73
1
74
3
75
0
76
0
77
0
78
0
0
ENDTAB
0
TABLE
2
LTYPE
5
5
330
0
100
AcDbSymbolTable
70
1
0
LTYPE
5
14
330
5
100
AcDbSymbolTableRecord
100
AcDbLinetypeTableRecord
2
BYBLOCK
70
0
3
72
65
73
0
40
0.0
0
LTYPE
5
15
330
5
100
AcDbSymbolTableRecord
100
AcDbLinetypeTableRecord
2
BYLAYER
70
0
3
72
65
73
0
40
0.0
0
LTYPE
5
16
330
5
100
AcDbSymbolTableRecord
100
AcDbLinetypeTableRecord
2
CONTINUOUS
70
0
3
Solid line
72
65
73
0
40
0.0
0
ENDTAB
0
TABLE
2
LAYER
5
2
330
0
100
AcDbSymbolTable
70
1
0
LAYER
5
10
330
2
100
AcDbSymbolTableRecord
100
AcDbLayerTableRecord
2
0
70
0
62
7
6
CONTINUOUS
0
ENDTAB
0
TABLE
2
STYLE
5
3
330
0
100
AcDbSymbolTable
70
1
0
STYLE
5
11
330
3
100
AcDbSymbolTableRecord
100
AcDbTextStyleTableRecord
2
STANDARD
70
0
40
0.0
41
1.0
50
0.0
71
0
42
2.5
3
txt
4
0
ENDTAB
0
TABLE
2
VIEW
5
6
330
0
100
AcDbSymbolTable
70
0
0
ENDTAB
0
TABLE
2
UCS
5
7
330
0
100
AcDbSymbolTable
70
0
0
ENDTAB
0
TABLE
2
APPID
5
9
330
0
100
AcDbSymbolTable
70
2
0
APPID
5
12
330
9
100
AcDbSymbolTableRecord
100
AcDbRegAppTableRecord
2
ACAD
70
0
0
ENDTAB
0
TABLE
2
DIMSTYLE
5
A
330
0
100
AcDbSymbolTable
70
1
0
DIMSTYLE
105
27
330
A
100
AcDbSymbolTableRecord
100
AcDbDimStyleTableRecord
2
ISO-25
70
0
3
4
5
6
7
40
1.0
41
2.5
42
0.625
43
3.75
44
1.25
45
0.0
46
0.0
47
0.0
48
0.0
140
2.5
141
2.5
142
0.0
143
0.03937007874016
144
1.0
145
0.0
146
1.0
147
0.625
71
0
72
0
73
0
74
0
75
0
76
0
77
1
78
8
170
0
171
3
172
1
173
0
174
0
175
0
176
0
177
0
178
0
270
2
271
2
272
2
273
2
274
3
340
11
275
0
280
0
281
0
282
0
283
0
284
8
285
0
286
0
287
3
288
0
0
ENDTAB
0
TABLE
2
BLOCK_RECORD
5
1
330
0
100
AcDbSymbolTable
70
1
0
BLOCK_RECORD
5
1F
330
1
100
AcDbSymbolTableRecord
100
AcDbBlockTableRecord
2
*MODEL_SPACE
0
BLOCK_RECORD
5
1B
330
1
100
AcDbSymbolTableRecord
100
AcDbBlockTableRecord
2
*PAPER_SPACE
0
ENDTAB
0
ENDSEC
0
SECTION
2
BLOCKS
0
BLOCK
5
20
330
1F
100
AcDbEntity
8
0
100
AcDbBlockBegin
2
*MODEL_SPACE
70
0
10
0.0
20
0.0
30
0.0
3
*MODEL_SPACE
1
0
ENDBLK
5
21
330
1F
100
AcDbEntity
8
0
100
AcDbBlockEnd
0
BLOCK
5
1C
330
1B
100
AcDbEntity
67
1
8
0
100
AcDbBlockBegin
2
*PAPER_SPACE
1
0
ENDBLK
5
1D
330
1B
100
AcDbEntity
67
1
8
0
100
AcDbBlockEnd
0
ENDSEC
0
SECTION
2
ENTITIES
'''
r14_footer = ''' 0
ENDSEC
0
SECTION
2
OBJECTS
0
DICTIONARY
5
C
330
0
100
AcDbDictionary
3
ACAD_GROUP
350
D
3
ACAD_MLINESTYLE
350
17
0
DICTIONARY
5
D
330
C
100
AcDbDictionary
0
DICTIONARY
5
1A
330
C
100
AcDbDictionary
0
DICTIONARY
5
17
330
C
100
AcDbDictionary
3
STANDARD
350
18
0
DICTIONARY
5
19
330
C
100
AcDbDictionary
0
ENDSEC
0
EOF'''

141
dxf_export/ffgeom.py Executable file
View file

@ -0,0 +1,141 @@
#!/usr/bin/env python
"""
ffgeom.py
Copyright (C) 2005 Aaron Cyril Spike, aaron@ekips.org
This file is part of FretFind 2-D.
FretFind 2-D 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.
FretFind 2-D 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 FretFind 2-D; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
"""
import math
try:
NaN = float('NaN')
except ValueError:
PosInf = 1e300000
NaN = PosInf/PosInf
class Point:
precision = 5
def __init__(self, x, y):
self.__coordinates = {'x' : float(x), 'y' : float(y)}
def __getitem__(self, key):
return self.__coordinates[key]
def __setitem__(self, key, value):
self.__coordinates[key] = float(value)
def __repr__(self):
return '(%s, %s)' % (round(self['x'],self.precision),round(self['y'],self.precision))
def copy(self):
return Point(self['x'],self['y'])
def translate(self, x, y):
self['x'] += x
self['y'] += y
def move(self, x, y):
self['x'] = float(x)
self['y'] = float(y)
class Segment:
def __init__(self, e0, e1):
self.__endpoints = [e0, e1]
def __getitem__(self, key):
return self.__endpoints[key]
def __setitem__(self, key, value):
self.__endpoints[key] = value
def __repr__(self):
return repr(self.__endpoints)
def copy(self):
return Segment(self[0],self[1])
def translate(self, x, y):
self[0].translate(x,y)
self[1].translate(x,y)
def move(self,e0,e1):
self[0] = e0
self[1] = e1
def delta_x(self):
return self[1]['x'] - self[0]['x']
def delta_y(self):
return self[1]['y'] - self[0]['y']
#alias functions
run = delta_x
rise = delta_y
def slope(self):
if self.delta_x() != 0:
return self.delta_x() / self.delta_y()
return NaN
def intercept(self):
if self.delta_x() != 0:
return self[1]['y'] - (self[0]['x'] * self.slope())
return NaN
def distanceToPoint(self, p):
s2 = Segment(self[0],p)
c1 = dot(s2,self)
if c1 <= 0:
return Segment(p,self[0]).length()
c2 = dot(self,self)
if c2 <= c1:
return Segment(p,self[1]).length()
return self.perpDistanceToPoint(p)
def perpDistanceToPoint(self, p):
len = self.length()
if len == 0: return NaN
return math.fabs(((self[1]['x'] - self[0]['x']) * (self[0]['y'] - p['y'])) - \
((self[0]['x'] - p['x']) * (self[1]['y'] - self[0]['y']))) / len
def angle(self):
return math.pi * (math.atan2(self.delta_y(), self.delta_x())) / 180
def length(self):
return math.sqrt((self.delta_x() ** 2) + (self.delta_y() ** 2))
def pointAtLength(self, len):
if self.length() == 0: return Point(NaN, NaN)
ratio = len / self.length()
x = self[0]['x'] + (ratio * self.delta_x())
y = self[0]['y'] + (ratio * self.delta_y())
return Point(x, y)
def pointAtRatio(self, ratio):
if self.length() == 0: return Point(NaN, NaN)
x = self[0]['x'] + (ratio * self.delta_x())
y = self[0]['y'] + (ratio * self.delta_y())
return Point(x, y)
def createParallel(self, p):
return Segment(Point(p['x'] + self.delta_x(), p['y'] + self.delta_y()), p)
def intersect(self, s):
return intersectSegments(self, s)
def intersectSegments(s1, s2):
x1 = s1[0]['x']
x2 = s1[1]['x']
x3 = s2[0]['x']
x4 = s2[1]['x']
y1 = s1[0]['y']
y2 = s1[1]['y']
y3 = s2[0]['y']
y4 = s2[1]['y']
denom = ((y4 - y3) * (x2 - x1)) - ((x4 - x3) * (y2 - y1))
num1 = ((x4 - x3) * (y1 - y3)) - ((y4 - y3) * (x1 - x3))
num2 = ((x2 - x1) * (y1 - y3)) - ((y2 - y1) * (x1 - x3))
num = num1
if denom != 0:
x = x1 + ((num / denom) * (x2 - x1))
y = y1 + ((num / denom) * (y2 - y1))
return Point(x, y)
return Point(NaN, NaN)
def dot(s1, s2):
return s1.delta_x() * s2.delta_x() + s1.delta_y() * s2.delta_y()
# vim: expandtab shiftwidth=4 tabstop=8 softtabstop=4 encoding=utf-8 textwidth=99

241
dxf_export/inkex.py Executable file
View file

@ -0,0 +1,241 @@
#!/usr/bin/env python
"""
inkex.py
A helper module for creating Inkscape extensions
Copyright (C) 2005,2007 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
"""
import sys, copy, optparse, random, re
import gettext
from math import *
_ = gettext.gettext
#a dictionary of all of the xmlns prefixes in a standard inkscape doc
NSS = {
u'sodipodi' :u'http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd',
u'cc' :u'http://creativecommons.org/ns#',
u'ccOLD' :u'http://web.resource.org/cc/',
u'svg' :u'http://www.w3.org/2000/svg',
u'dc' :u'http://purl.org/dc/elements/1.1/',
u'rdf' :u'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
u'inkscape' :u'http://www.inkscape.org/namespaces/inkscape',
u'xlink' :u'http://www.w3.org/1999/xlink',
u'xml' :u'http://www.w3.org/XML/1998/namespace'
}
#a dictionary of unit to user unit conversion factors
uuconv = {'in':90.0, 'pt':1.25, 'px':1, 'mm':3.5433070866, 'cm':35.433070866, 'm':3543.3070866,
'km':3543307.0866, 'pc':15.0, 'yd':3240 , 'ft':1080}
def unittouu(string):
'''Returns userunits given a string representation of units in another system'''
unit = re.compile('(%s)$' % '|'.join(uuconv.keys()))
param = re.compile(r'(([-+]?[0-9]+(\.[0-9]*)?|[-+]?\.[0-9]+)([eE][-+]?[0-9]+)?)')
p = param.match(string)
u = unit.search(string)
if p:
retval = float(p.string[p.start():p.end()])
else:
retval = 0.0
if u:
try:
return retval * uuconv[u.string[u.start():u.end()]]
except KeyError:
pass
return retval
def uutounit(val, unit):
return val/uuconv[unit]
try:
from lxml import etree
except:
sys.exit(_('The fantastic lxml wrapper for libxml2 is required by inkex.py and therefore this extension. Please download and install the latest version from http://cheeseshop.python.org/pypi/lxml/, or install it through your package manager by a command like: sudo apt-get install python-lxml'))
def debug(what):
sys.stderr.write(str(what) + "\n")
return what
def errormsg(msg):
"""Intended for end-user-visible error messages.
(Currently just writes to stderr with an appended newline, but could do
something better in future: e.g. could add markup to distinguish error
messages from status messages or debugging output.)
Note that this should always be combined with translation:
import gettext
_ = gettext.gettext
...
inkex.errormsg(_("This extension requires two selected paths."))
"""
sys.stderr.write((unicode(msg) + "\n").encode("UTF-8"))
def check_inkbool(option, opt, value):
if str(value).capitalize() == 'True':
return True
elif str(value).capitalize() == 'False':
return False
else:
raise optparse.OptionValueError("option %s: invalid inkbool value: %s" % (opt, value))
def addNS(tag, ns=None):
val = tag
if ns!=None and len(ns)>0 and NSS.has_key(ns) and len(tag)>0 and tag[0]!='{':
val = "{%s}%s" % (NSS[ns], tag)
return val
class InkOption(optparse.Option):
TYPES = optparse.Option.TYPES + ("inkbool",)
TYPE_CHECKER = copy.copy(optparse.Option.TYPE_CHECKER)
TYPE_CHECKER["inkbool"] = check_inkbool
class Effect:
"""A class for creating Inkscape SVG Effects"""
def __init__(self, *args, **kwargs):
self.document=None
self.original_document=None
self.ctx=None
self.selected={}
self.doc_ids={}
self.options=None
self.args=None
self.OptionParser = optparse.OptionParser(usage="usage: %prog [options] SVGfile",option_class=InkOption)
self.OptionParser.add_option("--id",
action="append", type="string", dest="ids", default=[],
help="id attribute of object to manipulate")
def effect(self):
pass
def getoptions(self,args=sys.argv[1:]):
"""Collect command line arguments"""
self.options, self.args = self.OptionParser.parse_args(args)
def parse(self,file=None):
"""Parse document in specified file or on stdin"""
try:
try:
stream = open(file,'r')
except:
stream = open(self.svg_file,'r')
except:
stream = sys.stdin
p = etree.XMLParser(huge_tree=True)
self.document = etree.parse(stream, parser=p)
self.original_document = copy.deepcopy(self.document)
stream.close()
def getposinlayer(self):
#defaults
self.current_layer = self.document.getroot()
self.view_center = (0.0,0.0)
layerattr = self.document.xpath('//sodipodi:namedview/@inkscape:current-layer', namespaces=NSS)
if layerattr:
layername = layerattr[0]
layer = self.document.xpath('//svg:g[@id="%s"]' % layername, namespaces=NSS)
if layer:
self.current_layer = layer[0]
xattr = self.document.xpath('//sodipodi:namedview/@inkscape:cx', namespaces=NSS)
yattr = self.document.xpath('//sodipodi:namedview/@inkscape:cy', namespaces=NSS)
doc_height = unittouu(self.document.getroot().get('height'))
if xattr and yattr:
x = xattr[0]
y = yattr[0]
if x and y:
self.view_center = (float(x), doc_height - float(y)) # FIXME: y-coordinate flip, eliminate it when it's gone in Inkscape
def getselected(self):
"""Collect selected nodes"""
for i in self.options.ids:
path = '//*[@id="%s"]' % i
for node in self.document.xpath(path, namespaces=NSS):
self.selected[i] = node
def getElementById(self, id):
path = '//*[@id="%s"]' % id
el_list = self.document.xpath(path, namespaces=NSS)
if el_list:
return el_list[0]
else:
return None
def getParentNode(self, node):
for parent in self.document.getiterator():
if node in parent.getchildren():
return parent
break
def getdocids(self):
docIdNodes = self.document.xpath('//@id', namespaces=NSS)
for m in docIdNodes:
self.doc_ids[m] = 1
def getNamedView(self):
return self.document.xpath('//sodipodi:namedview', namespaces=NSS)[0]
def createGuide(self, posX, posY, angle):
atts = {
'position': str(posX)+','+str(posY),
'orientation': str(sin(radians(angle)))+','+str(-cos(radians(angle)))
}
guide = etree.SubElement(
self.getNamedView(),
addNS('guide','sodipodi'), atts )
return guide
def output(self):
"""Serialize document into XML on stdout"""
original = etree.tostring(self.original_document)
result = etree.tostring(self.document)
if original != result:
self.document.write(sys.stdout)
def affect(self, args=sys.argv[1:], output=True):
"""Affect an SVG document with a callback effect"""
self.svg_file = args[-1]
self.getoptions(args)
self.parse()
self.getposinlayer()
self.getselected()
self.getdocids()
self.effect()
if output: self.output()
def uniqueId(self, old_id, make_new_id = True):
new_id = old_id
if make_new_id:
while new_id in self.doc_ids:
new_id += random.choice('0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ')
self.doc_ids[new_id] = 1
return new_id
def xpathSingle(self, path):
try:
retval = self.document.xpath(path, namespaces=NSS)[0]
except:
errormsg(_("No matching node for expression: %s") % path)
retval = None
return retval
# vim: expandtab shiftwidth=4 tabstop=8 softtabstop=4 encoding=utf-8 textwidth=99

36
dxf_export/main.sh Executable file
View file

@ -0,0 +1,36 @@
#! /usr/bin/env bash
in_file=$1
out_file=$2
# If environment variable DXF_EXPORT_DEBUG is set, the temporary file that is modified using Inkscape is saved in the same directory as the source file and not removed.
if [ "$DXF_EXPORT_DEBUG" ]; then
temp_file="$(dirname "$in_file")/$(basename "$in_file" '.svg')~temp.svg"
else
temp_dir=$(mktemp -d)
temp_file="$temp_dir/temp.svg"
fi
script_path=$(dirname "$BASH_SOURCE")
cp "$in_file" "$temp_file"
# Run a few commands using Inkscape on the SVG file to get in into a shape that makes a successful conversion to DXF more likely.
"$INKSCAPE" \
--verb UnlockAllInAllLayers \
--verb EditSelectAllInAllLayers \
--verb ObjectToPath \
--verb EditSelectAllInAllLayers \
--verb SelectionUnGroup \
--verb EditSelectAllInAllLayers \
--verb StrokeToPath \
--verb FileSave \
--verb FileClose \
"$temp_file"
# Convert the SVG to a DXF file.
python2 "$script_path/better_dxf_outlines.py" "$temp_file" > "$out_file"
if ! [ "$DXF_EXPORT_DEBUG" ]; then
rm -rf "$temp_dir"
fi

212
dxf_export/simplepath.py Executable file
View file

@ -0,0 +1,212 @@
#!/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
"""
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], ['r','r','a',0,'s','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 = lexer.next()
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 = lexer.next()
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
elif defs[3][i] == 'r': # radius parameter
params[i] *= x
elif defs[3][i] == 's': # sweep-flag parameter
if x*y < 0:
params[i] = 1 - params[i]
elif defs[3][i] == 'a': # x-axis-rotation angle
if y < 0:
params[i] = - params[i]
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
# vim: expandtab shiftwidth=4 tabstop=8 softtabstop=4 encoding=utf-8 textwidth=99

244
dxf_export/simplestyle.py Executable file
View file

@ -0,0 +1,244 @@
#!/usr/bin/env python
"""
simplestyle.py
Two simple functions for working with inline css
and some color handling on top.
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
"""
svgcolors={
'aliceblue':'#f0f8ff',
'antiquewhite':'#faebd7',
'aqua':'#00ffff',
'aquamarine':'#7fffd4',
'azure':'#f0ffff',
'beige':'#f5f5dc',
'bisque':'#ffe4c4',
'black':'#000000',
'blanchedalmond':'#ffebcd',
'blue':'#0000ff',
'blueviolet':'#8a2be2',
'brown':'#a52a2a',
'burlywood':'#deb887',
'cadetblue':'#5f9ea0',
'chartreuse':'#7fff00',
'chocolate':'#d2691e',
'coral':'#ff7f50',
'cornflowerblue':'#6495ed',
'cornsilk':'#fff8dc',
'crimson':'#dc143c',
'cyan':'#00ffff',
'darkblue':'#00008b',
'darkcyan':'#008b8b',
'darkgoldenrod':'#b8860b',
'darkgray':'#a9a9a9',
'darkgreen':'#006400',
'darkgrey':'#a9a9a9',
'darkkhaki':'#bdb76b',
'darkmagenta':'#8b008b',
'darkolivegreen':'#556b2f',
'darkorange':'#ff8c00',
'darkorchid':'#9932cc',
'darkred':'#8b0000',
'darksalmon':'#e9967a',
'darkseagreen':'#8fbc8f',
'darkslateblue':'#483d8b',
'darkslategray':'#2f4f4f',
'darkslategrey':'#2f4f4f',
'darkturquoise':'#00ced1',
'darkviolet':'#9400d3',
'deeppink':'#ff1493',
'deepskyblue':'#00bfff',
'dimgray':'#696969',
'dimgrey':'#696969',
'dodgerblue':'#1e90ff',
'firebrick':'#b22222',
'floralwhite':'#fffaf0',
'forestgreen':'#228b22',
'fuchsia':'#ff00ff',
'gainsboro':'#dcdcdc',
'ghostwhite':'#f8f8ff',
'gold':'#ffd700',
'goldenrod':'#daa520',
'gray':'#808080',
'grey':'#808080',
'green':'#008000',
'greenyellow':'#adff2f',
'honeydew':'#f0fff0',
'hotpink':'#ff69b4',
'indianred':'#cd5c5c',
'indigo':'#4b0082',
'ivory':'#fffff0',
'khaki':'#f0e68c',
'lavender':'#e6e6fa',
'lavenderblush':'#fff0f5',
'lawngreen':'#7cfc00',
'lemonchiffon':'#fffacd',
'lightblue':'#add8e6',
'lightcoral':'#f08080',
'lightcyan':'#e0ffff',
'lightgoldenrodyellow':'#fafad2',
'lightgray':'#d3d3d3',
'lightgreen':'#90ee90',
'lightgrey':'#d3d3d3',
'lightpink':'#ffb6c1',
'lightsalmon':'#ffa07a',
'lightseagreen':'#20b2aa',
'lightskyblue':'#87cefa',
'lightslategray':'#778899',
'lightslategrey':'#778899',
'lightsteelblue':'#b0c4de',
'lightyellow':'#ffffe0',
'lime':'#00ff00',
'limegreen':'#32cd32',
'linen':'#faf0e6',
'magenta':'#ff00ff',
'maroon':'#800000',
'mediumaquamarine':'#66cdaa',
'mediumblue':'#0000cd',
'mediumorchid':'#ba55d3',
'mediumpurple':'#9370db',
'mediumseagreen':'#3cb371',
'mediumslateblue':'#7b68ee',
'mediumspringgreen':'#00fa9a',
'mediumturquoise':'#48d1cc',
'mediumvioletred':'#c71585',
'midnightblue':'#191970',
'mintcream':'#f5fffa',
'mistyrose':'#ffe4e1',
'moccasin':'#ffe4b5',
'navajowhite':'#ffdead',
'navy':'#000080',
'oldlace':'#fdf5e6',
'olive':'#808000',
'olivedrab':'#6b8e23',
'orange':'#ffa500',
'orangered':'#ff4500',
'orchid':'#da70d6',
'palegoldenrod':'#eee8aa',
'palegreen':'#98fb98',
'paleturquoise':'#afeeee',
'palevioletred':'#db7093',
'papayawhip':'#ffefd5',
'peachpuff':'#ffdab9',
'peru':'#cd853f',
'pink':'#ffc0cb',
'plum':'#dda0dd',
'powderblue':'#b0e0e6',
'purple':'#800080',
'red':'#ff0000',
'rosybrown':'#bc8f8f',
'royalblue':'#4169e1',
'saddlebrown':'#8b4513',
'salmon':'#fa8072',
'sandybrown':'#f4a460',
'seagreen':'#2e8b57',
'seashell':'#fff5ee',
'sienna':'#a0522d',
'silver':'#c0c0c0',
'skyblue':'#87ceeb',
'slateblue':'#6a5acd',
'slategray':'#708090',
'slategrey':'#708090',
'snow':'#fffafa',
'springgreen':'#00ff7f',
'steelblue':'#4682b4',
'tan':'#d2b48c',
'teal':'#008080',
'thistle':'#d8bfd8',
'tomato':'#ff6347',
'turquoise':'#40e0d0',
'violet':'#ee82ee',
'wheat':'#f5deb3',
'white':'#ffffff',
'whitesmoke':'#f5f5f5',
'yellow':'#ffff00',
'yellowgreen':'#9acd32'
}
def parseStyle(s):
"""Create a dictionary from the value of an inline style attribute"""
if s is None:
return {}
else:
return dict([[x.strip() for x in i.split(":")] for i in s.split(";") if len(i.strip())])
def formatStyle(a):
"""Format an inline style attribute from a dictionary"""
return ";".join([att+":"+str(val) for att,val in a.iteritems()])
def isColor(c):
"""Determine if its a color we can use. If not, leave it unchanged."""
if c.startswith('#') and (len(c)==4 or len(c)==7):
return True
if c.lower() in svgcolors.keys():
return True
#might be "none" or some undefined color constant or rgb()
#however, rgb() shouldnt occur at this point
return False
def parseColor(c):
"""Creates a rgb int array"""
tmp = svgcolors.get(c.lower())
if tmp is not None:
c = tmp
elif c.startswith('#') and len(c)==4:
c='#'+c[1:2]+c[1:2]+c[2:3]+c[2:3]+c[3:]+c[3:]
elif c.startswith('rgb('):
# remove the rgb(...) stuff
tmp = c.strip()[4:-1]
numbers = [number.strip() for number in tmp.split(',')]
converted_numbers = []
if len(numbers) == 3:
for num in numbers:
if num.endswith(r'%'):
converted_numbers.append(int(float(num[0:-1])*255/100))
else:
converted_numbers.append(int(num))
return tuple(converted_numbers)
else:
return (0,0,0)
try:
r=int(c[1:3],16)
g=int(c[3:5],16)
b=int(c[5:],16)
except:
# unknown color ...
# Return a default color. Maybe not the best thing to do but probably
# better than raising an exception.
return(0,0,0)
return (r,g,b)
def formatColoria(a):
"""int array to #rrggbb"""
return '#%02x%02x%02x' % (a[0],a[1],a[2])
def formatColorfa(a):
"""float array to #rrggbb"""
return '#%02x%02x%02x' % (int(round(a[0]*255)),int(round(a[1]*255)),int(round(a[2]*255)))
def formatColor3i(r,g,b):
"""3 ints to #rrggbb"""
return '#%02x%02x%02x' % (r,g,b)
def formatColor3f(r,g,b):
"""3 floats to #rrggbb"""
return '#%02x%02x%02x' % (int(round(r*255)),int(round(g*255)),int(round(b*255)))
# vim: expandtab shiftwidth=4 tabstop=8 softtabstop=4 encoding=utf-8 textwidth=99

241
dxf_export/simpletransform.py Executable file
View file

@ -0,0 +1,241 @@
#!/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.
'''
import inkex, cubicsuperpath, bezmisc, simplestyle
import copy, math, re
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

9
generate_sources.sh Executable file
View file

@ -0,0 +1,9 @@
#! /usr/bin/env bash
if [ "$1" ]; then
# Print the content of the generated source named $1 here.
true
else
# Print a list of names of the files that should be generated using this script here.
true
fi

60
readme.creole Normal file
View file

@ -0,0 +1,60 @@
= Readme
== Prerequisites
* Inkscape
** Used to export DXF files to SVG.
* OpenSCAD
** The currently newest version (2014.03) has trouble with 2D shapes containing holes. The current development version solves these problems.
** It is assumed that the 'openscad' binary is on $PATH.
If any of the required binaries ('inkscape' for Inkscape and 'openscad' for OpenSCAD) is not available on $PATH, paths to these binaries can be configured by creating a file called 'config.mk' in the same directory as the makefile. There, the variables 'OPENSCAD' and 'INKSCAPE' can be set to the appropriate paths like this:
{{{
OPENSCAD := /Applications/OpenSCAD.app/Contents/MacOS/OpenSCAD
}}}
== Supported file types
=== SVG files
Any file whose name ends in `.svg` may be used from an OpenSCAD file like this:
{{{
import("file.dxf");
}}}
The makefile will automatically convert the SVG file to a DXF file.
=== OpenSCAD files
Any file whose name ends in `.scad` but does not start with `_` will be compiled to STL file using OpenSCAD. OpenSCAD files whose names start with `_` can be used as "library" files and can be used from other OpenSCAD files using one of the following commands:
{{{
include <filename>
use <filename>
}}}
Please see the [http://en.wikibooks.org/wiki/OpenSCAD_User_Manual/Print_version|manual] for details.
== Compiling
To compile the whole project, run `make` from the directory in which this readme is. This will process all necessary SVG files and produce an STL file for each OpenSCAD source file. Individual files may be created or updated by passing their names to the make command, as ussual.
To remove all generated files, run `make clean`.
=== Dependency tracking
OpenSCAD actually does have the ability to produce dependency files which can be read by `make` but does this in a way that makes using them in a project with multiple directories near-impossible. Because of this, dependencies between files are generated from a few assumptions based on simple naming-conventions:
* Only files in the `src` directory are compiled.
* Each generated STL file solely depends on the OpenSCAD file with the same name.
* Each generated DXF files solely depends on the SVG files with the same name.
* Each OpenSCAD file depends whose name does not begin with `_` depends on two sets of files:
** All OpenSCAD files in the same directory whose names do begin with `_`.
** All DXF files in the same directory.

17
src/example.scad Normal file
View file

@ -0,0 +1,17 @@
module extrude_layer(layer, height) {
linear_extrude(height = height)
import("example.dxf", layer = layer);
}
render(convexity = 10) {
union() {
difference() {
extrude_layer("base", 2);
translate([0, 0, -1e6])
extrude_layer("text", 2e6);
}
extrude_layer("struts", 1);
}
}

122
src/example.svg Normal file
View file

@ -0,0 +1,122 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 15.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
id="Ebene_2"
x="0px"
y="0px"
width="626.03119"
height="670.09412"
viewBox="0 0 626.03119 670.09412"
enable-background="new 0 0 4856 968"
xml:space="preserve"
inkscape:version="0.48.5 r10040"
sodipodi:docname="example.svg"><metadata
id="metadata41"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title /></cc:Work></rdf:RDF></metadata><defs
id="defs39"><inkscape:path-effect
is_visible="true"
id="path-effect3785"
effect="spiro" /><inkscape:path-effect
is_visible="true"
id="path-effect3777"
effect="spiro" /><inkscape:path-effect
effect="spiro"
id="path-effect3773"
is_visible="true" /><inkscape:path-effect
effect="spiro"
id="path-effect3769"
is_visible="true" /><inkscape:path-effect
effect="spiro"
id="path-effect3779"
is_visible="true" /></defs><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1275"
inkscape:window-height="1002"
id="namedview37"
showgrid="true"
fit-margin-top="20"
fit-margin-left="20"
fit-margin-right="20"
fit-margin-bottom="20"
inkscape:zoom="2.4630583"
inkscape:cx="100.04789"
inkscape:cy="-9.3088588"
inkscape:window-x="0"
inkscape:window-y="22"
inkscape:window-maximized="0"
inkscape:current-layer="layer1"
inkscape:document-units="mm"
units="mm"
inkscape:object-paths="true"
inkscape:snap-nodes="true"
inkscape:snap-grids="true"
inkscape:snap-to-guides="false"
inkscape:snap-intersection-paths="false"><inkscape:grid
type="xygrid"
id="grid2988"
empspacing="10"
visible="true"
enabled="true"
snapvisiblegridlinesonly="true"
spacingx="1mm"
spacingy="1mm"
dotted="false"
units="mm" /></sodipodi:namedview><g
inkscape:groupmode="layer"
id="layer2"
inkscape:label="base"
style="display:inline"><rect
style="color:#000000;fill:#000000;fill-opacity:0.39215686;fill-rule:nonzero;stroke:none;stroke-width:14.17322826;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
id="rect2989"
width="141.73228"
height="70.866119"
x="-1.2523757e-06"
y="599.22797" /></g><g
inkscape:groupmode="layer"
id="layer1"
inkscape:label="text"
style="display:inline"><text
xml:space="preserve"
style="font-size:60px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#0000ff;fill-opacity:0.39215686;stroke:none;font-family:Bitstream Vera Sans;-inkscape-font-specification:Bitstream Vera Sans"
x="17.716534"
y="655.9209"
id="text3779"
sodipodi:linespacing="125%"><tspan
sodipodi:role="line"
id="tspan3781"
x="17.716534"
y="655.9209">Abc</tspan></text>
</g><g
inkscape:groupmode="layer"
id="layer3"
inkscape:label="struts"
style="display:inline"><path
style="fill:none;stroke:#ff0000;stroke-width:2.83464575;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:0.39215686;stroke-dasharray:none"
d="m 38.164379,606.53629 0,45.8413"
id="path3767"
inkscape:path-effect="#path-effect3769"
inkscape:original-d="m 38.164379,606.53629 0,45.8413"
inkscape:connector-curvature="0" /><path
inkscape:connector-curvature="0"
inkscape:original-d="m 79.060068,617.16621 0,45.8413"
inkscape:path-effect="#path-effect3785"
id="path3783"
d="m 79.060068,617.16621 0,45.8413"
style="fill:none;stroke:#ff0000;stroke-width:2.83464575;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:0.39215686;stroke-dasharray:none" /></g></svg>

After

Width:  |  Height:  |  Size: 4.7 KiB