added cookie_cutter extension
This commit is contained in:
parent
466df1e710
commit
1dd7a4e368
18
extensions/fablabchemnitz/cookie_cutter/README.md
Normal file
18
extensions/fablabchemnitz/cookie_cutter/README.md
Normal file
@ -0,0 +1,18 @@
|
||||
Convert SVG (text is not supported; convert text to paths) and HPGL to gcode for a 3-axis GCode machine,
|
||||
where the Z-axis controls the pen height.
|
||||
|
||||
You can also convert the same SVG subset to HPGL.
|
||||
|
||||
Run with no arguments for some help.
|
||||
|
||||
Note on multiple pen usage:
|
||||
|
||||
The pen definition file is one-pen per line, in the format:
|
||||
|
||||
n (x,y) svgcolor comment
|
||||
|
||||
Here, n is the pen number (pen 1 is assumed to be loaded at the start), (x,y) is the offset from the
|
||||
default pen position (note: gcodeplot.py will correct the offset and will NOT check for clipping at
|
||||
drawing edges--it is your responsibility to make sure your tool doesn't crash into anything due to
|
||||
offset), svgcolor is a color specification in svg format, e.g., rgb(255,255,00), #FFFF00 or yellow,
|
||||
and the comment is a human-readable comment.
|
15
extensions/fablabchemnitz/cookie_cutter/cookiecutter.inx
Normal file
15
extensions/fablabchemnitz/cookie_cutter/cookiecutter.inx
Normal file
@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
|
||||
<name>OpenSCAD Cookie Cutter Export</name>
|
||||
<id>fablabchemnitz.de.cookie_cutter</id>
|
||||
<output>
|
||||
<extension>.scad</extension>
|
||||
<mimetype>text/plain</mimetype>
|
||||
<filetypename>OpenSCAD cookie cutter file (*.scad)</filetypename>
|
||||
<filetypetooltip>Export an OpenSCAD cookie cutter file</filetypetooltip>
|
||||
<dataloss>true</dataloss>
|
||||
</output>
|
||||
<script>
|
||||
<command location="inx" interpreter="python">svg2cookiecutter.py</command>
|
||||
</script>
|
||||
</inkscape-extension>
|
21
extensions/fablabchemnitz/cookie_cutter/meta.json
Normal file
21
extensions/fablabchemnitz/cookie_cutter/meta.json
Normal file
@ -0,0 +1,21 @@
|
||||
[
|
||||
{
|
||||
"name": "OpenSCAD Cookie Cutter Export",
|
||||
"id": "fablabchemnitz.de.cookie_cutter",
|
||||
"path": "cookie_cutte",
|
||||
"dependent_extensions": null,
|
||||
"original_name": "OpenSCAD Cookie Cutter Export",
|
||||
"original_id": "mobi.omegacentauri.cookiecutter",
|
||||
"license": "MIT License",
|
||||
"license_url": "https://inkscape.org/~arpruss/%E2%98%85openscad-cookie-cutter-file-output",
|
||||
"comment": "",
|
||||
"source_url": "https://gitea.fablabchemnitz.de/FabLab_Chemnitz/mightyscape-1.2/src/branch/master/extensions/fablabchemnitz/cookie_cutter",
|
||||
"fork_url": "https://github.com/arpruss/gcodeplot",
|
||||
"documentation_url": "https://stadtfabrikanten.org/display/IFM/OpenSCAD+cookie+cutter+file+output",
|
||||
"inkscape_gallery_url": null,
|
||||
"main_authors": [
|
||||
"github.com/arpruss",
|
||||
"github.com/eridur-de"
|
||||
]
|
||||
}
|
||||
]
|
238
extensions/fablabchemnitz/cookie_cutter/svg2cookiecutter.py
Executable file
238
extensions/fablabchemnitz/cookie_cutter/svg2cookiecutter.py
Executable file
@ -0,0 +1,238 @@
|
||||
import sys
|
||||
import svgpath.parser as parser
|
||||
|
||||
# colors: RED = outer wall
|
||||
# GREEN = inner wall
|
||||
# BLACK = inside feature (not cutting all the way)
|
||||
|
||||
PRELIM = """// OpenSCAD file automatically generated by svg2cookiercutter.py
|
||||
// parameters tunable by user
|
||||
wallHeight = 12;
|
||||
minWallThickness = 2;
|
||||
maxWallThickness = 3;
|
||||
minInsideWallThickness = 1;
|
||||
maxInsideWallThickness = 3;
|
||||
|
||||
wallFlareWidth = 5;
|
||||
wallFlareThickness = 3;
|
||||
insideWallFlareWidth = 5;
|
||||
insideWallFlareThickness = 3;
|
||||
|
||||
featureHeight = 8;
|
||||
minFeatureThickness = 1;
|
||||
maxFeatureThickness = 3;
|
||||
|
||||
connectorThickness = 1.75;
|
||||
cuttingTaperHeight = 2.5;
|
||||
cuttingEdgeThickness = 1.25;
|
||||
// set to non-zero value to generate a demoulding plate
|
||||
demouldingPlateHeight = 0;
|
||||
demouldingPlateSlack = 1.5;
|
||||
|
||||
// sizing
|
||||
function clamp(t,minimum,maximum) = min(maximum,max(t,minimum));
|
||||
function featureThickness(t) = clamp(t,minFeatureThickness,maxFeatureThickness);
|
||||
function wallThickness(t) = clamp(t,minWallThickness,maxWallThickness);
|
||||
function insideWallThickness(t) = clamp(t,minInsideWallThickness,maxInsideWallThickness);
|
||||
|
||||
size = $OVERALL_SIZE$;
|
||||
scale = size/$OVERALL_SIZE$;
|
||||
|
||||
// helper modules: subshapes
|
||||
module ribbon(points, thickness=1) {
|
||||
union() {
|
||||
for (i=[1:len(points)-1]) {
|
||||
hull() {
|
||||
translate(points[i-1]) circle(d=thickness, $fn=8);
|
||||
translate(points[i]) circle(d=thickness, $fn=8);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
module wall(points,height,thickness) {
|
||||
module profile() {
|
||||
if (height>=cuttingTaperHeight && cuttingTaperHeight>0 && cuttingEdgeThickness<thickness) {
|
||||
cylinder(h=height-cuttingTaperHeight+0.001,d=thickness,$fn=8);
|
||||
translate([0,0,height-cuttingTaperHeight]) cylinder(h=cuttingTaperHeight,d1=thickness,d2=cuttingEdgeThickness);
|
||||
}
|
||||
else {
|
||||
cylinder(h=height,d=thickness,$fn=8);
|
||||
}
|
||||
}
|
||||
for (i=[1:len(points)-1]) {
|
||||
hull() {
|
||||
translate(points[i-1]) profile();
|
||||
translate(points[i]) profile();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
module outerFlare(path) {
|
||||
difference() {
|
||||
render(convexity=10) linear_extrude(height=wallFlareThickness) ribbon(path,thickness=wallFlareWidth);
|
||||
translate([0,0,-0.01]) linear_extrude(height=wallFlareThickness+0.02) polygon(points=path);
|
||||
}
|
||||
}
|
||||
|
||||
module innerFlare(path) {
|
||||
intersection() {
|
||||
render(convexity=10) linear_extrude(height=insideWallFlareThickness) ribbon(path,thickness=insideWallFlareWidth);
|
||||
translate([0,0,-0.01]) linear_extrude(height=insideWallFlareThickness+0.02) polygon(points=path);
|
||||
}
|
||||
}
|
||||
|
||||
module fill(path,height) {
|
||||
render(convexity=10) linear_extrude(height=height) polygon(points=path);
|
||||
}
|
||||
|
||||
"""
|
||||
|
||||
def isRed(rgb):
|
||||
return rgb is not None and rgb[0] >= 0.4 and rgb[1]+rgb[2] < rgb[0] * 0.25
|
||||
|
||||
def isGreen(rgb):
|
||||
return rgb is not None and rgb[1] >= 0.4 and rgb[0]+rgb[2] < rgb[1] * 0.25
|
||||
|
||||
def isBlack(rgb):
|
||||
return rgb is not None and rgb[0]+rgb[1]+rgb[2]<0.2
|
||||
|
||||
class Line(object):
|
||||
def __init__(self, pathName, points, fill, stroke, strokeWidth):
|
||||
self.pathName = pathName
|
||||
self.points = points
|
||||
self.fill = fill
|
||||
self.stroke = stroke
|
||||
self.strokeWidth = strokeWidth
|
||||
|
||||
def pathCode(self):
|
||||
return self.pathName + ' = scale * [' + ','.join(('[%.3f,%.3f]'%tuple(p) for p in self.points)) + '];'
|
||||
|
||||
def shapesCode(self):
|
||||
code = []
|
||||
if self.stroke:
|
||||
code.append('wall('+self.pathName+','+self.height+','+self.width+');')
|
||||
if self.hasOuterFlare:
|
||||
code.append(' outerFlare('+self.pathName+');')
|
||||
elif self.hasInnerFlare:
|
||||
code.append(' innerFlare('+self.pathName+');')
|
||||
if self.fill:
|
||||
code.append(' fill('+self.pathName+','+self.fillHeight+');')
|
||||
return '\n'.join(code) # + '\n'
|
||||
|
||||
class OuterWall(Line):
|
||||
def __init__(self, pathName, points, fill, stroke, strokeWidth):
|
||||
super(OuterWall, self).__init__(pathName, points, fill, stroke, strokeWidth)
|
||||
self.height = "wallHeight"
|
||||
self.width = "wallThickness(%.3f)" % self.strokeWidth
|
||||
self.fillHeight = "wallHeight"
|
||||
self.hasOuterFlare = True
|
||||
self.hasInnerFlare = False
|
||||
|
||||
class InnerWall(Line):
|
||||
def __init__(self, pathName, points, fill, stroke, strokeWidth):
|
||||
super(InnerWall, self).__init__(pathName, points, fill, stroke, strokeWidth)
|
||||
self.height = "wallHeight"
|
||||
self.width = "insideWallThickness(%.3f)" % self.strokeWidth
|
||||
self.fillHeight = "wallHeight"
|
||||
self.hasOuterFlare = False
|
||||
self.hasInnerFlare = True
|
||||
|
||||
class Feature(Line):
|
||||
def __init__(self, pathName, points, fill, stroke, strokeWidth):
|
||||
super(Feature, self).__init__(pathName, points, fill, stroke, strokeWidth)
|
||||
self.height = "featureHeight"
|
||||
self.width = "featureThickness(%.3f)" % self.strokeWidth
|
||||
self.fillHeight = "featureHeight"
|
||||
self.hasOuterFlare = False
|
||||
self.hasInnerFlare = False
|
||||
|
||||
class Connector(Line):
|
||||
def __init__(self, pathName, points, fill):
|
||||
super(Connector, self).__init__(pathName, points, fill, False, None) # no stroke for connectors, thus no use of self.height and self.width
|
||||
self.width = None
|
||||
self.fillHeight = "connectorThickness"
|
||||
self.hasOuterFlare = False
|
||||
self.hasInnerFlare = False
|
||||
|
||||
def svgToCookieCutter(filename, tolerance=0.1, strokeAll = False):
|
||||
lines = []
|
||||
pathCount = 0;
|
||||
minXY = [float("inf"), float("inf")]
|
||||
maxXY = [float("-inf"), float("-inf")]
|
||||
|
||||
for superpath in parser.getPathsFromSVGFile(filename)[0]:
|
||||
for path in superpath.breakup():
|
||||
pathName = '_'+str(pathCount)
|
||||
pathCount += 1
|
||||
fill = path.svgState.fill is not None
|
||||
stroke = strokeAll or path.svgState.stroke is not None
|
||||
if not stroke and not fill: continue
|
||||
|
||||
linearPath = path.linearApproximation(error=tolerance)
|
||||
points = [(-l.start.real,l.start.imag) for l in linearPath]
|
||||
points.append((-linearPath[-1].end.real, linearPath[-1].end.imag))
|
||||
|
||||
if isRed (path.svgState.fill) or isRed (path.svgState.stroke):
|
||||
line = OuterWall('outerWall'+pathName, points, fill, stroke, path.svgState.strokeWidth)
|
||||
elif isGreen(path.svgState.fill) or isGreen(path.svgState.stroke):
|
||||
line = InnerWall('innerWall'+pathName, points, fill, stroke, path.svgState.strokeWidth)
|
||||
elif isBlack(path.svgState.fill) or isBlack(path.svgState.stroke):
|
||||
line = Feature ('feature' +pathName, points, fill, stroke, path.svgState.strokeWidth)
|
||||
else:
|
||||
line = Connector('connector'+pathName, points, fill)
|
||||
|
||||
for i in range(2):
|
||||
minXY[i] = min(minXY[i], min(p[i] for p in line.points))
|
||||
maxXY[i] = max(maxXY[i], max(p[i] for p in line.points))
|
||||
lines.append(line)
|
||||
|
||||
size = max(maxXY[0]-minXY[0], maxXY[1]-minXY[1])
|
||||
|
||||
code = [PRELIM]
|
||||
code.append('// data from svg file')
|
||||
code += [line.pathCode()+'\n' for line in lines]
|
||||
code.append(
|
||||
'// main modules\n'
|
||||
'module cookieCutter() {')
|
||||
code += [' ' + line.shapesCode() for line in lines]
|
||||
code.append('}\n')
|
||||
|
||||
# demoulding plate module
|
||||
positives = [line for line in lines if isinstance(line, OuterWall) and line.stroke and not line.fill]
|
||||
negatives_stroke = [line for line in lines]
|
||||
negatives_fill = [line for line in lines if not isinstance(line, OuterWall) and line.fill]
|
||||
code.append(
|
||||
"module demouldingPlate(){\n"
|
||||
" // a plate to help push on the cookie to turn it out\n"
|
||||
" render(convexity=10) difference() {\n"
|
||||
" linear_extrude(height=demouldingPlateHeight) union() {")
|
||||
for line in positives:
|
||||
code.append(' polygon(points='+line.pathName+');')
|
||||
|
||||
code.append(" }\n"
|
||||
" translate([0,0,-0.01]) linear_extrude(height=demouldingPlateHeight+0.02) union() {")
|
||||
for line in negatives_stroke:
|
||||
code.append(' ribbon('+line.pathName+',thickness=demouldingPlateSlack'+('+'+line.width if line.stroke else '')+');')
|
||||
for line in negatives_fill:
|
||||
code.append(' polygon(points='+line.pathName+');')
|
||||
# TODO: we should remove the interior of polygonal inner walls
|
||||
code.append(' }\n }\n}\n')
|
||||
|
||||
code.append('////////////////////////////////////////////////////////////////////////////////')
|
||||
code.append('// final call, use main modules')
|
||||
code.append('translate([%.3f*scale + wallFlareWidth/2, %.3f*scale + wallFlareWidth/2,0])' % (-minXY[0],-minXY[1]))
|
||||
code.append(' cookieCutter();\n')
|
||||
|
||||
code.append('// translate([-40,15,0]) cylinder(h=wallHeight+10,d=5,$fn=20); // handle')
|
||||
code.append('if (demouldingPlateHeight>0)')
|
||||
code.append(' mirror([1,0,0])')
|
||||
code.append(' translate([%.3f*scale + wallFlareWidth/2, %.3f*scale + wallFlareWidth/2,0])' % (-minXY[0],-minXY[1]))
|
||||
code.append(' demouldingPlate();')
|
||||
|
||||
return '\n'.join(code).replace('$OVERALL_SIZE$', '%.3f' % size)
|
||||
|
||||
if __name__ == '__main__':
|
||||
print(svgToCookieCutter(sys.argv[1]))
|
@ -0,0 +1,2 @@
|
||||
from .path import Path, Line, Arc, CubicBezier, QuadraticBezier
|
||||
from .parser import parse_path
|
700
extensions/fablabchemnitz/cookie_cutter/svgpath/parser.py
Normal file
700
extensions/fablabchemnitz/cookie_cutter/svgpath/parser.py
Normal file
@ -0,0 +1,700 @@
|
||||
# SVG Path specification parser
|
||||
|
||||
import re
|
||||
from . import path
|
||||
import xml.etree.ElementTree as ET
|
||||
import re
|
||||
import math
|
||||
|
||||
COMMANDS = set('MmZzLlHhVvCcSsQqTtAa')
|
||||
UPPERCASE = set('MZLHVCSQTA')
|
||||
|
||||
COMMAND_RE = re.compile("([MmZzLlHhVvCcSsQqTtAa])")
|
||||
FLOAT_RE = re.compile("[-+]?[0-9]*\.?[0-9]+(?:[eE][-+]?[0-9]+)?")
|
||||
|
||||
SVG_COLORS = {
|
||||
"aliceblue": (0.941176,0.972549,1),
|
||||
"antiquewhite": (0.980392,0.921569,0.843137),
|
||||
"aqua": (0,1,1),
|
||||
"aquamarine": (0.498039,1,0.831373),
|
||||
"azure": (0.941176,1,1),
|
||||
"beige": (0.960784,0.960784,0.862745),
|
||||
"bisque": (1,0.894118,0.768627),
|
||||
"black": (0,0,0),
|
||||
"blanchedalmond": (1,0.921569,0.803922),
|
||||
"blue": (0,0,1),
|
||||
"blueviolet": (0.541176,0.168627,0.886275),
|
||||
"brown": (0.647059,0.164706,0.164706),
|
||||
"burlywood": (0.870588,0.721569,0.529412),
|
||||
"cadetblue": (0.372549,0.619608,0.627451),
|
||||
"chartreuse": (0.498039,1,0),
|
||||
"chocolate": (0.823529,0.411765,0.117647),
|
||||
"coral": (1,0.498039,0.313725),
|
||||
"cornflowerblue": (0.392157,0.584314,0.929412),
|
||||
"cornsilk": (1,0.972549,0.862745),
|
||||
"crimson": (0.862745,0.0784314,0.235294),
|
||||
"cyan": (0,1,1),
|
||||
"darkblue": (0,0,0.545098),
|
||||
"darkcyan": (0,0.545098,0.545098),
|
||||
"darkgoldenrod": (0.721569,0.52549,0.0431373),
|
||||
"darkgray": (0.662745,0.662745,0.662745),
|
||||
"darkgreen": (0,0.392157,0),
|
||||
"darkgrey": (0.662745,0.662745,0.662745),
|
||||
"darkkhaki": (0.741176,0.717647,0.419608),
|
||||
"darkmagenta": (0.545098,0,0.545098),
|
||||
"darkolivegreen": (0.333333,0.419608,0.184314),
|
||||
"darkorange": (1,0.54902,0),
|
||||
"darkorchid": (0.6,0.196078,0.8),
|
||||
"darkred": (0.545098,0,0),
|
||||
"darksalmon": (0.913725,0.588235,0.478431),
|
||||
"darkseagreen": (0.560784,0.737255,0.560784),
|
||||
"darkslateblue": (0.282353,0.239216,0.545098),
|
||||
"darkslategray": (0.184314,0.309804,0.309804),
|
||||
"darkslategrey": (0.184314,0.309804,0.309804),
|
||||
"darkturquoise": (0,0.807843,0.819608),
|
||||
"darkviolet": (0.580392,0,0.827451),
|
||||
"deeppink": (1,0.0784314,0.576471),
|
||||
"deepskyblue": (0,0.74902,1),
|
||||
"dimgray": (0.411765,0.411765,0.411765),
|
||||
"dimgrey": (0.411765,0.411765,0.411765),
|
||||
"dodgerblue": (0.117647,0.564706,1),
|
||||
"firebrick": (0.698039,0.133333,0.133333),
|
||||
"floralwhite": (1,0.980392,0.941176),
|
||||
"forestgreen": (0.133333,0.545098,0.133333),
|
||||
"fuchsia": (1,0,1),
|
||||
"gainsboro": (0.862745,0.862745,0.862745),
|
||||
"ghostwhite": (0.972549,0.972549,1),
|
||||
"gold": (1,0.843137,0),
|
||||
"goldenrod": (0.854902,0.647059,0.12549),
|
||||
"gray": (0.501961,0.501961,0.501961),
|
||||
"grey": (0.501961,0.501961,0.501961),
|
||||
"green": (0,0.501961,0),
|
||||
"greenyellow": (0.678431,1,0.184314),
|
||||
"honeydew": (0.941176,1,0.941176),
|
||||
"hotpink": (1,0.411765,0.705882),
|
||||
"indianred": (0.803922,0.360784,0.360784),
|
||||
"indigo": (0.294118,0,0.509804),
|
||||
"ivory": (1,1,0.941176),
|
||||
"khaki": (0.941176,0.901961,0.54902),
|
||||
"lavender": (0.901961,0.901961,0.980392),
|
||||
"lavenderblush": (1,0.941176,0.960784),
|
||||
"lawngreen": (0.486275,0.988235,0),
|
||||
"lemonchiffon": (1,0.980392,0.803922),
|
||||
"lightblue": (0.678431,0.847059,0.901961),
|
||||
"lightcoral": (0.941176,0.501961,0.501961),
|
||||
"lightcyan": (0.878431,1,1),
|
||||
"lightgoldenrodyellow": (0.980392,0.980392,0.823529),
|
||||
"lightgray": (0.827451,0.827451,0.827451),
|
||||
"lightgreen": (0.564706,0.933333,0.564706),
|
||||
"lightgrey": (0.827451,0.827451,0.827451),
|
||||
"lightpink": (1,0.713725,0.756863),
|
||||
"lightsalmon": (1,0.627451,0.478431),
|
||||
"lightseagreen": (0.12549,0.698039,0.666667),
|
||||
"lightskyblue": (0.529412,0.807843,0.980392),
|
||||
"lightslategray": (0.466667,0.533333,0.6),
|
||||
"lightslategrey": (0.466667,0.533333,0.6),
|
||||
"lightsteelblue": (0.690196,0.768627,0.870588),
|
||||
"lightyellow": (1,1,0.878431),
|
||||
"lime": (0,1,0),
|
||||
"limegreen": (0.196078,0.803922,0.196078),
|
||||
"linen": (0.980392,0.941176,0.901961),
|
||||
"magenta": (1,0,1),
|
||||
"maroon": (0.501961,0,0),
|
||||
"mediumaquamarine": (0.4,0.803922,0.666667),
|
||||
"mediumblue": (0,0,0.803922),
|
||||
"mediumorchid": (0.729412,0.333333,0.827451),
|
||||
"mediumpurple": (0.576471,0.439216,0.858824),
|
||||
"mediumseagreen": (0.235294,0.701961,0.443137),
|
||||
"mediumslateblue": (0.482353,0.407843,0.933333),
|
||||
"mediumspringgreen": (0,0.980392,0.603922),
|
||||
"mediumturquoise": (0.282353,0.819608,0.8),
|
||||
"mediumvioletred": (0.780392,0.0823529,0.521569),
|
||||
"midnightblue": (0.0980392,0.0980392,0.439216),
|
||||
"mintcream": (0.960784,1,0.980392),
|
||||
"mistyrose": (1,0.894118,0.882353),
|
||||
"moccasin": (1,0.894118,0.709804),
|
||||
"navajowhite": (1,0.870588,0.678431),
|
||||
"navy": (0,0,0.501961),
|
||||
"oldlace": (0.992157,0.960784,0.901961),
|
||||
"olive": (0.501961,0.501961,0),
|
||||
"olivedrab": (0.419608,0.556863,0.137255),
|
||||
"orange": (1,0.647059,0),
|
||||
"orangered": (1,0.270588,0),
|
||||
"orchid": (0.854902,0.439216,0.839216),
|
||||
"palegoldenrod": (0.933333,0.909804,0.666667),
|
||||
"palegreen": (0.596078,0.984314,0.596078),
|
||||
"paleturquoise": (0.686275,0.933333,0.933333),
|
||||
"palevioletred": (0.858824,0.439216,0.576471),
|
||||
"papayawhip": (1,0.937255,0.835294),
|
||||
"peachpuff": (1,0.854902,0.72549),
|
||||
"peru": (0.803922,0.521569,0.247059),
|
||||
"pink": (1,0.752941,0.796078),
|
||||
"plum": (0.866667,0.627451,0.866667),
|
||||
"powderblue": (0.690196,0.878431,0.901961),
|
||||
"purple": (0.501961,0,0.501961),
|
||||
"red": (1,0,0),
|
||||
"rosybrown": (0.737255,0.560784,0.560784),
|
||||
"royalblue": (0.254902,0.411765,0.882353),
|
||||
"saddlebrown": (0.545098,0.270588,0.0745098),
|
||||
"salmon": (0.980392,0.501961,0.447059),
|
||||
"sandybrown": (0.956863,0.643137,0.376471),
|
||||
"seagreen": (0.180392,0.545098,0.341176),
|
||||
"seashell": (1,0.960784,0.933333),
|
||||
"sienna": (0.627451,0.321569,0.176471),
|
||||
"silver": (0.752941,0.752941,0.752941),
|
||||
"skyblue": (0.529412,0.807843,0.921569),
|
||||
"slateblue": (0.415686,0.352941,0.803922),
|
||||
"slategray": (0.439216,0.501961,0.564706),
|
||||
"slategrey": (0.439216,0.501961,0.564706),
|
||||
"snow": (1,0.980392,0.980392),
|
||||
"springgreen": (0,1,0.498039),
|
||||
"steelblue": (0.27451,0.509804,0.705882),
|
||||
"tan": (0.823529,0.705882,0.54902),
|
||||
"teal": (0,0.501961,0.501961),
|
||||
"thistle": (0.847059,0.74902,0.847059),
|
||||
"tomato": (1,0.388235,0.278431),
|
||||
"turquoise": (0.25098,0.878431,0.815686),
|
||||
"violet": (0.933333,0.509804,0.933333),
|
||||
"wheat": (0.960784,0.870588,0.701961),
|
||||
"white": (1,1,1),
|
||||
"whitesmoke": (0.960784,0.960784,0.960784),
|
||||
"yellow": (1,1,0),
|
||||
"yellowgreen": (0.603922,0.803922,0.196078),
|
||||
}
|
||||
|
||||
def _tokenize_path(pathdef):
|
||||
for x in COMMAND_RE.split(pathdef):
|
||||
if x in COMMANDS:
|
||||
yield x
|
||||
for token in FLOAT_RE.findall(x):
|
||||
yield token
|
||||
|
||||
def applyMatrix(matrix, z):
|
||||
return complex(z.real * matrix[0] + z.imag * matrix[1] + matrix[2],
|
||||
z.real * matrix[3] + z.imag * matrix[4] + matrix[5] )
|
||||
|
||||
def matrixMultiply(matrix1, matrix2):
|
||||
if matrix1 is None:
|
||||
return matrix2
|
||||
elif matrix2 is None:
|
||||
return matrix1
|
||||
|
||||
m1 = [matrix1[0:3], matrix1[3:6] ] # don't need last row
|
||||
m2 = [matrix2[0:3], matrix2[3:6], [0,0,1]]
|
||||
|
||||
out = []
|
||||
|
||||
for i in range(2):
|
||||
for j in range(3):
|
||||
out.append( sum(m1[i][k]*m2[k][j] for k in range(3)) )
|
||||
|
||||
return out
|
||||
|
||||
def parse_path(pathdef, current_pos=0j, matrix = None, svgState=None):
|
||||
if matrix is None:
|
||||
scaler=lambda z : z
|
||||
else:
|
||||
scaler=lambda z : applyMatrix(matrix, z)
|
||||
if svgState is None:
|
||||
svgState = path.SVGState()
|
||||
|
||||
# In the SVG specs, initial movetos are absolute, even if
|
||||
# specified as 'm'. This is the default behavior here as well.
|
||||
# But if you pass in a current_pos variable, the initial moveto
|
||||
# will be relative to that current_pos. This is useful.
|
||||
elements = list(_tokenize_path(pathdef))
|
||||
# Reverse for easy use of .pop()
|
||||
elements.reverse()
|
||||
|
||||
segments = path.Path(svgState = svgState)
|
||||
start_pos = None
|
||||
command = None
|
||||
|
||||
while elements:
|
||||
|
||||
if elements[-1] in COMMANDS:
|
||||
# New command.
|
||||
last_command = command # Used by S and T
|
||||
command = elements.pop()
|
||||
absolute = command in UPPERCASE
|
||||
command = command.upper()
|
||||
else:
|
||||
# If this element starts with numbers, it is an implicit command
|
||||
# and we don't change the command. Check that it's allowed:
|
||||
if command is None:
|
||||
raise ValueError("Unallowed implicit command in %s, position %s" % (
|
||||
pathdef, len(pathdef.split()) - len(elements)))
|
||||
last_command = command # Used by S and T
|
||||
|
||||
if command == 'M':
|
||||
# Moveto command.
|
||||
x = elements.pop()
|
||||
y = elements.pop()
|
||||
pos = float(x) + float(y) * 1j
|
||||
if absolute:
|
||||
current_pos = pos
|
||||
else:
|
||||
current_pos += pos
|
||||
|
||||
# when M is called, reset start_pos
|
||||
# This behavior of Z is defined in svg spec:
|
||||
# http://www.w3.org/TR/SVG/paths.html#PathDataClosePathCommand
|
||||
start_pos = current_pos
|
||||
|
||||
# Implicit moveto commands are treated as lineto commands.
|
||||
# So we set command to lineto here, in case there are
|
||||
# further implicit commands after this moveto.
|
||||
command = 'L'
|
||||
|
||||
elif command == 'Z':
|
||||
# Close path
|
||||
if current_pos != start_pos:
|
||||
segments.append(path.Line(scaler(current_pos), scaler(start_pos)))
|
||||
if len(segments):
|
||||
segments.closed = True
|
||||
current_pos = start_pos
|
||||
start_pos = None
|
||||
command = None # You can't have implicit commands after closing.
|
||||
|
||||
elif command == 'L':
|
||||
x = elements.pop()
|
||||
y = elements.pop()
|
||||
pos = float(x) + float(y) * 1j
|
||||
if not absolute:
|
||||
pos += current_pos
|
||||
segments.append(path.Line(scaler(current_pos), scaler(pos)))
|
||||
current_pos = pos
|
||||
|
||||
elif command == 'H':
|
||||
x = elements.pop()
|
||||
pos = float(x) + current_pos.imag * 1j
|
||||
if not absolute:
|
||||
pos += current_pos.real
|
||||
segments.append(path.Line(scaler(current_pos), scaler(pos)))
|
||||
current_pos = pos
|
||||
|
||||
elif command == 'V':
|
||||
y = elements.pop()
|
||||
pos = current_pos.real + float(y) * 1j
|
||||
if not absolute:
|
||||
pos += current_pos.imag * 1j
|
||||
segments.append(path.Line(scaler(current_pos), scaler(pos)))
|
||||
current_pos = pos
|
||||
|
||||
elif command == 'C':
|
||||
control1 = float(elements.pop()) + float(elements.pop()) * 1j
|
||||
control2 = float(elements.pop()) + float(elements.pop()) * 1j
|
||||
end = float(elements.pop()) + float(elements.pop()) * 1j
|
||||
|
||||
if not absolute:
|
||||
control1 += current_pos
|
||||
control2 += current_pos
|
||||
end += current_pos
|
||||
|
||||
segments.append(path.CubicBezier(scaler(current_pos), scaler(control1), scaler(control2), scaler(end)))
|
||||
current_pos = end
|
||||
|
||||
elif command == 'S':
|
||||
# Smooth curve. First control point is the "reflection" of
|
||||
# the second control point in the previous path.
|
||||
|
||||
if last_command not in 'CS':
|
||||
# If there is no previous command or if the previous command
|
||||
# was not an C, c, S or s, assume the first control point is
|
||||
# coincident with the current point.
|
||||
control1 = scaler(current_pos)
|
||||
else:
|
||||
# The first control point is assumed to be the reflection of
|
||||
# the second control point on the previous command relative
|
||||
# to the current point.
|
||||
control1 = 2 * scaler(current_pos) - segments[-1].control2
|
||||
|
||||
control2 = float(elements.pop()) + float(elements.pop()) * 1j
|
||||
end = float(elements.pop()) + float(elements.pop()) * 1j
|
||||
|
||||
if not absolute:
|
||||
control2 += current_pos
|
||||
end += current_pos
|
||||
|
||||
segments.append(path.CubicBezier(scaler(current_pos), control1, scaler(control2), scaler(end)))
|
||||
current_pos = end
|
||||
|
||||
elif command == 'Q':
|
||||
control = float(elements.pop()) + float(elements.pop()) * 1j
|
||||
end = float(elements.pop()) + float(elements.pop()) * 1j
|
||||
|
||||
if not absolute:
|
||||
control += current_pos
|
||||
end += current_pos
|
||||
|
||||
segments.append(path.QuadraticBezier(scaler(current_pos), scaler(control), scaler(end)))
|
||||
current_pos = end
|
||||
|
||||
elif command == 'T':
|
||||
# Smooth curve. Control point is the "reflection" of
|
||||
# the second control point in the previous path.
|
||||
|
||||
if last_command not in 'QT':
|
||||
# If there is no previous command or if the previous command
|
||||
# was not an Q, q, T or t, assume the first control point is
|
||||
# coincident with the current point.
|
||||
control = scaler(current_pos)
|
||||
else:
|
||||
# The control point is assumed to be the reflection of
|
||||
# the control point on the previous command relative
|
||||
# to the current point.
|
||||
control = 2 * scaler(current_pos) - segments[-1].control
|
||||
|
||||
end = float(elements.pop()) + float(elements.pop()) * 1j
|
||||
|
||||
if not absolute:
|
||||
end += current_pos
|
||||
|
||||
segments.append(path.QuadraticBezier(scaler(current_pos), control, scaler(end)))
|
||||
current_pos = end
|
||||
|
||||
elif command == 'A':
|
||||
radius = float(elements.pop()) + float(elements.pop()) * 1j
|
||||
rotation = float(elements.pop())
|
||||
arc = float(elements.pop())
|
||||
sweep = float(elements.pop())
|
||||
end = float(elements.pop()) + float(elements.pop()) * 1j
|
||||
|
||||
if not absolute:
|
||||
end += current_pos
|
||||
|
||||
segments.append(path.Arc(current_pos, radius, rotation, arc, sweep, end, scaler))
|
||||
current_pos = end
|
||||
|
||||
return segments
|
||||
|
||||
def path_from_ellipse(x, y, rx, ry, matrix, state):
|
||||
arc = "M %.9f %.9f " % (x-rx,y)
|
||||
arc += "A %.9f %.9f 0 0 1 %.9f %.9f " % (rx, ry, x+rx,y)
|
||||
arc += "A %.9f %.9f 0 0 1 %.9f %.9f" % (rx, ry, x-rx,y)
|
||||
return parse_path(arc, matrix=matrix, svgState=state)
|
||||
|
||||
def path_from_rect(x,y,w,h,rx,ry, matrix,state):
|
||||
if not rx and not ry:
|
||||
rect = "M %.9f %.9f h %.9f v %.9f h %.9f Z" % (x,y,w,h,-w)
|
||||
else:
|
||||
if rx is None:
|
||||
rx = ry
|
||||
elif ry is None:
|
||||
ry = rx
|
||||
rect = "M %.9f %.9f h %.9f " % (x+rx,y,w-2*rx)
|
||||
rect += "a %.9f %.9f 0 0 1 %.9f %.9f " % (rx, ry, rx, ry)
|
||||
rect += "v %.9f " % (h-2*ry)
|
||||
rect += "a %.9f %.9f 0 0 1 %.9f %.9f " % (rx, ry, -rx, ry)
|
||||
rect += "h %.9f " % -(w-2*rx)
|
||||
rect += "a %.9f %.9f 0 0 1 %.9f %.9f " % (rx, ry, -rx, -ry)
|
||||
rect += "v %.9f " % -(h-2*ry)
|
||||
rect += "a %.9f %.9f 0 0 1 %.9f %.9f Z" % (rx, ry, rx, -ry)
|
||||
return parse_path(rect, matrix=matrix, svgState=state)
|
||||
|
||||
def sizeFromString(text):
|
||||
"""
|
||||
Returns size in mm, if possible.
|
||||
"""
|
||||
text = re.sub(r'\s',r'', text)
|
||||
try:
|
||||
return float(text)*25.4/96 # px
|
||||
except:
|
||||
if text[-1] == '%':
|
||||
return float(text[:-1]) # NOT mm
|
||||
units = text[-2:].lower()
|
||||
x = float(text[:-2])
|
||||
convert = { 'mm':1, 'cm':10, 'in':25.4, 'px':25.4/96, 'pt':25.4/72, 'pc':12*25.4/72 }
|
||||
try:
|
||||
return x * convert[units]
|
||||
except:
|
||||
return x # NOT mm
|
||||
|
||||
def rgbFromColor(colorName):
|
||||
colorName = colorName.strip().lower()
|
||||
if colorName == 'none':
|
||||
return None
|
||||
cmd = re.split(r'[\s(),]+', colorName)
|
||||
if cmd[0] == 'rgb':
|
||||
colors = cmd[1:4]
|
||||
outColor = []
|
||||
for c in colors:
|
||||
if c.endswith('%'):
|
||||
outColor.append(float(c[:-1]) / 100.)
|
||||
else:
|
||||
outColor.append(float(c) / 255.)
|
||||
return tuple(outColor)
|
||||
elif colorName.startswith('#'):
|
||||
if len(colorName) == 4:
|
||||
return (int(colorName[1],16)/15., int(colorName[2],16)/15., int(colorName[3],16)/15.)
|
||||
else:
|
||||
return (int(colorName[1:3],16)/255., int(colorName[3:5],16)/255., int(colorName[5:7],16)/255.)
|
||||
else:
|
||||
return SVG_COLORS[colorName]
|
||||
|
||||
|
||||
def getPathsFromSVG(svg):
|
||||
def updateStateCommand(state,cmd,arg):
|
||||
if cmd == 'fill':
|
||||
state.fill = rgbFromColor(arg)
|
||||
elif cmd == 'fill-opacity':
|
||||
state.fillOpacity = float(arg)
|
||||
elif cmd == 'fill-rule':
|
||||
state.fillRule = arg
|
||||
# if state.fill is None:
|
||||
# state.fill = (0.,0.,0.)
|
||||
elif cmd == 'stroke':
|
||||
state.stroke = rgbFromColor(arg)
|
||||
elif cmd == 'stroke-opacity':
|
||||
state.strokeOpacity = rgbFromColor(arg)
|
||||
elif cmd == 'stroke-width':
|
||||
state.strokeWidth = float(arg)
|
||||
elif cmd == 'vector-effect':
|
||||
state.strokeWidthScaling = 'non-scaling-stroke' not in cmd
|
||||
# todo better scaling for non-uniform cases?
|
||||
|
||||
def updateState(tree,state,matrix):
|
||||
state = state.clone()
|
||||
try:
|
||||
style = re.sub(r'\s',r'', tree.attrib['style']).lower()
|
||||
for item in style.split(';'):
|
||||
cmd,arg = item.split(':')[:2]
|
||||
updateStateCommand(state,cmd,arg)
|
||||
except:
|
||||
pass
|
||||
|
||||
for item in tree.attrib:
|
||||
try:
|
||||
updateStateCommand(state,item,tree.attrib[item])
|
||||
except:
|
||||
pass
|
||||
|
||||
if state.strokeWidth and state.strokeWidthScaling:
|
||||
# this won't work great for non-uniform scaling
|
||||
h = abs(applyMatrix(matrix, complex(0,state.strokeWidth)) - applyMatrix(matrix, 0j))
|
||||
w = abs(applyMatrix(matrix, complex(state.strokeWidth,0)) - applyMatrix(matrix, 0j))
|
||||
state.strokeWidth = (h+w)/2
|
||||
return state
|
||||
|
||||
def reorder(a,b,c,d,e,f):
|
||||
return [a,c,e, b,d,f]
|
||||
|
||||
def updateMatrix(tree, matrix):
|
||||
try:
|
||||
transformList = re.split(r'\)[\s,]+', tree.attrib['transform'].strip().lower())
|
||||
except KeyError:
|
||||
return matrix
|
||||
|
||||
for transform in transformList:
|
||||
cmd = re.split(r'[,()\s]+', transform)
|
||||
|
||||
updateMatrix = None
|
||||
|
||||
if cmd[0] == 'matrix':
|
||||
updateMatrix = reorder(*list(map(float, cmd[1:7])))
|
||||
elif cmd[0] == 'translate':
|
||||
x = float(cmd[1])
|
||||
if len(cmd) >= 3 and cmd[2] != '':
|
||||
y = float(cmd[2])
|
||||
else:
|
||||
y = 0
|
||||
updateMatrix = reorder(1,0,0,1,x,y)
|
||||
elif cmd[0] == 'scale':
|
||||
x = float(cmd[1])
|
||||
if len(cmd) >= 3 and cmd[2] != '':
|
||||
y = float(cmd[2])
|
||||
else:
|
||||
y = x
|
||||
updateMatrix = reorder(x,0,0, y,0,0)
|
||||
elif cmd[0] == 'rotate':
|
||||
theta = float(cmd[1]) * math.pi / 180.
|
||||
c = math.cos(theta)
|
||||
s = math.sin(theta)
|
||||
updateMatrix = [c, -s, 0, s, c, 0]
|
||||
if len(cmd) >= 4 and cmd[2] != '':
|
||||
x = float(cmd[2])
|
||||
y = float(cmd[3])
|
||||
updateMatrix = matrixMultiply(updateMatrix, [1,0,-x, 0,1,-y])
|
||||
updateMatrix = matrixMultiply([1,0,x, 0,1,y], updateMatrix)
|
||||
elif cmd[0] == 'skewX':
|
||||
theta = float(cmd[1]) * math.pi / 180.
|
||||
updateMatrix = [1, math.tan(theta), 0, 0,1,0]
|
||||
elif cmd[0] == 'skewY':
|
||||
theta = float(cmd[1]) * math.pi / 180.
|
||||
updateMatrix = [1,0,0, math.tan(theta),1,0]
|
||||
|
||||
matrix = matrixMultiply(matrix, updateMatrix)
|
||||
|
||||
return matrix
|
||||
|
||||
def updateStateAndMatrix(tree,state,matrix):
|
||||
matrix = updateMatrix(tree,matrix)
|
||||
return updateState(tree,state,matrix),matrix
|
||||
|
||||
def getPaths(paths, matrix, tree, state, savedElements):
|
||||
def getFloat(attribute,default=0.):
|
||||
try:
|
||||
return float(tree.attrib[attribute].strip())
|
||||
except KeyError:
|
||||
return default
|
||||
|
||||
tag = re.sub(r'.*}', '', tree.tag).lower()
|
||||
try:
|
||||
savedElements[tree.attrib['id']] = tree
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
state, matrix = updateStateAndMatrix(tree, state, matrix)
|
||||
if tag == 'path':
|
||||
path = parse_path(tree.attrib['d'], matrix=matrix, svgState=state)
|
||||
if len(path):
|
||||
paths.append(path)
|
||||
elif tag == 'circle':
|
||||
path = path_from_ellipse(getFloat('cx'), getFloat('cy'), getFloat('r'), getFloat('r'), matrix, state)
|
||||
paths.append(path)
|
||||
elif tag == 'ellipse':
|
||||
path = path_from_ellipse(getFloat('cx'), getFloat('cy'), getFloat('rx'), getFloat('ry'), matrix, state)
|
||||
paths.append(path)
|
||||
elif tag == 'line':
|
||||
x1 = getFloat('x1')
|
||||
y1 = getFloat('y1')
|
||||
x2 = getFloat('x2')
|
||||
y2 = getFloat('y2')
|
||||
p = 'M %.9f %.9f L %.9f %.9f' % (x1,y1,x2,y2)
|
||||
path = parse_path(p, matrix=matrix, svgState=state)
|
||||
paths.append(path)
|
||||
elif tag == 'polygon':
|
||||
points = re.split(r'[\s,]+', tree.attrib['points'].strip())
|
||||
p = ' '.join(['M', points[0], points[1], 'L'] + points[2:] + ['Z'])
|
||||
path = parse_path(p, matrix=matrix, svgState=state)
|
||||
paths.append(path)
|
||||
elif tag == 'polyline':
|
||||
points = re.split(r'[\s,]+', tree.attrib['points'].strip())
|
||||
p = ' '.join(['M', points[0], points[1], 'L'] + points[2:])
|
||||
path = parse_path(p, matrix=matrix, svgState=state)
|
||||
paths.append(path)
|
||||
elif tag == 'rect':
|
||||
x = getFloat('x')
|
||||
y = getFloat('y')
|
||||
w = getFloat('width')
|
||||
h = getFloat('height')
|
||||
rx = getFloat('rx',default=None)
|
||||
ry = getFloat('ry',default=None)
|
||||
path = path_from_rect(x,y,w,h,rx,ry, matrix,state)
|
||||
paths.append(path)
|
||||
elif tag == 'g' or tag == 'svg':
|
||||
for child in tree:
|
||||
getPaths(paths, matrix, child, state, savedElements)
|
||||
elif tag == 'use':
|
||||
try:
|
||||
link = None
|
||||
for tag in tree.attrib:
|
||||
if tag.strip().lower().endswith("}href"):
|
||||
link = tree.attrib[tag]
|
||||
break
|
||||
if link is None or link[0] != '#':
|
||||
raise KeyError
|
||||
source = savedElements[link[1:]]
|
||||
x = 0
|
||||
y = 0
|
||||
try:
|
||||
x = float(tree.attrib['x'])
|
||||
except:
|
||||
pass
|
||||
try:
|
||||
y = float(tree.attrib['y'])
|
||||
except:
|
||||
pass
|
||||
# TODO: handle width and height? (Inkscape does not)
|
||||
matrix = matrixMultiply(matrix, reorder(1,0,0,1,x,y))
|
||||
getPaths(paths, matrix, source, state, dict(savedElements))
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
def scale(width, height, viewBox, z):
|
||||
x = (z.real - viewBox[0]) / (viewBox[2] - viewBox[0]) * width
|
||||
y = (viewBox[3]-z.imag) / (viewBox[3] - viewBox[1]) * height
|
||||
return complex(x,y)
|
||||
|
||||
paths = []
|
||||
|
||||
try:
|
||||
width = sizeFromString(svg.attrib['width'].strip())
|
||||
except KeyError:
|
||||
width = None
|
||||
try:
|
||||
height = sizeFromString(svg.attrib['height'].strip())
|
||||
except KeyError:
|
||||
height = None
|
||||
|
||||
try:
|
||||
viewBox = list(map(float, re.split(r'[\s,]+', svg.attrib['viewBox'].strip())))
|
||||
except KeyError:
|
||||
if width is None or height is None:
|
||||
raise KeyError
|
||||
viewBox = [0, 0, width*96/25.4, height*96/25.4]
|
||||
|
||||
if width is None:
|
||||
width = viewBox[2] * 25.4/96
|
||||
|
||||
if height is None:
|
||||
height = viewBox[3] * 25.4/96
|
||||
|
||||
viewBoxWidth = viewBox[2]
|
||||
viewBoxHeight = viewBox[3]
|
||||
|
||||
viewBox[2] += viewBox[0]
|
||||
viewBox[3] += viewBox[1]
|
||||
|
||||
try:
|
||||
preserve = svg.attrib['preserveAspectRatio'].strip().lower().split()
|
||||
if len(preserve[0]) != 8:
|
||||
raise KeyError
|
||||
if len(preserve)>=2 and preserve[1] == 'slice':
|
||||
if viewBoxWidth/viewBoxHeight > width/height:
|
||||
# viewbox is wider than viewport, so scale by height to ensure
|
||||
# viewbox covers the viewport
|
||||
rescale = height / viewBoxHeight
|
||||
else:
|
||||
rescale = width / viewBoxWidth
|
||||
else:
|
||||
if viewBoxWidth/viewBoxHeight > width/height:
|
||||
# viewbox is wider than viewport, so scale by width to ensure
|
||||
# viewport covers the viewbox
|
||||
rescale = width / viewBoxWidth
|
||||
else:
|
||||
rescale = height / viewBoxHeight
|
||||
matrix = [rescale, 0, 0,
|
||||
0, rescale, 0];
|
||||
|
||||
if preserve[0][0:4] == 'xmin':
|
||||
# viewBox[0] to 0
|
||||
matrix[2] = -viewBox[0] * rescale
|
||||
elif preserve[0][0:4] == 'xmid':
|
||||
# viewBox[0] to width/2
|
||||
matrix[2] = -viewBox[0] * rescale + width/2
|
||||
else: # preserve[0][0:4] == 'xmax':
|
||||
# viewBox[0] to width
|
||||
matrix[2] = -viewBox[0] * rescale + width
|
||||
|
||||
if preserve[0][4:8] == 'ymin':
|
||||
# viewBox[1] to 0
|
||||
matrix[5] = -viewBox[1] * rescale
|
||||
elif preserve[0][4:8] == 'ymid':
|
||||
# viewBox[0] to width/2
|
||||
matrix[5] = -viewBox[1] * rescale + height/2
|
||||
else: # preserve[0][4:8] == 'xmax':
|
||||
# viewBox[0] to width
|
||||
matrix[5] = -viewBox[1] * rescale + height
|
||||
except:
|
||||
matrix = [ width/viewBoxWidth, 0, -viewBox[0]* width/viewBoxWidth,
|
||||
0, -height/viewBoxHeight, viewBox[3]*height/viewBoxHeight ]
|
||||
|
||||
getPaths(paths, matrix, svg, path.SVGState(), {})
|
||||
|
||||
return ( paths, applyMatrix(matrix, complex(viewBox[0], viewBox[1])),
|
||||
applyMatrix(matrix, complex(viewBox[2], viewBox[3])) )
|
||||
|
||||
def getPathsFromSVGFile(filename):
|
||||
return getPathsFromSVG(ET.parse(filename).getroot())
|
||||
|
646
extensions/fablabchemnitz/cookie_cutter/svgpath/path.py
Normal file
646
extensions/fablabchemnitz/cookie_cutter/svgpath/path.py
Normal file
@ -0,0 +1,646 @@
|
||||
from __future__ import division
|
||||
from math import sqrt, cos, sin, acos, degrees, radians, log
|
||||
from collections.abc import MutableSequence
|
||||
|
||||
# This file contains classes for the different types of SVG path segments as
|
||||
# well as a Path object that contains a sequence of path segments.
|
||||
|
||||
MIN_DEPTH = 5
|
||||
ERROR = 1e-12
|
||||
|
||||
def segment_length(curve, start, end, start_point, end_point, error, min_depth, depth):
|
||||
"""Recursively approximates the length by straight lines"""
|
||||
mid = (start + end) / 2
|
||||
mid_point = curve.point(mid)
|
||||
length = abs(end_point - start_point)
|
||||
first_half = abs(mid_point - start_point)
|
||||
second_half = abs(end_point - mid_point)
|
||||
|
||||
length2 = first_half + second_half
|
||||
if (length2 - length > error) or (depth < min_depth):
|
||||
# Calculate the length of each segment:
|
||||
depth += 1
|
||||
return (segment_length(curve, start, mid, start_point, mid_point,
|
||||
error, min_depth, depth) +
|
||||
segment_length(curve, mid, end, mid_point, end_point,
|
||||
error, min_depth, depth))
|
||||
# This is accurate enough.
|
||||
return length2
|
||||
|
||||
def approximate(path, start, end, start_point, end_point, max_error, depth, max_depth):
|
||||
if depth >= max_depth:
|
||||
return [start_point, end_point]
|
||||
actual_length = path.measure(start, end, error=max_error/4)
|
||||
linear_length = abs(end_point - start_point)
|
||||
# Worst case deviation given a fixed linear_length and actual_length would probably be
|
||||
# a symmetric tent shape (I haven't proved it -- TODO).
|
||||
deviationSquared = (actual_length/2)**2 - (linear_length/2)**2
|
||||
if deviationSquared <= max_error ** 2:
|
||||
return [start_point, end_point]
|
||||
else:
|
||||
mid = (start+end)/2.
|
||||
mid_point = path.point(mid)
|
||||
return ( approximate(path, start, mid, start_point, mid_point, max_error, depth+1, max_depth)[:-1] +
|
||||
approximate(path, mid, end, mid_point, end_point, max_error, depth+1, max_depth) )
|
||||
|
||||
def removeCollinear(points, error, pointsToKeep=set()):
|
||||
out = []
|
||||
|
||||
lengths = [0]
|
||||
|
||||
for i in range(1,len(points)):
|
||||
lengths.append(lengths[-1] + abs(points[i]-points[i-1]))
|
||||
|
||||
def length(a,b):
|
||||
return lengths[b] - lengths[a]
|
||||
|
||||
i = 0
|
||||
|
||||
while i < len(points):
|
||||
j = len(points) - 1
|
||||
while i < j:
|
||||
deviationSquared = (length(i, j)/2)**2 - (abs(points[j]-points[i])/2)**2
|
||||
if deviationSquared <= error ** 2 and set(range(i+1,j)).isdisjoint(pointsToKeep):
|
||||
out.append(points[i])
|
||||
i = j
|
||||
break
|
||||
j -= 1
|
||||
out.append(points[j])
|
||||
i += 1
|
||||
|
||||
return out
|
||||
|
||||
class Segment(object):
|
||||
def __init__(self, start, end):
|
||||
self.start = start
|
||||
self.end = end
|
||||
|
||||
def measure(self, start, end, error=ERROR, min_depth=MIN_DEPTH):
|
||||
return Path(self).measure(start, end, error=error, min_depth=min_depth)
|
||||
|
||||
def getApproximatePoints(self, error=0.001, max_depth=32):
|
||||
points = approximate(self, 0., 1., self.point(0.), self.point(1.), error, 0, max_depth)
|
||||
return points
|
||||
|
||||
class Line(Segment):
|
||||
def __init__(self, start, end):
|
||||
super(Line, self).__init__(start,end)
|
||||
|
||||
def __repr__(self):
|
||||
return 'Line(start=%s, end=%s)' % (self.start, self.end)
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, Line):
|
||||
return NotImplemented
|
||||
return self.start == other.start and self.end == other.end
|
||||
|
||||
def __ne__(self, other):
|
||||
if not isinstance(other, Line):
|
||||
return NotImplemented
|
||||
return not self == other
|
||||
|
||||
def getApproximatePoints(self, error=0.001, max_depth=32):
|
||||
return [self.start, self.end]
|
||||
|
||||
def point(self, pos):
|
||||
if pos == 0.:
|
||||
return self.start
|
||||
elif pos == 1.:
|
||||
return self.end
|
||||
distance = self.end - self.start
|
||||
return self.start + distance * pos
|
||||
|
||||
def length(self, error=None, min_depth=None):
|
||||
distance = (self.end - self.start)
|
||||
return sqrt(distance.real ** 2 + distance.imag ** 2)
|
||||
|
||||
|
||||
class CubicBezier(Segment):
|
||||
def __init__(self, start, control1, control2, end):
|
||||
super(CubicBezier, self).__init__(start,end)
|
||||
self.control1 = control1
|
||||
self.control2 = control2
|
||||
|
||||
def __repr__(self):
|
||||
return 'CubicBezier(start=%s, control1=%s, control2=%s, end=%s)' % (
|
||||
self.start, self.control1, self.control2, self.end)
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, CubicBezier):
|
||||
return NotImplemented
|
||||
return self.start == other.start and self.end == other.end and \
|
||||
self.control1 == other.control1 and self.control2 == other.control2
|
||||
|
||||
def __ne__(self, other):
|
||||
if not isinstance(other, CubicBezier):
|
||||
return NotImplemented
|
||||
return not self == other
|
||||
|
||||
def is_smooth_from(self, previous):
|
||||
"""Checks if this segment would be a smooth segment following the previous"""
|
||||
if isinstance(previous, CubicBezier):
|
||||
return (self.start == previous.end and
|
||||
(self.control1 - self.start) == (previous.end - previous.control2))
|
||||
else:
|
||||
return self.control1 == self.start
|
||||
|
||||
def point(self, pos):
|
||||
"""Calculate the x,y position at a certain position of the path"""
|
||||
if pos == 0.:
|
||||
return self.start
|
||||
elif pos == 1.:
|
||||
return self.end
|
||||
return ((1 - pos) ** 3 * self.start) + \
|
||||
(3 * (1 - pos) ** 2 * pos * self.control1) + \
|
||||
(3 * (1 - pos) * pos ** 2 * self.control2) + \
|
||||
(pos ** 3 * self.end)
|
||||
|
||||
def length(self, error=ERROR, min_depth=MIN_DEPTH):
|
||||
"""Calculate the length of the path up to a certain position"""
|
||||
start_point = self.point(0)
|
||||
end_point = self.point(1)
|
||||
return segment_length(self, 0, 1, start_point, end_point, error, min_depth, 0)
|
||||
|
||||
|
||||
class QuadraticBezier(Segment):
|
||||
def __init__(self, start, control, end):
|
||||
super(QuadraticBezier, self).__init__(start,end)
|
||||
self.control = control
|
||||
|
||||
def __repr__(self):
|
||||
return 'QuadraticBezier(start=%s, control=%s, end=%s)' % (
|
||||
self.start, self.control, self.end)
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, QuadraticBezier):
|
||||
return NotImplemented
|
||||
return self.start == other.start and self.end == other.end and \
|
||||
self.control == other.control
|
||||
|
||||
def __ne__(self, other):
|
||||
if not isinstance(other, QuadraticBezier):
|
||||
return NotImplemented
|
||||
return not self == other
|
||||
|
||||
def is_smooth_from(self, previous):
|
||||
"""Checks if this segment would be a smooth segment following the previous"""
|
||||
if isinstance(previous, QuadraticBezier):
|
||||
return (self.start == previous.end and
|
||||
(self.control - self.start) == (previous.end - previous.control))
|
||||
else:
|
||||
return self.control == self.start
|
||||
|
||||
def point(self, pos):
|
||||
if pos == 0.:
|
||||
return self.start
|
||||
elif pos == 1.:
|
||||
return self.end
|
||||
return (1 - pos) ** 2 * self.start + 2 * (1 - pos) * pos * self.control + \
|
||||
pos ** 2 * self.end
|
||||
|
||||
def length(self, error=None, min_depth=None):
|
||||
a = self.start - 2*self.control + self.end
|
||||
b = 2*(self.control - self.start)
|
||||
a_dot_b = a.real*b.real + a.imag*b.imag
|
||||
|
||||
if abs(a) < 1e-12:
|
||||
s = abs(b)
|
||||
elif abs(a_dot_b + abs(a)*abs(b)) < 1e-12:
|
||||
k = abs(b)/abs(a)
|
||||
if k >= 2:
|
||||
s = abs(b) - abs(a)
|
||||
else:
|
||||
s = abs(a)*(k**2/2 - k + 1)
|
||||
else:
|
||||
# For an explanation of this case, see
|
||||
# http://www.malczak.info/blog/quadratic-bezier-curve-length/
|
||||
A = 4 * (a.real ** 2 + a.imag ** 2)
|
||||
B = 4 * (a.real * b.real + a.imag * b.imag)
|
||||
C = b.real ** 2 + b.imag ** 2
|
||||
|
||||
Sabc = 2 * sqrt(A + B + C)
|
||||
A2 = sqrt(A)
|
||||
A32 = 2 * A * A2
|
||||
C2 = 2 * sqrt(C)
|
||||
BA = B / A2
|
||||
|
||||
s = (A32 * Sabc + A2 * B * (Sabc - C2) + (4 * C * A - B ** 2) *
|
||||
log((2 * A2 + BA + Sabc) / (BA + C2))) / (4 * A32)
|
||||
return s
|
||||
|
||||
class Arc(Segment):
|
||||
def __init__(self, start, radius, rotation, arc, sweep, end, scaler=lambda z:z):
|
||||
"""radius is complex, rotation is in degrees,
|
||||
large and sweep are 1 or 0 (True/False also work)"""
|
||||
|
||||
super(Arc, self).__init__(scaler(start),scaler(end))
|
||||
self.start0 = start
|
||||
self.end0 = end
|
||||
self.radius = radius
|
||||
self.rotation = rotation
|
||||
self.arc = bool(arc)
|
||||
self.sweep = bool(sweep)
|
||||
self.scaler = scaler
|
||||
|
||||
self._parameterize()
|
||||
|
||||
def __repr__(self):
|
||||
return 'Arc(start0=%s, radius=%s, rotation=%s, arc=%s, sweep=%s, end0=%s, scaler=%s)' % (
|
||||
self.start0, self.radius, self.rotation, self.arc, self.sweep, self.end0, self.scaler)
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, Arc):
|
||||
return NotImplemented
|
||||
return self.start == other.start and self.end == other.end and \
|
||||
self.radius == other.radius and self.rotation == other.rotation and \
|
||||
self.arc == other.arc and self.sweep == other.sweep
|
||||
|
||||
def __ne__(self, other):
|
||||
if not isinstance(other, Arc):
|
||||
return NotImplemented
|
||||
return not self == other
|
||||
|
||||
def _parameterize(self):
|
||||
# Conversion from endpoint to center parameterization
|
||||
# http://www.w3.org/TR/SVG/implnote.html#ArcImplementationNotes
|
||||
|
||||
cosr = cos(radians(self.rotation))
|
||||
sinr = sin(radians(self.rotation))
|
||||
dx = (self.start0.real - self.end0.real) / 2
|
||||
dy = (self.start0.imag - self.end0.imag) / 2
|
||||
x1prim = cosr * dx + sinr * dy
|
||||
x1prim_sq = x1prim * x1prim
|
||||
y1prim = -sinr * dx + cosr * dy
|
||||
y1prim_sq = y1prim * y1prim
|
||||
|
||||
rx = self.radius.real
|
||||
rx_sq = rx * rx
|
||||
ry = self.radius.imag
|
||||
ry_sq = ry * ry
|
||||
|
||||
# Correct out of range radii
|
||||
radius_check = (x1prim_sq / rx_sq) + (y1prim_sq / ry_sq)
|
||||
if radius_check > 1:
|
||||
rx *= sqrt(radius_check)
|
||||
ry *= sqrt(radius_check)
|
||||
rx_sq = rx * rx
|
||||
ry_sq = ry * ry
|
||||
|
||||
t1 = rx_sq * y1prim_sq
|
||||
t2 = ry_sq * x1prim_sq
|
||||
c = sqrt(abs((rx_sq * ry_sq - t1 - t2) / (t1 + t2)))
|
||||
|
||||
if self.arc == self.sweep:
|
||||
c = -c
|
||||
cxprim = c * rx * y1prim / ry
|
||||
cyprim = -c * ry * x1prim / rx
|
||||
|
||||
self.center = complex((cosr * cxprim - sinr * cyprim) +
|
||||
((self.start0.real + self.end0.real) / 2),
|
||||
(sinr * cxprim + cosr * cyprim) +
|
||||
((self.start0.imag + self.end0.imag) / 2))
|
||||
|
||||
ux = (x1prim - cxprim) / rx
|
||||
uy = (y1prim - cyprim) / ry
|
||||
vx = (-x1prim - cxprim) / rx
|
||||
vy = (-y1prim - cyprim) / ry
|
||||
n = sqrt(ux * ux + uy * uy)
|
||||
p = ux
|
||||
theta = degrees(acos(p / n))
|
||||
if uy < 0:
|
||||
theta = -theta
|
||||
self.theta = theta % 360
|
||||
|
||||
n = sqrt((ux * ux + uy * uy) * (vx * vx + vy * vy))
|
||||
p = ux * vx + uy * vy
|
||||
d = p/n
|
||||
# In certain cases the above calculation can through inaccuracies
|
||||
# become just slightly out of range, f ex -1.0000000000000002.
|
||||
if d > 1.0:
|
||||
d = 1.0
|
||||
elif d < -1.0:
|
||||
d = -1.0
|
||||
delta = degrees(acos(d))
|
||||
if (ux * vy - uy * vx) < 0:
|
||||
delta = -delta
|
||||
self.delta = delta % 360
|
||||
if not self.sweep:
|
||||
self.delta -= 360
|
||||
|
||||
def point(self, pos):
|
||||
if pos == 0.:
|
||||
return self.start
|
||||
elif pos == 1.:
|
||||
return self.end
|
||||
angle = radians(self.theta + (self.delta * pos))
|
||||
cosr = cos(radians(self.rotation))
|
||||
sinr = sin(radians(self.rotation))
|
||||
|
||||
x = (cosr * cos(angle) * self.radius.real - sinr * sin(angle) *
|
||||
self.radius.imag + self.center.real)
|
||||
y = (sinr * cos(angle) * self.radius.real + cosr * sin(angle) *
|
||||
self.radius.imag + self.center.imag)
|
||||
return self.scaler(complex(x, y))
|
||||
|
||||
def length(self, error=ERROR, min_depth=MIN_DEPTH):
|
||||
"""The length of an elliptical arc segment requires numerical
|
||||
integration, and in that case it's simpler to just do a geometric
|
||||
approximation, as for cubic bezier curves.
|
||||
"""
|
||||
start_point = self.point(0)
|
||||
end_point = self.point(1)
|
||||
return segment_length(self, 0, 1, start_point, end_point, error, min_depth, 0)
|
||||
|
||||
class SVGState(object):
|
||||
def __init__(self, fill=(0.,0.,0.), fillOpacity=None, fillRule='nonzero', stroke=None, strokeOpacity=None, strokeWidth=0.1, strokeWidthScaling=True):
|
||||
self.fill = fill
|
||||
self.fillOpacity = fillOpacity
|
||||
self.fillRule = fillRule
|
||||
self.stroke = stroke
|
||||
self.strokeOpacity = strokeOpacity
|
||||
self.strokeWidth = strokeWidth
|
||||
self.strokeWidthScaling = strokeWidthScaling
|
||||
|
||||
def clone(self):
|
||||
return SVGState(fill=self.fill, fillOpacity=self.fillOpacity, fillRule=self.fillRule, stroke=self.stroke, strokeOpacity=self.strokeOpacity,
|
||||
strokeWidth=self.strokeWidth, strokeWidthScaling=self.strokeWidthScaling)
|
||||
|
||||
class Path(MutableSequence):
|
||||
"""A Path is a sequence of path segments"""
|
||||
|
||||
# Put it here, so there is a default if unpickled.
|
||||
_closed = False
|
||||
|
||||
def __init__(self, *segments, **kw):
|
||||
self._segments = list(segments)
|
||||
self._length = None
|
||||
self._lengths = None
|
||||
if 'closed' in kw:
|
||||
self.closed = kw['closed']
|
||||
if 'svgState' in kw:
|
||||
self.svgState = kw['svgState']
|
||||
else:
|
||||
self.svgState = SVGState()
|
||||
|
||||
def __getitem__(self, index):
|
||||
return self._segments[index]
|
||||
|
||||
def __setitem__(self, index, value):
|
||||
self._segments[index] = value
|
||||
self._length = None
|
||||
|
||||
def __delitem__(self, index):
|
||||
del self._segments[index]
|
||||
self._length = None
|
||||
|
||||
def insert(self, index, value):
|
||||
self._segments.insert(index, value)
|
||||
self._length = None
|
||||
|
||||
def reverse(self):
|
||||
# Reversing the order of a path would require reversing each element
|
||||
# as well. That's not implemented.
|
||||
raise NotImplementedError
|
||||
|
||||
def __len__(self):
|
||||
return len(self._segments)
|
||||
|
||||
def __repr__(self):
|
||||
return 'Path(%s, closed=%s)' % (
|
||||
', '.join(repr(x) for x in self._segments), self.closed)
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, Path):
|
||||
return NotImplemented
|
||||
if len(self) != len(other):
|
||||
return False
|
||||
for s, o in zip(self._segments, other._segments):
|
||||
if not s == o:
|
||||
return False
|
||||
return True
|
||||
|
||||
def __ne__(self, other):
|
||||
if not isinstance(other, Path):
|
||||
return NotImplemented
|
||||
return not self == other
|
||||
|
||||
def _calc_lengths(self, error=ERROR, min_depth=MIN_DEPTH):
|
||||
## TODO: check if error has decreased since last calculation
|
||||
if self._length is not None:
|
||||
return
|
||||
|
||||
lengths = [each.length(error=error, min_depth=min_depth) for each in self._segments]
|
||||
self._length = sum(lengths)
|
||||
self._lengths = [each / (1 if self._length==0. else self._length) for each in lengths]
|
||||
|
||||
def point(self, pos, error=ERROR):
|
||||
# Shortcuts
|
||||
if pos == 0.0:
|
||||
return self._segments[0].point(pos)
|
||||
if pos == 1.0:
|
||||
return self._segments[-1].point(pos)
|
||||
|
||||
self._calc_lengths(error=error)
|
||||
# Find which segment the point we search for is located on:
|
||||
segment_start = 0
|
||||
for index, segment in enumerate(self._segments):
|
||||
segment_end = segment_start + self._lengths[index]
|
||||
if segment_end >= pos:
|
||||
# This is the segment! How far in on the segment is the point?
|
||||
segment_pos = (pos - segment_start) / (segment_end - segment_start)
|
||||
break
|
||||
segment_start = segment_end
|
||||
|
||||
return segment.point(segment_pos)
|
||||
|
||||
def length(self, error=ERROR, min_depth=MIN_DEPTH):
|
||||
self._calc_lengths(error, min_depth)
|
||||
return self._length
|
||||
|
||||
def measure(self, start, end, error=ERROR, min_depth=MIN_DEPTH):
|
||||
self._calc_lengths(error=error)
|
||||
if start == 0.0 and end == 1.0:
|
||||
return self.length()
|
||||
length = 0
|
||||
segment_start = 0
|
||||
for index, segment in enumerate(self._segments):
|
||||
if end <= segment_start:
|
||||
break
|
||||
segment_end = segment_start + self._lengths[index]
|
||||
if start < segment_end:
|
||||
# this segment intersects the part of the path we want
|
||||
if start <= segment_start and segment_end <= end:
|
||||
# whole segment is contained in the part of the path
|
||||
length += self._lengths[index] * self._length
|
||||
else:
|
||||
if start <= segment_start:
|
||||
start_in_segment = 0.
|
||||
else:
|
||||
start_in_segment = (start-segment_start)/(segment_end-segment_start)
|
||||
if segment_end <= end:
|
||||
end_in_segment = 1.
|
||||
else:
|
||||
end_in_segment = (end-segment_start)/(segment_end-segment_start)
|
||||
segment = self._segments[index]
|
||||
length += segment_length(segment, start_in_segment, end_in_segment, segment.point(start_in_segment),
|
||||
segment.point(end_in_segment), error, MIN_DEPTH, 0)
|
||||
segment_start = segment_end
|
||||
return length
|
||||
|
||||
def _is_closable(self):
|
||||
"""Returns true if the end is on the start of a segment"""
|
||||
try:
|
||||
end = self[-1].end
|
||||
except:
|
||||
return False
|
||||
for segment in self:
|
||||
if segment.start == end:
|
||||
return True
|
||||
return False
|
||||
|
||||
def breakup(self):
|
||||
paths = []
|
||||
prevEnd = None
|
||||
segments = []
|
||||
for segment in self._segments:
|
||||
if prevEnd is None or segment.point(0.) == prevEnd:
|
||||
segments.append(segment)
|
||||
else:
|
||||
paths.append(Path(*segments, svgState=self.svgState))
|
||||
segments = [segment]
|
||||
prevEnd = segment.point(1.)
|
||||
|
||||
if len(segments) > 0:
|
||||
paths.append(Path(*segments, svgState=self.svgState))
|
||||
|
||||
return paths
|
||||
|
||||
def linearApproximation(self, error=0.001, max_depth=32):
|
||||
closed = False
|
||||
keepSegmentIndex = 0
|
||||
if self.closed:
|
||||
end = self[-1].end
|
||||
for i,segment in enumerate(self):
|
||||
if segment.start == end:
|
||||
keepSegmentIndex = i
|
||||
closed = True
|
||||
break
|
||||
|
||||
keepSubpathIndex = 0
|
||||
keepPointIndex = 0
|
||||
|
||||
subpaths = []
|
||||
subpath = []
|
||||
prevEnd = None
|
||||
for i,segment in enumerate(self._segments):
|
||||
if prevEnd is None or segment.start == prevEnd:
|
||||
if i == keepSegmentIndex:
|
||||
keepSubpathIndex = len(subpaths)
|
||||
keepPointIndex = len(subpath)
|
||||
else:
|
||||
subpaths.append(subpath)
|
||||
subpath = []
|
||||
subpath += segment.getApproximatePoints(error=error/2., max_depth=max_depth)
|
||||
prevEnd = segment.end
|
||||
|
||||
if len(subpath) > 0:
|
||||
subpaths.append(subpath)
|
||||
|
||||
linearPath = Path(svgState=self.svgState)
|
||||
|
||||
for i,subpath in enumerate(subpaths):
|
||||
keep = set((keepPointIndex,)) if i == keepSubpathIndex else set()
|
||||
special = None
|
||||
if i == keepSubpathIndex:
|
||||
special = subpath[keepPointIndex]
|
||||
points = removeCollinear(subpath, error=error/2., pointsToKeep=keep)
|
||||
# points = subpath
|
||||
|
||||
for j in range(len(points)-1):
|
||||
linearPath.append(Line(points[j], points[j+1]))
|
||||
|
||||
linearPath.closed = self.closed and linearPath._is_closable()
|
||||
linearPath.svgState = self.svgState
|
||||
|
||||
return linearPath
|
||||
|
||||
def getApproximateLines(self, error=0.001, max_depth=32):
|
||||
lines = []
|
||||
for subpath in self.breakup():
|
||||
points = subpath.getApproximatePoints(error=error, max_depth=max_depth)
|
||||
for i in range(len(points)-1):
|
||||
lines.append(points[i],points[i+1])
|
||||
return lines
|
||||
|
||||
@property
|
||||
def closed(self):
|
||||
"""Checks that the path is closed"""
|
||||
return self._closed and self._is_closable()
|
||||
|
||||
@closed.setter
|
||||
def closed(self, value):
|
||||
value = bool(value)
|
||||
if value and not self._is_closable():
|
||||
raise ValueError("End does not coincide with a segment start.")
|
||||
self._closed = value
|
||||
|
||||
def d(self):
|
||||
if self.closed:
|
||||
segments = self[:-1]
|
||||
else:
|
||||
segments = self[:]
|
||||
|
||||
current_pos = None
|
||||
parts = []
|
||||
previous_segment = None
|
||||
end = self[-1].end
|
||||
|
||||
for segment in segments:
|
||||
start = segment.start
|
||||
# If the start of this segment does not coincide with the end of
|
||||
# the last segment or if this segment is actually the close point
|
||||
# of a closed path, then we should start a new subpath here.
|
||||
if current_pos != start or (self.closed and start == end):
|
||||
parts.append('M {0:G},{1:G}'.format(start.real, start.imag))
|
||||
|
||||
if isinstance(segment, Line):
|
||||
parts.append('L {0:G},{1:G}'.format(
|
||||
segment.end.real, segment.end.imag)
|
||||
)
|
||||
elif isinstance(segment, CubicBezier):
|
||||
if segment.is_smooth_from(previous_segment):
|
||||
parts.append('S {0:G},{1:G} {2:G},{3:G}'.format(
|
||||
segment.control2.real, segment.control2.imag,
|
||||
segment.end.real, segment.end.imag)
|
||||
)
|
||||
else:
|
||||
parts.append('C {0:G},{1:G} {2:G},{3:G} {4:G},{5:G}'.format(
|
||||
segment.control1.real, segment.control1.imag,
|
||||
segment.control2.real, segment.control2.imag,
|
||||
segment.end.real, segment.end.imag)
|
||||
)
|
||||
elif isinstance(segment, QuadraticBezier):
|
||||
if segment.is_smooth_from(previous_segment):
|
||||
parts.append('T {0:G},{1:G}'.format(
|
||||
segment.end.real, segment.end.imag)
|
||||
)
|
||||
else:
|
||||
parts.append('Q {0:G},{1:G} {2:G},{3:G}'.format(
|
||||
segment.control.real, segment.control.imag,
|
||||
segment.end.real, segment.end.imag)
|
||||
)
|
||||
|
||||
elif isinstance(segment, Arc):
|
||||
parts.append('A {0:G},{1:G} {2:G} {3:d},{4:d} {5:G},{6:G}'.format(
|
||||
segment.radius.real, segment.radius.imag, segment.rotation,
|
||||
int(segment.arc), int(segment.sweep),
|
||||
segment.end.real, segment.end.imag)
|
||||
)
|
||||
current_pos = segment.end
|
||||
previous_segment = segment
|
||||
|
||||
if self.closed:
|
||||
parts.append('Z')
|
||||
|
||||
return ' '.join(parts)
|
||||
|
136
extensions/fablabchemnitz/cookie_cutter/svgpath/shader.py
Executable file
136
extensions/fablabchemnitz/cookie_cutter/svgpath/shader.py
Executable file
@ -0,0 +1,136 @@
|
||||
import math
|
||||
from operator import itemgetter
|
||||
|
||||
class Shader(object):
|
||||
MODE_EVEN_ODD = 0
|
||||
MODE_NONZERO = 1
|
||||
|
||||
def __init__(self, unshadedThreshold=1., lightestSpacing=3., darkestSpacing=0.5, angle=45, crossHatch=False):
|
||||
self.unshadedThreshold = unshadedThreshold
|
||||
self.lightestSpacing = lightestSpacing
|
||||
self.darkestSpacing = darkestSpacing
|
||||
self.angle = angle
|
||||
self.secondaryAngle = angle + 90
|
||||
self.crossHatch = False
|
||||
|
||||
def isActive(self):
|
||||
return self.unshadedThreshold > 0.000001
|
||||
|
||||
def setDrawingDirectionAngle(self, drawingDirectionAngle):
|
||||
self.drawingDirectionAngle = drawingDirectionAngle
|
||||
|
||||
if drawingDirectionAngle is None:
|
||||
return
|
||||
|
||||
if 90 < (self.angle - drawingDirectionAngle) % 360 < 270:
|
||||
self.angle = (self.angle + 180) % 360
|
||||
if 90 < (self.secondaryAngle - drawingDirectionAngle) % 360 < 270:
|
||||
self.secondaryAngle = (self.secondaryAngle + 180) % 360
|
||||
|
||||
def shade(self, polygon, grayscale, avoidOutline=True, mode=None):
|
||||
if mode is None:
|
||||
mode = Shader.MODE_EVEN_ODD
|
||||
if grayscale >= self.unshadedThreshold:
|
||||
return []
|
||||
intensity = (self.unshadedThreshold-grayscale) / float(self.unshadedThreshold)
|
||||
spacing = self.lightestSpacing * (1-intensity) + self.darkestSpacing * intensity
|
||||
lines = Shader.shadePolygon(polygon, self.angle, spacing, avoidOutline=avoidOutline, mode=mode, alternate=(self.drawingDirectionAngle is None))
|
||||
if self.crossHatch:
|
||||
lines += Shader.shadePolygon(polygon, self.angle+90, spacing, avoidOutline=avoidOutline, mode=mode, alternate=(self.drawingDirectionAngle is None))
|
||||
return lines
|
||||
|
||||
@staticmethod
|
||||
def shadePolygon(polygon, angleDegrees, spacing, avoidOutline=True, mode=None, alternate=True):
|
||||
if mode is None:
|
||||
mode = Shader.MODE_EVEN_ODD
|
||||
|
||||
rotate = complex(math.cos(angleDegrees * math.pi / 180.), math.sin(angleDegrees * math.pi / 180.))
|
||||
|
||||
polygon = [(line[0] / rotate,line[1] / rotate) for line in polygon]
|
||||
|
||||
spacing = float(spacing)
|
||||
|
||||
toAvoid = list(set(line[0].imag for line in polygon)|set(line[1].imag for line in polygon))
|
||||
|
||||
if len(toAvoid) <= 1:
|
||||
deltaY = (toAvoid[0]-spacing/2.) % spacing
|
||||
else:
|
||||
# find largest interval
|
||||
toAvoid.sort()
|
||||
largestIndex = 0
|
||||
largestLen = 0
|
||||
for i in range(len(toAvoid)):
|
||||
l = ( toAvoid[i] - toAvoid[i-1] ) % spacing
|
||||
if l > largestLen:
|
||||
largestIndex = i
|
||||
largestLen = l
|
||||
deltaY = (toAvoid[largestIndex-1] + largestLen / 2.) % spacing
|
||||
|
||||
minY = min(min(line[0].imag,line[1].imag) for line in polygon)
|
||||
maxY = max(max(line[0].imag,line[1].imag) for line in polygon)
|
||||
|
||||
y = minY + ( - minY ) % spacing + deltaY
|
||||
|
||||
if y > minY + spacing:
|
||||
y -= spacing
|
||||
|
||||
y += 0.01
|
||||
|
||||
odd = False
|
||||
|
||||
all = []
|
||||
|
||||
while y < maxY:
|
||||
intersections = []
|
||||
for line in polygon:
|
||||
z = line[0]
|
||||
z1 = line[1]
|
||||
if z1.imag == y or z.imag == y: # roundoff generated corner case -- ignore -- TODO
|
||||
break
|
||||
if z1.imag < y < z.imag or z.imag < y < z1.imag:
|
||||
if z1.real == z.real:
|
||||
intersections.append(( complex(z.real, y), z.imag<y, line))
|
||||
else:
|
||||
m = (z1.imag-z.imag)/(z1.real-z.real)
|
||||
# m * (x - z.real) = y - z.imag
|
||||
# so: x = (y - z.imag) / m + z.real
|
||||
intersections.append( (complex((y-z.imag)/m + z.real, y), z.imag<y, line) )
|
||||
|
||||
intersections.sort(key=lambda datum: datum[0].real)
|
||||
|
||||
thisLine = []
|
||||
if mode == Shader.MODE_EVEN_ODD:
|
||||
for i in range(0,len(intersections)-1,2):
|
||||
thisLine.append((intersections[i], intersections[i+1]))
|
||||
elif mode == Shader.MODE_NONZERO:
|
||||
count = 0
|
||||
for i in range(0,len(intersections)-1):
|
||||
if intersections[i][1]:
|
||||
count += 1
|
||||
else:
|
||||
count -= 1
|
||||
if count != 0:
|
||||
thisLine.append((intersections[i], intersections[i+1]))
|
||||
else:
|
||||
raise ValueError()
|
||||
|
||||
if odd and alternate:
|
||||
thisLine = list(reversed([(l[1],l[0]) for l in thisLine]))
|
||||
|
||||
if not avoidOutline and len(thisLine) and len(all) and all[-1][1][2] == thisLine[0][0][2]:
|
||||
# follow along outline to avoid an extra pen bob
|
||||
all.append( (all[-1][1], thisLine[0][0]) )
|
||||
|
||||
all += thisLine
|
||||
|
||||
odd = not odd
|
||||
|
||||
y += spacing
|
||||
|
||||
return [(line[0][0]*rotate, line[1][0]*rotate) for line in all]
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
polygon=(0+0j, 10+10j, 10+0j, 0+0j)
|
||||
print(shadePolygon(polygon,90,1))
|
||||
|
Loading…
Reference in New Issue
Block a user