summaryrefslogtreecommitdiff
path: root/embroider.py
diff options
context:
space:
mode:
Diffstat (limited to 'embroider.py')
-rw-r--r--embroider.py755
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: