Commit 5dd35d48 authored by Gary Hodgson's avatar Gary Hodgson

initial hack to accept Slic3r SVG

parent 0603d9c7
......@@ -15,29 +15,49 @@
import xml.etree.ElementTree
import wx
import time
import svg.document as wxpsvgdocument
def parsesvg(name):
et= xml.etree.ElementTree.ElementTree(file=name)
#xml.etree.ElementTree.dump(et)
slicer = 'Slic3r' if et.getroot().find('{http://www.w3.org/2000/svg}metadata') == None else 'Skeinforge'
zlast=0
zdiff=0
ol=[]
if (slicer == 'Slic3r'):
height = et.getroot().get('height')
width = et.getroot().get('width')
for i in et.findall("{http://www.w3.org/2000/svg}g"):
z=float(i.get('{http://slic3r.org/namespaces/slic3r}z'))
zdiff=z-zlast
zlast=z
svgSnippet = xml.etree.ElementTree.Element('{http://www.w3.org/2000/svg}svg')
svgSnippet.set('height', height+'mm')
svgSnippet.set('width', width+'mm')
svgSnippet.set('viewBox', '0 0 '+height+' '+width)
svgSnippet.append(i)
ol+=[svgSnippet]
else :
for i in et.findall("{http://www.w3.org/2000/svg}g")[0].findall("{http://www.w3.org/2000/svg}g"):
z=float(i.get('id').split("z:")[-1])
zdiff=z-zlast
zlast=z
path=i.find('{http://www.w3.org/2000/svg}path')
ol+=[(path.get("d").split("z"))[:-1]]
return ol,zdiff
return ol,zdiff,slicer
class dispframe(wx.Frame):
def __init__(self, parent, title, res=(1600,1200),printer=None):
def __init__(self, parent, title, res=(800,600),printer=None):
wx.Frame.__init__(self, parent=parent, title=title)
self.p=printer
self.pic=wx.StaticBitmap(self)
self.bitmap=wx.EmptyBitmap(*res)
self.bbitmap=wx.EmptyBitmap(*res)
self.slicer='Skeinforge'
dc=wx.MemoryDC()
dc.SelectObject(self.bbitmap)
dc.SetBackground(wx.Brush("black"))
......@@ -50,6 +70,7 @@ class dispframe(wx.Frame):
self.brush=wx.Brush("white")
self.SetDoubleBuffered(True)
self.Show()
def drawlayer(self,svg):
try:
dc=wx.MemoryDC()
......@@ -58,18 +79,22 @@ class dispframe(wx.Frame):
dc.Clear()
dc.SetPen(self.pen)
dc.SetBrush(self.brush)
if self.slicer == 'Skeinforge':
for i in svg:
#print i
points=[wx.Point(*map(lambda x:int(round(float(x)*self.scale)),j.strip().split())) for j in i.strip().split("M")[1].split("L")]
dc.DrawPolygon(points,self.size[0]/2,self.size[1]/2)
else:
gc = wx.GraphicsContext_Create(dc)
gc.Translate(*self.offset)
gc.Scale(self.scale, self.scale)
wxpsvgdocument.SVGDocument(svg).render(gc)
dc.SelectObject(wx.NullBitmap)
self.pic.SetBitmap(self.bitmap)
self.pic.Show()
self.Refresh()
#self.pic.SetBitmap(self.bitmap)
except:
raise
......@@ -79,19 +104,18 @@ class dispframe(wx.Frame):
self.drawlayer(image)
self.pic.Show()
self.Refresh()
# time.sleep(self.interval)
#self.pic.Hide()
self.Refresh()
if self.p!=None and self.p.online:
if self.p != None and self.p.online:
self.p.send_now("G91")
self.p.send_now("G1 Z%f F300"%(self.thickness,))
self.p.send_now("G90")
def nextimg(self,event):
#print "b"
if self.index<len(self.layers):
i=self.index
#print self.layers[i]
print i
wx.CallAfter(self.showimgdelay,self.layers[i])
wx.FutureCall(1000*self.interval,self.pic.Hide)
......@@ -104,7 +128,7 @@ class dispframe(wx.Frame):
wx.CallAfter(self.timer.Stop)
def present(self,layers,interval=0.5,pause=0.2,thickness=0.4,scale=20,size=(800,600)):
def present(self,layers,interval=0.5,pause=0.2,thickness=0.4,scale=20,size=(800,600),offset=(0,0)):
wx.CallAfter(self.pic.Hide)
wx.CallAfter(self.Refresh)
self.layers=layers
......@@ -113,12 +137,11 @@ class dispframe(wx.Frame):
self.index=0
self.size=size
self.interval=interval
self.offset=offset
self.timer=wx.Timer(self,1)
self.timer.Bind(wx.EVT_TIMER,self.nextimg)
self.Bind(wx.EVT_TIMER,self.nextimg)
self.timer.Start(1000*interval+1000*pause)
#print "x"
class setframe(wx.Frame):
......@@ -129,29 +152,43 @@ class setframe(wx.Frame):
self.panel.SetBackgroundColour("orange")
self.bload=wx.Button(self.panel,-1,"Load",pos=(0,0))
self.bload.Bind(wx.EVT_BUTTON,self.loadfile)
wx.StaticText(self.panel,-1,"Layer:",pos=(0,30))
wx.StaticText(self.panel,-1,"mm",pos=(130,30))
self.thickness=wx.TextCtrl(self.panel,-1,"0.5",pos=(50,30))
wx.StaticText(self.panel,-1,"Exposure:",pos=(0,60))
wx.StaticText(self.panel,-1,"s",pos=(130,60))
self.interval=wx.TextCtrl(self.panel,-1,"0.5",pos=(50,60))
wx.StaticText(self.panel,-1,"Blank:",pos=(0,90))
wx.StaticText(self.panel,-1,"s",pos=(130,90))
self.delay=wx.TextCtrl(self.panel,-1,"0.5",pos=(50,90))
wx.StaticText(self.panel,-1,"Scale:",pos=(0,120))
wx.StaticText(self.panel,-1,"x",pos=(130,120))
self.scale=wx.TextCtrl(self.panel,-1,"5",pos=(50,120))
wx.StaticText(self.panel,-1,"X:",pos=(160,30))
self.X=wx.TextCtrl(self.panel,-1,"1024",pos=(180,30))
self.X=wx.TextCtrl(self.panel,-1,"1024",pos=(210,30))
wx.StaticText(self.panel,-1,"Y:",pos=(160,60))
self.Y=wx.TextCtrl(self.panel,-1,"768",pos=(180,60))
self.Y=wx.TextCtrl(self.panel,-1,"768",pos=(210,60))
wx.StaticText(self.panel,-1,"OffsetX:",pos=(160,90))
self.offsetX=wx.TextCtrl(self.panel,-1,"50",pos=(210,90))
wx.StaticText(self.panel,-1,"OffsetY:",pos=(160,120))
self.offsetY=wx.TextCtrl(self.panel,-1,"50",pos=(210,120))
self.bload=wx.Button(self.panel,-1,"Present",pos=(0,150))
self.bload.Bind(wx.EVT_BUTTON,self.startdisplay)
self.Show()
def loadfile(self,event):
dlg=wx.FileDialog(self,("Open file to print"),style=wx.FD_OPEN|wx.FD_FILE_MUST_EXIST)
dlg.SetWildcard(("Skeinforge svg files (;*.svg;*.SVG;)"))
dlg.SetWildcard(("Slic3r or Skeinforge svg files (;*.svg;*.SVG;)"))
if(dlg.ShowModal() == wx.ID_OK):
name=dlg.GetPath()
import os
......@@ -162,6 +199,7 @@ class setframe(wx.Frame):
print "Layer thickness detected:",layers[1], "mm"
print len(layers[0]), "layers found, total height", layers[1]*len(layers[0]), "mm"
self.thickness.SetValue(str(layers[1]))
self.f.slicer = layers[2]
self.layers=layers
dlg.Destroy()
......@@ -169,11 +207,15 @@ class setframe(wx.Frame):
self.f.Raise()
self.f.ShowFullScreen(1)
l=self.layers[0][:]
#l=list(reversed(l))
self.f.present(l,thickness=float(self.thickness.GetValue()),interval=float(self.interval.GetValue()),scale=float(self.scale.GetValue()),pause=float(self.delay.GetValue()), size=(float(self.X.GetValue()),float(self.Y.GetValue())))
self.f.present(l,
thickness=float(self.thickness.GetValue()),
interval=float(self.interval.GetValue()),
scale=float(self.scale.GetValue()),
pause=float(self.delay.GetValue()),
size=(float(self.X.GetValue()),float(self.Y.GetValue())),
offset=(float(self.offsetX.GetValue()),float(self.offsetY.GetValue())))
if __name__=="__main__":
a=wx.App()
setframe(None).Show()
a.MainLoop()
"""
"""
import wx
def AddEllipticalArc(self, x, y, w, h, startAngle, endAngle, clockwise=False):
""" Draws an arc of an ellipse within bounding rect (x,y,w,h)
from startArc to endArc (in radians, relative to the horizontal line of the eclipse)"""
if True:
import warnings
warnings.warn("elliptical arcs are not supported")
w = w/2.0
h = h/2.0
self.AddArc(x+w, y+h, ((w+h)/2), startAngle, endAngle, clockwise)
return
else:
#implement in terms of AddArc by applying a transformation matrix
#Sigh this can't work, still need to patch wx to allow
#either a) AddPath that's not a closed path or
#b) allow pushing and popping of states on a path, not just on a context
#a) is possible in GDI+, need to investigate other renderers.
#b) is possible in Quartz and Cairo, but not in GDI+. It could
#possibly be simulated by combining the current transform with option a.
mtx = wx.GraphicsRenderer_GetDefaultRenderer().CreateMatrix()
path = wx.GraphicsRenderer_GetDefaultRenderer().CreatePath()
mtx.Translate(x+(w/2.0), y+(h/2.0))
mtx.Scale(w/2.0, y/2.0)
path.AddArc(0, 0, 1, startAngle, endAngle, clockwise)
path.Transform(mtx)
self.AddPath(path)
self.MoveToPoint(path.GetCurrentPoint())
self.CloseSubpath()
if not hasattr(wx.GraphicsPath, "AddEllipticalArc"):
wx.GraphicsPath.AddEllipticalArc = AddEllipticalArc
del AddEllipticalArc
\ No newline at end of file
"""
Parsers for specific attributes
"""
import urlparse
from pyparsing import (Literal,
Optional, oneOf, Group, StringEnd, Combine, Word, alphas, hexnums,
CaselessLiteral, SkipTo
)
from css.colour import colourValue
import string
##Paint values
none = CaselessLiteral("none").setParseAction(lambda t: ["NONE", ()])
currentColor = CaselessLiteral("currentColor").setParseAction(lambda t: ["CURRENTCOLOR", ()])
def parsePossibleURL(t):
possibleURL, fallback = t[0]
return [urlparse.urlsplit(possibleURL), fallback]
#Normal color declaration
colorDeclaration = none | currentColor | colourValue
urlEnd = (
Literal(")").suppress() +
Optional(Group(colorDeclaration), default=()) +
StringEnd()
)
url = (
CaselessLiteral("URL")
+
Literal("(").suppress()+
Group(SkipTo(urlEnd, include=True).setParseAction(parsePossibleURL))
)
#paint value will parse into a (type, details) tuple.
#For none and currentColor, the details tuple will be the empty tuple
#for CSS color declarations, it will be (type, (R,G,B))
#for URLs, it will be ("URL", ((url tuple), fallback))
#The url tuple will be as returned by urlparse.urlsplit, and can be
#an empty tuple if the parser has an error
#The fallback will be another (type, details) tuple as a parsed
#colorDeclaration, but may be the empty tuple if it is not present
paintValue = url | colorDeclaration
from __future__ import absolute_import
from .transform import transformList
from .inline import inlineStyle
""" CSS at-rules"""
from pyparsing import Literal, Combine
from .identifier import identifier
atkeyword = Combine(Literal("@") + identifier)
\ No newline at end of file
"""
CSS blocks
"""
from pyparsing import nestedExpr
block = nestedExpr(opener="{", closer="}")
"""
Parsing for CSS colour values.
Supported formats:
hex literal short: #fff
hex literal long: #fafafa
rgb bytes: rgb(255,100,0)
rgb percent: rgb(100%,100%,0%)
named color: black
"""
import wx
import string
import urlparse
from pyparsing import nums, Literal, Optional, oneOf, Group, StringEnd, Combine, Word, alphas, hexnums
from ..pathdata import number, sign
number = number.copy()
integerConstant = Word(nums+"+-").setParseAction(lambda t:int(t[0]))
#rgb format parser
comma = Literal(",").suppress()
def clampColourByte(val):
val = int(val)
return min(max(0,val), 255)
def clampColourPerc(val):
val = float(val)
return min(max(0,val), 100)
def parseColorPerc(token):
val = token[0]
val = clampColourPerc(val)
#normalize to bytes
return int(255 * (val / 100.0))
colorByte = Optional(sign) + integerConstant.setParseAction(lambda t: clampColourByte(t[0]))
colorPerc = number.setParseAction(parseColorPerc) + Literal("%").suppress()
rgb = (
Literal("rgb(").setParseAction(lambda t: "RGB") +
(
#integer constants, ie 255,255,255
Group(colorByte + comma + colorByte + comma + colorByte) ^
#percentage values, ie 100%, 50%
Group(colorPerc + comma + colorPerc + comma + colorPerc)
)
+
Literal(")").suppress() + StringEnd()
)
def parseShortHex(t):
return tuple(int(x*2, 16) for x in t[0])
doubleHex = Word(hexnums, exact=2).setParseAction(lambda t: int(t[0], 16))
hexLiteral = (Literal("#").setParseAction(lambda t: "RGB") +
(
Group(doubleHex + doubleHex + doubleHex) |
Word(hexnums, exact=3).setParseAction(parseShortHex)
) + StringEnd()
)
def parseNamedColour(t):
try:
return ["RGB", NamedColours[t[0].lower()]]
except KeyError:
return ["RGB", (0,0,0)]
namedColour = Word(alphas).setParseAction(parseNamedColour)
colourValue = rgb | hexLiteral | namedColour
##constants
NamedColours = {
#~ #html named colours
#~ "black":(0,0,0),
#~ "silver": (0xc0, 0xc0, 0xc0, 255),
#~ "gray": (0x80, 0x80, 0x80),
#~ "white":(255,255,255),
#~ "maroon":(0x80, 0, 0),
#~ "red":(0xff, 0, 0),
#~ "purple":(0x80, 0, 0x80),
#~ "fuchsia":(0xff, 0, 0xff),
#~ "green": (0, 0x80, 0),
#~ "lime": (0, 0xff, 0),
#~ "olive": (0x80, 0x80, 00),
#~ "yellow":(0xff, 0xff, 00),
#~ "navy": (0, 0, 0x80),
#~ "blue": (0, 0, 0xff),
#~ "teal": (0, 0x80, 0x80),
#~ "aqua": (0, 0xff, 0xff),
#expanded named colors from SVG spc
'aliceblue' : (240, 248, 255) ,
'antiquewhite' : (250, 235, 215) ,
'aqua' : ( 0, 255, 255) ,
'aquamarine' : (127, 255, 212) ,
'azure' : (240, 255, 255) ,
'beige' : (245, 245, 220) ,
'bisque' : (255, 228, 196) ,
'black' : ( 0, 0, 0) ,
'blanchedalmond' : (255, 235, 205) ,
'blue' : ( 0, 0, 255) ,
'blueviolet' : (138, 43, 226) ,
'brown' : (165, 42, 42) ,
'burlywood' : (222, 184, 135) ,
'cadetblue' : ( 95, 158, 160) ,
'chartreuse' : (127, 255, 0) ,
'chocolate' : (210, 105, 30) ,
'coral' : (255, 127, 80) ,
'cornflowerblue' : (100, 149, 237) ,
'cornsilk' : (255, 248, 220) ,
'crimson' : (220, 20, 60) ,
'cyan' : ( 0, 255, 255) ,
'darkblue' : ( 0, 0, 139) ,
'darkcyan' : ( 0, 139, 139) ,
'darkgoldenrod' : (184, 134, 11) ,
'darkgray' : (169, 169, 169) ,
'darkgreen' : ( 0, 100, 0) ,
'darkgrey' : (169, 169, 169) ,
'darkkhaki' : (189, 183, 107) ,
'darkmagenta' : (139, 0, 139) ,
'darkolivegreen' : ( 85, 107, 47) ,
'darkorange' : (255, 140, 0) ,
'darkorchid' : (153, 50, 204) ,
'darkred' : (139, 0, 0) ,
'darksalmon' : (233, 150, 122) ,
'darkseagreen' : (143, 188, 143) ,
'darkslateblue' : ( 72, 61, 139) ,
'darkslategray' : ( 47, 79, 79) ,
'darkslategrey' : ( 47, 79, 79) ,
'darkturquoise' : ( 0, 206, 209) ,
'darkviolet' : (148, 0, 211) ,
'deeppink' : (255, 20, 147) ,
'deepskyblue' : ( 0, 191, 255) ,
'dimgray' : (105, 105, 105) ,
'dimgrey' : (105, 105, 105) ,
'dodgerblue' : ( 30, 144, 255) ,
'firebrick' : (178, 34, 34) ,
'floralwhite' : (255, 250, 240) ,
'forestgreen' : ( 34, 139, 34) ,
'fuchsia' : (255, 0, 255) ,
'gainsboro' : (220, 220, 220) ,
'ghostwhite' : (248, 248, 255) ,
'gold' : (255, 215, 0) ,
'goldenrod' : (218, 165, 32) ,
'gray' : (128, 128, 128) ,
'grey' : (128, 128, 128) ,
'green' : ( 0, 128, 0) ,
'greenyellow' : (173, 255, 47) ,
'honeydew' : (240, 255, 240) ,
'hotpink' : (255, 105, 180) ,
'indianred' : (205, 92, 92) ,
'indigo' : ( 75, 0, 130) ,
'ivory' : (255, 255, 240) ,
'khaki' : (240, 230, 140) ,
'lavender' : (230, 230, 250) ,
'lavenderblush' : (255, 240, 245) ,
'lawngreen' : (124, 252, 0) ,
'lemonchiffon' : (255, 250, 205) ,
'lightblue' : (173, 216, 230) ,
'lightcoral' : (240, 128, 128) ,
'lightcyan' : (224, 255, 255) ,
'lightgoldenrodyellow' : (250, 250, 210) ,
'lightgray' : (211, 211, 211) ,
'lightgreen' : (144, 238, 144) ,
'lightgrey' : (211, 211, 211) ,
'lightpink' : (255, 182, 193) ,
'lightsalmon' : (255, 160, 122) ,
'lightseagreen' : ( 32, 178, 170) ,
'lightskyblue' : (135, 206, 250) ,
'lightslategray' : (119, 136, 153) ,
'lightslategrey' : (119, 136, 153) ,
'lightsteelblue' : (176, 196, 222) ,
'lightyellow' : (255, 255, 224) ,
'lime' : ( 0, 255, 0) ,
'limegreen' : ( 50, 205, 50) ,
'linen' : (250, 240, 230) ,
'magenta' : (255, 0, 255) ,
'maroon' : (128, 0, 0) ,
'mediumaquamarine' : (102, 205, 170) ,
'mediumblue' : ( 0, 0, 205) ,
'mediumorchid' : (186, 85, 211) ,
'mediumpurple' : (147, 112, 219) ,
'mediumseagreen' : ( 60, 179, 113) ,
'mediumslateblue' : (123, 104, 238) ,
'mediumspringgreen' : ( 0, 250, 154) ,
'mediumturquoise' : ( 72, 209, 204) ,
'mediumvioletred' : (199, 21, 133) ,
'midnightblue' : ( 25, 25, 112) ,
'mintcream' : (245, 255, 250) ,
'mistyrose' : (255, 228, 225) ,
'moccasin' : (255, 228, 181) ,
'navajowhite' : (255, 222, 173) ,
'navy' : ( 0, 0, 128) ,
'oldlace' : (253, 245, 230) ,
'olive' : (128, 128, 0) ,
'olivedrab' : (107, 142, 35) ,
'orange' : (255, 165, 0) ,
'orangered' : (255, 69, 0) ,
'orchid' : (218, 112, 214) ,
'palegoldenrod' : (238, 232, 170) ,
'palegreen' : (152, 251, 152) ,
'paleturquoise' : (175, 238, 238) ,
'palevioletred' : (219, 112, 147) ,
'papayawhip' : (255, 239, 213) ,
'peachpuff' : (255, 218, 185) ,
'peru' : (205, 133, 63) ,
'pink' : (255, 192, 203) ,
'plum' : (221, 160, 221) ,
'powderblue' : (176, 224, 230) ,
'purple' : (128, 0, 128) ,
'red' : (255, 0, 0) ,
'rosybrown' : (188, 143, 143) ,
'royalblue' : ( 65, 105, 225) ,
'saddlebrown' : (139, 69, 19) ,
'salmon' : (250, 128, 114) ,
'sandybrown' : (244, 164, 96) ,
'seagreen' : ( 46, 139, 87) ,
'seashell' : (255, 245, 238) ,
'sienna' : (160, 82, 45) ,
'silver' : (192, 192, 192) ,
'skyblue' : (135, 206, 235) ,
'slateblue' : (106, 90, 205) ,
'slategray' : (112, 128, 144) ,
'slategrey' : (112, 128, 144) ,
'snow' : (255, 250, 250) ,
'springgreen' : ( 0, 255, 127) ,
'steelblue' : ( 70, 130, 180) ,
'tan' : (210, 180, 140) ,
'teal' : ( 0, 128, 128) ,
'thistle' : (216, 191, 216) ,
'tomato' : (255, 99, 71) ,
'turquoise' : ( 64, 224, 208) ,
'violet' : (238, 130, 238) ,
'wheat' : (245, 222, 179) ,
'white' : (255, 255, 255) ,
'whitesmoke' : (245, 245, 245) ,
'yellow' : (255, 255, 0) ,
'yellowgreen' : (154, 205, 50) ,
}
def fillCSS2SystemColours():
#The system colours require a wxApp to be present to retrieve,
#so if you wnat support for them you'll need
#to call this function after your wxApp instance starts
systemColors = {
"ActiveBorder": wx.SYS_COLOUR_ACTIVEBORDER,
"ActiveCaption": wx.SYS_COLOUR_ACTIVECAPTION,
"AppWorkspace": wx.SYS_COLOUR_APPWORKSPACE,
"Background": wx.SYS_COLOUR_BACKGROUND,
"ButtonFace": wx.SYS_COLOUR_BTNFACE,
"ButtonHighlight": wx.SYS_COLOUR_BTNHIGHLIGHT,
"ButtonShadow": wx.SYS_COLOUR_BTNSHADOW,
"ButtonText": wx.SYS_COLOUR_BTNTEXT,
"CaptionText": wx.SYS_COLOUR_CAPTIONTEXT,
"GrayText": wx.SYS_COLOUR_GRAYTEXT,
"Highlight": wx.SYS_COLOUR_HIGHLIGHT,
"HighlightText": wx.SYS_COLOUR_HIGHLIGHTTEXT,
"InactiveBorder": wx.SYS_COLOUR_INACTIVEBORDER,
"InactiveCaption": wx.SYS_COLOUR_INACTIVECAPTION,
"InfoBackground": wx.SYS_COLOUR_INFOBK,
"InfoText": wx.SYS_COLOUR_INFOTEXT,
"Menu": wx.SYS_COLOUR_MENU,
"MenuText": wx.SYS_COLOUR_MENUTEXT,
"Scrollbar": wx.SYS_COLOUR_SCROLLBAR,
"ThreeDDarkShadow": wx.SYS_COLOUR_3DDKSHADOW,
"ThreeDFace": wx.SYS_COLOUR_3DFACE,
"ThreeDHighlight": wx.SYS_COLOUR_3DHIGHLIGHT,
"ThreeDLightShadow": wx.SYS_COLOUR_3DLIGHT,
"ThreeDShadow": wx.SYS_COLOUR_3DSHADOW,
"Window": wx.SYS_COLOUR_WINDOW,
"WindowFrame": wx.SYS_COLOUR_WINDOWFRAME,
"WindowText": wx.SYS_COLOUR_WINDOWTEXT
}
NamedColours.update(
#strip the alpha from the system colors. Is this really what we want to do?
(k.lower(), wx.SystemSettings.GetColour(v)[:3]) for (k,v) in systemColors.iteritems()
)
\ No newline at end of file
""" Parse CSS identifiers. More complicated than it sounds"""
from pyparsing import Word, Literal, Regex, Combine, Optional, White, oneOf, ZeroOrMore
import string
import re
class White(White):
""" Customize whitespace to match the CSS spec values"""
def __init__(self, ws=" \t\r\n\f", min=1, max=0, exact=0):
super(White, self).__init__(ws, min, max, exact)
escaped = (
Literal("\\").suppress() +
#chr(20)-chr(126) + chr(128)-unichr(sys.maxunicode)
Regex(u"[\u0020-\u007e\u0080-\uffff]", re.IGNORECASE)
)
def convertToUnicode(t):
return unichr(int(t[0], 16))
hex_unicode = (
Literal("\\").suppress() +
Regex("[0-9a-f]{1,6}", re.IGNORECASE) +
Optional(White(exact=1)).suppress()
).setParseAction(convertToUnicode)
escape = hex_unicode | escaped
#any unicode literal outside the 0-127 ascii range
nonascii = Regex(u"[^\u0000-\u007f]")
#single character for starting an identifier.
nmstart = Regex(u"[A-Z]", re.IGNORECASE) | nonascii | escape
nmchar = Regex(u"[0-9A-Z-]", re.IGNORECASE) | nonascii | escape
identifier = Combine(nmstart + ZeroOrMore(nmchar))
\ No newline at end of file
""" Parser for inline CSS in style attributes """
def inlineStyle(styleString):
if not styleString:
return {}
styles = styleString.split(";")
rv = dict(style.split(":") for style in styles if len(style) != 0)
return rv
\ No newline at end of file
"""
Parsing for CSS and CSS-style values, such as transform and filter attributes.
"""
from pyparsing import (Literal, Word, CaselessLiteral,
Optional, Combine, Forward, ZeroOrMore, nums, oneOf, Group, delimitedList)
#some shared definitions from pathdata
from ..pathdata import number, maybeComma
paren = Literal("(").suppress()
cparen = Literal(")").suppress()
def Parenthised(exp):
return Group(paren + exp + cparen)
skewY = Literal("skewY") + Parenthised(number)
skewX = Literal("skewX") + Parenthised(number)
rotate = Literal("rotate") + Parenthised(
number + Optional(maybeComma + number + maybeComma + number)
)
scale = Literal("scale") + Parenthised(
number + Optional(maybeComma + number)
)
translate = Literal("translate") + Parenthised(
number + Optional(maybeComma + number)
)
matrix = Literal("matrix") + Parenthised(
#there's got to be a better way to write this
number + maybeComma +
number + maybeComma +
number + maybeComma +
number + maybeComma +
number + maybeComma +
number
)
transform = (skewY | skewX | rotate | scale | translate | matrix)
transformList = delimitedList(Group(transform), delim=maybeComma)
if __name__ == '__main__':
from tests.test_css import *
unittest.main()
\ No newline at end of file
"""
Parser for various kinds of CSS values as per CSS2 spec section 4.3
"""
from pyparsing import Word, Combine, Optional, Literal, oneOf, CaselessLiteral, StringEnd
def asInt(s,l,t):
return int(t[0])
def asFloat(s,l,t):
return float(t[0])
def asFloatOrInt(s,l,t):
""" Return an int if possible, otherwise a float"""
v = t[0]
try:
return int(v)
except ValueError:
return float(v)
integer = Word("0123456789").setParseAction(asInt)
number = Combine(
Optional(Word("0123456789")) + Literal(".") + Word("01234567890")
| integer
)
number.setName('number')
sign = oneOf("+ -")
signedNumber = Combine(Optional(sign) + number).setParseAction(asFloat)
lengthValue = Combine(Optional(sign) + number).setParseAction(asFloatOrInt)
lengthValue.setName('lengthValue')
#TODO: The physical units like in, mm
lengthUnit = oneOf(['em', 'ex', 'px', 'pt', '%'], caseless=True)
#the spec says that the unit is only optional for a 0 length, but
#there are just too many places where a default is permitted.
#TODO: Maybe should use a ctor like optional to let clients declare it?
length = lengthValue + Optional(lengthUnit, default=None) + StringEnd()
length.leaveWhitespace()
#set the parse action aftward so it doesn't "infect" the parsers that build on it
number.setParseAction(asFloat)
\ No newline at end of file
"""
SVGDocument
"""
import wx
from cStringIO import StringIO
import warnings
import math
from functools import wraps
import pathdata
import css
from svg.css.colour import colourValue
from svg.css import values
from attributes import paintValue
document = """<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="4cm" height="4cm" viewBox="0 0 400 400"
xmlns="http://www.w3.org/2000/svg" version="1.1">
<title>Example triangle01- simple example of a 'path'</title>
<desc>A path that draws a triangle</desc>
<rect x="1" y="1" width="398" height="398"
fill="none" stroke="blue" />
<path d="M 100 100 L 300 100 L 200 300 z"
fill="red" stroke="blue" stroke-width="3" />
</svg>"""
makePath = lambda: wx.GraphicsRenderer_GetDefaultRenderer().CreatePath()
def attrAsFloat(node, attr, defaultValue="0"):
val = node.get(attr, defaultValue)
#TODO: process stuff like "inherit" by walking back up the nodes
#fast path optimization - if it's a valid float, don't
#try to parse it.
try:
return float(val)
except ValueError:
return valueToPixels(val)
def valueToPixels(val, defaultUnits="px"):
#TODO manage default units
from pyparsing import ParseException
try:
val, unit = values.length.parseString(val)
except ParseException:
print "***", val
raise
#todo: unit conversion to something other than pixels
return val
def pathHandler(func):
"""decorator for methods which return a path operation
Creates the path they will fill,
and generates the path operations for the node
"""
@wraps(func)
def inner(self, node):
#brush = self.getBrushFromState()
#pen = self.getPenFromState()
#if not (brush or pen):
# return None, []
path = wx.GraphicsRenderer_GetDefaultRenderer().CreatePath()
func(self, node, path)
ops = self.generatePathOps(path)
return path, ops
return inner
class SVGDocument(object):
lastControl = None
brushCache = {}
penCache = {}
def __init__(self, element):
"""
Create an SVG document from an ElementTree node.
"""
self.handlers = {
'{http://www.w3.org/2000/svg}svg':self.addGroupToDocument,
'{http://www.w3.org/2000/svg}a':self.addGroupToDocument,
'{http://www.w3.org/2000/svg}g':self.addGroupToDocument,
'{http://www.w3.org/2000/svg}rect':self.addRectToDocument,
'{http://www.w3.org/2000/svg}circle': self.addCircleToDocument,
'{http://www.w3.org/2000/svg}ellipse': self.addEllipseToDocument,
'{http://www.w3.org/2000/svg}line': self.addLineToDocument,
'{http://www.w3.org/2000/svg}polyline': self.addPolyLineToDocument,
'{http://www.w3.org/2000/svg}polygon': self.addPolygonToDocument,
'{http://www.w3.org/2000/svg}path':self.addPathDataToDocument,
'{http://www.w3.org/2000/svg}text':self.addTextToDocument
}
assert element.tag == '{http://www.w3.org/2000/svg}svg', 'Not an SVG fragment'
self.tree = element
self.paths = {}
self.stateStack = [{}]
path, ops = self.processElement(element)
self.ops = ops
@property
def state(self):
""" Retrieve the current state, without popping"""
return self.stateStack[-1]
def processElement(self, element):
""" Process one element of the XML tree.
Returns the path representing the node,
and an operation list for drawing the node.
Parent nodes should return a path (for hittesting), but
no draw operations
"""
#copy the current state
current = dict(self.state)
current.update(element.items())
current.update(css.inlineStyle(element.get("style", "")))
self.stateStack.append(current)
handler = self.handlers.get(element.tag, lambda *any: (None, None))
path, ops = handler(element)
self.paths[element] = path
self.stateStack.pop()
return path, ops
def createTransformOpsFromNode(self, node):
""" Returns an oplist for transformations.
This applies to a node, not the current state because
the transform stack is saved in the wxGraphicsContext.
This oplist does *not* include the push/pop state commands
"""
ops = []
transform = node.get('transform')
#todo: replace this with a mapping list
if transform:
for transform, args in css.transformList.parseString(transform):
if transform == 'scale':
if len(args) == 1:
x = y = args[0]
else:
x, y = args
ops.append(
(wx.GraphicsContext.Scale, (x, y))
)
if transform == 'translate':
if len(args) == 1:
x = args[0]
y = 0
else:
x, y = args
ops.append(
(wx.GraphicsContext.Translate, (x, y))
)
if transform == 'rotate':
if len(args) == 3:
angle, cx, cy = args
angle = math.radians(angle)
ops.extend([
(wx.GraphicsContext.Translate, (cx, cy)),
(wx.GraphicsContext.Rotate, (angle,)),
(wx.GraphicsContext.Translate, (-cx, -cy)),
])
else:
angle = args[0]
angle = math.radians(angle)
ops.append(
(wx.GraphicsContext.Rotate, (angle,))
)
if transform == 'matrix':
matrix = wx.GraphicsRenderer_GetDefaultRenderer().CreateMatrix(
*args
)
ops.append(
(wx.GraphicsContext.ConcatTransform, (matrix,))
)
if transform == 'skewX':
matrix = wx.GraphicsRenderer_GetDefaultRenderer().CreateMatrix(
1,0,math.tan(math.radians(args[0])),1,0,0
)
ops.append(
(wx.GraphicsContext.ConcatTransform, (matrix,))
)
if transform == 'skewY':
matrix = wx.GraphicsRenderer_GetDefaultRenderer().CreateMatrix(
1,math.tan(math.radians(args[0])),0,1,0,0
)
ops.append(
(wx.GraphicsContext.ConcatTransform, (matrix,))
)
return ops
def addGroupToDocument(self, node):
""" For parent elements: push on a state,
then process all child elements
"""
ops = [
(wx.GraphicsContext.PushState, ())
]
path = makePath()
ops.extend(self.createTransformOpsFromNode(node))
for child in node.getchildren():
cpath, cops = self.processElement(child)
if cpath:
path.AddPath(cpath)
if cops:
ops.extend(cops)
ops.append(
(wx.GraphicsContext.PopState, ())
)
return path, ops
def getFontFromState(self):
font = wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT)
family = self.state.get("font-family")
if False:#family:
#print "setting font", family
font.SetFaceName(family)
size = self.state.get("font-size")
#I'm not sure if this is right or not
if size:
val, unit = values.length.parseString(size)
if '__WXMSW__' in wx.PlatformInfo:
i = int(val)
font.SetPixelSize((i, i))
else:
font.SetPointSize(int(val))
return font
def addTextToDocument(self, node):
x, y = [attrAsFloat(node, attr) for attr in ('x', 'y')]
def DoDrawText(context, text, x, y, brush=wx.NullGraphicsBrush):
#SVG spec appears to originate text from the bottom
#rather than the top as with our API. This function
#will measure and then re-orient the text as needed.
w, h = context.GetTextExtent(text)
y -= h
context.DrawText(text, x, y, brush)
font = self.getFontFromState()
brush = self.getBrushFromState()
if not (brush and brush.IsOk()):
brush = wx.BLACK_BRUSH
#normalize whitespace
#TODO: SVG probably has rules for this
text = ' '.join(node.text.split() if node.text else "")
if text is None:
return None, []
ops = [
(wx.GraphicsContext.SetFont, (font, brush.Colour)),
(DoDrawText, (text, x, y))
]
return None, ops
@pathHandler
def addRectToDocument(self, node, path):
x, y, w, h = (attrAsFloat(node, attr) for attr in ['x', 'y', 'width', 'height'])
rx = node.get('rx')
ry = node.get('ry')
if not (w and h):
path.MoveToPoint(x,y) #keep the current point correct
return
if rx or ry:
if rx and ry:
rx, ry = float(rx), float(ry)
elif rx:
rx = ry = float(rx)
elif ry:
rx = ry = float(ry)
#value clamping as per spec section 9.2
rx = min(rx, w/2)
ry = min(ry, h/2)
#origin
path.MoveToPoint(x+rx, y)
path.AddLineToPoint(x+w-rx, y)
#top right
cx = rx * 2
cy = ry * 2
path.AddEllipticalArc(
x+w-cx, y,
cx, cy,
math.radians(270), math.radians(0),
True
)
path.AddLineToPoint(x+w, y+h-ry)
#bottom right
path.AddEllipticalArc(
x+w-cx, y+h-cy,
cx, cy,
math.radians(0), math.radians(90),
True
)
path.AddLineToPoint(x+rx, y+h)
#bottom left
path.AddEllipticalArc(
x, y+h-cy,
cx, cy,
math.radians(90),
math.radians(180),
True
)
path.AddLineToPoint(x, y+ry)
#bottom right
path.AddEllipticalArc(
x, y,
cx, cy,
math.radians(180),
math.radians(270),
True
)
path.CloseSubpath()
else:
path.AddRectangle(
x, y, w, h
)
@pathHandler
def addCircleToDocument(self, node, path):
cx, cy, r = [attrAsFloat(node, attr) for attr in ('cx', 'cy', 'r')]
path.AddCircle(cx, cy, r)
@pathHandler
def addEllipseToDocument(self, node, path):
cx, cy, rx, ry = [float(node.get(attr, 0)) for attr in ('cx', 'cy', 'rx', 'ry')]
#cx, cy are centerpoint.
#rx, ry are radius.
#wxGC coords are the rect of the ellipse bounding box.
if rx <= 0 or ry <= 0:
return
x = cx - rx
y = cy - ry
path.AddEllipse(x, y, rx*2, ry*2)
@pathHandler
def addLineToDocument(self, node, path):
x1, y1, x2, y2 = [attrAsFloat(node, attr) for attr in ('x1', 'y1', 'x2', 'y2')]
path.MoveToPoint(x1, y1)
path.AddLineToPoint(x2, y2)
@pathHandler
def addPolyLineToDocument(self, node, path):
#translate to pathdata and render that
data = "M " + node.get("points")
self.addPathDataToPath(data, path)
@pathHandler
def addPolygonToDocument(self, node, path):
#translate to pathdata and render that
data = "M " + node.get("points") + " Z"
self.addPathDataToPath(data, path)
@pathHandler
def addPathDataToDocument(self, node, path):
self.addPathDataToPath(node.get('d', ''), path)
def addPathDataToPath(self, data, path):
self.lastControl = None
self.lastControlQ = None
self.firstPoints = []
def normalizeStrokes(parseResults):
""" The data comes from the parser in the
form of (command, [list of arguments]).
We translate that to [(command, args[0]), (command, args[1])]
via a generator.
M is special cased because its subsequent arguments
become linetos.
"""
for command, arguments in parseResults:
if not arguments:
yield (command, ())
else:
arguments = iter(arguments)
if command == 'm':
yield (command, arguments.next())
command = "l"
elif command == "M":
yield (command, arguments.next())
command = "L"
for arg in arguments:
yield (command, arg)
#print "data length", len(data)
import time
t = time.time()
parsed = pathdata.svg.parseString(data)
#print "parsed in", time.time()-t
for stroke in normalizeStrokes(parsed):
self.addStrokeToPath(path, stroke)
def generatePathOps(self, path):
""" Look at the current state and generate the
draw operations (fill, stroke, neither) for the path"""
ops = []
brush = self.getBrushFromState(path)
fillRule = self.state.get('fill-rule', 'nonzero')
frMap = {'nonzero':wx.WINDING_RULE, 'evenodd': wx.ODDEVEN_RULE}
fr = frMap.get(fillRule, wx.ODDEVEN_RULE)
if brush:
ops.append(
(wx.GraphicsContext.SetBrush, (brush,))
)
ops.append(
(wx.GraphicsContext.FillPath, (path, fr))
)
pen = self.getPenFromState()
if pen:
ops.append(
(wx.GraphicsContext.SetPen, (pen,))
)
ops.append(
(wx.GraphicsContext.StrokePath, (path,))
)
return ops
def getPenFromState(self):
pencolour = self.state.get('stroke', 'none')
if pencolour == 'currentColor':
pencolour = self.state.get('color', 'none')
if pencolour == 'transparent':
return wx.TRANSPARENT_PEN
if pencolour == 'none':
return wx.NullPen
type, value = colourValue.parseString(pencolour)
if type == 'URL':
warnings.warn("Color servers for stroking not implemented")
return wx.NullPen
else:
if value[:3] == (-1, -1, -1):
return wx.NullPen
pen = wx.Pen(wx.Colour(*value))
width = self.state.get('stroke-width')
if width:
width, units = values.length.parseString(width)
pen.SetWidth(width)
capmap = {
'butt':wx.CAP_BUTT,
'round':wx.CAP_ROUND,
'square':wx.CAP_PROJECTING
}
joinmap = {
'miter':wx.JOIN_MITER,
'round':wx.JOIN_ROUND,
'bevel':wx.JOIN_BEVEL
}
pen.SetCap(capmap.get(self.state.get('stroke-linecap', None), wx.CAP_BUTT))
pen.SetJoin(joinmap.get(self.state.get('stroke-linejoin', None), wx.JOIN_MITER))
return wx.GraphicsRenderer_GetDefaultRenderer().CreatePen(pen)
def getBrushFromState(self, path=None):
brushcolour = self.state.get('fill', 'black').strip()
type, details = paintValue.parseString(brushcolour)
if type == "URL":
url, fallback = details
element = self.resolveURL(url)
if not element:
if fallback:
type, details = fallback
else:
r, g, b, = 0, 0, 0
else:
if element.tag == '{http://www.w3.org/2000/svg}linearGradient':
box = path.GetBox()
x, y, w, h = box.Get()
return wx.GraphicsRenderer.GetDefaultRenderer().CreateLinearGradientBrush(
x,y,x+w,y+h,wx.Colour(0,0,255,128), wx.RED
)
elif element.tag == '{http://www.w3.org/2000/svg}radialGradient':
box = path.GetBox()
x, y, w, h = box.Get()
#print w
mx = wx.GraphicsRenderer.GetDefaultRenderer().CreateMatrix(x,y,w,h)
cx, cy = mx.TransformPoint(0.5, 0.5)
fx, fy = cx, cy
return wx.GraphicsRenderer.GetDefaultRenderer().CreateRadialGradientBrush(
cx,cy,
fx,fy,
(max(w,h))/2,
wx.Colour(0,0,255,128), wx.RED
)
else:
#invlid gradient specified
return wx.NullBrush
r,g,b = 0,0,0
if type == 'CURRENTCOLOR':
type, details = paintValue.parseString(self.state.get('color', 'none'))
if type == 'RGB':
r,g,b = details
elif type == "NONE":
return wx.NullBrush
opacity = self.state.get('fill-opacity', self.state.get('opacity', '1'))
opacity = float(opacity)
opacity = min(max(opacity, 0.0), 1.0)
a = 255 * opacity
#using try/except block instead of
#just setdefault because the wxBrush and wxColour would
#be created every time anyway in order to pass them,
#defeating the purpose of the cache
try:
return SVGDocument.brushCache[(r,g,b,a)]
except KeyError:
return SVGDocument.brushCache.setdefault((r,g,b,a), wx.Brush(wx.Colour(r,g,b,a)))
def resolveURL(self, urlData):
"""
Resolve a URL and return an elementTree Element from it.
Return None if unresolvable
"""
scheme, netloc, path, query, fragment = urlData
if scheme == netloc == path == '':
#horrible. There's got to be a better way?
if self.tree.get("id") == fragment:
return self.tree
else:
for child in self.tree.getiterator():
#print child.get("id")
if child.get("id") == fragment:
return child
return None
else:
return self.resolveRemoteURL(urlData)
def resolveRemoteURL(self, url):
return None
def addStrokeToPath(self, path, stroke):
""" Given a stroke from a path command
(in the form (command, arguments)) create the path
commands that represent it.
TODO: break out into (yet another) class/module,
especially so we can get O(1) dispatch on type?
"""
type, arg = stroke
relative = False
if type == type.lower():
relative = True
ox, oy = path.GetCurrentPoint().Get()
else:
ox = oy = 0
def normalizePoint(arg):
x, y = arg
return x+ox, y+oy
def reflectPoint(point, relativeTo):
x, y = point
a, b = relativeTo
return ((a*2)-x), ((b*2)-y)
type = type.upper()
if type == 'M':
pt = normalizePoint(arg)
self.firstPoints.append(pt)
path.MoveToPoint(pt)
elif type == 'L':
path.AddLineToPoint(normalizePoint(arg))
elif type == 'C':
#control1, control2, endpoint = arg
control1, control2, endpoint = map(
normalizePoint, arg
)
self.lastControl = control2
path.AddCurveToPoint(
control1,
control2,
endpoint
)
#~ cp = path.GetCurrentPoint()
#~ path.AddCircle(c1x, c1y, 5)
#~ path.AddCircle(c2x, c2y, 3)
#~ path.AddCircle(x,y, 7)
#~ path.MoveToPoint(cp)
#~ print "C", control1, control2, endpoint
elif type == 'S':
#control2, endpoint = arg
control2, endpoint = map(
normalizePoint, arg
)
if self.lastControl:
control1 = reflectPoint(self.lastControl, path.GetCurrentPoint())
else:
control1 = path.GetCurrentPoint()
#~ print "S", self.lastControl,":",control1, control2, endpoint
self.lastControl = control2
path.AddCurveToPoint(
control1,
control2,
endpoint
)
elif type == "Q":
(cx, cy), (x,y) = map(normalizePoint, arg)
self.lastControlQ = (cx, cy)
path.AddQuadCurveToPoint(cx, cy, x, y)
elif type == "T":
x, y, = normalizePoint(arg)
if self.lastControlQ:
cx, cy = reflectPoint(self.lastControlQ, path.GetCurrentPoint())
else:
cx, cy = path.GetCurrentPoint()
self.lastControlQ = (cx, cy)
path.AddQuadCurveToPoint(cx, cy, x, y)
elif type == "V":
_, y = normalizePoint((0, arg))
x, _ = path.GetCurrentPoint()
path.AddLineToPoint(x,y)
elif type == "H":
x, _ = normalizePoint((arg, 0))
_, y = path.GetCurrentPoint()
path.AddLineToPoint(x,y)
elif type == "A":
#wxGC currently only supports circular arcs,
#not eliptical ones
(
(rx, ry), #radii of ellipse
angle, #angle of rotation on the ellipse in degrees
(fa, fs), #arc and stroke angle flags
(x, y) #endpoint on the arc
) = arg
x, y = normalizePoint((x,y))
cx, cy = path.GetCurrentPoint()
if (cx, cy) == (x, y):
return #noop
if (rx == 0 or ry == 0):
#no radius is effectively a line
path.AddLineToPoint(x,y)
return
#find the center point for the ellipse
#translation via the angle
angle = angle % 360
angle = math.radians(angle)
#translated endpoint
xPrime = math.cos(angle) * ((cx-x)/2)
xPrime += math.sin(angle) * ((cx-x)/2)
yPrime = -(math.sin(angle)) * ((cy-y)/2)
yPrime += (math.cos(angle)) * ((cy-y)/2)
temp = ((rx**2) * (ry**2)) - ((rx**2) * (yPrime**2)) - ((ry**2) * (xPrime**2))
temp /= ((rx**2) * (yPrime**2)) + ((ry**2)*(xPrime**2))
temp = abs(temp)
try:
temp = math.sqrt(temp)
except ValueError:
import pdb
pdb.set_trace()
cxPrime = temp * ((rx * yPrime) / ry)
cyPrime = temp * -((ry * xPrime) / rx)
if fa == fs:
cxPrime, cyPrime = -cxPrime, -cyPrime
#reflect backwards now for the origin
cnx = math.cos(angle) * cxPrime
cnx += math.sin(angle) * cxPrime
cny = -(math.sin(angle)) * cyPrime
cny += (math.cos(angle)) * cyPrime
cnx += ((cx+x)/2.0)
cny += ((cy+y)/2.0)
#calculate the angle between the two endpoints
lastArc = wx.Point2D(x-cnx, y-cny).GetVectorAngle()
firstArc = wx.Point2D(cx-cnx, cy-cny).GetVectorAngle()
lastArc = math.radians(lastArc)
firstArc = math.radians(firstArc)
#aargh buggines.
#AddArc draws a straight line between
#the endpoints of the arc.
#putting it in a subpath makes the strokes come out
#correctly, but it still only fills the slice
#taking out the MoveToPoint fills correctly.
path.AddEllipse(cnx-rx, cny-ry, rx*2, ry*2)
path.MoveToPoint(x, y)
#~ npath = makePath()
#~ npath.AddEllipticalArc(cnx-rx, cny-ry, rx*2, ry*2, firstArc, lastArc, False)
#~ npath.MoveToPoint(x,y)
#~ path.AddPath(npath)
elif type == 'Z':
#~ Bugginess:
#~ CloseSubpath() doesn't change the
#~ current point, as SVG spec requires.
#~ However, manually moving to the endpoint afterward opens a new subpath
#~ and (apparently) messes with stroked but not filled paths.
#~ This is possibly a bug in GDI+?
#~ Manually closing the path via AddLineTo gives incorrect line join
#~ results
#~ Manually closing the path *and* calling CloseSubpath() appears
#~ to give correct results on win32
pt = self.firstPoints.pop()
path.AddLineToPoint(pt)
path.CloseSubpath()
def render(self, context):
if not hasattr(self, "ops"):
return
for op, args in self.ops:
op(context, *args)
if __name__ == '__main__':
from tests.test_document import *
unittest.main()
"""
SVG path data parser
Usage:
steps = svg.parseString(pathdata)
for command, arguments in steps:
pass
"""
from pyparsing import (ParserElement, Literal, Word, CaselessLiteral,
Optional, Combine, Forward, ZeroOrMore, nums, oneOf, Group, ParseException, OneOrMore)
#ParserElement.enablePackrat()
def Command(char):
""" Case insensitive but case preserving"""
return CaselessPreservingLiteral(char)
def Arguments(token):
return Group(token)
class CaselessPreservingLiteral(CaselessLiteral):
""" Like CaselessLiteral, but returns the match as found
instead of as defined.
"""
def __init__( self, matchString ):
super(CaselessPreservingLiteral,self).__init__( matchString.upper() )
self.name = "'%s'" % matchString
self.errmsg = "Expected " + self.name
self.myException.msg = self.errmsg
def parseImpl( self, instring, loc, doActions=True ):
test = instring[ loc:loc+self.matchLen ]
if test.upper() == self.match:
return loc+self.matchLen, test
#~ raise ParseException( instring, loc, self.errmsg )
exc = self.myException
exc.loc = loc
exc.pstr = instring
raise exc
def Sequence(token):
""" A sequence of the token"""
return OneOrMore(token+maybeComma)
digit_sequence = Word(nums)
sign = oneOf("+ -")
def convertToFloat(s, loc, toks):
try:
return float(toks[0])
except:
raise ParseException(loc, "invalid float format %s"%toks[0])
exponent = CaselessLiteral("e")+Optional(sign)+Word(nums)
#note that almost all these fields are optional,
#and this can match almost anything. We rely on Pythons built-in
#float() function to clear out invalid values - loosely matching like this
#speeds up parsing quite a lot
floatingPointConstant = Combine(
Optional(sign) +
Optional(Word(nums)) +
Optional(Literal(".") + Optional(Word(nums)))+
Optional(exponent)
)
floatingPointConstant.setParseAction(convertToFloat)
number = floatingPointConstant
#same as FP constant but don't allow a - sign
nonnegativeNumber = Combine(
Optional(Word(nums)) +
Optional(Literal(".") + Optional(Word(nums)))+
Optional(exponent)
)
nonnegativeNumber.setParseAction(convertToFloat)
coordinate = number
#comma or whitespace can seperate values all over the place in SVG
maybeComma = Optional(Literal(',')).suppress()
coordinateSequence = Sequence(coordinate)
coordinatePair = (coordinate + maybeComma + coordinate).setParseAction(lambda t: tuple(t))
coordinatePairSequence = Sequence(coordinatePair)
coordinatePairPair = coordinatePair + maybeComma + coordinatePair
coordinatePairPairSequence = Sequence(Group(coordinatePairPair))
coordinatePairTriple = coordinatePair + maybeComma + coordinatePair + maybeComma + coordinatePair
coordinatePairTripleSequence = Sequence(Group(coordinatePairTriple))
#commands
lineTo = Group(Command("L") + Arguments(coordinatePairSequence))
moveTo = Group(Command("M") + Arguments(coordinatePairSequence))
closePath = Group(Command("Z")).setParseAction(lambda t: ('Z', (None,)))
flag = oneOf("1 0").setParseAction(lambda t: bool(int((t[0]))))
arcRadius = (
nonnegativeNumber + maybeComma + #rx
nonnegativeNumber #ry
).setParseAction(lambda t: tuple(t))
arcFlags = (flag + maybeComma + flag).setParseAction(lambda t: tuple(t))
ellipticalArcArgument = Group(
arcRadius + maybeComma + #rx, ry
number + maybeComma +#rotation
arcFlags + #large-arc-flag, sweep-flag
coordinatePair #(x,y)
)
ellipticalArc = Group(Command("A") + Arguments(Sequence(ellipticalArcArgument)))
smoothQuadraticBezierCurveto = Group(Command("T") + Arguments(coordinatePairSequence))
quadraticBezierCurveto = Group(Command("Q") + Arguments(coordinatePairPairSequence))
smoothCurve = Group(Command("S") + Arguments(coordinatePairPairSequence))
curve = Group(Command("C") + Arguments(coordinatePairTripleSequence))
horizontalLine = Group(Command("H") + Arguments(coordinateSequence))
verticalLine = Group(Command("V") + Arguments(coordinateSequence))
drawToCommand = (
lineTo | moveTo | closePath | ellipticalArc | smoothQuadraticBezierCurveto |
quadraticBezierCurveto | smoothCurve | curve | horizontalLine | verticalLine
)
#~ number.debug = True
moveToDrawToCommands = moveTo + ZeroOrMore(drawToCommand)
svg = ZeroOrMore(moveToDrawToCommands)
svg.keepTabs = True
def profile():
import cProfile
p = cProfile.Profile()
p.enable()
ptest()
ptest()
ptest()
p.disable()
p.print_stats()
bpath = """M204.33 139.83 C196.33 133.33 206.68 132.82 206.58 132.58 C192.33 97.08 169.35
81.41 167.58 80.58 C162.12 78.02 159.48 78.26 160.45 76.97 C161.41 75.68 167.72 79.72 168.58
80.33 C193.83 98.33 207.58 132.33 207.58 132.33 C207.58 132.33 209.33 133.33 209.58 132.58
C219.58 103.08 239.58 87.58 246.33 81.33 C253.08 75.08 256.63 74.47 247.33 81.58 C218.58 103.58
210.34 132.23 210.83 132.33 C222.33 134.83 211.33 140.33 211.83 139.83 C214.85 136.81 214.83 145.83 214.83
145.83 C214.83 145.83 231.83 110.83 298.33 66.33 C302.43 63.59 445.83 -14.67 395.83 80.83 C393.24 85.79 375.83
105.83 375.83 105.83 C375.83 105.83 377.33 114.33 371.33 121.33 C370.3 122.53 367.83 134.33 361.83 140.83 C360.14 142.67
361.81 139.25 361.83 140.83 C362.33 170.83 337.76 170.17 339.33 170.33 C348.83 171.33 350.19 183.66 350.33 183.83 C355.83
190.33 353.83 191.83 355.83 194.83 C366.63 211.02 355.24 210.05 356.83 212.83 C360.83 219.83 355.99 222.72 357.33 224.83
C360.83 230.33 354.75 233.84 354.83 235.33 C355.33 243.83 349.67 240.73 349.83 244.33 C350.33 255.33 346.33 250.83 343.83 254.83
C336.33 266.83 333.46 262.38 332.83 263.83 C329.83 270.83 325.81 269.15 324.33 270.83 C320.83 274.83 317.33 274.83 315.83 276.33
C308.83 283.33 304.86 278.39 303.83 278.83 C287.83 285.83 280.33 280.17 277.83 280.33 C270.33 280.83 271.48 279.67 269.33 277.83
C237.83 250.83 219.33 211.83 215.83 206.83 C214.4 204.79 211.35 193.12 212.33 195.83 C214.33 201.33 213.33 250.33 207.83 250.33
C202.33 250.33 201.83 204.33 205.33 195.83 C206.43 193.16 204.4 203.72 201.79 206.83 C196.33 213.33 179.5 250.83 147.59 277.83
C145.42 279.67 146.58 280.83 138.98 280.33 C136.46 280.17 128.85 285.83 112.65 278.83 C111.61 278.39 107.58 283.33 100.49 276.33
C98.97 274.83 95.43 274.83 91.88 270.83 C90.39 269.15 86.31 270.83 83.27 263.83 C82.64 262.38 79.73 266.83 72.13 254.83 C69.6 250.83
65.54 255.33 66.05 244.33 C66.22 240.73 60.48 243.83 60.99 235.33 C61.08 233.84 54.91 230.33 58.45 224.83 C59.81 222.72 54.91 219.83
58.96 212.83 C60.57 210.05 49.04 211.02 59.97 194.83 C62 191.83 59.97 190.33 65.54 183.83 C65.69 183.66 67.06 171.33 76.69 170.33
C78.28 170.17 53.39 170.83 53.9 140.83 C53.92 139.25 55.61 142.67 53.9 140.83 C47.82 134.33 45.32 122.53 44.27 121.33 C38.19 114.33
39.71 105.83 39.71 105.83 C39.71 105.83 22.08 85.79 19.46 80.83 C-31.19 -14.67 114.07 63.59 118.22 66.33 C185.58 110.83 202 145.83
202 145.83 C202 145.83 202.36 143.28 203 141.83 C203.64 140.39 204.56 140.02 204.33 139.83 z"""
def ptest():
svg.parseString(bpath)
if __name__ == '__main__':
#~ from tests.test_pathdata import *
#~ unittest.main()
profile()
This source diff could not be displayed because it is too large. You can view the blob instead.
<?xml version='1.0' standalone='no'?>
<!--Written in KDevelop (http://www.kdevelop.org/)-->
<svg baseProfile='full' contentScriptType='text/ecmascript' contentStyleType='text/css' height='4003.518px' onload='init()' preserveAspectRatio='xMidYMid meet' version='1.0' width='516.0px' xmlns='http://www.w3.org/2000/svg' xmlns:inkscape='http://www.inkscape.org/namespaces/inkscape' xmlns:slice='http://www.reprap.org/slice' xmlns:xlink='http://www.w3.org/1999/xlink' zoomAndPan='magnify'>
<metadata>
<slice:layers id='sliceData' controlBoxHeight='720' controlBoxWidth='240' decimalPlacesCarried='4' edgeWidth='0.72' layerHeight='0.4' margin='20' marginTop='60' maxX='11.919' maxY='11.919' maxZ='10.0' minX='-11.919' minY='-11.919' minZ='-2.0' procedureName='carve' svgMinWidth='516' template='svg_layer' textHeight='22.5' unitScale='3.7' version='0.1' yAxisPointingUpward='true' />
</metadata>
<script type='text/ecmascript'>
<![CDATA[
globalMetadata = document.getElementsByTagNameNS('http://www.reprap.org/slice', 'layers')[0];
globalSliceMinX = globalMetadata.getAttribute('minX') * 1;
globalSliceMaxX = globalMetadata.getAttribute('maxX') * 1;
globalSliceMinY = globalMetadata.getAttribute('minY') * 1;
globalSliceMaxY = globalMetadata.getAttribute('maxY') * 1;
globalSliceMinZ = globalMetadata.getAttribute('minZ') * 1;
globalSliceMaxZ = globalMetadata.getAttribute('maxZ') * 1;
//Control var's
globalLayerIndex = 0; //Number of currently viewed layer (zero index)
globalLatitude = 60.0;
globalLongitude = 45.0;
globalControlBoxHeight = globalMetadata.getAttribute('controlBoxHeight') * 1;
globalControlBoxWidth = globalMetadata.getAttribute('controlBoxWidth') * 1;
globalSliding = false;
globalObserving = false
//Display var's
globalMargin = globalMetadata.getAttribute('margin') * 1;
globalMarginTop = globalMetadata.getAttribute('marginTop') * 1;
globalDimensionX = globalSliceMaxX - globalSliceMinX;
globalDimensionY = globalSliceMaxY - globalSliceMinY;
globalDimensionZ = globalSliceMaxZ - globalSliceMinZ;
magnitudeXYSquared = globalDimensionX * globalDimensionX + globalDimensionY * globalDimensionY;
globalDimensionMagnitude = Math.sqrt(magnitudeXYSquared + globalDimensionZ * globalDimensionZ);
globalGlobeTravelRadian = 0.0;
globalGlobeInnerObserverRadius = 0.0;
globalTextHeight = globalMetadata.getAttribute('textHeight') * 1.0;
globalUnitScale = globalMetadata.getAttribute('unitScale') * 1.0;
globalXOffset = globalControlBoxWidth + globalMargin + globalMargin;
globalYOffset = globalMargin + globalMarginTop;
globalZoomScale = 1.0; //Default 1:1 may need smaller scale for large objects
globalScale = globalUnitScale * globalZoomScale;
//Globals to be set in init
globalLayers = [];
globalSlider = 0;
globalThumb = 0;
globalThumbRadius = 0.0;
globalSliderWidthMinusDiameter = 0.0;
function changeScale(newScale) {
globalZoomScale = newScale;
globalScale = globalUnitScale * globalZoomScale;
if (globalZoomScale >=1) { //dont scale line thickness for large display scale
document.getElementById('layerData').setAttributeNS(null, 'stroke-width', 2.0 / (globalScale));
}
}
function changeScaleIso(newScale) {
changeScale(newScale);
viewIso();
}
function changeScaleLayer(newScale) {
changeScale(newScale);
viewLayer();
}
function changeScaleScroll(newScale) {
changeScale(newScale);
viewScroll();
}
function displayIso(latitude, longitude) {
latitude = Math.max(0.0, latitude);
latitude = Math.min(89.0, latitude);
globalLatitude = Math.round(latitude);
globalLongitude = Math.round((longitude + 540.0) % 360.0) - 180.0;
latitudeRadians = getRadians(globalLatitude);
longitudeWiddershinsRadians = getRadians(90.0 - globalLongitude);
unitX = Math.cos(longitudeWiddershinsRadians);
unitY = Math.sin(longitudeWiddershinsRadians);
latitudeOverLayers = globalDimensionZ / globalLayers.length;
globalOneOverUnitScaleString = (1.0 / globalUnitScale).toString();
yScale = -1.0 * globalScale * Math.cos(latitudeRadians);
scaleRotateCenterString = ' scale(' + globalScale + ', ' + yScale + ')';
scaleRotateCenterString += ' rotate(' + (-1.0 * globalLongitude).toString() + ')';
centerX = (-0.5 * globalDimensionX - globalSliceMinX).toString();
centerY = (-0.5 * globalDimensionY - globalSliceMinY).toString();
scaleRotateCenterString += ' translate(' + centerX + ', ' + centerY + ')';
x = 0.5 * globalDimensionMagnitude * globalScale + globalXOffset;
halfLengthMinusOne = 0.5 * (globalLayers.length - 1);
for (var i in globalLayers) {
latitudeZSin = Math.sin(latitudeRadians) * (i - halfLengthMinusOne) * latitudeOverLayers
y = globalScale * (0.5 * globalDimensionMagnitude - latitudeZSin) + globalYOffset;
// y = globalDimensionY * globalScale + globalYOffset - i * unitY * latitudeOverLayers;
transformString = ' translate(' + x + ', ' + y + ')';
globalLayers[i].setAttributeNS(null, 'transform', transformString + scaleRotateCenterString);
}
setText('latitudeIso', 'Latitude: ' + globalLatitude.toString() + '°');
setText('longitudeIso', 'Longitude: ' + globalLongitude.toString() + '°');
globeMoveableRadius = globalGlobeTravelRadian * latitudeRadians + globalGlobeInnerObserverRadius;
globalObserver.setAttribute('cx', globalGlobeCenterX + unitX * globeMoveableRadius);
globalObserver.setAttribute('cy', globalGlobeCenterY - unitY * globeMoveableRadius);
}
function displayIsoByLatitude(latitude) {
displayIso(latitude, globalLongitude);
}
function displayIsoByLongitude(longitude) {
displayIso(globalLatitude, longitude);
}
function displayLayer(layerNum) {
if (globalLayers.length <= 1) {
document.getElementById('maxIndexLayer').setAttributeNS(null, 'visibility', 'hidden');
document.getElementById('minIndexLayer').setAttributeNS(null, 'visibility', 'hidden');
globalSlider.setAttributeNS(null, 'visibility', 'hidden');
globalThumb.setAttributeNS(null, 'visibility', 'hidden');
}
if (layerNum <= 0) {
document.getElementById('decreaseLayerIndex').setAttributeNS(null, 'visibility', 'hidden');
}
else {
document.getElementById('decreaseLayerIndex').setAttributeNS(null, 'visibility', 'visible');
}
if (layerNum >= globalLayers.length - 1) {
document.getElementById('increaseLayerIndex').setAttributeNS(null, 'visibility', 'hidden');
}
else {
document.getElementById('increaseLayerIndex').setAttributeNS(null, 'visibility', 'visible');
}
if (layerNum < 0 || layerNum >= globalLayers.length) {
return
}
globalLayers[globalLayerIndex].setAttributeNS(null, 'visibility', 'hidden');
currentLayerElement = globalLayers[layerNum]
currentLayerElement.setAttributeNS(null, 'visibility', 'visible');
globalLayerIndex = layerNum;
setText('currentIndexLayer', 'Layer: ' + globalLayerIndex.toString() + ', ' + currentLayerElement.getAttribute('id'));
//Slider
if (!globalSliding) {
placeThumb(globalSliderWidthMinusDiameter / (globalLayers.length - 1) * globalLayerIndex + globalThumbRadius);
}
}
function getDegrees(radians) {
return radians / Math.PI * 180.0;
}
function getRadians(degrees) {
return degrees / 180.0 * Math.PI;
}
function getScaleTransformString(scale) {
scaleTransformString = 'scale(' + scale + ' ' + (scale * - 1)
return scaleTransformString + ') translate(' + (globalSliceMinX * - 1) + ' ' + (globalSliceMinY * - 1) + ')';
}
function getWidth() {
return (globalDimensionX * globalScale) + globalXOffset
}
function hideElements(elementNames) {
for (var elementNameIndex in elementNames) {
document.getElementById(elementNames[elementNameIndex]).setAttributeNS(null, 'visibility', 'hidden');
}
}
function init() {
//Find only layer groups
globe = document.getElementById('globe');
globalGlobeCenterX = globe.getAttribute('cx') * 1;
globalGlobeCenterXWindow = globalGlobeCenterX + globalMargin;
globalGlobeCenterY = globe.getAttribute('cy') * 1;
globalGlobeCenterYWindow = globalGlobeCenterX + globalMarginTop;
globalObserver = document.getElementById('observer');
globeRadius = globe.getAttribute('r') * 1.0;
observerRadius = globalObserver.getAttribute('r') * 1.0;
globalGlobeInnerObserverRadius = document.getElementById('cover').getAttribute('r') * 1.0 + observerRadius;
globalGlobeTravelRadian = getDegrees(globeRadius - globalGlobeInnerObserverRadius - observerRadius) / 89.0;
globalSlider = document.getElementById('slider');
globalSliderX = globalSlider.getAttribute('x') * 1;
globalSliderXWindow = globalSliderX + globalMargin
globalThumb = document.getElementById('thumb');
globalThumbRadius = globalThumb.getAttribute('r') * 1;
globalSliderWidthMinusRadius = globalSlider.getAttribute('width') * 1 - globalThumbRadius;
globalSliderWidthMinusDiameter = globalSliderWidthMinusRadius - globalThumbRadius;
var allGroups = document.getElementsByTagName('g');
for (var i = 0; i < allGroups.length; i++) {
if (allGroups[i].id.indexOf('z:') == 0) {
globalLayers.push(allGroups[i]);
}
}
//Slider
layerControlBox = document.getElementById('layerControlBox');
layerControlBox.addEventListener('mouseup', sliderUp, false);
layerControlBox.addEventListener('mousemove', sliderMove, false);
globalSlider.addEventListener('mousedown', sliderDownMove, false);
globalThumb.addEventListener('mousedown', sliderDownMove, false);
//Observer
isoControlBox = document.getElementById('isoControlBox');
isoControlBox.addEventListener('mouseup', observerUp, false);
isoControlBox.addEventListener('mousemove', observerMove, false);
globe.addEventListener('mousedown', observerDownMove, false);
globalObserver.addEventListener('mousedown', observerDownMove, false);
//Control box data
setText('maxIndexLayer', globalLayers.length - 1);
changeScaleLayer(globalZoomScale);
}
function observerDown(event) {
globalObserving = true;
}
function observerDownMove(event) {
globalObserving = true;
observerMove(event);
}
function observerMove(event) {
if (!globalObserving) {
return;
}
observerX = event.clientX - globalGlobeCenterXWindow + window.pageXOffset;
observerY = event.clientY - globalGlobeCenterYWindow + window.pageYOffset;
distanceFromCenter = Math.sqrt(observerX * observerX + observerY * observerY);
latitudeRadians = (distanceFromCenter - globalGlobeInnerObserverRadius) / globalGlobeTravelRadian;
longitudeRadians = Math.atan2(observerX, -observerY);
displayIso(getDegrees(latitudeRadians), getDegrees(longitudeRadians));
}
function observerUp(event) {
globalObserving = false;
}
function placeThumb(value) {
if (globalLayers.length > 1) {
globalThumb.setAttribute('cx', globalSliderX + value);
}
}
function setScaleText(scaleID) {
setText(scaleID, ': ' + globalZoomScale);
}
function setSVG(width, height) {
rootSVG = document.getElementsByTagName('svg')[0];
svgMinWidth = globalMetadata.getAttribute('svgMinWidth') * 1;
height = Math.max(globalControlBoxHeight + globalMargin + globalMarginTop, height);
width = Math.max(svgMinWidth, width);
rootSVG.setAttributeNS(null, 'width', width + 'px')
rootSVG.setAttributeNS(null, 'height', height + 'px')
}
function setText(id, str) {
e = document.getElementById(id)
if (e != null)
e.firstChild.nodeValue = str;
}
function showElements(elementNames) {
for (var elementNameIndex in elementNames) {
document.getElementById(elementNames[elementNameIndex]).setAttributeNS(null, 'visibility', 'visible');
}
}
function sliderDown(event) {
globalSliding = true;
}
function sliderDownMove(event) {
globalSliding = true;
sliderMove(event);
}
function sliderMove(event) {
if (!globalSliding) {
return;
}
value = event.clientX - globalSliderXWindow + window.pageXOffset;
if (value >= globalThumbRadius && value <= globalSliderWidthMinusRadius) {
placeThumb(value);
zoneWidth = globalSliderWidthMinusDiameter / (globalLayers.length);
newLayer = Math.round((value - globalThumbRadius - 0.5 * zoneWidth) / zoneWidth)
if (newLayer != globalLayerIndex) {
displayLayer(newLayer)
}
}
}
function sliderUp(event) {
globalSliding = false;
}
function viewIso() {
height = (globalDimensionMagnitude * globalScale) + globalYOffset;
setSVG((globalDimensionMagnitude * globalScale) + globalXOffset, height);
for (var i in globalLayers) {
// globalLayers[i].setAttributeNS(null, 'transform', 'translate(' + globalMargin + ' ' + height + ')');
globalLayers[i].setAttributeNS(null, 'visibility', 'visible');
globalLayers[i].getElementsByTagName('text')[0].setAttributeNS(null, 'visibility', 'hidden');
pathElements = globalLayers[i].getElementsByTagName('path');
for (var pathElementIndex = 0; pathElementIndex < pathElements.length; pathElementIndex++) {
pathElements[pathElementIndex].setAttributeNS(null, 'transform', '');
}
}
//show control box
hideElements(['decreaseLayerIndex', 'increaseLayerIndex', 'isoViewButton', 'layerControlBox', 'layerViewLabel', 'scrollControlBox', 'scrollViewLabel']);
showElements(['isoControlBox', 'isoViewLabel', 'layerViewButton', 'scrollViewButton']);
displayIso(globalLatitude, globalLongitude);
setScaleText('scaleIso');
}
function viewLayer() {
//Set svg size and view port
height = (globalDimensionY * globalScale) + globalYOffset;
setSVG(getWidth(), height);
//move and hide all layers
for (var i in globalLayers) {
globalLayers[i].setAttributeNS(null, 'transform', 'translate(' + globalXOffset + ' ' + height + ')');
globalLayers[i].setAttributeNS(null, 'visibility', 'hidden');
globalLayers[i].getElementsByTagName('text')[0].setAttributeNS(null, 'visibility', 'hidden');
transform = getScaleTransformString(globalScale);
pathElements = globalLayers[i].getElementsByTagName('path');
for (var pathElementIndex = 0; pathElementIndex < pathElements.length; pathElementIndex++) {
pathElements[pathElementIndex].setAttributeNS(null, 'transform', transform);
}
}
//show control box
hideElements(['isoControlBox', 'isoViewLabel', 'layerViewButton', 'scrollControlBox', 'scrollViewLabel'])
showElements(['isoViewButton', 'layerControlBox', 'layerViewLabel', 'scrollViewButton'])
//show current layer
displayLayer(globalLayerIndex);
setScaleText('scaleLayer');
}
function viewScroll() {
//Set svg size and view port
yDimensionScale = globalDimensionY * globalScale
singleHeight = (globalMargin + yDimensionScale + globalTextHeight)
height = globalLayers.length * singleHeight + globalMargin + globalMargin + globalYOffset;
setSVG(getWidth(), height);
//move and show all layers
for (var i in globalLayers) {
x = globalXOffset;
y = i * singleHeight + yDimensionScale + globalYOffset;
transform = getScaleTransformString(globalScale);
globalLayers[i].setAttributeNS(null, 'transform', 'translate(' + x + ', ' + y + ')');
pathElements = globalLayers[i].getElementsByTagName('path');
for (var pathElementIndex = 0; pathElementIndex < pathElements.length; pathElementIndex++) {
pathElements[pathElementIndex].setAttributeNS(null, 'transform', transform);
}
globalLayers[i].setAttributeNS(null, 'visibility', 'visible');
globalLayers[i].getElementsByTagName('text')[0].setAttributeNS(null, 'visibility', 'visible');
}
//show control box
hideElements(['isoControlBox', 'isoViewLabel', 'layerControlBox', 'layerViewLabel', 'scrollViewButton'])
showElements(['isoViewButton', 'layerViewButton', 'scrollControlBox', 'scrollViewLabel'])
setScaleText('scaleScroll');
}
]]>
</script>
<title >belt_pulley3.stl - Slice Layers</title>
<!--Begin Layer Data -->
<g id='layerData' fill='#556B2F' fill-rule='evenodd' font-family='Arial' font-size='15px' font-weight='bold' inkscape:groupmode='layer' inkscape:label='Slice Layers' stroke='#00F' stroke-width='0.54px'>
<!--id='sliceElementTemplate' must be there or else the slice template will not be found-->
<g id='z:-1.8' inkscape:groupmode='layer' inkscape:label='Layer 0, z:-1.8' transform='translate(280.0, 168.2006)'>
<!--id='layerTextTemplate' must be there so that the text could be removed if necessary-->
<text id='layerTextTemplate-1.8' fill='#000' stroke='none' y='15' >Layer 0, z:-1.8</text>
<path d='M -11.6336 1.4126 L -11.719 0.0 L -11.6336 -1.4126 L -11.3785 -2.8045 L -10.9575 -4.1556 L -10.3766 -5.4461 L -9.6445 -6.6571 L -8.7718 -7.7711 L -7.7711 -8.7718 L -6.6571 -9.6445 L -5.4461 -10.3766 L -4.1556 -10.9575 L -2.8045 -11.3785 L -1.4126 -11.6336 L 0.0 -11.719 L 1.4126 -11.6336 L 2.8045 -11.3785 L 4.1556 -10.9575 L 5.4461 -10.3766 L 6.6571 -9.6445 L 7.7711 -8.7718 L 8.7718 -7.7711 L 9.6445 -6.6571 L 10.3766 -5.4461 L 10.9575 -4.1556 L 11.3785 -2.8045 L 11.6336 -1.4126 L 11.719 0.0 L 11.6336 1.4126 L 11.3785 2.8045 L 10.9575 4.1556 L 10.3766 5.4461 L 9.6445 6.6571 L 8.7718 7.7711 L 7.7711 8.7718 L 6.6571 9.6445 L 5.4461 10.3766 L 4.1556 10.9575 L 2.8045 11.3785 L 1.4126 11.6336 L 0.0 11.719 L -1.4126 11.6336 L -2.8045 11.3785 L -4.1556 10.9575 L -5.4461 10.3766 L -6.6571 9.6445 L -7.7711 8.7718 L -8.7718 7.7711 L -9.6445 6.6571 L -10.3766 5.4461 L -10.9575 4.1556 L -11.3785 2.8045 z M -4.0 -1.5 L -4.0 1.5 L -6.8343 1.5 L -6.6072 2.3119 L -6.3068 3.0372 L -5.9271 3.7242 L -5.4728 4.3644 L -4.9497 4.9497 L -4.3644 5.4728 L -3.7242 5.9271 L -3.0372 6.3068 L -2.3119 6.6072 L -1.5 6.8343 L -1.5 4.0 L 1.5 4.0 L 1.5 6.8343 L 2.3119 6.6072 L 3.0372 6.3068 L 3.7242 5.9271 L 4.3644 5.4728 L 4.9497 4.9497 L 5.4728 4.3644 L 5.9271 3.7242 L 6.3068 3.0372 L 6.6072 2.3119 L 6.8343 1.5 L 4.0 1.5 L 4.0 -1.5 L 6.8343 -1.5 L 6.6072 -2.3119 L 6.3068 -3.0372 L 5.9271 -3.7242 L 5.4728 -4.3644 L 4.9497 -4.9497 L 4.3644 -5.4728 L 3.7242 -5.9271 L 3.0372 -6.3068 L 2.3119 -6.6072 L 1.5 -6.8343 L 1.5 -4.0 L -1.5 -4.0 L -1.5 -6.8343 L -2.3119 -6.6072 L -3.0372 -6.3068 L -3.7242 -5.9271 L -4.3644 -5.4728 L -4.9497 -4.9497 L -5.4728 -4.3644 L -5.9271 -3.7242 L -6.3068 -3.0372 L -6.6072 -2.3119 L -6.8343 -1.5 z' transform='scale(3.7, -3.7) translate(11.919, 11.919)' />
</g>
<g id='z:-1.4' inkscape:groupmode='layer' inkscape:label='Layer 1, z:-1.4' transform='translate(280.0, 298.9012)'>
<!--id='layerTextTemplate' must be there so that the text could be removed if necessary-->
<text id='layerTextTemplate-1.4' fill='#000' stroke='none' y='15' >Layer 1, z:-1.4</text>
<path d='M -11.2365 1.3644 L -11.319 0.0 L -11.2365 -1.3644 L -10.9901 -2.7088 L -10.5835 -4.0138 L -10.0224 -5.2602 L -9.3154 -6.4299 L -8.4724 -7.5059 L -7.5059 -8.4724 L -6.4299 -9.3154 L -5.2602 -10.0224 L -4.0138 -10.5835 L -2.7088 -10.9901 L -1.3644 -11.2365 L 0.0 -11.319 L 1.3644 -11.2365 L 2.7088 -10.9901 L 4.0138 -10.5835 L 5.2602 -10.0224 L 6.4299 -9.3154 L 7.5059 -8.4724 L 8.4724 -7.5059 L 9.3154 -6.4299 L 10.0224 -5.2602 L 10.5835 -4.0138 L 10.9901 -2.7088 L 11.2365 -1.3644 L 11.319 0.0 L 11.2365 1.3644 L 10.9901 2.7088 L 10.5835 4.0138 L 10.0224 5.2602 L 9.3154 6.4299 L 8.4724 7.5059 L 7.5059 8.4724 L 6.4299 9.3154 L 5.2602 10.0224 L 4.0138 10.5835 L 2.7088 10.9901 L 1.3644 11.2365 L 0.0 11.319 L -1.3644 11.2365 L -2.7088 10.9901 L -4.0138 10.5835 L -5.2602 10.0224 L -6.4299 9.3154 L -7.5059 8.4724 L -8.4724 7.5059 L -9.3154 6.4299 L -10.0224 5.2602 L -10.5835 4.0138 L -10.9901 2.7088 z M -4.0 -1.5 L -4.0 1.5 L -6.8343 1.5 L -6.6072 2.3119 L -6.3068 3.0372 L -5.9271 3.7242 L -5.4728 4.3644 L -4.9497 4.9497 L -4.3644 5.4728 L -3.7242 5.9271 L -3.0372 6.3068 L -2.3119 6.6072 L -1.5 6.8343 L -1.5 4.0 L 1.5 4.0 L 1.5 6.8343 L 2.3119 6.6072 L 3.0372 6.3068 L 3.7242 5.9271 L 4.3644 5.4728 L 4.9497 4.9497 L 5.4728 4.3644 L 5.9271 3.7242 L 6.3068 3.0372 L 6.6072 2.3119 L 6.8343 1.5 L 4.0 1.5 L 4.0 -1.5 L 6.8343 -1.5 L 6.6072 -2.3119 L 6.3068 -3.0372 L 5.9271 -3.7242 L 5.4728 -4.3644 L 4.9497 -4.9497 L 4.3644 -5.4728 L 3.7242 -5.9271 L 3.0372 -6.3068 L 2.3119 -6.6072 L 1.5 -6.8343 L 1.5 -4.0 L -1.5 -4.0 L -1.5 -6.8343 L -2.3119 -6.6072 L -3.0372 -6.3068 L -3.7242 -5.9271 L -4.3644 -5.4728 L -4.9497 -4.9497 L -5.4728 -4.3644 L -5.9271 -3.7242 L -6.3068 -3.0372 L -6.6072 -2.3119 L -6.8343 -1.5 z' transform='scale(3.7, -3.7) translate(11.919, 11.919)' />
</g>
</g>
<!--End Layer Data-->
<!--beginningOfControlSection='true' must be there or else the control boxes will be carved-->
<g id='controls' beginningOfControlSection='true' inkscape:groupmode='layer' inkscape:label='Controls'>
<!--id='isoControlBox' must be there so that the controls could be removed if necessary-->
<g id='isoControlBox' fill='#000' font-family='Arial' font-size='15px' font-weight='bold' transform='translate(20, 60)' visibility='hidden'>
<rect fill='silver' height='720' stroke='gray' stroke-width='4px' width='240' />
<circle id='globe' cx='120' cy='120' fill='gray' r='100' />
<circle id='cover' cx='120' cy='120' fill='silver' r='33' />
<circle id='observer' fill='darkslateblue' r='12' />
<text id='latitudeIso' x='20' y='260' >Latitude</text>
<text fill='darkslateblue' onclick='displayIsoByLatitude(globalLatitude-1.0)' x='198' y='260' >&lt;</text>
<text fill='darkslateblue' onclick='displayIsoByLatitude(globalLatitude+1.0)' x='213' y='260' >&gt;</text>
<text id='longitudeIso' x='20' y='280' >Longitude</text>
<text fill='darkslateblue' onclick='displayIsoByLongitude(globalLongitude-1.0)' x='198' y='280' >&lt;</text>
<text fill='darkslateblue' onclick='displayIsoByLongitude(globalLongitude+1.0)' x='213' y='280' >&gt;</text>
<text x='20' y='300' >Scale</text>
<text id='scaleIso' x='65' y='300' >1</text>
<text fill='darkslateblue' onclick='changeScaleIso(globalZoomScale/2)' x='198' y='300' >&lt;</text>
<text fill='darkslateblue' onclick='changeScaleIso(globalZoomScale*2)' x='213' y='300' >&gt;</text>
<g transform='translate(20, 340)'>
<text >Min</text>
<text id='minXIso' y='20' >X: -11.919 mm</text>
<text id='minYIso' y='40' >Y: -11.919 mm</text>
<text id='minZIso' y='60' >Z: -2.0 mm</text>
</g>
<g transform='translate(20, 440)'>
<text >Max</text>
<text id='maxXIso' y='20' >X: 11.919 mm</text>
<text id='maxYIso' y='40' >Y: 11.919 mm</text>
<text id='maxZIso' y='60' >Z: 10.0 mm</text>
</g>
<g transform='translate(20, 540)'>
<text >Dimension</text>
<text id='dimXIso' y='20' >X: 23.838 mm</text>
<text id='dimYIso' y='40' >Y: 23.838 mm</text>
<text id='dimZIso' y='60' >Z: 12.0 mm</text>
</g>
<g transform='translate(20, 640)'>
<text >Statistics</text>
<text id='layerHeightIso' y='20' >Layer Height: 0.4 mm</text>
<text id='numberOfLayersIso' y='40' >Number of Layers: 30</text>
<text id='volumeIso' y='60' >Volume: 1.8836 cm3</text>
</g>
</g>
<!--id='layerControlBox' must be there so that the controls could be removed if necessary-->
<g id='layerControlBox' fill='#000' font-family='Arial' font-size='15px' font-weight='bold' transform='translate(20, 60)' visibility='hidden'>
<rect fill='silver' height='720' stroke='gray' stroke-width='4px' width='240' />
<path d='M 66 164 h76 v6 l18 -9 l-18 -9 v6 h-70 v-70 h6 l-9 -18 l-9 18 h6 z' stroke-width='0' />
<!--<path stroke='#000' stroke-width='3' d='M 20 20 h5 l-5 -10 l-5 10 h5 v35 h35 v-5 l10 5 l-10 5 v-5 h-35 z'/>-->
<text text-anchor='middle' x='68' y='64' >Y</text>
<text x='165' y='166' >X</text>
<text id='minIndexLayer' x='20' y='245' >0</text>
<rect id='slider' fill='gray' height='24' width='170' x='32' y='230' />
<circle id='thumb' cx='42' cy='242' fill='darkslateblue' r='12' />
<text id='maxIndexLayer' x='203' y='245' >1</text>
<text id='currentIndexLayer' x='20' y='280' >Layer</text>
<text id='decreaseLayerIndex' fill='darkslateblue' onclick='displayLayer(globalLayerIndex-1)' x='198' y='280' >&lt;</text>
<text id='increaseLayerIndex' fill='darkslateblue' onclick='displayLayer(globalLayerIndex+1)' x='213' y='280' >&gt;</text>
<text x='20' y='300' >Scale</text>
<text id='scaleLayer' x='65' y='300' >1</text>
<text fill='darkslateblue' onclick='changeScaleLayer(globalZoomScale/2)' x='198' y='300' >&lt;</text>
<text fill='darkslateblue' onclick='changeScaleLayer(globalZoomScale*2)' x='213' y='300' >&gt;</text>
<g transform='translate(20, 340)'>
<text >Min</text>
<text id='minXLayer' y='20' >X: -11.919 mm</text>
<text id='minYLayer' y='40' >Y: -11.919 mm</text>
<text id='minZLayer' y='60' >Z: -2.0 mm</text>
</g>
<g transform='translate(20, 440)'>
<text >Max</text>
<text id='maxXLayer' y='20' >X: 11.919 mm</text>
<text id='maxYLayer' y='40' >Y: 11.919 mm</text>
<text id='maxZLayer' y='60' >Z: 10.0 mm</text>
</g>
<g transform='translate(20, 540)'>
<text >Dimension</text>
<text id='dimXLayer' y='20' >X: 23.838 mm</text>
<text id='dimYLayer' y='40' >Y: 23.838 mm</text>
<text id='dimZLayer' y='60' >Z: 12.0 mm</text>
</g>
<g transform='translate(20, 640)'>
<text >Statistics</text>
<text id='layerHeightLayer' y='20' >Layer Height: 0.4 mm</text>
<text id='numberOfLayersLayer' y='40' >Number of Layers: 30</text>
<text id='volumeLayer' y='60' >Volume: 1.8836 cm3</text>
</g>
</g>
<!--id='scrollControlBox' must be there so that the controls could be removed if necessary-->
<g id='scrollControlBox' fill='#000' font-family='Arial' font-size='15px' font-weight='bold' transform='translate(20, 60)' visibility='visible'>
<rect fill='silver' height='720' stroke='gray' stroke-width='4px' width='240' />
<path d='M 66 164 h76 v6 l18 -9 l-18 -9 v6 h-70 v-70 h6 l-9 -18 l-9 18 h6 z' stroke-width='0' />
<text text-anchor='middle' x='68' y='64' >Y</text>
<text x='165' y='166' >X</text>
<text x='20' y='300' >Scale</text>
<text id='scaleScroll' x='65' y='300' >: 1</text>
<text fill='darkslateblue' onclick='changeScaleScroll(globalZoomScale/2)' x='198' y='300' >&lt;</text>
<text fill='darkslateblue' onclick='changeScaleScroll(globalZoomScale*2)' x='213' y='300' >&gt;</text>
<g transform='translate(20, 340)'>
<text >Min</text>
<text id='minXScroll' y='20' >X: -11.919 mm</text>
<text id='minYScroll' y='40' >Y: -11.919 mm</text>
<text id='minZScroll' y='60' >Z: -2.0 mm</text>
</g>
<g transform='translate(20, 440)'>
<text >Max</text>
<text id='maxXScroll' y='20' >X: 11.919 mm</text>
<text id='maxYScroll' y='40' >Y: 11.919 mm</text>
<text id='maxZScroll' y='60' >Z: 10.0 mm</text>
</g>
<g transform='translate(20, 540)'>
<text >Dimension</text>
<text id='dimXScroll' y='20' >X: 23.838 mm</text>
<text id='dimYScroll' y='40' >Y: 23.838 mm</text>
<text id='dimZScroll' y='60' >Z: 12.0 mm</text>
</g>
<g transform='translate(20, 640)'>
<text >Statistics</text>
<text id='layerHeightScroll' y='20' >Layer Height: 0.4 mm</text>
<text id='numberOfLayersScroll' y='40' >Number of Layers: 30</text>
<text id='volumeScroll' y='60' >Volume: 1.8836 cm3</text>
</g>
</g>
<text id='isoViewButton' fill='darkslateblue' font-size='18px' font-weight='normal' onclick='viewIso()' text-anchor='middle' visibility='visible' x='80' y='30' >[Iso View]</text>
<text id='isoViewLabel' fill='darkslateblue' font-size='24px' font-weight='bold' text-anchor='middle' visibility='hidden' x='76' y='30' >Iso View</text>
<text id='layerViewButton' fill='darkslateblue' font-size='18px' font-weight='normal' onclick='viewLayer()' text-anchor='middle' visibility='visible' x='240' y='30' >[Layer View]</text>
<text id='layerViewLabel' fill='darkslateblue' font-size='24px' font-weight='bold' text-anchor='middle' visibility='hidden' x='236' y='30' >Layer View</text>
<text id='scrollViewButton' fill='darkslateblue' font-size='18px' font-weight='normal' onclick='viewScroll()' text-anchor='middle' visibility='hidden' x='400' y='30' >[Scroll View]</text>
<text id='scrollViewLabel' fill='darkslateblue' font-size='24px' font-weight='bold' text-anchor='middle' visibility='visible' x='396' y='30' >Scroll View</text>
</g>
</svg>
This source diff could not be displayed because it is too large. You can view the blob instead.
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg width="23.838" height="23.838" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:slic3r="http://slic3r.org/namespaces/slic3r">
<!--
Generated using Slic3r 0.7.2b
http://slic3r.org/
-->
<g id="layer0" slic3r:z="0.03" >
<polygon slic3r:type="contour" points="11.919,23.457361 10.528202,23.373235 9.157683,23.122118 7.827451,22.707599 6.556855,22.135663 5.364459,21.414874 4.267648,20.555585 3.282414,19.570351 2.423125,18.47354 1.702336,17.281144 1.1304,16.010548 0.715881,14.680316 0.464764,13.309797 0.380639,11.919 0.464764,10.528202 0.715881,9.157683 1.1304,7.827451 1.702336,6.556855 2.423125,5.364459 3.282414,4.267648 4.267648,3.282414 5.364459,2.423125 6.556855,1.702336 7.827451,1.1304 9.157683,0.715881 10.528202,0.464764 11.919,0.380639 13.309797,0.464764 14.680316,0.715881 16.010548,1.1304 17.281144,1.702336 18.47354,2.423125 19.570351,3.282414 20.555585,4.267648 21.414874,5.364459 22.135663,6.556855 22.707599,7.827451 23.122118,9.157683 23.373235,10.528202 23.457361,11.919 23.373235,13.309797 23.122118,14.680316 22.707599,16.010548 22.135663,17.281144 21.414874,18.47354 20.555585,19.570351 19.570351,20.555585 18.47354,21.414874 17.281144,22.135663 16.010548,22.707599 14.680316,23.122118 13.309797,23.373235" style="fill: white" />
<polygon slic3r:type="hole" points="10.769,19.167808 10.769,16.269 13.069,16.269 13.069,19.167808 13.554669,19.085248 14.346729,18.85706 15.108288,18.541616 15.829724,18.14289 16.501994,17.665893 17.116627,17.116627 17.665893,16.501994 18.14289,15.829724 18.541616,15.108288 18.85706,14.346729 19.085248,13.554669 19.167808,13.069 16.269,13.069 16.269,10.769 19.167808,10.769 19.085248,10.283331 18.85706,9.491271 18.541616,8.729712 18.14289,8.008276 17.665893,7.336006 17.116627,6.721373 16.501994,6.172107 15.829724,5.69511 15.108288,5.296384 14.346729,4.98094 13.554669,4.752752 13.069,4.670192 13.069,7.569 10.769,7.569 10.769,4.670192 10.283331,4.752752 9.491271,4.98094 8.729712,5.296384 8.008276,5.69511 7.336006,6.172107 6.721373,6.721373 6.172107,7.336006 5.69511,8.008276 5.296384,8.729712 4.98094,9.491271 4.752752,10.283331 4.670192,10.769 7.569,10.769 7.569,13.069 4.670192,13.069 4.752752,13.554669 4.98094,14.346729 5.296384,15.108288 5.69511,15.829724 6.172107,16.501994 6.721373,17.116627 7.336006,17.665893 8.008276,18.14289 8.729712,18.541616 9.491271,18.85706 10.283331,19.085248" style="fill: black" />
</g>
<g id="layer1" slic3r:z="0.11">
<polygon slic3r:type="contour" points="11.919,23.377361 10.537846,23.293818 9.176828,23.04444 7.855816,22.632796 6.594036,22.06483 5.409906,21.349036 4.320698,20.495704 3.342295,19.517301 2.488963,18.428093 1.773169,17.243963 1.205203,15.982183 0.793559,14.661171 0.544181,13.300153 0.460639,11.919 0.544181,10.537846 0.793559,9.176828 1.205203,7.855816 1.773169,6.594036 2.488963,5.409906 3.342295,4.320698 4.320698,3.342295 5.409906,2.488963 6.594036,1.773169 7.855816,1.205203 9.176828,0.793559 10.537846,0.544181 11.919,0.460639 13.300153,0.544181 14.661171,0.793559 15.982183,1.205203 17.243963,1.773169 18.428093,2.488963 19.517301,3.342295 20.495704,4.320698 21.349036,5.409906 22.06483,6.594036 22.632796,7.855816 23.04444,9.176828 23.293818,10.537846 23.377361,11.919 23.293818,13.300153 23.04444,14.661171 22.632796,15.982183 22.06483,17.243963 21.349036,18.428093 20.495704,19.517301 19.517301,20.495704 18.428093,21.349036 17.243963,22.06483 15.982183,22.632796 14.661171,23.04444 13.300153,23.293818" style="fill: white" />
<polygon slic3r:type="hole" points="10.769,19.167808 10.769,16.269 13.069,16.269 13.069,19.167808 13.554669,19.085248 14.346729,18.85706 15.108288,18.541616 15.829724,18.14289 16.501994,17.665893 17.116627,17.116627 17.665893,16.501994 18.14289,15.829724 18.541616,15.108288 18.85706,14.346729 19.085248,13.554669 19.167808,13.069 16.269,13.069 16.269,10.769 19.167808,10.769 19.085248,10.283331 18.85706,9.491271 18.541616,8.729712 18.14289,8.008276 17.665893,7.336006 17.116627,6.721373 16.501994,6.172107 15.829724,5.69511 15.108288,5.296384 14.346729,4.98094 13.554669,4.752752 13.069,4.670192 13.069,7.569 10.769,7.569 10.769,4.670192 10.283331,4.752752 9.491271,4.98094 8.729712,5.296384 8.008276,5.69511 7.336006,6.172107 6.721373,6.721373 6.172107,7.336006 5.69511,8.008276 5.296384,8.729712 4.98094,9.491271 4.752752,10.283331 4.670192,10.769 7.569,10.769 7.569,13.069 4.670192,13.069 4.752752,13.554669 4.98094,14.346729 5.296384,15.108288 5.69511,15.829724 6.172107,16.501994 6.721373,17.116627 7.336006,17.665893 8.008276,18.14289 8.729712,18.541616 9.491271,18.85706 10.283331,19.085248" style="fill: black" />
</g>
<g id="layer2" slic3r:z="0.21">
<polygon slic3r:type="contour" points="11.919,23.277361 10.549901,23.194547 9.200759,22.947343 7.891273,22.539291 6.640509,21.976288 5.466718,21.266742 4.387008,20.42085 3.417149,19.450991 2.571257,18.371281 1.861711,17.19749 1.298708,15.946726 0.890656,14.63724 0.643452,13.288098 0.560639,11.919 0.643452,10.549901 0.890656,9.200759 1.298708,7.891273 1.861711,6.640509 2.571257,5.466718 3.417149,4.387008 4.387008,3.417149 5.466718,2.571257 6.640509,1.861711 7.891273,1.298708 9.200759,0.890656 10.549901,0.643452 11.919,0.560639 13.288098,0.643452 14.63724,0.890656 15.946726,1.298708 17.19749,1.861711 18.371281,2.571257 19.450991,3.417149 20.42085,4.387008 21.266742,5.466718 21.976288,6.640509 22.539291,7.891273 22.947343,9.200759 23.194547,10.549901 23.277361,11.919 23.194547,13.288098 22.947343,14.63724 22.539291,15.946726 21.976288,17.19749 21.266742,18.371281 20.42085,19.450991 19.450991,20.42085 18.371281,21.266742 17.19749,21.976288 15.946726,22.539291 14.63724,22.947343 13.288098,23.194547" style="fill: white" />
<polygon slic3r:type="hole" points="10.769,19.167808 10.769,16.269 13.069,16.269 13.069,19.167808 13.554669,19.085248 14.346729,18.85706 15.108288,18.541616 15.829724,18.14289 16.501994,17.665893 17.116627,17.116627 17.665893,16.501994 18.14289,15.829724 18.541616,15.108288 18.85706,14.346729 19.085248,13.554669 19.167808,13.069 16.269,13.069 16.269,10.769 19.167808,10.769 19.085248,10.283331 18.85706,9.491271 18.541616,8.729712 18.14289,8.008276 17.665893,7.336006 17.116627,6.721373 16.501994,6.172107 15.829724,5.69511 15.108288,5.296384 14.346729,4.98094 13.554669,4.752752 13.069,4.670192 13.069,7.569 10.769,7.569 10.769,4.670192 10.283331,4.752752 9.491271,4.98094 8.729712,5.296384 8.008276,5.69511 7.336006,6.172107 6.721373,6.721373 6.172107,7.336006 5.69511,8.008276 5.296384,8.729712 4.98094,9.491271 4.752752,10.283331 4.670192,10.769 7.569,10.769 7.569,13.069 4.670192,13.069 4.752752,13.554669 4.98094,14.346729 5.296384,15.108288 5.69511,15.829724 6.172107,16.501994 6.721373,17.116627 7.336006,17.665893 8.008276,18.14289 8.729712,18.541616 9.491271,18.85706 10.283331,19.085248" style="fill: black" />
</g>
</svg>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment