summaryrefslogtreecommitdiff
path: root/embroider.py
diff options
context:
space:
mode:
Diffstat (limited to 'embroider.py')
-rw-r--r--embroider.py191
1 files changed, 143 insertions, 48 deletions
diff --git a/embroider.py b/embroider.py
index aba43833..3f88ed3c 100644
--- a/embroider.py
+++ b/embroider.py
@@ -50,12 +50,52 @@ SVG_DEFS_TAG = inkex.addNS('defs', 'svg')
SVG_GROUP_TAG = inkex.addNS('g', 'svg')
-class EmbroideryElement(object):
+class Param(object):
+ def __init__(self, name, description, unit=None, values=[], type=None, group=None, inverse=False, default=None):
+ self.name = name
+ self.description = description
+ self.unit = unit
+ self.values = values or [""]
+ self.type = type
+ self.group = group
+ self.inverse = inverse
+ self.default = default
+
+ def __repr__(self):
+ return "Param(%s)" % vars(self)
+
+# Decorate a member function or property with information about
+# the embroidery parameter it corresponds to
+def param(*args, **kwargs):
+ p = Param(*args, **kwargs)
+
+ def decorator(func):
+ func.param = p
+ return func
+
+ return decorator
- def __init__(self, node, options):
+class EmbroideryElement(object):
+ def __init__(self, node, options=None):
self.node = node
self.options = options
+ @property
+ def id(self):
+ return self.node.get('id')
+
+ @classmethod
+ def get_params(cls):
+ params = []
+ for attr in dir(cls):
+ prop = getattr(cls, attr)
+ if isinstance(prop, property):
+ # The 'param' attribute is set by the 'param' decorator defined above.
+ if hasattr(prop.fget, 'param'):
+ params.append(prop.fget.param)
+
+ return params
+
@cache
def get_param(self, param, default):
value = self.node.get("embroider_" + param, "").strip()
@@ -99,6 +139,9 @@ class EmbroideryElement(object):
return value
+ def set_param(self, name, value):
+ self.node.set("embroider_%s" % name, value)
+
@cache
def get_style(self, style_name):
style = simplestyle.parseStyle(self.node.get("style"))
@@ -184,11 +227,16 @@ class EmbroideryElement(object):
class Fill(EmbroideryElement):
-
def __init__(self, *args, **kwargs):
super(Fill, self).__init__(*args, **kwargs)
@property
+ @param('auto_fill', 'Manually routed fill stitching', type='toggle', inverse=True, default=True)
+ def auto_fill(self):
+ return self.get_boolean_param('auto_fill', True)
+
+ @property
+ @param('angle', 'Angle of lines of stitches', unit='deg', type='float')
@cache
def angle(self):
return math.radians(self.get_float_param('angle', 0))
@@ -198,18 +246,22 @@ class Fill(EmbroideryElement):
return self.get_style("fill")
@property
+ @param('flip', 'Flip fill (start right-to-left)', type='boolean')
def flip(self):
return self.get_boolean_param("flip", False)
@property
+ @param('row_spacing_mm', 'Spacing between rows', unit='mm', type='float')
def row_spacing(self):
return self.get_float_param("row_spacing_mm")
@property
+ @param('max_stitch_length_mm', 'Maximum fill stitch length', unit='mm', type='float')
def max_stitch_length(self):
return self.get_float_param("max_stitch_length_mm")
@property
+ @param('staggers', 'Stagger rows this many times before repeating', type='int')
def staggers(self):
return self.get_int_param("staggers", 4)
@@ -479,6 +531,11 @@ class Fill(EmbroideryElement):
class AutoFill(Fill):
@property
+ @param('auto_fill', 'Automatically routed fill stitching', type='toggle', default=True)
+ def auto_fill(self):
+ return self.get_boolean_param('auto_fill', True)
+
+ @property
@cache
def outline(self):
return self.shape.boundary[0]
@@ -493,14 +550,17 @@ class AutoFill(Fill):
return False
@property
+ @param('running_stitch_length_mm', 'Running stitch length (traversal between sections)', unit='mm', type='float')
def running_stitch_length(self):
return self.get_float_param("running_stitch_length_mm")
@property
+ @param('fill_underlay', 'Underlay', type='toggle', group='AutoFill Underlay')
def fill_underlay(self):
return self.get_boolean_param("fill_underlay")
@property
+ @param('fill_underlay_angle', 'Fill angle (default: fill angle + 90 deg)', unit='deg', group='AutoFill Underlay', type='float')
@cache
def fill_underlay_angle(self):
underlay_angle = self.get_float_param("fill_underlay_angle")
@@ -511,11 +571,13 @@ class AutoFill(Fill):
return self.angle + math.pi / 2.0
@property
+ @param('fill_underlay_row_spacing_mm', 'Row spacing (default: 3x fill row spacing)', unit='mm', group='AutoFill Underlay', type='float')
@cache
def fill_underlay_row_spacing(self):
return self.get_float_param("fill_underlay_row_spacing_mm") or self.row_spacing * 3
@property
+ @param('fill_underlay_max_stitch_length_mm', 'Max stitch length', unit='mm', group='AutoFill Underlay', type='float')
@cache
def fill_underlay_max_stitch_length(self):
return self.get_float_param("fill_underlay_max_stitch_length_mm" or self.max_stitch_length)
@@ -607,7 +669,7 @@ class AutoFill(Fill):
return self.section_to_patch(section, angle, row_spacing, max_stitch_length)
- def auto_fill(self, angle, row_spacing, max_stitch_length, starting_point=None):
+ def do_auto_fill(self, angle, row_spacing, max_stitch_length, starting_point=None):
rows_of_segments = self.intersect_region_with_grating(angle, row_spacing)
sections = self.pull_runs(rows_of_segments)
@@ -637,15 +699,19 @@ class AutoFill(Fill):
last_stitch = last_patch.stitches[-1]
if self.fill_underlay:
- patches.extend(self.auto_fill(self.fill_underlay_angle, self.fill_underlay_row_spacing, self.fill_underlay_max_stitch_length, last_stitch))
+ patches.extend(self.do_auto_fill(self.fill_underlay_angle, self.fill_underlay_row_spacing, self.fill_underlay_max_stitch_length, last_stitch))
last_stitch = patches[-1].stitches[-1]
- patches.extend(self.auto_fill(self.angle, self.row_spacing, self.max_stitch_length, last_stitch))
+ patches.extend(self.do_auto_fill(self.angle, self.row_spacing, self.max_stitch_length, last_stitch))
return patches
class Stroke(EmbroideryElement):
+ @property
+ @param('satin_column', 'Satin along paths', type='toggle', inverse=True)
+ def satin_column(self):
+ return self.get_boolean_param("satin_column")
@property
def color(self):
@@ -666,15 +732,18 @@ class Stroke(EmbroideryElement):
return self.get_style("stroke-dasharray") is not None
@property
+ @param('running_stitch_length_mm', 'Running stitch length', unit='mm', type='float')
def running_stitch_length(self):
return self.get_float_param("running_stitch_length_mm")
@property
+ @param('zigzag_spacing_mm', 'Zig-zag spacing (peak-to-peak)', unit='mm', type='float')
@cache
def zigzag_spacing(self):
return self.get_float_param("zigzag_spacing_mm")
@property
+ @param('repeats', 'Repeats', type='int')
def repeats(self):
return self.get_int_param("repeats", 1)
@@ -751,25 +820,26 @@ class Stroke(EmbroideryElement):
class SatinColumn(EmbroideryElement):
-
def __init__(self, *args, **kwargs):
super(SatinColumn, self).__init__(*args, **kwargs)
- self.csp = self.parse_path()
- self.flattened_beziers = self.get_flattened_paths()
-
- # print >> dbg, "flattened beziers", self.flattened_beziers
+ @property
+ @param('satin_column', 'Custom satin column', type='toggle')
+ def satin_column(self):
+ return self.get_boolean_param("satin_column")
@property
def color(self):
return self.get_style("stroke")
@property
+ @param('zigzag_spacing_mm', 'Zig-zag spacing (peak-to-peak)', unit='mm', type='float')
def zigzag_spacing(self):
# peak-to-peak distance between zigzags
return self.get_float_param("zigzag_spacing_mm")
@property
+ @param('pull_compensation_mm', 'Pull compensation', unit='mm', type='float')
def pull_compensation(self):
# In satin stitch, the stitches have a tendency to pull together and
# narrow the entire column. We can compensate for this by stitching
@@ -777,42 +847,50 @@ class SatinColumn(EmbroideryElement):
return self.get_float_param("pull_compensation_mm", 0)
@property
+ @param('contour_underlay', 'Contour underlay', type='toggle', group='Contour Underlay')
def contour_underlay(self):
# "Contour underlay" is stitching just inside the rectangular shape
# of the satin column; that is, up one side and down the other.
return self.get_boolean_param("contour_underlay")
@property
+ @param('contour_underlay_stitch_length_mm', 'Stitch length', unit='mm', group='Contour Underlay', type='float')
def contour_underlay_stitch_length(self):
# use "contour_underlay_stitch_length", or, if not set, default to "stitch_length"
return self.get_float_param("contour_underlay_stitch_length_mm") or self.get_float_param("running_stitch_length_mm")
@property
+ @param('contour_underlay_inset_mm', 'Contour underlay inset amount', unit='mm', group='Contour Underlay', type='float')
def contour_underlay_inset(self):
# how far inside the edge of the column to stitch the underlay
return self.get_float_param("contour_underlay_inset_mm", 0.4)
@property
+ @param('center_walk_underlay', 'Center-walk underlay', type='toggle', group='Center-Walk Underlay')
def center_walk_underlay(self):
# "Center walk underlay" is stitching down and back in the centerline
# between the two sides of the satin column.
return self.get_boolean_param("center_walk_underlay")
@property
+ @param('center_walk_underlay_stitch_length_mm', 'Stitch length', unit='mm', group='Center-Walk Underlay', type='float')
def center_walk_underlay_stitch_length(self):
# use "center_walk_underlay_stitch_length", or, if not set, default to "stitch_length"
return self.get_float_param("center_walk_underlay_stitch_length_mm") or self.get_float_param("running_stitch_length_mm")
@property
+ @param('zigzag_underlay', 'Zig-zag underlay', type='toggle', group='Zig-zag Underlay')
def zigzag_underlay(self):
return self.get_boolean_param("zigzag_underlay")
@property
+ @param('zigzag_underlay_spacing_mm', 'Zig-Zag spacing (peak-to-peak)', unit='mm', group='Zig-zag Underlay', type='float')
def zigzag_underlay_spacing(self):
# peak-to-peak distance between zigzags in zigzag underlay
return self.get_float_param("zigzag_underlay_spacing_mm", 1)
@property
+ @param('zigzag_underlay_inset', 'Inset amount (default: half of contour underlay inset)', unit='mm', group='Zig-zag Underlay', type='float')
def zigzag_underlay_inset(self):
# how far in from the edge of the satin the points in the zigzags
# should be
@@ -823,7 +901,14 @@ class SatinColumn(EmbroideryElement):
# the edges of the satin column.
return self.get_float_param("zigzag_underlay_inset_mm") or self.contour_underlay_inset / 2.0
- def get_flattened_paths(self):
+ @property
+ @cache
+ def csp(self):
+ return self.parse_path()
+
+ @property
+ @cache
+ def flattened_beziers(self):
# Given a pair of paths made up of bezier segments, flatten
# each individual bezier segment into line segments that approximate
# the curves. Retain the divisions between beziers -- we'll use those
@@ -1088,8 +1173,48 @@ class SatinColumn(EmbroideryElement):
return patches
-class Patch:
+def detect_classes(node):
+ 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 == SVG_PATH_TAG:
+ nodes.append(node)
+
+ return nodes
+
+class Patch:
def __init__(self, color=None, stitches=None):
self.color = color
self.stitches = stitches or []
@@ -1227,41 +1352,11 @@ class Embroider(inkex.Effect):
def handle_node(self, node):
print >> dbg, "handling node", node.get('id'), node.get('tag')
-
- element = EmbroideryElement(node, self.options)
-
- if element.has_style('display') and element.get_style('display') is None:
- return
-
- if node.tag == SVG_DEFS_TAG:
- return
-
- for child in node:
- self.handle_node(child)
-
- if node.tag != SVG_PATH_TAG:
- return
-
- # dbg.write("Node: %s\n"%str((id, etree.tostring(node, pretty_print=True))))
-
- if element.get_boolean_param("satin_column"):
- self.elements.append(SatinColumn(node, self.options))
- else:
- elements = []
-
- if element.get_style("fill"):
- if element.get_boolean_param("auto_fill", True):
- elements.append(AutoFill(node, self.options))
- else:
- elements.append(Fill(node, self.options))
-
- if element.get_style("stroke"):
- elements.append(Stroke(node, self.options))
-
- if element.get_boolean_param("stroke_first", False):
- elements.reverse()
-
- self.elements.extend(elements)
+ 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):
svg_filename = self.document.getroot().get(inkex.addNS('docname', 'sodipodi'))