#*************************************************************************** #* * #* Copyright (c) 2009 Yorik van Havre * #* * #* This program is free software; you can redistribute it and/or modify * #* it under the terms of the GNU Lesser General Public License (LGPL) * #* as published by the Free Software Foundation; either version 2 of * #* the License, or (at your option) any later version. * #* for detail see the LICENCE text file. * #* * #* 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 Library General Public License for more details. * #* * #* You should have received a copy of the GNU Library 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 * #* * #*************************************************************************** __title__="FreeCAD Draft Workbench - SVG importer/exporter" __author__ = "Yorik van Havre, Sebastian Hoogen" __url__ = ["http://www.freecadweb.org"] ''' This script imports SVG files in FreeCAD. Currently only reads the following entities: paths, lines, circular arcs ,rects, circles, ellipses, polygons, polylines. currently unsupported: use, image ''' #ToDo: # ignoring CDATA # handle image element (external references and inline base64) # debug Problem with 'Sans' font from Inkscape # debug Problem with fill color # implement inherting fill style from group # handle relative units import xml.sax, string, FreeCAD, os, math, re, Draft, DraftVecUtils from FreeCAD import Vector try: import FreeCADGui except ImportError: gui = False else: gui = True try: draftui = FreeCADGui.draftToolBar except AttributeError: draftui = None if open.__module__ == '__builtin__': pythonopen = open svgcolors = { 'Pink': (255, 192, 203), 'Blue': (0, 0, 255), 'Honeydew': (240, 255, 240), 'Purple': (128, 0, 128), 'Fuchsia': (255, 0, 255), 'LawnGreen': (124, 252, 0), 'Amethyst': (153, 102, 204), 'Crimson': (220, 20, 60), 'White': (255, 255, 255), 'NavajoWhite': (255, 222, 173), 'Cornsilk': (255, 248, 220), 'Bisque': (255, 228, 196), 'PaleGreen': (152, 251, 152), 'Brown': (165, 42, 42), 'DarkTurquoise': (0, 206, 209), 'DarkGreen': (0, 100, 0), 'MediumOrchid': (186, 85, 211), 'Chocolate': (210, 105, 30), 'PapayaWhip': (255, 239, 213), 'Olive': (128, 128, 0), 'Silver': (192, 192, 192), 'PeachPuff': (255, 218, 185), 'Plum': (221, 160, 221), 'DarkGoldenrod': (184, 134, 11), 'SlateGrey': (112, 128, 144), 'MintCream': (245, 255, 250), 'CornflowerBlue': (100, 149, 237), 'Gold': (255, 215, 0), 'HotPink': (255, 105, 180), 'DarkBlue': (0, 0, 139), 'LimeGreen': (50, 205, 50), 'DeepSkyBlue': (0, 191, 255), 'DarkKhaki': (189, 183, 107), 'LightGrey': (211, 211, 211), 'Yellow': (255, 255, 0), 'Gainsboro': (220, 220, 220), 'MistyRose': (255, 228, 225), 'SandyBrown': (244, 164, 96), 'DeepPink': (255, 20, 147), 'Magenta': (255, 0, 255), 'AliceBlue': (240, 248, 255), 'DarkCyan': (0, 139, 139), 'DarkSlateGrey': (47, 79, 79), 'GreenYellow': (173, 255, 47), 'DarkOrchid': (153, 50, 204), 'OliveDrab': (107, 142, 35), 'Chartreuse': (127, 255, 0), 'Peru': (205, 133, 63), 'Orange': (255, 165, 0), 'Red': (255, 0, 0), 'Wheat': (245, 222, 179), 'LightCyan': (224, 255, 255), 'LightSeaGreen': (32, 178, 170), 'BlueViolet': (138, 43, 226), 'LightSlateGrey': (119, 136, 153), 'Cyan': (0, 255, 255), 'MediumPurple': (147, 112, 219), 'MidnightBlue': (25, 25, 112), 'FireBrick': (178, 34, 34), 'PaleTurquoise': (175, 238, 238), 'PaleGoldenrod': (238, 232, 170), 'Gray': (128, 128, 128), 'MediumSeaGreen': (60, 179, 113), 'Moccasin': (255, 228, 181), 'Ivory': (255, 255, 240), 'DarkSlateBlue': (72, 61, 139), 'Beige': (245, 245, 220), 'Green': (0, 128, 0), 'SlateBlue': (106, 90, 205), 'Teal': (0, 128, 128), 'Azure': (240, 255, 255), 'LightSteelBlue': (176, 196, 222), 'DimGrey': (105, 105, 105), 'Tan': (210, 180, 140), 'AntiqueWhite': (250, 235, 215), 'SkyBlue': (135, 206, 235), 'GhostWhite': (248, 248, 255), 'MediumTurquoise': (72, 209, 204), 'FloralWhite': (255, 250, 240), 'LavenderBlush': (255, 240, 245), 'SeaGreen': (46, 139, 87), 'Lavender': (230, 230, 250), 'BlanchedAlmond': (255, 235, 205), 'DarkOliveGreen': (85, 107, 47), 'DarkSeaGreen': (143, 188, 143), 'SpringGreen': (0, 255, 127), 'Navy': (0, 0, 128), 'Orchid': (218, 112, 214), 'SaddleBrown': (139, 69, 19), 'IndianRed': (205, 92, 92), 'Snow': (255, 250, 250), 'SteelBlue': (70, 130, 180), 'MediumSlateBlue': (123, 104, 238), 'Black': (0, 0, 0), 'LightBlue': (173, 216, 230), 'Turquoise': (64, 224, 208), 'MediumVioletRed': (199, 21, 133), 'DarkViolet': (148, 0, 211), 'DarkGray': (169, 169, 169), 'Salmon': (250, 128, 114), 'DarkMagenta': (139, 0, 139), 'Tomato': (255, 99, 71), 'WhiteSmoke': (245, 245, 245), 'Goldenrod': (218, 165, 32), 'MediumSpringGreen': (0, 250, 154), 'DodgerBlue': (30, 144, 255), 'Aqua': (0, 255, 255), 'ForestGreen': (34, 139, 34), 'LemonChiffon': (255, 250, 205), 'LightSlateGray': (119, 136, 153), 'SlateGray': (112, 128, 144), 'LightGray': (211, 211, 211), 'Indigo': (75, 0, 130), 'CadetBlue': (95, 158, 160), 'LightYellow': (255, 255, 224), 'DarkOrange': (255, 140, 0), 'PowderBlue': (176, 224, 230), 'RoyalBlue': (65, 105, 225), 'Sienna': (160, 82, 45), 'Thistle': (216, 191, 216), 'Lime': (0, 255, 0), 'Seashell': (255, 245, 238), 'DarkRed': (139, 0, 0), 'LightSkyBlue': (135, 206, 250), 'YellowGreen': (154, 205, 50), 'Aquamarine': (127, 255, 212), 'LightCoral': (240, 128, 128), 'DarkSlateGray': (47, 79, 79), 'Khaki': (240, 230, 140), 'DarkGrey': (169, 169, 169), 'BurlyWood': (222, 184, 135), 'LightGoldenrodYellow': (250, 250, 210), 'MediumBlue': (0, 0, 205), 'DarkSalmon': (233, 150, 122), 'RosyBrown': (188, 143, 143), 'LightSalmon': (255, 160, 122), 'PaleVioletRed': (219, 112, 147), 'Coral': (255, 127, 80), 'Violet': (238, 130, 238), 'Grey': (128, 128, 128), 'LightGreen': (144, 238, 144), 'Linen': (250, 240, 230), 'OrangeRed': (255, 69, 0), 'DimGray': (105, 105, 105), 'Maroon': (128, 0, 0), 'LightPink': (255, 182, 193), 'MediumAquamarine': (102, 205, 170), 'OldLace': (253, 245, 230) } svgcolorslower = dict((key.lower(),value) for key,value in \ list(svgcolors.items())) def getcolor(color): "checks if the given string is a RGB value, or if it is a named color. returns 1-based RGBA tuple." if (color[0] == "#"): if len(color) == 7: r = float(int(color[1:3],16)/255.0) g = float(int(color[3:5],16)/255.0) b = float(int(color[5:],16)/255.0) elif len(color) == 4: #expand the hex digits r = float(int(color[1],16)*17/255.0) g = float(int(color[2],16)*17/255.0) b = float(int(color[3],16)*17/255.0) return (r,g,b,0.0) elif color.lower().startswith('rgb('): cvalues=color[3:].lstrip('(').rstrip(')').replace('%',' ').split(',') if '%' in color: r,g,b = [int(cv)/100.0 for cv in cvalues] else: r,g,b = [int(cv)/255.0 for cv in cvalues] return (r,g,b,0.0) else: v=svgcolorslower.get(color.lower()) if v: r,g,b = [float(vf)/255.0 for vf in v] return (r,g,b,0.0) #for k,v in svgcolors.iteritems(): # if (k.lower() == color.lower()): pass def transformCopyShape(shape,m): """apply transformation matrix m on given shape since OCCT 6.8.0 transformShape can be used to apply certian non-orthogonal transformations on shapes. This way a conversion to BSplines in transformGeometry can be avoided.""" if abs(m.A11**2+m.A12**2 -m.A21**2-m.A22**2) < 1e-8 and \ abs(m.A11*m.A21+m.A12*m.A22) < 1e-8: #no shear try: newshape=shape.copy() newshape.transformShape(m) return newshape except Part.OCCError: # older versions of OCCT will refuse to work on pass # non-orthogonal matrices return shape.transformGeometry(m) def getsize(length,mode='discard',base=1): """parses length values containing number and unit with mode 'discard': extracts a number from the given string (removes unit suffixes) with mode 'tuple': return number and unit as a tuple with mode 'css': convert the unit to px assuming 90dpi with mode 'mm': convert the unit to millimeter assuming 90dpi""" tomm={ '' : 25.4/90, #default 'px' : 25.4/90, 'pt' : 1.25*25.4/90, 'pc' : 15*25.4/90, 'mm' : 1.0, 'cm' : 10.0, 'in' : 25.4, 'em': 15*2.54/90, #arbitrarily chosen; has to depend on font size 'ex': 10*2.54/90, #arbitrarily chosen; has to depend on font size '%': 100 #arbitrarily chosen; has to depend on vieport size or (for filling patterns) on bounding box } topx={ '' : 1.0, #default 'px' : 1.0, 'pt' : 1.25, 'pc' : 15, 'mm' : 90.0/25.4, 'cm' : 90.0/254.0, 'in' : 90, 'em': 15, #arbitrarily chosen; has to depend on font size 'ex': 10, #arbitrarily chosen; has to depend on font size '%': 100 #arbitrarily chosen; has to depend on vieport size or (for filling patterns) on bounding box } number, exponent, unit=re.findall('([-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?)(px|pt|pc|mm|cm|in|em|ex|%)?',length)[0] if mode =='discard': return float(number) elif mode == 'tuple': return float(number),unit elif mode == 'isabsolute': return unit in ('mm','cm','in','px','pt') elif mode == 'mm': return float(number)*tomm[unit] elif mode == 'css': if unit != '%': return float(number)*topx[unit] else: return float(number)*base def makewire(path,checkclosed=False,donttry=False): '''try to make a wire out of the list of edges. If the 'Wire' functions fails or the wire is not closed if required the 'connectEdgesToWires' function is used''' if not donttry: try: import DraftGeomUtils sh = Part.Wire(DraftGeomUtils.sortEdges(path)) #sh = Part.Wire(path) isok = (not checkclosed) or sh.isClosed() except Part.OCCError:# BRep_API:command not done isok = False if donttry or not isok: #Code from wmayer forum p15549 to fix the tolerance problem #original tolerance = 0.00001 comp=Part.Compound(path) sh = comp.connectEdgesToWires(False,10**(-1*(Draft.precision()-2))).Wires[0] return sh def arccenter2end(center,rx,ry,angle1,angledelta,xrotation=0.0): '''calculate start and end vector and flags of an arc given in center parametrization see http://www.w3.org/TR/SVG/implnote.html#ArcImplementationNotes returns (v1,v2,largerc,sweep)''' vr1=Vector(rx*math.cos(angle1),ry*math.sin(angle1),0) vr2=Vector(rx*math.cos(angle1+angledelta),ry*math.sin(angle1+angledelta),0) mxrot=FreeCAD.Matrix() mxrot.rotateZ(xrotation) v1 = mxrot.multiply(vr1).add(center) v2 = mxrot.multiply(vr2).add(center) fa = ((abs(angledelta) / math.pi) % 2) > 1 # <180deg fs = angledelta < 0 return v1,v2,fa,fs def arcend2center(lastvec,currentvec,rx,ry,xrotation=0.0,correction=False): '''calculate (positive and negative) possible centers for an arc in endpoint parameterization see http://www.w3.org/TR/SVG/implnote.html#ArcImplementationNotes rotation or x-axis has to be specified in radians (CCW) the sweepflag is interpreted as: sweepflag <==> arc is travelled clockwise returns [(vcenter+,angle1+,angledelta+),(...-)]''' #scalefacsign = 1 if (largeflag != sweepflag) else -1 rx = float(rx) ry = float(ry) v0 = lastvec.sub(currentvec) v0.multiply(0.5) m1=FreeCAD.Matrix() m1.rotateZ(-xrotation) #Formular 6.5.1 v1=m1.multiply(v0) if correction: eparam = v1.x**2 / rx**2 + v1.y**2 / ry**2 if eparam > 1: eproot = math.sqrt(eparam) rx = eproot * rx ry = eproot * ry denom = rx**2 * v1.y**2+ ry**2 * v1.x**2 numer = rx**2 * ry**2 -denom results=[] if abs(numer/denom) < 10**(-1*(Draft.precision())): scalefacpos = 0 else: try: scalefacpos = math.sqrt(numer/denom) except ValueError: FreeCAD.Console.PrintMessage('sqrt(%f/%f)\n' % (numer,denom)) scalefacpos = 0 for scalefacsign in (1,-1): scalefac = scalefacpos * scalefacsign vcx1 = Vector(v1.y*rx/ry,-v1.x*ry/rx,0).multiply(scalefac) # Step2 F.6.5.2 m2=FreeCAD.Matrix() m2.rotateZ(xrotation) centeroff = currentvec.add(lastvec) centeroff.multiply(.5) vcenter = m2.multiply(vcx1).add(centeroff) # Step3 F.6.5.3 #angle1 = Vector(1,0,0).getAngle(Vector((v1.x-vcx1.x)/rx,(v1.y-vcx1.y)/ry,0)) # F.6.5.5 #angledelta = Vector((v1.x-vcx1.x)/rx,(v1.y-vcx1.y)/ry,0).getAngle(Vector((-v1.x-vcx1.x)/rx,(-v1.y-vcx1.y)/ry,0)) # F.6.5.6 #we need the right sign for the angle angle1 = DraftVecUtils.angle(Vector(1,0,0),Vector((v1.x-vcx1.x)/rx,(v1.y-vcx1.y)/ry,0)) # F.6.5.5 angledelta = DraftVecUtils.angle(Vector((v1.x-vcx1.x)/rx,(v1.y-vcx1.y)/ry,0),Vector((-v1.x-vcx1.x)/rx,(-v1.y-vcx1.y)/ry,0)) # F.6.5.6 results.append((vcenter,angle1,angledelta)) return results,(rx,ry) def getrgb(color): "returns a rgb value #000000 from a freecad color" r = str(hex(int(color[0]*255)))[2:].zfill(2) g = str(hex(int(color[1]*255)))[2:].zfill(2) b = str(hex(int(color[2]*255)))[2:].zfill(2) return "#"+r+g+b class svgHandler(xml.sax.ContentHandler): "this handler parses the svg files and creates freecad objects" def __init__(self): "retrieving Draft parameters" params = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Draft") self.style = params.GetInt("svgstyle") self.count = 0 self.transform = None self.grouptransform = [] self.lastdim = None self.viewbox = None global Part import Part if gui and draftui: r = float(draftui.color.red()/255.0) g = float(draftui.color.green()/255.0) b = float(draftui.color.blue()/255.0) self.lw = float(draftui.linewidth) else: self.lw = float(params.GetInt("linewidth")) c = params.GetUnsigned("color") r = float(((c>>24)&0xFF)/255) g = float(((c>>16)&0xFF)/255) b = float(((c>>8)&0xFF)/255) self.col = (r,g,b,0.0) def format(self,obj): "applies styles to passed object" if self.style and gui: v = obj.ViewObject if self.color: v.LineColor = self.color if self.width: v.LineWidth = self.width if self.fill: v.ShapeColor = self.fill def startElement(self, name, attrs): # reorganizing data into a nice clean dictionary self.count += 1 FreeCAD.Console.PrintMessage('processing element %d: %s\n'%(self.count,name)) FreeCAD.Console.PrintMessage('existing group transform: %s\n'%(str(self.grouptransform))) data = {} for (keyword,content) in list(attrs.items()): #print keyword,content content = content.replace(',',' ') content = content.split() #print keyword,content data[keyword]=content if 'style' in data: if not data['style']: pass#empty style attribute stops inhertig from parent else: content = data['style'][0].replace(' ','') content = content.split(';') for i in content: pair = i.split(':') if len(pair)>1: data[pair[0]]=pair[1] for k in ['x','y','x1','y1','x2','y2','r','rx','ry','cx','cy','width','height']: if k in data: data[k] = getsize(data[k][0],'css') for k in ['fill','stroke','stroke-width','font-size']: if k in data: if isinstance(data[k],list): if data[k][0].lower().startswith("rgb("): data[k] = ",".join(data[k]) else: data[k]=data[k][0] # extracting style info self.fill = None self.color = None self.width = None self.text = None if name == 'svg': m=FreeCAD.Matrix() if 'width' in data and 'height' in data and \ 'viewBox' in data: vbw=float(data['viewBox'][2]) vbh=float(data['viewBox'][3]) w=attrs.getValue('width') h=attrs.getValue('height') self.viewbox=(vbw,vbh) if len(self.grouptransform)==0: unitmode='mm' else: #nested svg element unitmode='css' abw = getsize(w,unitmode) abh = getsize(h,unitmode) sx=abw/vbw sy=abh/vbh preservearstr=' '.join(data.get('preserveAspectRatio',[])).lower() uniformscaling = round(sx/sy,5) == 1 if uniformscaling: m.scale(Vector(sx,sy,1)) else: FreeCAD.Console.PrintWarning('Scaling Factors do not match!!!\n') if preservearstr.startswith('none'): m.scale(Vector(sx,sy,1)) else: #preserve the aspect ratio if preservearstr.endswith('slice'): sxy=max(sx,sy) else: sxy=min(sx,sy) m.scale(Vector(sxy,sxy,1)) elif len(self.grouptransform)==0: #fallback to 90 dpi m.scale(Vector(25.4/90.0,25.4/90.0,1)) self.grouptransform.append(m) if 'fill' in data: if data['fill'][0] != 'none': self.fill = getcolor(data['fill']) if 'stroke' in data: if data['stroke'][0] != 'none': self.color = getcolor(data['stroke']) if 'stroke-width' in data: if data['stroke-width'] != 'none': self.width = getsize(data['stroke-width'],'css') if 'transform' in data: m = self.getMatrix(attrs.getValue('transform')) if name == "g": self.grouptransform.append(m) else: self.transform = m else: if name == "g": self.grouptransform.append(FreeCAD.Matrix()) if (self.style == 1): self.color = self.col self.width = self.lw pathname = None if 'id' in data: pathname = data['id'][0] FreeCAD.Console.PrintMessage('name: %s\n'%pathname) # processing paths if name == "path": FreeCAD.Console.PrintMessage('data: %s\n'%str(data)) if not pathname: pathname = 'Path' path = [] point = [] lastvec = Vector(0,0,0) lastpole = None command = None relative = False firstvec = None if "freecad:basepoint1" in data: p1 = data["freecad:basepoint1"] p1 = Vector(float(p1[0]),-float(p1[1]),0) p2 = data["freecad:basepoint2"] p2 = Vector(float(p2[0]),-float(p2[1]),0) p3 = data["freecad:dimpoint"] p3 = Vector(float(p3[0]),-float(p3[1]),0) obj = Draft.makeDimension(p1,p2,p3) self.applyTrans(obj) self.format(obj) self.lastdim = obj data['d']=[] pathcommandsre=re.compile('\s*?([mMlLhHvVaAcCqQsStTzZ])\s*?([^mMlLhHvVaAcCqQsStTzZ]*)\s*?',re.DOTALL) pointsre=re.compile('([-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?)',re.DOTALL) for d,pointsstr in pathcommandsre.findall(' '.join(data['d'])): relative = d.islower() pointlist = [float(number) for number,exponent in pointsre.findall(pointsstr.replace(',',' '))] if (d == "M" or d == "m"): x = pointlist.pop(0) y = pointlist.pop(0) if path: #sh = Part.Wire(path) sh = makewire(path) if self.fill and sh.isClosed(): sh = Part.Face(sh) sh = self.applyTrans(sh) obj = self.doc.addObject("Part::Feature",pathname) obj.Shape = sh self.format(obj) path = [] #if firstvec: # lastvec = firstvec #Move relative to last move command not last draw command if relative: lastvec = lastvec.add(Vector(x,-y,0)) else: lastvec = Vector(x,-y,0) firstvec = lastvec FreeCAD.Console.PrintMessage('move %s\n'%str(lastvec)) lastpole = None if (d == "L" or d == "l") or \ ((d == 'm' or d == 'M') and pointlist) : for x,y in zip(pointlist[0::2],pointlist[1::2]): if relative: currentvec = lastvec.add(Vector(x,-y,0)) else: currentvec = Vector(x,-y,0) if not DraftVecUtils.equals(lastvec,currentvec): seg = Part.Line(lastvec,currentvec).toShape() FreeCAD.Console.PrintMessage("line %s %s\n" %(lastvec,currentvec)) lastvec = currentvec path.append(seg) lastpole = None elif (d == "H" or d == "h"): for x in pointlist: if relative: currentvec = lastvec.add(Vector(x,0,0)) else: currentvec = Vector(x,lastvec.y,0) seg = Part.Line(lastvec,currentvec).toShape() lastvec = currentvec lastpole = None path.append(seg) elif (d == "V" or d == "v"): for y in pointlist: if relative: currentvec = lastvec.add(Vector(0,-y,0)) else: currentvec = Vector(lastvec.x,-y,0) seg = Part.Line(lastvec,currentvec).toShape() lastvec = currentvec lastpole = None path.append(seg) elif (d == "A" or d == "a"): for rx,ry,xrotation, largeflag, sweepflag,x,y in \ zip(pointlist[0::7],pointlist[1::7],pointlist[2::7],pointlist[3::7],pointlist[4::7],pointlist[5::7],pointlist[6::7]): #support for large-arc and x-rotation are missing if relative: currentvec = lastvec.add(Vector(x,-y,0)) else: currentvec = Vector(x,-y,0) chord = currentvec.sub(lastvec) if (not largeflag) and abs(rx-ry) < 10**(-1*Draft.precision()): # small circular arc # perp = chord.cross(Vector(0,0,-1)) # here is a better way to find the perpendicular if sweepflag == 1: # clockwise perp = DraftVecUtils.rotate2D(chord,-math.pi/2) else: # anticlockwise perp = DraftVecUtils.rotate2D(chord,math.pi/2) chord.multiply(.5) if chord.Length > rx: a = 0 else: a = math.sqrt(rx**2-chord.Length**2) s = rx - a perp.multiply(s/perp.Length) midpoint = lastvec.add(chord.add(perp)) seg = Part.Arc(lastvec,midpoint,currentvec).toShape() else:# big arc or elliptical arc solution,(rx,ry) = arcend2center(lastvec,currentvec,rx,ry,math.radians(-xrotation),True) negsol = (largeflag != sweepflag) vcenter,angle1,angledelta = solution[negsol] #print angle1 #print angledelta if ry > rx: rx,ry=ry,rx swapaxis = True else: swapaxis = False #print 'Elliptical arc %s rx=%f ry=%f' % (vcenter,rx,ry) e1 = Part.Ellipse(vcenter,rx,ry) if sweepflag: #angledelta=-(-angledelta % (math.pi *2)) # Step4 #angledelta=(-angledelta % (math.pi *2)) # Step4 angle1 = angle1+angledelta angledelta = -angledelta #angle1 = math.pi - angle1 e1a = Part.Arc(e1,angle1-swapaxis*math.radians(90),\ angle1+angledelta-swapaxis*math.radians(90)) #e1a = Part.Arc(e1,angle1-0*swapaxis*math.radians(90),angle1+angledelta-0*swapaxis*math.radians(90)) if swapaxis or xrotation > 10**(-1*Draft.precision()): m3=FreeCAD.Matrix() m3.move(vcenter) rot90=FreeCAD.Matrix(0,-1,0,0,1,0) #90 #swapaxism=FreeCAD.Matrix(0,1,0,0,1,0) if swapaxis: m3=m3.multiply(rot90) m3.rotateZ(math.radians(-xrotation)) m3.move(vcenter.multiply(-1)) e1a.transform(m3) seg = e1a.toShape() if sweepflag: seg.reverse() #obj = self.doc.addObject("Part::Feature",'DEBUG %s'%pathname) #DEBUG #obj.Shape = seg #DEBUG #seg = Part.Line(lastvec,currentvec).toShape() #DEBUG lastvec = currentvec lastpole = None path.append(seg) elif (d == "C" or d == "c") or\ (d =="S" or d == "s"): smooth = (d == 'S' or d == 's') if smooth: piter = list(zip(pointlist[2::4],pointlist[3::4],pointlist[0::4],pointlist[1::4],pointlist[2::4],pointlist[3::4])) else: piter = list(zip(pointlist[0::6],pointlist[1::6],pointlist[2::6],pointlist[3::6],pointlist[4::6],pointlist[5::6])) for p1x,p1y,p2x,p2y,x,y in piter: if smooth: if lastpole is not None and lastpole[0]=='cubic': pole1 = lastvec.sub(lastpole[1]).add(lastvec) else: pole1 = lastvec else: if relative: pole1 = lastvec.add(Vector(p1x,-p1y,0)) else: pole1 = Vector(p1x,-p1y,0) if relative: currentvec = lastvec.add(Vector(x,-y,0)) pole2 = lastvec.add(Vector(p2x,-p2y,0)) else: currentvec = Vector(x,-y,0) pole2 = Vector(p2x,-p2y,0) if not DraftVecUtils.equals(currentvec,lastvec): mainv = currentvec.sub(lastvec) pole1v = lastvec.add(pole1) pole2v = currentvec.add(pole2) #print "cubic curve data:",mainv.normalize(),pole1v.normalize(),pole2v.normalize() if True and \ pole1.distanceToLine(lastvec,currentvec) < 10**(-1*(2+Draft.precision())) and \ pole2.distanceToLine(lastvec,currentvec) < 10**(-1*(2+Draft.precision())): #print "straight segment" seg = Part.Line(lastvec,currentvec).toShape() else: #print "cubic bezier segment" b = Part.BezierCurve() b.setPoles([lastvec,pole1,pole2,currentvec]) seg = b.toShape() #print "connect ",lastvec,currentvec lastvec = currentvec lastpole = ('cubic',pole2) path.append(seg) elif (d == "Q" or d == "q") or\ (d =="T" or d == "t"): smooth = (d == 'T' or d == 't') if smooth: piter = list(zip(pointlist[1::2],pointlist[1::2],pointlist[0::2],pointlist[1::2])) else: piter = list(zip(pointlist[0::4],pointlist[1::4],pointlist[2::4],pointlist[3::4])) for px,py,x,y in piter: if smooth: if lastpole is not None and lastpole[0]=='quadratic': pole = lastvec.sub(lastpole[1]).add(lastvec) else: pole = lastvec else: if relative: pole = lastvec.add(Vector(px,-py,0)) else: pole = Vector(px,-py,0) if relative: currentvec = lastvec.add(Vector(x,-y,0)) else: currentvec = Vector(x,-y,0) if not DraftVecUtils.equals(currentvec,lastvec): if True and \ pole.distanceToLine(lastvec,currentvec) < 20**(-1*(2+Draft.precision())): #print "straight segment" seg = Part.Line(lastvec,currentvec).toShape() else: #print "quadratic bezier segment" b = Part.BezierCurve() b.setPoles([lastvec,pole,currentvec]) seg = b.toShape() #print "connect ",lastvec,currentvec lastvec = currentvec lastpole = ('quadratic',pole) path.append(seg) elif (d == "Z") or (d == "z"): if not DraftVecUtils.equals(lastvec,firstvec): try: seg = Part.Line(lastvec,firstvec).toShape() except Part.OCCError: pass else: path.append(seg) if path: #the path should be closed by now #sh=makewire(path,True) sh=makewire(path,donttry=False) if self.fill: sh = Part.Face(sh) sh = self.applyTrans(sh) obj = self.doc.addObject("Part::Feature",pathname) obj.Shape = sh self.format(obj) path = [] if firstvec: lastvec = firstvec #Move relative to recent draw command point = [] command = None if path: sh=makewire(path,checkclosed=False) #sh = Part.Wire(path) if self.fill and sh.isClosed(): sh = Part.Face(sh) sh = self.applyTrans(sh) obj = self.doc.addObject("Part::Feature",pathname) obj.Shape = sh self.format(obj) # processing rects if name == "rect": if not pathname: pathname = 'Rectangle' edges = [] if ('rx' not in data or data['rx'] < 10**(-1*Draft.precision())) and \ ('ry' not in data or data['ry'] < 10**(-1*Draft.precision())): #negative values are invalid # if True: p1 = Vector(data['x'],-data['y'],0) p2 = Vector(data['x']+data['width'],-data['y'],0) p3 = Vector(data['x']+data['width'],-data['y']-data['height'],0) p4 = Vector(data['x'],-data['y']-data['height'],0) edges.append(Part.Line(p1,p2).toShape()) edges.append(Part.Line(p2,p3).toShape()) edges.append(Part.Line(p3,p4).toShape()) edges.append(Part.Line(p4,p1).toShape()) else: #rounded edges #ToTo: check for ry>rx !!!! rx = data.get('rx') ry = data.get('ry') or rx rx = rx or ry if rx > 2 * data['width']: rx = data['width'] / 2.0 if ry > 2 * data['height']: ry = data['height'] / 2.0 if rx > ry: mj = rx mi = ry else: mj = ry mi = rx p1=Vector(data['x']+rx,-data['y']-data['height']+ry,0) e1=Part.Ellipse(p1,mj,mi) p2=Vector(data['x']+data['width']-rx,-data['y']-data['height']+ry,0) e2=Part.Ellipse(p2,mj,mi) p3=Vector(data['x']+data['width']-rx,-data['y']-ry,0) e3=Part.Ellipse(p3,mj,mi) p4=Vector(data['x']+rx,-data['y']-ry,0) e4=Part.Ellipse(p4,mj,mi) if rx > ry: e1a=Part.Arc(e1,math.radians(180),math.radians(270)) e2a=Part.Arc(e2,math.radians(270),math.radians(360)) e3a=Part.Arc(e3,math.radians(0),math.radians(90)) e4a=Part.Arc(e4,math.radians(90),math.radians(180)) esh=[e1a.toShape(),e2a.toShape(),e3a.toShape(),e4a.toShape()] else: e1a=Part.Arc(e1,math.radians(90),math.radians(180)) e2a=Part.Arc(e2,math.radians(180),math.radians(270)) e3a=Part.Arc(e3,math.radians(270),math.radians(360)) e4a=Part.Arc(e4,math.radians(0),math.radians(90)) rot90=FreeCAD.Matrix(0,-1,0,0,1,0) esh=[] for arc,point in ((e1a,p1),(e2a,p2),(e3a,p3),(e4a,p4)): m1=FreeCAD.Matrix() m1.move(point.multiply(1)) m1=m1.multiply(rot90) m1.move(point.multiply(-1)) #m1.move(point) arc.transform(m1) esh.append(arc.toShape()) for esh1,esh2 in zip(esh[-1:]+esh[:-1],esh): p1,p2 = esh1.Vertexes[-1].Point,esh2.Vertexes[0].Point if not DraftVecUtils.equals(p1,p2): edges.append(Part.Line(esh1.Vertexes[-1].Point,esh2.Vertexes[0].Point).toShape()) #straight segments edges.append(esh2) # elliptical segments sh = Part.Wire(edges) if self.fill: sh = Part.Face(sh) sh = self.applyTrans(sh) obj = self.doc.addObject("Part::Feature",pathname) obj.Shape = sh self.format(obj) # processing lines if name == "line": if not pathname: pathname = 'Line' p1 = Vector(data['x1'],-data['y1'],0) p2 = Vector(data['x2'],-data['y2'],0) sh = Part.Line(p1,p2).toShape() sh = self.applyTrans(sh) obj = self.doc.addObject("Part::Feature",pathname) obj.Shape = sh self.format(obj) # processing polylines and polygons if name == "polyline" or name == "polygon": '''a simpler implementation would be sh = Part.makePolygon([Vector(svgx,-svgy,0) for svgx,svgy in zip(points[0::2],points[1::2])]) but there would be more difficlult to search for duplicate points beforehand.''' if not pathname: pathname = 'Polyline' points=[float(d) for d in data['points']] FreeCAD.Console.PrintMessage('points %s\n'%str(points)) lenpoints=len(points) if lenpoints>=4 and lenpoints % 2 == 0: lastvec = Vector(points[0],-points[1],0) path=[] if name == 'polygon': points=points+points[:2] # emulate closepath for svgx,svgy in zip(points[2::2],points[3::2]): currentvec = Vector(svgx,-svgy,0) if not DraftVecUtils.equals(lastvec,currentvec): seg = Part.Line(lastvec,currentvec).toShape() #print "polyline seg ",lastvec,currentvec lastvec = currentvec path.append(seg) if path: sh = Part.Wire(path) if self.fill and sh.isClosed(): sh = Part.Face(sh) sh = self.applyTrans(sh) obj = self.doc.addObject("Part::Feature",pathname) obj.Shape = sh # processing ellipses if (name == "ellipse") : if not pathname: pathname = 'Ellipse' c = Vector(data.get('cx',0),-data.get('cy',0),0) rx = data['rx'] ry = data['ry'] if rx > ry: sh = Part.Ellipse(c,rx,ry).toShape() else: sh = Part.Ellipse(c,ry,rx).toShape() m3=FreeCAD.Matrix() m3.move(c) rot90=FreeCAD.Matrix(0,-1,0,0,1,0) #90 m3=m3.multiply(rot90) m3.move(c.multiply(-1)) sh.transformShape(m3) #sh = sh.transformGeometry(m3) if self.fill: sh = Part.Wire([sh]) sh = Part.Face(sh) sh = self.applyTrans(sh) obj = self.doc.addObject("Part::Feature",pathname) obj.Shape = sh self.format(obj) # processing circles if (name == "circle") and (not ("freecad:skip" in data)) : if not pathname: pathname = 'Circle' c = Vector(data.get('cx',0),-data.get('cy',0),0) r = data['r'] sh = Part.makeCircle(r) if self.fill: sh = Part.Wire([sh]) sh = Part.Face(sh) sh.translate(c) sh = self.applyTrans(sh) obj = self.doc.addObject("Part::Feature",pathname) obj.Shape = sh self.format(obj) # processing texts if name in ["text","tspan"]: if not("freecad:skip" in data): FreeCAD.Console.PrintMessage("processing a text\n") if 'x' in data: self.x = data['x'] else: self.x = 0 if 'y' in data: self.y = data['y'] else: self.y = 0 if 'font-size' in data: if data['font-size'] != 'none': self.text = getsize(data['font-size'],'css') else: self.text = 1 else: if self.lastdim: self.lastdim.ViewObject.FontSize = int(getsize(data['font-size'])) FreeCAD.Console.PrintMessage("done processing element %d\n"%self.count) def characters(self,content): if self.text: FreeCAD.Console.PrintMessage("reading characters %s\n" % content) obj=self.doc.addObject("App::Annotation",'Text') obj.LabelText = content.encode('latin1') vec = Vector(self.x,-self.y,0) if self.transform: vec = self.translateVec(vec,self.transform) #print "own transform: ",self.transform, vec for transform in self.grouptransform[::-1]: #vec = self.translateVec(vec,transform) vec = transform.multiply(vec) #print "applying vector: ",vec obj.Position = vec if gui: obj.ViewObject.FontSize = int(self.text) if self.fill: obj.ViewObject.TextColor = self.fill else: obj.ViewObject.TextColor = (0.0,0.0,0.0,0.0) def endElement(self, name): if not name in ["tspan"]: self.transform = None self.text = None if name == "g" or name == "svg": FreeCAD.Console.PrintMessage("closing group\n") self.grouptransform.pop() def applyTrans(self,sh): if isinstance(sh,Part.Shape): if self.transform: FreeCAD.Console.PrintMessage("applying object transform: %s\n" % self.transform) sh = transformCopyShape(sh,self.transform) #sh = sh.transformGeometry(self.transform) for transform in self.grouptransform[::-1]: FreeCAD.Console.PrintMessage("applying group transform: %s\n" % transform) sh = transformCopyShape(sh,transform) #sh = sh.transformGeometry(transform) return sh elif Draft.getType(sh) == "Dimension": pts = [] for p in [sh.Start,sh.End,sh.Dimline]: cp = Vector(p) if self.transform: FreeCAD.Console.PrintMessage("applying object transform: %s\n" % self.transform) cp = self.transform.multiply(cp) for transform in self.grouptransform[::-1]: FreeCAD.Console.PrintMessage("applying group transform: %s\n" % transform) cp = transform.multiply(cp) pts.append(cp) sh.Start = pts[0] sh.End = pts[1] sh.Dimline = pts[2] def translateVec(self,vec,mat): v = Vector(mat.A14,mat.A24,mat.A34) return vec.add(v) def getMatrix(self,tr): "returns a FreeCAD matrix from a svg transform attribute" transformre=re.compile('(matrix|translate|scale|rotate|skewX|skewY)\s*?\((.*?)\)',re.DOTALL) m = FreeCAD.Matrix() for transformation, arguments in transformre.findall(tr): argsplit=[float(arg) for arg in arguments.replace(',',' ').split()] #m.multiply(FreeCAD.Matrix (1,0,0,0,0,-1)) #print '%s:%s %s %d' % (transformation, arguments,argsplit,len(argsplit)) if transformation == 'translate': tx = argsplit[0] ty = argsplit[1] if len(argsplit) > 1 else 0.0 m.move(Vector(tx,-ty,0)) elif transformation == 'scale': sx = argsplit[0] sy = argsplit[1] if len(argsplit) > 1 else sx m.scale(Vector(sx,sy,1)) elif transformation == 'rotate': angle = argsplit[0] if len(argsplit) >= 3: cx = argsplit[1] cy = argsplit[2] m.move(Vector(cx,-cy,0)) m.rotateZ(math.radians(-angle)) #mirroring one axis equals changing the direction of rotaion if len(argsplit) >= 3: m.move(Vector(-cx,cy,0)) elif transformation == 'skewX': m=m.multiply(FreeCAD.Matrix(1,-math.tan(math.radians(argsplit[0])))) elif transformation == 'skewY': m=m.multiply(FreeCAD.Matrix(1,0,0,0,-math.tan(math.radians(argsplit[0])))) elif transformation == 'matrix': # '''transformation matrix: # FreeCAD SVG # (+A -C +0 +E) (A C 0 E) # (-B +D -0 -F) = (-Y) * (B D 0 F) *(-Y) # (+0 -0 +1 +0) (0 0 1 0) # (+0 -0 +0 +1) (0 0 0 1)''' m=m.multiply(FreeCAD.Matrix(argsplit[0],-argsplit[2],0,argsplit[4],-argsplit[1],argsplit[3],0,-argsplit[5])) #else: #print 'SKIPPED %s' % transformation #print "m= ",m #print "generating transformation: ",m return m def decodeName(name): "decodes encoded strings" try: decodedName = (name.decode("utf8")) except UnicodeDecodeError: try: decodedName = (name.decode("latin1")) except UnicodeDecodeError: FreeCAD.Console.PrintError("svg: error: couldn't determine character encoding\n") decodedName = name return decodedName def getContents(filename,tag,stringmode=False): "gets the contents of all the occurences of the given tag in the given file" result = {} if stringmode: contents = filename else: f = pythonopen(filename) contents = f.read() f.close() contents = contents.replace('\n','_linebreak') searchpat = '<'+tag+'.*?' tags = re.findall(searchpat,contents) for t in tags: tagid = re.findall('id="(.*?)"',t) if tagid: tagid = tagid[0] else: tagid = 'none' res = t.replace('_linebreak','\n') result[tagid] = res return result def open(filename): docname=os.path.split(filename)[1] doc=FreeCAD.newDocument(docname) doc.Label = decodeName(docname[:-4]) parser = xml.sax.make_parser() parser.setContentHandler(svgHandler()) parser._cont_handler.doc = doc f = pythonopen(filename) parser.parse(f) f.close() doc.recompute() return doc def insert(filename,docname): try: doc=FreeCAD.getDocument(docname) except NameError: doc=FreeCAD.newDocument(docname) FreeCAD.ActiveDocument = doc parser = xml.sax.make_parser() parser.setContentHandler(svgHandler()) parser._cont_handler.doc = doc parser.parse(pythonopen(filename)) doc.recompute() def export(exportList,filename): "called when freecad exports a file" svg_export_style = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Draft").GetInt("svg_export_style") if svg_export_style != 0 and svg_export_style != 1: FreeCAD.Console.PrintMessage("unknown svg export style, switching to Translated\n") svg_export_style = 0 # finding sheet size bb = None for ob in exportList: if ob.isDerivedFrom("Part::Feature"): if bb: bb.add(ob.Shape.BoundBox) else: bb = ob.Shape.BoundBox if bb: minx = bb.XMin maxx = bb.XMax miny = bb.YMin maxy = bb.YMax else: FreeCAD.Console.PrintError("The export list contains no shape\n") return if svg_export_style == 0: # translated-style exports get a bit of a margin margin = (maxx-minx)*.01 else: # raw-style exports get no margin margin = 0 minx -= margin maxx += margin miny -= margin maxy += margin sizex = maxx-minx sizey = maxy-miny miny += margin # writing header # we specify the svg width and height in FreeCAD's physical units (mm), # and specify the viewBox so that user units maps one-to-one to mm svg = pythonopen(filename,'wb') svg.write('\n') svg.write('\n') svg.write('\n') # writing paths for ob in exportList: if svg_export_style == 0: # translated-style exports have the entire sketch translated to fit in the X>0, Y>0 quadrant #svg.write('\n') svg.write('\n'% (ob.Name,-minx,maxy)) else: # raw-style exports do not translate the sketch svg.write('\n' %\ ob.Name) svg.write(Draft.getSVG(ob)) svg.write('%s\n' % ob.Label.encode('utf8')\ .replace('<','<').replace('>','>')) # replace('"',\ """) svg.write('\n') # closing svg.write('') svg.close()