239 lines
9.2 KiB
Python
Executable File
239 lines
9.2 KiB
Python
Executable File
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]))
|