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)