can also be done with a , and
# much more.
#
# Notably, EmbroiderModder2 uses elements when converting from
# common machine embroidery file formats to SVG. Handling those here lets
# users use File -> Import to pull in existing designs they may have
# obtained, for example purchased fonts.
@property
def points(self):
# example: "1,2 0,0 1.5,3 4,2"
points = self.node.get('points')
points = points.split(" ")
points = [[float(coord) for coord in point.split(",")] for point in points]
return points
@property
def path(self):
# A polyline is a series of connected line segments described by their
# points. In order to make use of the existing logic for incorporating
# svg transforms that is in our superclass, we'll convert the polyline
# to a degenerate cubic superpath in which the bezier handles are on
# the segment endpoints.
path = [[[point[:], point[:], point[:]] for point in self.points]]
return path
@property
@cache
def csp(self):
csp = self.parse_path()
return csp
@property
def color(self):
# EmbroiderModder2 likes to use the `stroke` property directly instead
# of CSS.
return self.get_style("stroke") or self.node.get("stroke")
@property
def stitches(self):
# For a , we'll stitch the points exactly as they exist in
# the SVG, with no stitch spacing interpolation, flattening, etc.
# See the comments in the parent class's parse_path method for a
# description of the CSP data structure.
stitches = [point for handle_before, point, handle_after in self.csp[0]]
return stitches
def to_patches(self, last_patch):
patch = Patch(color=self.color)
for stitch in self.stitches:
patch.add_stitch(PyEmb.Point(*stitch))
return [patch]
def detect_classes(node):
if node.tag == SVG_POLYLINE_TAG:
return [Polyline]
else:
element = EmbroideryElement(node)
if element.get_boolean_param("satin_column"):
return [SatinColumn]
else:
classes = []
if element.get_style("fill"):
if element.get_boolean_param("auto_fill", True):
classes.append(AutoFill)
else:
classes.append(Fill)
if element.get_style("stroke"):
classes.append(Stroke)
if element.get_boolean_param("stroke_first", False):
classes.reverse()
return classes
def descendants(node):
nodes = []
element = EmbroideryElement(node)
if element.has_style('display') and element.get_style('display') is None:
return []
if node.tag == SVG_DEFS_TAG:
return []
for child in node:
nodes.extend(descendants(child))
if node.tag in EMBROIDERABLE_TAGS:
nodes.append(node)
return nodes
class Patch:
def __init__(self, color=None, stitches=None):
self.color = color
self.stitches = stitches or []
def __add__(self, other):
if isinstance(other, Patch):
return Patch(self.color, self.stitches + other.stitches)
else:
raise TypeError("Patch can only be added to another Patch")
def add_stitch(self, stitch):
self.stitches.append(stitch)
def reverse(self):
return Patch(self.color, self.stitches[::-1])
def patches_to_stitches(patch_list, collapse_len_px=0):
stitches = []
last_stitch = None
last_color = None
for patch in patch_list:
jump_stitch = True
for stitch in patch.stitches:
if last_stitch and last_color == patch.color:
l = (stitch - last_stitch).length()
if l <= 0.1:
# filter out duplicate successive stitches
jump_stitch = False
continue
if jump_stitch:
# consider collapsing jump stitch, if it is pretty short
if l < collapse_len_px:
# dbg.write("... collapsed\n")
jump_stitch = False
# dbg.write("stitch color %s\n" % patch.color)
newStitch = PyEmb.Stitch(stitch.x, stitch.y, patch.color, jump_stitch)
stitches.append(newStitch)
jump_stitch = False
last_stitch = stitch
last_color = patch.color
return stitches
def stitches_to_paths(stitches):
paths = []
last_color = None
last_stitch = None
for stitch in stitches:
if stitch.jump_stitch:
if last_color == stitch.color:
paths.append([None, []])
if last_stitch is not None:
paths[-1][1].append(['M', last_stitch.as_tuple()])
paths[-1][1].append(['L', stitch.as_tuple()])
last_color = None
if stitch.color != last_color:
paths.append([stitch.color, []])
paths[-1][1].append(['L' if len(paths[-1][1]) > 0 else 'M', stitch.as_tuple()])
last_color = stitch.color
last_stitch = stitch
return paths
def emit_inkscape(parent, stitches):
for color, path in stitches_to_paths(stitches):
# dbg.write('path: %s %s\n' % (color, repr(path)))
inkex.etree.SubElement(parent,
inkex.addNS('path', 'svg'),
{'style': simplestyle.formatStyle(
{'stroke': color if color is not None else '#000000',
'stroke-width': "0.4",
'fill': 'none'}),
'd': simplepath.formatPath(path),
})
class Embroider(inkex.Effect):
def __init__(self, *args, **kwargs):
inkex.Effect.__init__(self)
self.OptionParser.add_option("-r", "--row_spacing_mm",
action="store", type="float",
dest="row_spacing_mm", default=0.4,
help="row spacing (mm)")
self.OptionParser.add_option("-z", "--zigzag_spacing_mm",
action="store", type="float",
dest="zigzag_spacing_mm", default=1.0,
help="zigzag spacing (mm)")
self.OptionParser.add_option("-l", "--max_stitch_len_mm",
action="store", type="float",
dest="max_stitch_length_mm", default=3.0,
help="max stitch length (mm)")
self.OptionParser.add_option("--running_stitch_len_mm",
action="store", type="float",
dest="running_stitch_length_mm", default=3.0,
help="running stitch length (mm)")
self.OptionParser.add_option("-c", "--collapse_len_mm",
action="store", type="float",
dest="collapse_length_mm", default=0.0,
help="max collapse length (mm)")
self.OptionParser.add_option("-f", "--flatness",
action="store", type="float",
dest="flat", default=0.1,
help="Minimum flatness of the subdivided curves")
self.OptionParser.add_option("--hide_layers",
action="store", type="choice",
choices=["true", "false"],
dest="hide_layers", default="true",
help="Hide all other layers when the embroidery layer is generated")
self.OptionParser.add_option("-O", "--output_format",
action="store", type="choice",
choices=["melco", "csv", "gcode"],
dest="output_format", default="melco",
help="File output format")
self.OptionParser.add_option("-P", "--path",
action="store", type="string",
dest="path", default=".",
help="Directory in which to store output file")
self.OptionParser.add_option("-F", "--output-file",
action="store", type="string",
dest="output_file", default=".",
help="Output filename.")
self.OptionParser.add_option("-b", "--max-backups",
action="store", type="int",
dest="max_backups", default=5,
help="Max number of backups of output files to keep.")
self.OptionParser.add_option("-p", "--pixels_per_mm",
action="store", type="float",
dest="pixels_per_mm", default=10,
help="Number of on-screen pixels per millimeter.")
self.patches = []
def handle_node(self, node):
print >> dbg, "handling node", node.get('id'), node.tag
nodes = descendants(node)
for node in nodes:
classes = detect_classes(node)
print >> dbg, "classes:", classes
self.elements.extend(cls(node, self.options) for cls in classes)
def get_output_path(self):
if self.options.output_file:
output_path = os.path.join(self.options.path, self.options.output_file)
else:
svg_filename = self.document.getroot().get(inkex.addNS('docname', 'sodipodi'), "embroidery.svg")
csv_filename = svg_filename.replace('.svg', '.csv')
output_path = os.path.join(self.options.path, csv_filename)
def add_suffix(path, suffix):
if suffix > 0:
path = "%s.%s" % (path, suffix)
return path
def move_if_exists(path, suffix=0):
source = add_suffix(path, suffix)
if suffix >= self.options.max_backups:
return
dest = add_suffix(path, suffix + 1)
if os.path.exists(source):
move_if_exists(path, suffix + 1)
os.rename(source, dest)
move_if_exists(output_path)
return output_path
def hide_layers(self):
for g in self.document.getroot().findall(SVG_GROUP_TAG):
if g.get(inkex.addNS("groupmode", "inkscape")) == "layer":
g.set("style", "display:none")
def effect(self):
# Printing anything other than a valid SVG on stdout blows inkscape up.
old_stdout = sys.stdout
sys.stdout = sys.stderr
self.patch_list = []
print >> dbg, "starting nodes: %s\n" % time.time()
dbg.flush()
self.elements = []
if self.selected:
# be sure to visit selected nodes in the order they're stacked in
# the document
for node in self.document.getroot().iter():
if node.get("id") in self.selected:
self.handle_node(node)
else:
self.handle_node(self.document.getroot())
print >> dbg, "finished nodes: %s" % time.time()
dbg.flush()
if not self.elements:
if self.selected:
inkex.errormsg("No embroiderable paths selected.")
else:
inkex.errormsg("No embroiderable paths found in document.")
inkex.errormsg("Tip: use Path -> Object to Path to convert non-paths before embroidering.")
return
if self.options.hide_layers:
self.hide_layers()
patches = []
for element in self.elements:
if patches:
last_patch = patches[-1]
else:
last_patch = None
patches.extend(element.to_patches(last_patch))
stitches = patches_to_stitches(patches, self.options.collapse_length_mm * self.options.pixels_per_mm)
emb = PyEmb.Embroidery(stitches, self.options.pixels_per_mm)
emb.export(self.get_output_path(), self.options.output_format)
new_layer = inkex.etree.SubElement(self.document.getroot(), SVG_GROUP_TAG, {})
new_layer.set('id', self.uniqueId("embroidery"))
new_layer.set(inkex.addNS('label', 'inkscape'), 'Embroidery')
new_layer.set(inkex.addNS('groupmode', 'inkscape'), 'layer')
emit_inkscape(new_layer, stitches)
sys.stdout = old_stdout
if __name__ == '__main__':
sys.setrecursionlimit(100000)
e = Embroider()
e.affect()
dbg.flush()
dbg.close()