diff --git a/PyEmb.py b/PyEmb.py
deleted file mode 100644
index 8c02ecc3f..000000000
--- a/PyEmb.py
+++ /dev/null
@@ -1,160 +0,0 @@
-#!/usr/bin/env python
-# http://www.achatina.de/sewing/main/TECHNICL.HTM
-
-import math
-import libembroidery
-
-PIXELS_PER_MM = 96 / 25.4
-
-try:
- from functools import lru_cache
-except ImportError:
- from backports.functools_lru_cache import lru_cache
-
-# simplify use of lru_cache decorator
-def cache(*args, **kwargs):
- return lru_cache(maxsize=None)(*args, **kwargs)
-
-class Point:
-
- def __init__(self, x, y):
- self.x = x
- self.y = y
-
- def __add__(self, other):
- return Point(self.x + other.x, self.y + other.y)
-
- def __sub__(self, other):
- return Point(self.x - other.x, self.y - other.y)
-
- def mul(self, scalar):
- return Point(self.x * scalar, self.y * scalar)
-
- def __mul__(self, other):
- if isinstance(other, Point):
- # dot product
- return self.x * other.x + self.y * other.y
- elif isinstance(other, (int, float)):
- return Point(self.x * other, self.y * other)
- else:
- raise ValueError("cannot multiply Point by %s" % type(other))
-
- def __rmul__(self, other):
- if isinstance(other, (int, float)):
- return self.__mul__(other)
- else:
- raise ValueError("cannot multiply Point by %s" % type(other))
-
- def __repr__(self):
- return "Point(%s,%s)" % (self.x, self.y)
-
- def length(self):
- return math.sqrt(math.pow(self.x, 2.0) + math.pow(self.y, 2.0))
-
- def unit(self):
- return self.mul(1.0 / self.length())
-
- def rotate_left(self):
- return Point(-self.y, self.x)
-
- def rotate(self, angle):
- return Point(self.x * math.cos(angle) - self.y * math.sin(angle), self.y * math.cos(angle) + self.x * math.sin(angle))
-
- def as_int(self):
- return Point(int(round(self.x)), int(round(self.y)))
-
- def as_tuple(self):
- return (self.x, self.y)
-
- def __cmp__(self, other):
- return cmp(self.as_tuple(), other.as_tuple())
-
- def __getitem__(self, item):
- return self.as_tuple()[item]
-
- def __len__(self):
- return 2
-
-
-class Stitch(Point):
- def __init__(self, x, y, color=None, jump=False, stop=False, trim=False):
- self.x = x
- self.y = y
- self.color = color
- self.jump = jump
- self.trim = trim
- self.stop = stop
-
- def __repr__(self):
- return "Stitch(%s, %s, %s, %s, %s, %s)" % (self.x, self.y, self.color, "JUMP" if self.jump else "", "TRIM" if self.trim else "", "STOP" if self.stop else "")
-
-def make_thread(color):
- # strip off the leading "#"
- if color.startswith("#"):
- color = color[1:]
-
- thread = libembroidery.EmbThread()
- thread.color = libembroidery.embColor_fromHexStr(color)
-
- thread.description = color
- thread.catalogNumber = ""
-
- return thread
-
-def add_thread(pattern, thread):
- """Add a thread to a pattern and return the thread's index"""
-
- libembroidery.embPattern_addThread(pattern, thread)
-
- return libembroidery.embThreadList_count(pattern.threadList) - 1
-
-def get_flags(stitch):
- flags = 0
-
- if stitch.jump:
- flags |= libembroidery.JUMP
-
- if stitch.trim:
- flags |= libembroidery.TRIM
-
- if stitch.stop:
- flags |= libembroidery.STOP
-
- return flags
-
-def write_embroidery_file(file_path, stitches):
- # Embroidery machines don't care about our canvas size, so we relocate the
- # design to the origin. It might make sense to center it about the origin
- # instead.
- min_x = min(stitch.x for stitch in stitches)
- min_y = min(stitch.y for stitch in stitches)
-
- pattern = libembroidery.embPattern_create()
- threads = {}
-
- last_color = None
-
- for stitch in stitches:
- if stitch.color != last_color:
- if stitch.color not in threads:
- thread = make_thread(stitch.color)
- thread_index = add_thread(pattern, thread)
- threads[stitch.color] = thread_index
- else:
- thread_index = threads[stitch.color]
-
- libembroidery.embPattern_changeColor(pattern, thread_index)
- last_color = stitch.color
-
- flags = get_flags(stitch)
- libembroidery.embPattern_addStitchAbs(pattern, stitch.x - min_x, stitch.y - min_y, flags, 0)
-
- libembroidery.embPattern_addStitchAbs(pattern, stitch.x - min_x, stitch.y - min_y, libembroidery.END, 0)
-
- # convert from pixels to millimeters
- libembroidery.embPattern_scale(pattern, 1/PIXELS_PER_MM)
-
- # SVG and embroidery disagree on the direction of the Y axis
- libembroidery.embPattern_flipVertical(pattern)
-
- libembroidery.embPattern_write(pattern, file_path)
diff --git a/embroider.inx b/embroider.inx
index 909df8d2d..a139ad785 100644
--- a/embroider.inx
+++ b/embroider.inx
@@ -4,10 +4,6 @@
jonh.embroider
embroider.py
inkex.py
- 0.40
- 0.25
- 3.0
- 1.5
0.0
true
diff --git a/embroider.py b/embroider.py
index 25884316e..feda368f5 100644
--- a/embroider.py
+++ b/embroider.py
@@ -34,319 +34,8 @@ import shapely.ops
import networkx
from pprint import pformat
-import PyEmb
-from PyEmb import cache
-
-dbg = open("/tmp/embroider-debug.txt", "w")
-PyEmb.dbg = dbg
-
-SVG_PATH_TAG = inkex.addNS('path', 'svg')
-SVG_POLYLINE_TAG = inkex.addNS('polyline', 'svg')
-SVG_DEFS_TAG = inkex.addNS('defs', 'svg')
-SVG_GROUP_TAG = inkex.addNS('g', 'svg')
-
-EMBROIDERABLE_TAGS = (SVG_PATH_TAG, SVG_POLYLINE_TAG)
-
-# modern versions of Inkscape use 96 pixels per inch as per the CSS standard
-PIXELS_PER_MM = 96 / 25.4
-
-class Param(object):
- def __init__(self, name, description, unit=None, values=[], type=None, group=None, inverse=False, default=None, tooltip=None, sort_index=0):
- 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
- self.tooltip = tooltip
- self.sort_index = sort_index
-
- 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
-
-# cribbed from inkscape-silhouette
-def parse_length_with_units( str ):
-
- '''
- Parse an SVG value which may or may not have units attached
- This version is greatly simplified in that it only allows: no units,
- units of px, mm, and %. Everything else, it returns None for.
- There is a more general routine to consider in scour.py if more
- generality is ever needed.
- '''
-
- u = 'px'
- s = str.strip()
- if s[-2:] == 'px':
- s = s[:-2]
- elif s[-2:] == 'mm':
- u = 'mm'
- s = s[:-2]
- elif s[-2:] == 'pt':
- u = 'pt'
- s = s[:-2]
- elif s[-2:] == 'pc':
- u = 'pc'
- s = s[:-2]
- elif s[-2:] == 'cm':
- u = 'cm'
- s = s[:-2]
- elif s[-2:] == 'in':
- u = 'in'
- s = s[:-2]
- elif s[-1:] == '%':
- u = '%'
- s = s[:-1]
- try:
- v = float( s )
- except:
- raise ValueError("parseLengthWithUnits: unknown unit %s" % s)
-
- return v, u
-
-
-def convert_length(length):
- value, units = parse_length_with_units(length)
-
- if not units or units == "px":
- return value
-
- if units == 'cm':
- value *= 10
- units == 'mm'
-
- if units == 'mm':
- value = value / 25.4
- units = 'in'
-
- if units == 'in':
- # modern versions of Inkscape use CSS's 96 pixels per inch. When you
- # open an old document, inkscape will add a viewbox for you.
- return value * 96
-
- raise ValueError("Unknown unit: %s" % units)
-
-@cache
-def get_viewbox_transform(node):
- # somewhat cribbed from inkscape-silhouette
-
- doc_width = convert_length(node.get('width'))
- doc_height = convert_length(node.get('height'))
-
- viewbox = node.get('viewBox').strip().replace(',', ' ').split()
-
- dx = -float(viewbox[0])
- dy = -float(viewbox[1])
- transform = simpletransform.parseTransform("translate(%f, %f)" % (dx, dy))
-
- try:
- sx = doc_width / float(viewbox[2])
- sy = doc_height / float(viewbox[3])
- scale_transform = simpletransform.parseTransform("scale(%f, %f)" % (sx, sy))
- transform = simpletransform.composeTransform(transform, scale_transform)
- except ZeroDivisionError:
- pass
-
- return transform
-
-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()
-
- if not value:
- value = getattr(self.options, param, default)
-
- return value
-
- @cache
- def get_boolean_param(self, param, default=None):
- value = self.get_param(param, default)
-
- if isinstance(value, bool):
- return value
- else:
- return value and (value.lower() in ('yes', 'y', 'true', 't', '1'))
-
- @cache
- def get_float_param(self, param, default=None):
- try:
- value = float(self.get_param(param, default))
- except (TypeError, ValueError):
- return default
-
- if param.endswith('_mm'):
- value = value * PIXELS_PER_MM
-
- return value
-
- @cache
- def get_int_param(self, param, default=None):
- try:
- value = int(self.get_param(param, default))
- except (TypeError, ValueError):
- return default
-
- if param.endswith('_mm'):
- value = int(value * PIXELS_PER_MM)
-
- return value
-
- def set_param(self, name, value):
- self.node.set("embroider_%s" % name, str(value))
-
- @cache
- def get_style(self, style_name):
- style = simplestyle.parseStyle(self.node.get("style"))
- if (style_name not in style):
- return None
- value = style[style_name]
- if value == 'none':
- return None
- return value
-
- @cache
- def has_style(self, style_name):
- style = simplestyle.parseStyle(self.node.get("style"))
- return style_name in style
-
- @property
- def path(self):
- return cubicsuperpath.parsePath(self.node.get("d"))
-
-
- @cache
- def parse_path(self):
- # A CSP is a "cubic superpath".
- #
- # A "path" is a sequence of strung-together bezier curves.
- #
- # A "superpath" is a collection of paths that are all in one object.
- #
- # The "cubic" bit in "cubic superpath" is because the bezier curves
- # inkscape uses involve cubic polynomials.
- #
- # Each path is a collection of tuples, each of the form:
- #
- # (control_before, point, control_after)
- #
- # A bezier curve segment is defined by an endpoint, a control point,
- # a second control point, and a final endpoint. A path is a bunch of
- # bezier curves strung together. One could represent a path as a set
- # of four-tuples, but there would be redundancy because the ending
- # point of one bezier is the starting point of the next. Instead, a
- # path is a set of 3-tuples as shown above, and one must construct
- # each bezier curve by taking the appropriate endpoints and control
- # points. Bleh. It should be noted that a straight segment is
- # represented by having the control point on each end equal to that
- # end's point.
- #
- # In a path, each element in the 3-tuple is itself a tuple of (x, y).
- # Tuples all the way down. Hasn't anyone heard of using classes?
-
- path = self.path
-
- # start with the identity transform
- transform = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]
-
- # combine this node's transform with all parent groups' transforms
- transform = simpletransform.composeParents(self.node, transform)
-
- # add in the transform implied by the viewBox
- viewbox_transform = get_viewbox_transform(self.node.getroottree().getroot())
- transform = simpletransform.composeTransform(viewbox_transform, transform)
-
- # apply the combined transform to this node's path
- simpletransform.applyTransformToPath(transform, path)
-
-
- return path
-
- def flatten(self, path):
- """approximate a path containing beziers with a series of points"""
-
- path = deepcopy(path)
-
- cspsubdiv(path, getattr(self.options, "flat", 0.1))
-
- flattened = []
-
- for comp in path:
- vertices = []
- for ctl in comp:
- vertices.append((ctl[1][0], ctl[1][1]))
- flattened.append(vertices)
-
- return flattened
-
- @property
- @param('trim_after',
- 'TRIM after',
- tooltip='Trim thread after this object (for supported machines and file formats)',
- type='boolean',
- default=False,
- sort_index=1000)
- def trim_after(self):
- return self.get_boolean_param('trim_after', False)
-
- @property
- @param('stop_after',
- 'STOP after',
- tooltip='Add STOP instruction after this object (for supported machines and file formats)',
- type='boolean',
- default=False,
- sort_index=1000)
- def stop_after(self):
- return self.get_boolean_param('stop_after', False)
-
- def to_patches(self, last_patch):
- raise NotImplementedError("%s must implement to_patches()" % self.__class__.__name__)
-
- def embroider(self, last_patch):
- patches = self.to_patches(last_patch)
-
- if patches:
- patches[-1].trim_after = self.trim_after
- patches[-1].stop_after = self.stop_after
-
- return patches
-
- def fatal(self, message):
- print >> sys.stderr, "error:", message
- sys.exit(1)
+import inkstitch
+from inkstitch import cache, dbg, param, EmbroideryElement, get_nodes, SVG_POLYLINE_TAG, SVG_GROUP_TAG, PIXELS_PER_MM, get_viewbox_transform
class Fill(EmbroideryElement):
@@ -359,7 +48,7 @@ class Fill(EmbroideryElement):
return self.get_boolean_param('auto_fill', True)
@property
- @param('angle', 'Angle of lines of stitches', unit='deg', type='float')
+ @param('angle', 'Angle of lines of stitches', unit='deg', type='float', default=0)
@cache
def angle(self):
return math.radians(self.get_float_param('angle', 0))
@@ -369,26 +58,26 @@ class Fill(EmbroideryElement):
return self.get_style("fill")
@property
- @param('flip', 'Flip fill (start right-to-left)', type='boolean')
+ @param('flip', 'Flip fill (start right-to-left)', type='boolean', default=False)
def flip(self):
return self.get_boolean_param("flip", False)
@property
- @param('row_spacing_mm', 'Spacing between rows', unit='mm', type='float')
+ @param('row_spacing_mm', 'Spacing between rows', unit='mm', type='float', default=0.25)
def row_spacing(self):
- return self.get_float_param("row_spacing_mm")
+ return self.get_float_param("row_spacing_mm", 0.25)
@property
def end_row_spacing(self):
return self.get_float_param("end_row_spacing_mm")
@property
- @param('max_stitch_length_mm', 'Maximum fill stitch length', unit='mm', type='float')
+ @param('max_stitch_length_mm', 'Maximum fill stitch length', unit='mm', type='float', default=3.0)
def max_stitch_length(self):
- return self.get_float_param("max_stitch_length_mm")
+ return self.get_float_param("max_stitch_length_mm", 3.0)
@property
- @param('staggers', 'Stagger rows this many times before repeating', type='int')
+ @param('staggers', 'Stagger rows this many times before repeating', type='int', default=4)
def staggers(self):
return self.get_int_param("staggers", 4)
@@ -431,7 +120,7 @@ class Fill(EmbroideryElement):
@cache
def east(self, angle):
# "east" is the name of the direction that is to the right along a row
- return PyEmb.Point(1, 0).rotate(-angle)
+ return inkstitch.Point(1, 0).rotate(-angle)
@cache
def north(self, angle):
@@ -460,15 +149,15 @@ class Fill(EmbroideryElement):
# the max line length I'll need to intersect the whole shape is the diagonal
(minx, miny, maxx, maxy) = self.shape.bounds
- upper_left = PyEmb.Point(minx, miny)
- lower_right = PyEmb.Point(maxx, maxy)
+ upper_left = inkstitch.Point(minx, miny)
+ lower_right = inkstitch.Point(maxx, maxy)
length = (upper_left - lower_right).length()
half_length = length / 2.0
# Now get a unit vector rotated to the requested angle. I use -angle
# because shapely rotates clockwise, but my geometry textbooks taught
# me to consider angles as counter-clockwise from the X axis.
- direction = PyEmb.Point(1, 0).rotate(-angle)
+ direction = inkstitch.Point(1, 0).rotate(-angle)
# and get a normal vector
normal = direction.rotate(math.pi / 2)
@@ -476,7 +165,7 @@ class Fill(EmbroideryElement):
# I'll start from the center, move in the normal direction some amount,
# and then walk left and right half_length in each direction to create
# a line segment in the grating.
- center = PyEmb.Point((minx + maxx) / 2.0, (miny + maxy) / 2.0)
+ center = inkstitch.Point((minx + maxx) / 2.0, (miny + maxy) / 2.0)
# I need to figure out how far I need to go along the normal to get to
# the edge of the shape. To do that, I'll rotate the bounding box
@@ -520,7 +209,7 @@ class Fill(EmbroideryElement):
runs = [res.coords]
if runs:
- runs.sort(key=lambda seg: (PyEmb.Point(*seg[0]) - upper_left).length())
+ runs.sort(key=lambda seg: (inkstitch.Point(*seg[0]) - upper_left).length())
if self.flip:
runs.reverse()
@@ -614,8 +303,8 @@ class Fill(EmbroideryElement):
# abutting fill regions from pull_runs().
- beg = PyEmb.Point(*beg)
- end = PyEmb.Point(*end)
+ beg = inkstitch.Point(*beg)
+ end = inkstitch.Point(*end)
row_direction = (end - beg).unit()
segment_length = (end - beg).length()
@@ -701,14 +390,14 @@ class AutoFill(Fill):
return False
@property
- @param('running_stitch_length_mm', 'Running stitch length (traversal between sections)', unit='mm', type='float')
+ @param('running_stitch_length_mm', 'Running stitch length (traversal between sections)', unit='mm', type='float', default=1.5)
def running_stitch_length(self):
- return self.get_float_param("running_stitch_length_mm")
+ return self.get_float_param("running_stitch_length_mm", 1.5)
@property
- @param('fill_underlay', 'Underlay', type='toggle', group='AutoFill Underlay')
+ @param('fill_underlay', 'Underlay', type='toggle', group='AutoFill Underlay', default=False)
def fill_underlay(self):
- return self.get_boolean_param("fill_underlay")
+ return self.get_boolean_param("fill_underlay", default=False)
@property
@param('fill_underlay_angle', 'Fill angle (default: fill angle + 90 deg)', unit='deg', group='AutoFill Underlay', type='float')
@@ -816,8 +505,8 @@ class AutoFill(Fill):
# If the start and endpoints are in the same row, I can't tell which row
# I should treat it as being in.
for i in xrange(len(nodes)):
- row0 = self.row_num(PyEmb.Point(*nodes[0]), angle, row_spacing)
- row1 = self.row_num(PyEmb.Point(*nodes[1]), angle, row_spacing)
+ row0 = self.row_num(inkstitch.Point(*nodes[0]), angle, row_spacing)
+ row1 = self.row_num(inkstitch.Point(*nodes[1]), angle, row_spacing)
if row0 == row1:
nodes = nodes[1:] + [nodes[0]]
@@ -832,7 +521,7 @@ class AutoFill(Fill):
else:
edge_set = 1
- #print >> sys.stderr, outline_index, "es", edge_set, "rn", row_num, PyEmb.Point(*nodes[0]) * self.north(angle), PyEmb.Point(*nodes[1]) * self.north(angle)
+ #print >> sys.stderr, outline_index, "es", edge_set, "rn", row_num, inkstitch.Point(*nodes[0]) * self.north(angle), inkstitch.Point(*nodes[1]) * self.north(angle)
# add an edge between each successive node
for i, (node1, node2) in enumerate(zip(nodes, nodes[1:] + [nodes[0]])):
@@ -840,8 +529,8 @@ class AutoFill(Fill):
# duplicate edges contained in every other row (exactly half
# will be duplicated)
- row_num = min(self.row_num(PyEmb.Point(*node1), angle, row_spacing),
- self.row_num(PyEmb.Point(*node2), angle, row_spacing))
+ row_num = min(self.row_num(inkstitch.Point(*node1), angle, row_spacing),
+ self.row_num(inkstitch.Point(*node2), angle, row_spacing))
# duplicate every other edge around this outline
if i % 2 == edge_set:
@@ -1114,14 +803,14 @@ class AutoFill(Fill):
print >> dbg, "connect_points:", outline_index, start, end, distance, stitches, direction
dbg.flush()
- patch.add_stitch(PyEmb.Point(*start))
+ patch.add_stitch(inkstitch.Point(*start))
for i in xrange(stitches):
pos = (pos + one_stitch) % self.outline_length
- patch.add_stitch(PyEmb.Point(*outline.interpolate(pos).coords[0]))
+ patch.add_stitch(inkstitch.Point(*outline.interpolate(pos).coords[0]))
- end = PyEmb.Point(*end)
+ end = inkstitch.Point(*end)
if (end - patch.stitches[-1]).length() > 0.1 * PIXELS_PER_MM:
patch.add_stitch(end)
@@ -1132,10 +821,10 @@ class AutoFill(Fill):
path = self.collapse_sequential_outline_edges(graph, path)
patch = Patch(color=self.color)
- #patch.add_stitch(PyEmb.Point(*path[0][0]))
+ #patch.add_stitch(inkstitch.Point(*path[0][0]))
#for edge in path:
- # patch.add_stitch(PyEmb.Point(*edge[1]))
+ # patch.add_stitch(inkstitch.Point(*edge[1]))
for edge in path:
if graph.has_edge(*edge, key="segment"):
@@ -1177,7 +866,7 @@ class AutoFill(Fill):
starting_point = None
else:
nearest_point = self.outline.interpolate(self.outline.project(shgeo.Point(last_patch.stitches[-1])))
- starting_point = PyEmb.Point(*nearest_point.coords[0])
+ starting_point = inkstitch.Point(*nearest_point.coords[0])
if self.fill_underlay:
patches.extend(self.do_auto_fill(self.fill_underlay_angle, self.fill_underlay_row_spacing, self.fill_underlay_max_stitch_length, starting_point))
@@ -1216,15 +905,15 @@ 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')
+ @param('running_stitch_length_mm', 'Running stitch length', unit='mm', type='float', default=1.5)
def running_stitch_length(self):
- return self.get_float_param("running_stitch_length_mm")
+ return self.get_float_param("running_stitch_length_mm", 1.5)
@property
- @param('zigzag_spacing_mm', 'Zig-zag spacing (peak-to-peak)', unit='mm', type='float')
+ @param('zigzag_spacing_mm', 'Zig-zag spacing (peak-to-peak)', unit='mm', type='float', default=0.4)
@cache
def zigzag_spacing(self):
- return self.get_float_param("zigzag_spacing_mm")
+ return self.get_float_param("zigzag_spacing_mm", 0.4)
@property
@param('repeats', 'Repeats', type='int', default="1")
@@ -1292,7 +981,7 @@ class Stroke(EmbroideryElement):
patches = []
for path in self.paths:
- path = [PyEmb.Point(x, y) for x, y in path]
+ path = [inkstitch.Point(x, y) for x, y in path]
if self.is_running_stitch():
patch = self.stroke_points(path, self.running_stitch_length, stroke_width=0.0)
else:
@@ -1317,10 +1006,10 @@ class SatinColumn(EmbroideryElement):
return self.get_style("stroke")
@property
- @param('zigzag_spacing_mm', 'Zig-zag spacing (peak-to-peak)', unit='mm', type='float')
+ @param('zigzag_spacing_mm', 'Zig-zag spacing (peak-to-peak)', unit='mm', type='float', default=0.4)
def zigzag_spacing(self):
# peak-to-peak distance between zigzags
- return self.get_float_param("zigzag_spacing_mm")
+ return self.get_float_param("zigzag_spacing_mm", 0.4)
@property
@param('pull_compensation_mm', 'Pull compensation', unit='mm', type='float')
@@ -1338,13 +1027,12 @@ class SatinColumn(EmbroideryElement):
return self.get_boolean_param("contour_underlay")
@property
- @param('contour_underlay_stitch_length_mm', 'Stitch length', unit='mm', group='Contour Underlay', type='float')
+ @param('contour_underlay_stitch_length_mm', 'Stitch length', unit='mm', group='Contour Underlay', type='float', default=1.5)
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")
+ return self.get_float_param("contour_underlay_stitch_length_mm", 1.5)
@property
- @param('contour_underlay_inset_mm', 'Contour underlay inset amount', unit='mm', group='Contour Underlay', type='float')
+ @param('contour_underlay_inset_mm', 'Contour underlay inset amount', unit='mm', group='Contour Underlay', type='float', default=0.4)
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)
@@ -1357,10 +1045,9 @@ class SatinColumn(EmbroideryElement):
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')
+ @param('center_walk_underlay_stitch_length_mm', 'Stitch length', unit='mm', group='Center-Walk Underlay', type='float', default=1.5)
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")
+ return self.get_float_param("center_walk_underlay_stitch_length_mm", 1.5)
@property
@param('zigzag_underlay', 'Zig-zag underlay', type='toggle', group='Zig-zag Underlay')
@@ -1368,10 +1055,9 @@ class SatinColumn(EmbroideryElement):
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')
+ @param('zigzag_underlay_spacing_mm', 'Zig-Zag spacing (peak-to-peak)', unit='mm', group='Zig-zag Underlay', type='float', default=3)
def zigzag_underlay_spacing(self):
- # peak-to-peak distance between zigzags in zigzag underlay
- return self.get_float_param("zigzag_underlay_spacing_mm", 1)
+ return self.get_float_param("zigzag_underlay_spacing_mm", 3)
@property
@param('zigzag_underlay_inset_mm', 'Inset amount (default: half of contour underlay inset)', unit='mm', group='Zig-zag Underlay', type='float')
@@ -1429,7 +1115,7 @@ class SatinColumn(EmbroideryElement):
print >> dbg, [str(rail) for rail in rails], [str(rung) for rung in rungs]
self.fatal("Expected %d linestrings, got %d" % (len(rungs.geoms) + 1, len(linestrings.geoms)))
- paths = [[PyEmb.Point(*coord) for coord in ls.coords] for ls in linestrings.geoms]
+ paths = [[inkstitch.Point(*coord) for coord in ls.coords] for ls in linestrings.geoms]
result.append(paths)
return zip(*result)
@@ -1453,7 +1139,7 @@ class SatinColumn(EmbroideryElement):
# iterate over pairs of 3-tuples
for prev, current in zip(path[:-1], path[1:]):
flattened_segment = self.flatten([[prev, current]])
- flattened_segment = [PyEmb.Point(x, y) for x, y in flattened_segment[0]]
+ flattened_segment = [inkstitch.Point(x, y) for x, y in flattened_segment[0]]
flattened_path.append(flattened_segment)
paths.append(flattened_path)
@@ -1762,7 +1448,7 @@ class Polyline(EmbroideryElement):
patch = Patch(color=self.color)
for stitch in self.stitches:
- patch.add_stitch(PyEmb.Point(*stitch))
+ patch.add_stitch(inkstitch.Point(*stitch))
return [patch]
@@ -1792,25 +1478,6 @@ def detect_classes(node):
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, trim_after=False, stop_after=False):
self.color = color
@@ -1872,7 +1539,7 @@ def process_trim(stitches, next_stitch):
for i in xrange(3):
pos += delta
- stitches.append(PyEmb.Stitch(pos.x, pos.y, stitches[-1].color, jump=True))
+ stitches.append(inkstitch.Stitch(pos.x, pos.y, stitches[-1].color, jump=True))
# first one should be TRIM instead of JUMP
stitches[-3].jump = False
@@ -1906,16 +1573,16 @@ def patches_to_stitches(patch_list, collapse_len_px=0):
if stitches and last_color and last_color != patch.color:
# add a color change
- stitches.append(PyEmb.Stitch(last_stitch.x, last_stitch.y, last_color, stop=True))
+ stitches.append(inkstitch.Stitch(last_stitch.x, last_stitch.y, last_color, stop=True))
if need_trim:
process_trim(stitches, stitch)
need_trim = False
if jump_stitch:
- stitches.append(PyEmb.Stitch(stitch.x, stitch.y, patch.color, jump=True))
+ stitches.append(inkstitch.Stitch(stitch.x, stitch.y, patch.color, jump=True))
- stitches.append(PyEmb.Stitch(stitch.x, stitch.y, patch.color, jump=False))
+ stitches.append(inkstitch.Stitch(stitch.x, stitch.y, patch.color, jump=False))
jump_stitch = False
last_stitch = stitch
@@ -1971,35 +1638,37 @@ def emit_inkscape(parent, stitches):
'transform': transform
})
+def get_elements(effect):
+ elements = []
+ nodes = get_nodes(effect)
+
+ for node in nodes:
+ classes = detect_classes(node)
+ elements.extend(cls(node) for cls in classes)
+
+ return elements
+
+
+def elements_to_patches(elements):
+ patches = []
+ for element in elements:
+ if patches:
+ last_patch = patches[-1]
+ else:
+ last_patch = None
+
+ patches.extend(element.embroider(last_patch))
+
+ return patches
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"],
@@ -2021,15 +1690,7 @@ class Embroider(inkex.Effect):
action="store", type="int",
dest="max_backups", default=5,
help="Max number of backups of output files to keep.")
- 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)
+ self.OptionParser.usage += "\n\nSeeing a 'no such option' message? Please restart Inkscape to fix."
def get_output_path(self):
if self.options.output_file:
@@ -2073,22 +1734,7 @@ class Embroider(inkex.Effect):
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()
+ self.elements = get_elements(self)
if not self.elements:
if self.selected:
@@ -2101,17 +1747,9 @@ class Embroider(inkex.Effect):
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.embroider(last_patch))
-
+ patches = elements_to_patches(self.elements)
stitches = patches_to_stitches(patches, self.options.collapse_length_mm * PIXELS_PER_MM)
- PyEmb.write_embroidery_file(self.get_output_path(), stitches)
+ inkstitch.write_embroidery_file(self.get_output_path(), stitches)
new_layer = inkex.etree.SubElement(self.document.getroot(), SVG_GROUP_TAG, {})
new_layer.set('id', self.uniqueId("embroidery"))
diff --git a/embroider_params.py b/embroider_params.py
index 78b5001cf..b53a586d8 100644
--- a/embroider_params.py
+++ b/embroider_params.py
@@ -13,7 +13,8 @@ import wx
from wx.lib.scrolledpanel import ScrolledPanel
from collections import defaultdict
import inkex
-from embroider import Param, EmbroideryElement, Fill, AutoFill, Stroke, SatinColumn, descendants
+from inkstitch import Param, EmbroideryElement, get_nodes
+from embroider import Fill, AutoFill, Stroke, SatinColumn
from functools import partial
from itertools import groupby
from embroider_simulate import EmbroiderySimulator
@@ -307,7 +308,7 @@ class ParamsTab(ScrolledPanel):
input.Bind(wx.EVT_TEXT, self.changed)
else:
value = param.values[0] if param.values else ""
- input = wx.TextCtrl(self, wx.ID_ANY, value=value)
+ input = wx.TextCtrl(self, wx.ID_ANY, value=str(value))
input.Bind(wx.EVT_TEXT, self.changed)
self.param_inputs[param.name] = input
@@ -328,7 +329,8 @@ class SettingsFrame(wx.Frame):
self.tabs_factory = kwargs.pop('tabs_factory', [])
self.cancel_hook = kwargs.pop('on_cancel', None)
wx.Frame.__init__(self, None, wx.ID_ANY,
- "Embroidery Params"
+ "Embroidery Params",
+ pos=wx.Point(0,0)
)
self.notebook = wx.Notebook(self, wx.ID_ANY)
self.tabs = self.tabs_factory(self.notebook)
@@ -623,17 +625,6 @@ class EmbroiderParams(inkex.Effect):
self.cancelled = False
inkex.Effect.__init__(self, *args, **kwargs)
- def get_nodes(self):
- if self.selected:
- nodes = []
- for node in self.document.getroot().iter():
- if node.get("id") in self.selected:
- nodes.extend(descendants(node))
- else:
- nodes = descendants(self.document.getroot())
-
- return nodes
-
def embroidery_classes(self, node):
element = EmbroideryElement(node)
classes = []
@@ -651,10 +642,10 @@ class EmbroiderParams(inkex.Effect):
return classes
def get_nodes_by_class(self):
- nodes = self.get_nodes()
+ nodes = get_nodes(self)
nodes_by_class = defaultdict(list)
- for z, node in enumerate(self.get_nodes()):
+ for z, node in enumerate(nodes):
for cls in self.embroidery_classes(node):
element = cls(node)
element.order = z
diff --git a/embroider_simulate.py b/embroider_simulate.py
index 4b772692b..667ef7d0e 100644
--- a/embroider_simulate.py
+++ b/embroider_simulate.py
@@ -6,7 +6,8 @@ import inkex
import simplestyle
import colorsys
-from embroider import patches_to_stitches, stitches_to_polylines, PIXELS_PER_MM
+from inkstitch import PIXELS_PER_MM
+from embroider import patches_to_stitches, get_elements, elements_to_patches
class EmbroiderySimulator(wx.Frame):
def __init__(self, *args, **kwargs):
@@ -134,12 +135,25 @@ class EmbroiderySimulator(wx.Frame):
last_pos = None
last_color = None
pen = None
+ trimming = False
for stitch in stitches:
+ if stitch.trim:
+ trimming = True
+ last_pos = None
+ continue
+
+ if trimming:
+ if stitch.jump:
+ continue
+ else:
+ trimming = False
+
pos = (stitch.x, stitch.y)
if stitch.color == last_color:
- segments.append(((last_pos, pos), pen))
+ if last_pos:
+ segments.append(((last_pos, pos), pen))
else:
pen = self.color_to_pen(simplestyle.parseColor(stitch.color))
@@ -148,55 +162,6 @@ class EmbroiderySimulator(wx.Frame):
return segments
- def _parse_stitch_file(self, stitch_file_path):
- # "#", "comment"
- # "$","1","229","229","229","(null)","(null)"
- # "*","JUMP","1.595898","48.731899"
- # "*","STITCH","1.595898","48.731899"
-
- segments = []
-
- pos = (0, 0)
- pen = wx.Pen('black')
- cut = True
-
- with open(stitch_file_path) as stitch_file:
- for line in stitch_file:
- line = line.strip()
- if not line:
- continue
-
- fields = line.strip().split(",")
- fields = [self._strip_quotes(field) for field in fields]
-
- symbol, command = fields[:2]
-
- if symbol == "$":
- red, green, blue = fields[2:5]
- pen = self.color_to_pen((int(red), int(green), int(blue)))
- elif symbol == "*":
- if command == "COLOR":
- # change color
- # The next command should be a JUMP, and we'll need to skip stitching.
- cut = True
- elif command == "JUMP" or command == "STITCH":
- # JUMP just means a long stitch, really.
-
- x, y = fields[2:]
- new_pos = (float(x) * PIXELS_PER_MM, float(y) * PIXELS_PER_MM)
-
- if not segments and new_pos == (0.0, 0.0):
- # libembroidery likes to throw an extra JUMP in at the start
- continue
-
- if not cut:
- segments.append(((pos, new_pos), pen))
-
- cut = False
- pos = new_pos
-
- return segments
-
def all_coordinates(self):
for segment in self.segments:
start, end = segment[0]
@@ -330,20 +295,14 @@ class SimulateEffect(inkex.Effect):
help="Directory in which to store output file")
def effect(self):
+ patches = elements_to_patches(get_elements(self))
app = wx.App()
- frame = EmbroiderySimulator(None, -1, "Embroidery Simulation", wx.DefaultPosition, size=(1000, 1000), stitch_file=self.get_stitch_file())
+ frame = EmbroiderySimulator(None, -1, "Embroidery Simulation", wx.DefaultPosition, size=(1000, 1000), patches=patches)
app.SetTopWindow(frame)
frame.Show()
wx.CallAfter(frame.go)
app.MainLoop()
- def get_stitch_file(self):
- svg_filename = self.document.getroot().get(inkex.addNS('docname', 'sodipodi'))
- csv_filename = svg_filename.replace('.svg', '.csv')
- stitch_file = os.path.join(self.options.path, csv_filename)
-
- return stitch_file
-
if __name__ == "__main__":
effect = SimulateEffect()
diff --git a/inkstitch.py b/inkstitch.py
new file mode 100644
index 000000000..06e26bb34
--- /dev/null
+++ b/inkstitch.py
@@ -0,0 +1,514 @@
+#!/usr/bin/env python
+# http://www.achatina.de/sewing/main/TECHNICL.HTM
+
+import sys
+from copy import deepcopy
+import math
+import libembroidery
+import inkex
+import simplepath
+import simplestyle
+import simpletransform
+from bezmisc import bezierlength, beziertatlength, bezierpointatt
+from cspsubdiv import cspsubdiv
+import cubicsuperpath
+
+
+try:
+ from functools import lru_cache
+except ImportError:
+ from backports.functools_lru_cache import lru_cache
+
+# modern versions of Inkscape use 96 pixels per inch as per the CSS standard
+PIXELS_PER_MM = 96 / 25.4
+
+SVG_PATH_TAG = inkex.addNS('path', 'svg')
+SVG_POLYLINE_TAG = inkex.addNS('polyline', 'svg')
+SVG_DEFS_TAG = inkex.addNS('defs', 'svg')
+SVG_GROUP_TAG = inkex.addNS('g', 'svg')
+
+EMBROIDERABLE_TAGS = (SVG_PATH_TAG, SVG_POLYLINE_TAG)
+
+dbg = open("/tmp/embroider-debug.txt", "w")
+
+# simplify use of lru_cache decorator
+def cache(*args, **kwargs):
+ return lru_cache(maxsize=None)(*args, **kwargs)
+
+# cribbed from inkscape-silhouette
+def parse_length_with_units( str ):
+
+ '''
+ Parse an SVG value which may or may not have units attached
+ This version is greatly simplified in that it only allows: no units,
+ units of px, mm, and %. Everything else, it returns None for.
+ There is a more general routine to consider in scour.py if more
+ generality is ever needed.
+ '''
+
+ u = 'px'
+ s = str.strip()
+ if s[-2:] == 'px':
+ s = s[:-2]
+ elif s[-2:] == 'mm':
+ u = 'mm'
+ s = s[:-2]
+ elif s[-2:] == 'pt':
+ u = 'pt'
+ s = s[:-2]
+ elif s[-2:] == 'pc':
+ u = 'pc'
+ s = s[:-2]
+ elif s[-2:] == 'cm':
+ u = 'cm'
+ s = s[:-2]
+ elif s[-2:] == 'in':
+ u = 'in'
+ s = s[:-2]
+ elif s[-1:] == '%':
+ u = '%'
+ s = s[:-1]
+ try:
+ v = float( s )
+ except:
+ raise ValueError("parseLengthWithUnits: unknown unit %s" % s)
+
+ return v, u
+
+
+def convert_length(length):
+ value, units = parse_length_with_units(length)
+
+ if not units or units == "px":
+ return value
+
+ if units == 'cm':
+ value *= 10
+ units == 'mm'
+
+ if units == 'mm':
+ value = value / 25.4
+ units = 'in'
+
+ if units == 'in':
+ # modern versions of Inkscape use CSS's 96 pixels per inch. When you
+ # open an old document, inkscape will add a viewbox for you.
+ return value * 96
+
+ raise ValueError("Unknown unit: %s" % units)
+
+
+@cache
+def get_viewbox_transform(node):
+ # somewhat cribbed from inkscape-silhouette
+
+ doc_width = convert_length(node.get('width'))
+ doc_height = convert_length(node.get('height'))
+
+ viewbox = node.get('viewBox').strip().replace(',', ' ').split()
+
+ dx = -float(viewbox[0])
+ dy = -float(viewbox[1])
+ transform = simpletransform.parseTransform("translate(%f, %f)" % (dx, dy))
+
+ try:
+ sx = doc_width / float(viewbox[2])
+ sy = doc_height / float(viewbox[3])
+ scale_transform = simpletransform.parseTransform("scale(%f, %f)" % (sx, sy))
+ transform = simpletransform.composeTransform(transform, scale_transform)
+ except ZeroDivisionError:
+ pass
+
+ return transform
+
+class Param(object):
+ def __init__(self, name, description, unit=None, values=[], type=None, group=None, inverse=False, default=None, tooltip=None, sort_index=0):
+ 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
+ self.tooltip = tooltip
+ self.sort_index = sort_index
+
+ 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
+
+
+class EmbroideryElement(object):
+ def __init__(self, node):
+ self.node = node
+
+ @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()
+
+ return value or default
+
+ @cache
+ def get_boolean_param(self, param, default=None):
+ value = self.get_param(param, default)
+
+ if isinstance(value, bool):
+ return value
+ else:
+ return value and (value.lower() in ('yes', 'y', 'true', 't', '1'))
+
+ @cache
+ def get_float_param(self, param, default=None):
+ try:
+ value = float(self.get_param(param, default))
+ except (TypeError, ValueError):
+ return default
+
+ if param.endswith('_mm'):
+ value = value * PIXELS_PER_MM
+
+ return value
+
+ @cache
+ def get_int_param(self, param, default=None):
+ try:
+ value = int(self.get_param(param, default))
+ except (TypeError, ValueError):
+ return default
+
+ if param.endswith('_mm'):
+ value = int(value * PIXELS_PER_MM)
+
+ return value
+
+ def set_param(self, name, value):
+ self.node.set("embroider_%s" % name, str(value))
+
+ @cache
+ def get_style(self, style_name):
+ style = simplestyle.parseStyle(self.node.get("style"))
+ if (style_name not in style):
+ return None
+ value = style[style_name]
+ if value == 'none':
+ return None
+ return value
+
+ @cache
+ def has_style(self, style_name):
+ style = simplestyle.parseStyle(self.node.get("style"))
+ return style_name in style
+
+ @property
+ def path(self):
+ return cubicsuperpath.parsePath(self.node.get("d"))
+
+
+ @cache
+ def parse_path(self):
+ # A CSP is a "cubic superpath".
+ #
+ # A "path" is a sequence of strung-together bezier curves.
+ #
+ # A "superpath" is a collection of paths that are all in one object.
+ #
+ # The "cubic" bit in "cubic superpath" is because the bezier curves
+ # inkscape uses involve cubic polynomials.
+ #
+ # Each path is a collection of tuples, each of the form:
+ #
+ # (control_before, point, control_after)
+ #
+ # A bezier curve segment is defined by an endpoint, a control point,
+ # a second control point, and a final endpoint. A path is a bunch of
+ # bezier curves strung together. One could represent a path as a set
+ # of four-tuples, but there would be redundancy because the ending
+ # point of one bezier is the starting point of the next. Instead, a
+ # path is a set of 3-tuples as shown above, and one must construct
+ # each bezier curve by taking the appropriate endpoints and control
+ # points. Bleh. It should be noted that a straight segment is
+ # represented by having the control point on each end equal to that
+ # end's point.
+ #
+ # In a path, each element in the 3-tuple is itself a tuple of (x, y).
+ # Tuples all the way down. Hasn't anyone heard of using classes?
+
+ path = self.path
+
+ # start with the identity transform
+ transform = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]
+
+ # combine this node's transform with all parent groups' transforms
+ transform = simpletransform.composeParents(self.node, transform)
+
+ # add in the transform implied by the viewBox
+ viewbox_transform = get_viewbox_transform(self.node.getroottree().getroot())
+ transform = simpletransform.composeTransform(viewbox_transform, transform)
+
+ # apply the combined transform to this node's path
+ simpletransform.applyTransformToPath(transform, path)
+
+
+ return path
+
+ def flatten(self, path):
+ """approximate a path containing beziers with a series of points"""
+
+ path = deepcopy(path)
+
+ cspsubdiv(path, 0.1)
+
+ flattened = []
+
+ for comp in path:
+ vertices = []
+ for ctl in comp:
+ vertices.append((ctl[1][0], ctl[1][1]))
+ flattened.append(vertices)
+
+ return flattened
+
+ @property
+ @param('trim_after',
+ 'TRIM after',
+ tooltip='Trim thread after this object (for supported machines and file formats)',
+ type='boolean',
+ default=False,
+ sort_index=1000)
+ def trim_after(self):
+ return self.get_boolean_param('trim_after', False)
+
+ @property
+ @param('stop_after',
+ 'STOP after',
+ tooltip='Add STOP instruction after this object (for supported machines and file formats)',
+ type='boolean',
+ default=False,
+ sort_index=1000)
+ def stop_after(self):
+ return self.get_boolean_param('stop_after', False)
+
+ def to_patches(self, last_patch):
+ raise NotImplementedError("%s must implement to_patches()" % self.__class__.__name__)
+
+ def embroider(self, last_patch):
+ patches = self.to_patches(last_patch)
+
+ if patches:
+ patches[-1].trim_after = self.trim_after
+ patches[-1].stop_after = self.stop_after
+
+ return patches
+
+ def fatal(self, message):
+ print >> sys.stderr, "error:", message
+ sys.exit(1)
+
+
+class Point:
+ def __init__(self, x, y):
+ self.x = x
+ self.y = y
+
+ def __add__(self, other):
+ return Point(self.x + other.x, self.y + other.y)
+
+ def __sub__(self, other):
+ return Point(self.x - other.x, self.y - other.y)
+
+ def mul(self, scalar):
+ return Point(self.x * scalar, self.y * scalar)
+
+ def __mul__(self, other):
+ if isinstance(other, Point):
+ # dot product
+ return self.x * other.x + self.y * other.y
+ elif isinstance(other, (int, float)):
+ return Point(self.x * other, self.y * other)
+ else:
+ raise ValueError("cannot multiply Point by %s" % type(other))
+
+ def __rmul__(self, other):
+ if isinstance(other, (int, float)):
+ return self.__mul__(other)
+ else:
+ raise ValueError("cannot multiply Point by %s" % type(other))
+
+ def __repr__(self):
+ return "Point(%s,%s)" % (self.x, self.y)
+
+ def length(self):
+ return math.sqrt(math.pow(self.x, 2.0) + math.pow(self.y, 2.0))
+
+ def unit(self):
+ return self.mul(1.0 / self.length())
+
+ def rotate_left(self):
+ return Point(-self.y, self.x)
+
+ def rotate(self, angle):
+ return Point(self.x * math.cos(angle) - self.y * math.sin(angle), self.y * math.cos(angle) + self.x * math.sin(angle))
+
+ def as_int(self):
+ return Point(int(round(self.x)), int(round(self.y)))
+
+ def as_tuple(self):
+ return (self.x, self.y)
+
+ def __cmp__(self, other):
+ return cmp(self.as_tuple(), other.as_tuple())
+
+ def __getitem__(self, item):
+ return self.as_tuple()[item]
+
+ def __len__(self):
+ return 2
+
+
+class Stitch(Point):
+ def __init__(self, x, y, color=None, jump=False, stop=False, trim=False):
+ self.x = x
+ self.y = y
+ self.color = color
+ self.jump = jump
+ self.trim = trim
+ self.stop = stop
+
+ def __repr__(self):
+ return "Stitch(%s, %s, %s, %s, %s, %s)" % (self.x, self.y, self.color, "JUMP" if self.jump else "", "TRIM" if self.trim else "", "STOP" if self.stop else "")
+
+
+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
+
+
+def get_nodes(effect):
+ """Get all XML nodes, or just those selected
+
+ effect is an instance of a subclass of inkex.Effect.
+ """
+
+ if effect.selected:
+ nodes = []
+ for node in effect.document.getroot().iter():
+ if node.get("id") in effect.selected:
+ nodes.extend(descendants(node))
+ else:
+ nodes = descendants(effect.document.getroot())
+
+ return nodes
+
+
+def make_thread(color):
+ # strip off the leading "#"
+ if color.startswith("#"):
+ color = color[1:]
+
+ thread = libembroidery.EmbThread()
+ thread.color = libembroidery.embColor_fromHexStr(color)
+
+ thread.description = color
+ thread.catalogNumber = ""
+
+ return thread
+
+def add_thread(pattern, thread):
+ """Add a thread to a pattern and return the thread's index"""
+
+ libembroidery.embPattern_addThread(pattern, thread)
+
+ return libembroidery.embThreadList_count(pattern.threadList) - 1
+
+def get_flags(stitch):
+ flags = 0
+
+ if stitch.jump:
+ flags |= libembroidery.JUMP
+
+ if stitch.trim:
+ flags |= libembroidery.TRIM
+
+ if stitch.stop:
+ flags |= libembroidery.STOP
+
+ return flags
+
+def write_embroidery_file(file_path, stitches):
+ # Embroidery machines don't care about our canvas size, so we relocate the
+ # design to the origin. It might make sense to center it about the origin
+ # instead.
+ min_x = min(stitch.x for stitch in stitches)
+ min_y = min(stitch.y for stitch in stitches)
+
+ pattern = libembroidery.embPattern_create()
+ threads = {}
+
+ last_color = None
+
+ for stitch in stitches:
+ if stitch.color != last_color:
+ if stitch.color not in threads:
+ thread = make_thread(stitch.color)
+ thread_index = add_thread(pattern, thread)
+ threads[stitch.color] = thread_index
+ else:
+ thread_index = threads[stitch.color]
+
+ libembroidery.embPattern_changeColor(pattern, thread_index)
+ last_color = stitch.color
+
+ flags = get_flags(stitch)
+ libembroidery.embPattern_addStitchAbs(pattern, stitch.x - min_x, stitch.y - min_y, flags, 0)
+
+ libembroidery.embPattern_addStitchAbs(pattern, stitch.x - min_x, stitch.y - min_y, libembroidery.END, 0)
+
+ # convert from pixels to millimeters
+ libembroidery.embPattern_scale(pattern, 1/PIXELS_PER_MM)
+
+ # SVG and embroidery disagree on the direction of the Y axis
+ libembroidery.embPattern_flipVertical(pattern)
+
+ libembroidery.embPattern_write(pattern, file_path)