diff options
Diffstat (limited to 'embroider.py')
| -rw-r--r-- | embroider.py | 755 |
1 files changed, 614 insertions, 141 deletions
diff --git a/embroider.py b/embroider.py index c207c670..f9c5d147 100644 --- a/embroider.py +++ b/embroider.py @@ -24,7 +24,8 @@ import os import subprocess from copy import deepcopy import time -from itertools import chain, izip +from itertools import chain, izip, groupby +from collections import deque import inkex import simplepath import simplestyle @@ -37,6 +38,7 @@ import lxml.etree as etree import shapely.geometry as shgeo import shapely.affinity as affinity import shapely.ops +import networkx from pprint import pformat import PyEmb @@ -46,9 +48,12 @@ 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) + class Param(object): def __init__(self, name, description, unit=None, values=[], type=None, group=None, inverse=False, default=None): @@ -157,6 +162,11 @@ class EmbroideryElement(object): 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". @@ -186,9 +196,7 @@ class EmbroideryElement(object): # 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 = cubicsuperpath.parsePath(self.node.get("d")) - - # print >> sys.stderr, pformat(path) + path = self.path # start with the identity transform transform = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]] @@ -288,7 +296,8 @@ class Fill(EmbroideryElement): last_pt = pt else: last_pt = pt - poly_ary.append(point_ary) + if point_ary: + poly_ary.append(point_ary) # shapely's idea of "holes" are to subtract everything in the second set # from the first. So let's at least make sure the "first" thing is the @@ -309,8 +318,11 @@ class Fill(EmbroideryElement): def north(self, angle): return self.east(angle).rotate(math.pi / 2) + def row_num(self, point, angle, row_spacing): + return round((point * self.north(angle)) / row_spacing) + def adjust_stagger(self, stitch, angle, row_spacing, max_stitch_length): - row_num = round((stitch * self.north(angle)) / row_spacing) + row_num = self.row_num(stitch, angle, row_spacing) row_stagger = row_num % self.staggers stagger_offset = (float(row_stagger) / self.staggers) * max_stitch_length offset = ((stitch * self.east(angle)) - stagger_offset) % max_stitch_length @@ -448,6 +460,55 @@ class Fill(EmbroideryElement): return runs + def stitch_row(self, patch, beg, end, angle, row_spacing, max_stitch_length): + # We want our stitches to look like this: + # + # ---*-----------*----------- + # ------*-----------*-------- + # ---------*-----------*----- + # ------------*-----------*-- + # ---*-----------*----------- + # + # Each successive row of stitches will be staggered, with + # num_staggers rows before the pattern repeats. A value of + # 4 gives a nice fill while hiding the needle holes. The + # first row is offset 0%, the second 25%, the third 50%, and + # the fourth 75%. + # + # Actually, instead of just starting at an offset of 0, we + # can calculate a row's offset relative to the origin. This + # way if we have two abutting fill regions, they'll perfectly + # tile with each other. That's important because we often get + # abutting fill regions from pull_runs(). + + + beg = PyEmb.Point(*beg) + end = PyEmb.Point(*end) + + row_direction = (end - beg).unit() + segment_length = (end - beg).length() + + # only stitch the first point if it's a reasonable distance away from the + # last stitch + if not patch.stitches or (beg - patch.stitches[-1]).length() > 0.5 * self.options.pixels_per_mm: + patch.add_stitch(beg) + + first_stitch = self.adjust_stagger(beg, angle, row_spacing, max_stitch_length) + + # we might have chosen our first stitch just outside this row, so move back in + if (first_stitch - beg) * row_direction < 0: + first_stitch += row_direction * max_stitch_length + + offset = (first_stitch - beg).length() + + while offset < segment_length: + patch.add_stitch(beg + offset * row_direction) + offset += max_stitch_length + + if (end - patch.stitches[-1]).length() > 0.1 * self.options.pixels_per_mm: + patch.add_stitch(end) + + def section_to_patch(self, group_of_segments, angle=None, row_spacing=None, max_stitch_length=None): if max_stitch_length is None: max_stitch_length = self.max_stitch_length @@ -466,58 +527,13 @@ class Fill(EmbroideryElement): last_end = None for segment in group_of_segments: - # We want our stitches to look like this: - # - # ---*-----------*----------- - # ------*-----------*-------- - # ---------*-----------*----- - # ------------*-----------*-- - # ---*-----------*----------- - # - # Each successive row of stitches will be staggered, with - # num_staggers rows before the pattern repeats. A value of - # 4 gives a nice fill while hiding the needle holes. The - # first row is offset 0%, the second 25%, the third 50%, and - # the fourth 75%. - # - # Actually, instead of just starting at an offset of 0, we - # can calculate a row's offset relative to the origin. This - # way if we have two abutting fill regions, they'll perfectly - # tile with each other. That's important because we often get - # abutting fill regions from pull_runs(). - (beg, end) = segment if (swap): (beg, end) = (end, beg) - beg = PyEmb.Point(*beg) - end = PyEmb.Point(*end) - - row_direction = (end - beg).unit() - segment_length = (end - beg).length() + self.stitch_row(patch, beg, end, angle, row_spacing, max_stitch_length) - # only stitch the first point if it's a reasonable distance away from the - # last stitch - if last_end is None or (beg - last_end).length() > 0.5 * self.options.pixels_per_mm: - patch.add_stitch(beg) - - first_stitch = self.adjust_stagger(beg, angle, row_spacing, max_stitch_length) - - # we might have chosen our first stitch just outside this row, so move back in - if (first_stitch - beg) * row_direction < 0: - first_stitch += row_direction * max_stitch_length - - offset = (first_stitch - beg).length() - - while offset < segment_length: - patch.add_stitch(beg + offset * row_direction) - offset += max_stitch_length - - if (end - patch.stitches[-1]).length() > 0.1 * self.options.pixels_per_mm: - patch.add_stitch(end) - - last_end = end swap = not swap return patch @@ -529,6 +545,9 @@ class Fill(EmbroideryElement): return [self.section_to_patch(group) for group in groups_of_segments] +class MaxQueueLengthExceeded(Exception): + pass + class AutoFill(Fill): @property @param('auto_fill', 'Automatically routed fill stitching', type='toggle', default=True) @@ -580,27 +599,358 @@ class AutoFill(Fill): @param('fill_underlay_max_stitch_length_mm', 'Max stitch length', unit='mm', group='AutoFill Underlay', type='float') @cache def fill_underlay_max_stitch_length(self): - return self.get_float_param("fill_underlay_max_stitch_length_mm" or self.max_stitch_length) + return self.get_float_param("fill_underlay_max_stitch_length_mm") or self.max_stitch_length - def validate(self): - if len(self.shape.boundary) > 1: - self.fatal("auto-fill: object %s cannot be auto-filled because it has one or more holes. Please disable auto-fill for this object or break it into separate objects without holes." % self.node.get('id')) + def which_outline(self, coords): + """return the index of the outline on which the point resides - def is_same_run(self, segment1, segment2): - if shgeo.Point(segment1[0]).distance(shgeo.Point(segment2[0])) > self.max_stitch_length: - return False + Index 0 is the outer boundary of the fill region. 1+ are the + outlines of the holes. + """ - if shgeo.Point(segment1[1]).distance(shgeo.Point(segment2[1])) > self.max_stitch_length: - return False + # I'd use an intersection check, but floating point errors make it + # fail sometimes. + + point = shgeo.Point(*coords) + outlines = list(enumerate(self.shape.boundary)) + closest = min(outlines, key=lambda (index, outline): outline.distance(point)) + + return closest[0] + + def project(self, coords, outline_index): + """project the point onto the specified outline + + This returns the distance along the outline at which the point resides. + """ + + return self.shape.boundary.project(shgeo.Point(*coords)) + + def build_graph(self, segments, angle, row_spacing): + """build a graph representation of the grating segments + + This function builds a specialized graph (as in graph theory) that will + help us determine a stitching path. The idea comes from this paper: + + http://www.sciencedirect.com/science/article/pii/S0925772100000158 + + The goal is to build a graph that we know must have an Eulerian Path. + An Eulerian Path is a path from edge to edge in the graph that visits + every edge exactly once and ends at the node it started at. Algorithms + exist to build such a path, and we'll use Hierholzer's algorithm. + + A graph must have an Eulerian Path if every node in the graph has an + even number of edges touching it. Our goal here is to build a graph + that will have this property. + + Based on the paper linked above, we'll build the graph as follows: + + * nodes are the endpoints of the grating segments, where they meet + with the outer outline of the region the outlines of the interior + holes in the region. + * edges are: + * each section of the outer and inner outlines of the region, + between nodes + * double every other edge in the outer and inner hole outlines + + Doubling up on some of the edges seems as if it will just mean we have + to stitch those spots twice. This may be true, but it also ensures + that every node has 4 edges touching it, ensuring that a valid stitch + path must exist. + """ + + graph = networkx.MultiGraph() + + # First, add the grating segments as edges. We'll use the coordinates + # of the endpoints as nodes, which networkx will add automatically. + for segment in segments: + # networkx allows us to label nodes with arbitrary data. We'll + # mark this one as a grating segment. + graph.add_edge(*segment, key="segment") + + for node in graph.nodes(): + outline_index = self.which_outline(node) + outline_projection = self.project(node, outline_index) + + # Tag each node with its index and projection. + graph.add_node(node, index=outline_index, projection=outline_projection) + + nodes = graph.nodes(data=True) + nodes.sort(key=lambda node: (node[1]['index'], node[1]['projection'])) + + for outline_index, nodes in groupby(nodes, key=lambda node: node[1]['index']): + nodes = [ node for node, data in nodes ] + + # heuristic: change the order I visit the nodes in the outline if necessary. + # If the start and endpoints are in the same row, I can't tell which row + # I should treat it as being in. + while True: + row0 = self.row_num(PyEmb.Point(*nodes[0]), angle, row_spacing) + row1 = self.row_num(PyEmb.Point(*nodes[1]), angle, row_spacing) + + if row0 == row1: + nodes = nodes[1:] + [nodes[0]] + else: + break + + # heuristic: it's useful to try to keep the duplicated edges in the same rows. + # this prevents the BFS from having to search a ton of edges. + row_num = min(row0, row1) + if row_num % 2 == 0: + edge_set = 0 + 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) + + # add an edge between each successive node + for i, (node1, node2) in enumerate(zip(nodes, nodes[1:] + [nodes[0]])): + graph.add_edge(node1, node2, key="outline") + + # 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)) + + # duplicate every other edge around this outline + if i % 2 == edge_set: + graph.add_edge(node1, node2, key="extra") + + + if not networkx.is_eulerian(graph): + raise Exception("something went wrong: graph is not eulerian") + + return graph + + def node_list_to_edge_list(self, node_list): + return zip(node_list[:-1], node_list[1:]) + + def bfs_for_loop(self, graph, starting_node, max_queue_length=2000): + to_search = deque() + to_search.appendleft(([starting_node], set(), 0)) + + while to_search: + if len(to_search) > max_queue_length: + raise MaxQueueLengthExceeded() + + path, visited_edges, visited_segments = to_search.pop() + ending_node = path[-1] + + # get a list of neighbors paired with the key of the edge I can follow to get there + neighbors = [ + (node, key) + for node, adj in graph.adj[ending_node].iteritems() + for key in adj + ] + + # heuristic: try grating segments first + neighbors.sort(key=lambda (dest, key): key == "segment", reverse=True) + + for next_node, key in neighbors: + # skip if I've already followed this edge + edge = (tuple(sorted((ending_node, next_node))), key) + if edge in visited_edges: + continue + + new_path = path + [next_node] + + if key == "segment": + new_visited_segments = visited_segments + 1 + else: + new_visited_segments = visited_segments + + if next_node == starting_node: + # ignore trivial loops (down and back a doubled edge) + if len(new_path) > 3: + return self.node_list_to_edge_list(new_path), new_visited_segments + + new_visited_edges = visited_edges.copy() + new_visited_edges.add(edge) + + to_search.appendleft((new_path, new_visited_edges, new_visited_segments)) + + def find_loop(self, graph, starting_nodes): + """find a loop in the graph that is connected to the existing path + + Start at a candidate node and search through edges to find a path + back to that node. We'll use a breadth-first search (BFS) in order to + find the shortest available loop. + + In most cases, the BFS should not need to search far to find a loop. + The queue should stay relatively short. + + An added heuristic will be used: if the BFS queue's length becomes + too long, we'll abort and try a different starting point. Due to + the way we've set up the graph, there's bound to be a better choice + somewhere else. + """ + + #loop = self.simple_loop(graph, starting_nodes[-2]) + + #if loop: + # print >> sys.stderr, "simple_loop success" + # starting_nodes.pop() + # starting_nodes.pop() + # return loop + + loop = None + retry = [] + max_queue_length = 2000 + + while not loop: + while not loop and starting_nodes: + starting_node = starting_nodes.pop() + #print >> sys.stderr, "find loop from", starting_node + + try: + # Note: if bfs_for_loop() returns None, no loop can be + # constructed from the starting_node (because the + # necessary edges have already been consumed). In that + # case we discard that node and try the next. + loop = self.bfs_for_loop(graph, starting_node, max_queue_length) + + if not loop: + print >> dbg, "failed on", starting_node + dbg.flush() + except MaxQueueLengthExceeded: + print >> dbg, "gave up on", starting_node + dbg.flush() + # We're giving up on this node for now. We could try + # this node again later, so add it to the bottm of the + # stack. + retry.append(starting_node) + + # Darn, couldn't find a loop. Try harder. + starting_nodes.extendleft(retry) + max_queue_length *= 2 + + starting_nodes.extendleft(retry) + return loop + + def insert_loop(self, path, loop): + """insert a sub-loop into an existing path + + The path will be a series of edges describing a path through the graph + that ends where it starts. The loop will be similar, and its starting + point will be somewhere along the path. + + Insert the loop into the path, resulting in a longer path. + + Both the path and the loop will be a list of edges specified as a + start and end point. The points will be specified in order, such + that they will look like this: + + ((p1, p2), (p2, p3), (p3, p4) ... (pn, p1)) + + path will be modified in place. + """ - return True + loop_start = loop[0][0] - def perimeter_distance(self, p1, p2): - # how far around the perimeter (and in what direction) do I need to go + for i, (start, end) in enumerate(path): + if start == loop_start: + break + + path[i:i] = loop + + def find_stitch_path(self, graph, segments): + """find a path that visits every grating segment exactly once + + Theoretically, we just need to find an Eulerian Path in the graph. + However, we don't actually care whether every single edge is visited. + The edges on the outline of the region are only there to help us get + from one grating segment to the next. + + We'll build a "cycle" (a path that ends where it starts) using + Hierholzer's algorithm. We'll stop once we've visited every grating + segment. + + Hierholzer's algorithm says to select an arbitrary starting node at + each step. In order to produce a reasonable stitch path, we'll select + the vertex carefully such that we get back-and-forth traversal like + mowing a lawn. + + To do this, we'll use a simple heuristic: try to start from nodes in + the order of most-recently-visited first. + """ + + original_graph = graph + graph = graph.copy() + num_segments = len(segments) + segments_visited = 0 + nodes_visited = deque() + + # start with a simple loop: down one segment and then back along the + # outer border to the starting point. + path = [segments[0], list(reversed(segments[0]))] + + graph.remove_edges_from(path) + + segments_visited += 1 + nodes_visited.extend(segments[0]) + + while segments_visited < num_segments: + result = self.find_loop(graph, nodes_visited) + + if not result: + print >> sys.stderr, "Unexpected error filling region. Please send your SVG to lexelby@github." + break + + loop, segments = result + + print >> dbg, "found loop:", loop + dbg.flush() + + segments_visited += segments + nodes_visited += [edge[0] for edge in loop] + graph.remove_edges_from(loop) + + self.insert_loop(path, loop) + + #if segments_visited >= 12: + # break + + # Now we have a loop that covers every grating segment. It returns to + # where it started, which is unnecessary, so we'll snip the last bit off. + #while original_graph.has_edge(*path[-1], key="outline"): + # path.pop() + + return path + + def collapse_sequential_outline_edges(self, graph, path): + """collapse sequential edges that fall on the same outline + + When the path follows multiple edges along the outline of the region, + replace those edges with the starting and ending points. We'll use + these to stitch along the outline later on. + """ + + start_of_run = None + new_path = [] + + for edge in path: + if graph.has_edge(*edge, key="segment"): + if start_of_run: + # close off the last run + new_path.append((start_of_run, edge[0])) + start_of_run = None + + new_path.append(edge) + else: + if not start_of_run: + start_of_run = edge[0] + + if start_of_run: + # if we were still in a run, close it off + new_path.append((start_of_run, edge[1])) + + return new_path + + def outline_distance(self, outline, p1, p2): + # how far around the outline (and in what direction) do I need to go # to get from p1 to p2? - p1_projection = self.outline.project(shgeo.Point(p1)) - p2_projection = self.outline.project(shgeo.Point(p2)) + p1_projection = outline.project(shgeo.Point(p1)) + p2_projection = outline.project(shgeo.Point(p2)) distance = p2_projection - p1_projection @@ -618,91 +968,95 @@ class AutoFill(Fill): else: return distance - def connect_points(self, p1, p2): - patch = Patch(color=self.color) + def connect_points(self, patch, start, end): + outline_index = self.which_outline(start) + outline = self.shape.boundary[outline_index] - pos = self.outline.project(shgeo.Point(p1)) - distance = self.perimeter_distance(p1, p2) + pos = outline.project(shgeo.Point(start)) + distance = self.outline_distance(outline, start, end) stitches = abs(int(distance / self.running_stitch_length)) direction = math.copysign(1.0, distance) one_stitch = self.running_stitch_length * direction + print >> dbg, "connect_points:", start, end, distance, stitches, direction + + patch.add_stitch(PyEmb.Point(*start)) + for i in xrange(stitches): pos = (pos + one_stitch) % self.outline_length - stitch = PyEmb.Point(*self.outline.interpolate(pos).coords[0]) + patch.add_stitch(PyEmb.Point(*outline.interpolate(pos).coords[0])) - # if we're moving along the fill direction, adjust the stitch to - # match the fill so it blends in - if patch.stitches: - if abs((stitch - patch.stitches[-1]) * self.north(self.angle)) < 0.01: - new_stitch = self.adjust_stagger(stitch, self.angle, self.row_spacing, self.max_stitch_length) + end = PyEmb.Point(*end) + if (end - patch.stitches[-1]).length() > 0.1 * self.options.pixels_per_mm: + patch.add_stitch(end) - # don't push the point past the end of this section of the outline - if self.outline.distance(shgeo.Point(new_stitch)) <= 0.01: - stitch = new_stitch + def path_to_patch(self, graph, path, angle, row_spacing, max_stitch_length): + path = self.collapse_sequential_outline_edges(graph, path) - patch.add_stitch(stitch) + patch = Patch(color=self.color) + #patch.add_stitch(PyEmb.Point(*path[0][0])) - return patch + #for edge in path: + # patch.add_stitch(PyEmb.Point(*edge[1])) - def get_corner_points(self, section): - return section[0][0], section[0][-1], section[-1][0], section[-1][-1] + for edge in path: + if graph.has_edge(*edge, key="segment"): + self.stitch_row(patch, edge[0], edge[1], angle, row_spacing, max_stitch_length) + else: + self.connect_points(patch, *edge) + + return patch - def nearest_corner(self, section, point): - return min(self.get_corner_points(section), - key=lambda corner: abs(self.perimeter_distance(point, corner))) + def visualize_graph(self, graph): + patches = [] - def find_nearest_section(self, sections, point): - sections_with_nearest_corner = [(i, self.nearest_corner(section, point)) - for i, section in enumerate(sections)] - return min(sections_with_nearest_corner, - key=lambda(section, corner): abs(self.perimeter_distance(point, corner))) + graph = graph.copy() - def section_from_corner(self, section, start_corner, angle, row_spacing, max_stitch_length): - if start_corner not in section[0]: - section = list(reversed(section)) + for start, end, key in graph.edges_iter(keys=True): + if key == "extra": + patch = Patch(color="#FF0000") + patch.add_stitch(PyEmb.Point(*start)) + patch.add_stitch(PyEmb.Point(*end)) + patches.append(patch) - if section[0][0] != start_corner: - section = [list(reversed(row)) for row in section] + return patches - return self.section_to_patch(section, angle, row_spacing, max_stitch_length) def do_auto_fill(self, angle, row_spacing, max_stitch_length, starting_point=None): + patches = [] + rows_of_segments = self.intersect_region_with_grating(angle, row_spacing) - sections = self.pull_runs(rows_of_segments) + segments = [segment for row in rows_of_segments for segment in row] - patches = [] - last_stitch = starting_point - while sections: - if last_stitch: - section_index, start_corner = self.find_nearest_section(sections, last_stitch) - patches.append(self.connect_points(last_stitch, start_corner)) - patches.append(self.section_from_corner(sections.pop(section_index), start_corner, angle, row_spacing, max_stitch_length)) - else: - patches.append(self.section_to_patch(sections.pop(0), angle, row_spacing, max_stitch_length)) + graph = self.build_graph(segments, angle, row_spacing) + path = self.find_stitch_path(graph, segments) - last_stitch = patches[-1].stitches[-1] + if starting_point: + patch = Patch(self.color) + self.connect_points(patch, starting_point, path[0][0]) + patches.append(patch) + + patches.append(self.path_to_patch(graph, path, angle, row_spacing, max_stitch_length)) return patches - def to_patches(self, last_patch): - print >> dbg, "autofill" - self.validate() + def to_patches(self, last_patch): patches = [] if last_patch is None: - last_stitch = None + starting_point = None else: - last_stitch = last_patch.stitches[-1] + nearest_point = self.outline.interpolate(self.outline.project(shgeo.Point(last_patch.stitches[-1]))) + starting_point = PyEmb.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, last_stitch)) - last_stitch = patches[-1].stitches[-1] + patches.extend(self.do_auto_fill(self.fill_underlay_angle, self.fill_underlay_row_spacing, self.fill_underlay_max_stitch_length, starting_point)) + starting_point = patches[-1].stitches[-1] - patches.extend(self.do_auto_fill(self.angle, self.row_spacing, self.max_stitch_length, last_stitch)) + patches.extend(self.do_auto_fill(self.angle, self.row_spacing, self.max_stitch_length, starting_point)) return patches @@ -909,6 +1263,49 @@ class SatinColumn(EmbroideryElement): @property @cache def flattened_beziers(self): + if len(self.csp) == 2: + return self.simple_flatten_beziers() + else: + return self.flatten_beziers_with_rungs() + + + def flatten_beziers_with_rungs(self): + input_paths = [self.flatten([path]) for path in self.csp] + input_paths = [shgeo.LineString(path[0]) for path in input_paths] + + paths = input_paths[:] + paths.sort(key=lambda path: path.length, reverse=True) + + # Imagine a satin column as a curvy ladder. + # The two long paths are the "rails" of the ladder. The remainder are + # the "rungs". + rails = input_paths[:2] + rungs = shgeo.MultiLineString(input_paths[2:]) + + # The rails should stay in the order they were in the original CSP. + # (this lets the user control where the satin starts and ends) + rails.sort(key=lambda rail: input_paths.index(rail)) + + result = [] + + for rail in rails: + if not rail.is_simple: + self.fatal("One or more rails crosses itself, and this is not allowed. Please split into multiple satin columns.") + + # handle null intersections here? + linestrings = shapely.ops.split(rail, rungs) + + if len(linestrings.geoms) < len(rungs.geoms) + 1: + 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] + result.append(paths) + + return zip(*result) + + + def simple_flatten_beziers(self): # Given a pair of paths made up of bezier segments, flatten # each individual bezier segment into line segments that approximate # the curves. Retain the divisions between beziers -- we'll use those @@ -940,14 +1337,12 @@ class SatinColumn(EmbroideryElement): node_id = self.node.get("id") - if len(self.csp) != 2: - self.fatal("satin column: object %s invalid: expected exactly two sub-paths, but there are %s" % (node_id, len(self.csp))) - if self.get_style("fill") is not None: self.fatal("satin column: object %s has a fill (but should not)" % node_id) - if len(self.csp[0]) != len(self.csp[1]): - self.fatal("satin column: object %s has two paths with an unequal number of points (%s and %s)" % (node_id, len(self.csp[0]), len(self.csp[1]))) + if len(self.csp) == 2: + if len(self.csp[0]) != len(self.csp[1]): + self.fatal("satin column: object %s has two paths with an unequal number of points (%s and %s)" % (node_id, len(self.csp[0]), len(self.csp[1]))) def offset_points(self, pos1, pos2, offset_px): # Expand or contract two points about their midpoint. This is @@ -1173,27 +1568,98 @@ class SatinColumn(EmbroideryElement): return patches -def detect_classes(node): - element = EmbroideryElement(node) +class Polyline(EmbroideryElement): + # Handle a <polyline> element, which is treated as a set of points to + # stitch exactly. + # + # <polyline> elements are pretty rare in SVG, from what I can tell. + # Anything you can do with a <polyline> can also be done with a <p>, and + # much more. + # + # Notably, EmbroiderModder2 uses <polyline> elements when converting from + # common machine embroidery file formats to SVG. Handling those here lets + # users use File -> Import to pull in existing designs they may have + # obtained, for example purchased fonts. + + @property + def points(self): + # example: "1,2 0,0 1.5,3 4,2" + + points = self.node.get('points') + points = points.split(" ") + points = [[float(coord) for coord in point.split(",")] for point in points] + + return points - if element.get_boolean_param("satin_column"): - return [SatinColumn] + @property + def path(self): + # A polyline is a series of connected line segments described by their + # points. In order to make use of the existing logic for incorporating + # svg transforms that is in our superclass, we'll convert the polyline + # to a degenerate cubic superpath in which the bezier handles are on + # the segment endpoints. + + path = [[[point[:], point[:], point[:]] for point in self.points]] + + return path + + @property + @cache + def csp(self): + csp = self.parse_path() + + return csp + + @property + def color(self): + # EmbroiderModder2 likes to use the `stroke` property directly instead + # of CSS. + return self.get_style("stroke") or self.node.get("stroke") + + @property + def stitches(self): + # For a <polyline>, we'll stitch the points exactly as they exist in + # the SVG, with no stitch spacing interpolation, flattening, etc. + + # See the comments in the parent class's parse_path method for a + # description of the CSP data structure. + + stitches = [point for handle_before, point, handle_after in self.csp[0]] + + return stitches + + def to_patches(self, last_patch): + patch = Patch(color=self.color) + + for stitch in self.stitches: + patch.add_stitch(PyEmb.Point(*stitch)) + + return [patch] + +def detect_classes(node): + if node.tag == SVG_POLYLINE_TAG: + return [Polyline] else: - classes = [] + element = EmbroideryElement(node) - if element.get_style("fill"): - if element.get_boolean_param("auto_fill", True): - classes.append(AutoFill) - else: - classes.append(Fill) + if element.get_boolean_param("satin_column"): + return [SatinColumn] + else: + classes = [] + + if element.get_style("fill"): + if element.get_boolean_param("auto_fill", True): + classes.append(AutoFill) + else: + classes.append(Fill) - if element.get_style("stroke"): - classes.append(Stroke) + if element.get_style("stroke"): + classes.append(Stroke) - if element.get_boolean_param("stroke_first", False): - classes.reverse() + if element.get_boolean_param("stroke_first", False): + classes.reverse() - return classes + return classes def descendants(node): @@ -1209,7 +1675,7 @@ def descendants(node): for child in node: nodes.extend(descendants(child)) - if node.tag == SVG_PATH_TAG: + if node.tag in EMBROIDERABLE_TAGS: nodes.append(node) return nodes @@ -1340,6 +1806,10 @@ class Embroider(inkex.Effect): action="store", type="string", dest="path", default=".", help="Directory in which to store output file") + self.OptionParser.add_option("-F", "--output-file", + action="store", type="string", + dest="output_file", default=".", + help="Output filename.") self.OptionParser.add_option("-b", "--max-backups", action="store", type="int", dest="max_backups", default=5, @@ -1351,7 +1821,7 @@ class Embroider(inkex.Effect): self.patches = [] def handle_node(self, node): - print >> dbg, "handling node", node.get('id'), node.get('tag') + print >> dbg, "handling node", node.get('id'), node.tag nodes = descendants(node) for node in nodes: classes = detect_classes(node) @@ -1359,9 +1829,12 @@ class Embroider(inkex.Effect): self.elements.extend(cls(node, self.options) for cls in classes) def get_output_path(self): - svg_filename = self.document.getroot().get(inkex.addNS('docname', 'sodipodi')) - csv_filename = svg_filename.replace('.svg', '.csv') - output_path = os.path.join(self.options.path, csv_filename) + if self.options.output_file: + output_path = os.path.join(self.options.path, self.options.output_file) + else: + svg_filename = self.document.getroot().get(inkex.addNS('docname', 'sodipodi'), "embroidery.svg") + csv_filename = svg_filename.replace('.svg', '.csv') + output_path = os.path.join(self.options.path, csv_filename) def add_suffix(path, suffix): if suffix > 0: |
