summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorLex Neva <lexelby@users.noreply.github.com>2018-06-29 21:26:03 -0400
committerGitHub <noreply@github.com>2018-06-29 21:26:03 -0400
commit4d7b07ff02e13f237fac8eb8715fe4a6324ea6dc (patch)
treef1c29df5f29d5bfefb48513043116b73209f202d /lib
parent7c8cd648a36e10e5ebfb94ce27be76e5ba47f334 (diff)
parentde4ead1ad467997fa81a4459e194769dfab185e2 (diff)
Merge pull request #212 from inkstitch/lexelby-fill-markers
fill start/end
Diffstat (limited to 'lib')
-rw-r--r--lib/commands.py89
-rw-r--r--lib/elements/auto_fill.py26
-rw-r--r--lib/elements/element.py41
-rw-r--r--lib/elements/stroke.py3
-rw-r--r--lib/extensions/__init__.py2
-rw-r--r--lib/extensions/base.py4
-rw-r--r--lib/extensions/install.py (renamed from lib/extensions/palettes.py)61
-rw-r--r--lib/extensions/params.py3
-rw-r--r--lib/stitches/auto_fill.py197
-rw-r--r--lib/svg/__init__.py1
-rw-r--r--lib/svg/path.py20
-rw-r--r--lib/svg/tags.py5
12 files changed, 323 insertions, 129 deletions
diff --git a/lib/commands.py b/lib/commands.py
new file mode 100644
index 00000000..ec62d716
--- /dev/null
+++ b/lib/commands.py
@@ -0,0 +1,89 @@
+import inkex
+import cubicsuperpath
+
+from .svg import apply_transforms
+from .svg.tags import SVG_USE_TAG, SVG_SYMBOL_TAG, CONNECTION_START, CONNECTION_END, XLINK_HREF
+
+
+class Command(object):
+ def __init__(self, connector):
+ self.connector = connector
+ self.svg = self.connector.getroottree().getroot()
+
+ self.parse_command()
+
+ def get_node_by_url(self, url):
+ # url will be #path12345. Find the object at the other end.
+
+ if url is None:
+ raise ValueError("url is None")
+
+ if not url.startswith('#'):
+ raise ValueError("invalid connection url: %s" % url)
+
+ id = url[1:]
+
+ try:
+ return self.svg.xpath(".//*[@id='%s']" % id)[0]
+ except (IndexError, AttributeError):
+ raise ValueError("could not find node by url %s" % id)
+
+ def parse_connector_path(self):
+ path = cubicsuperpath.parsePath(self.connector.get('d'))
+ return apply_transforms(path, self.connector)
+
+ def parse_command(self):
+ path = self.parse_connector_path()
+
+ neighbors = [
+ (self.get_node_by_url(self.connector.get(CONNECTION_START)), path[0][0][1]),
+ (self.get_node_by_url(self.connector.get(CONNECTION_END)), path[0][-1][1])
+ ]
+
+ if neighbors[0][0].tag != SVG_USE_TAG:
+ neighbors.reverse()
+
+ if neighbors[0][0].tag != SVG_USE_TAG:
+ raise ValueError("connector does not point to a use tag")
+
+ self.symbol = self.get_node_by_url(neighbors[0][0].get(XLINK_HREF))
+
+ if self.symbol.tag != SVG_SYMBOL_TAG:
+ raise ValueError("use points to non-symbol")
+
+ self.command = self.symbol.get('id')
+
+ if self.command.startswith('inkstitch_'):
+ self.command = self.command[10:]
+ else:
+ raise ValueError("symbol is not an Ink/Stitch command")
+
+ self.target = neighbors[1][0]
+ self.target_point = neighbors[1][1]
+
+ def __repr__(self):
+ return "Command('%s', %s)" % (self.command, self.target_point)
+
+def find_commands(node):
+ """Find the symbols this node is connected to and return them as Commands"""
+
+ # find all paths that have this object as a connection
+ xpath = ".//*[@inkscape:connection-start='#%(id)s' or @inkscape:connection-end='#%(id)s']" % dict(id=node.get('id'))
+ connectors = node.getroottree().getroot().xpath(xpath, namespaces=inkex.NSS)
+
+ # try to turn them into commands
+ commands = []
+ for connector in connectors:
+ try:
+ commands.append(Command(connector))
+ except ValueError:
+ import sys
+ import traceback
+ print >> sys.stderr, "not a Command:", connector.get('id'), traceback.format_exc()
+ # Parsing the connector failed, meaning it's not actually an Ink/Stitch command.
+ pass
+
+ return commands
+
+def is_command(node):
+ return CONNECTION_START in node.attrib or CONNECTION_END in node.attrib
diff --git a/lib/elements/auto_fill.py b/lib/elements/auto_fill.py
index 504bae2a..59816878 100644
--- a/lib/elements/auto_fill.py
+++ b/lib/elements/auto_fill.py
@@ -100,13 +100,28 @@ class AutoFill(Fill):
def fill_shape(self):
return self.shrink_or_grow_shape(self.expand)
+ def get_starting_point(self, last_patch):
+ # If there is a "fill_start" Command, then use that; otherwise pick
+ # the point closest to the end of the last patch.
+
+ if self.get_command('fill_start'):
+ return self.get_command('fill_start').target_point
+ elif last_patch:
+ return last_patch.stitches[-1]
+ else:
+ return None
+
+ def get_ending_point(self):
+ if self.get_command('fill_end'):
+ return self.get_command('fill_end').target_point
+ else:
+ return None
+
def to_patches(self, last_patch):
stitches = []
- if last_patch is None:
- starting_point = None
- else:
- starting_point = last_patch.stitches[-1]
+ starting_point = self.get_starting_point(last_patch)
+ ending_point = self.get_ending_point()
if self.fill_underlay:
stitches.extend(auto_fill(self.underlay_shape,
@@ -126,6 +141,7 @@ class AutoFill(Fill):
self.max_stitch_length,
self.running_stitch_length,
self.staggers,
- starting_point))
+ starting_point,
+ ending_point))
return [Patch(stitches=stitches, color=self.color)]
diff --git a/lib/elements/element.py b/lib/elements/element.py
index 39437c9f..3c31f1b0 100644
--- a/lib/elements/element.py
+++ b/lib/elements/element.py
@@ -4,7 +4,8 @@ from shapely import geometry as shgeo
from ..i18n import _
from ..utils import cache
-from ..svg import PIXELS_PER_MM, get_viewbox_transform, convert_length, get_doc_size
+from ..svg import PIXELS_PER_MM, convert_length, get_doc_size, apply_transforms
+from ..commands import find_commands
# inkscape-provided utilities
import simpletransform
@@ -171,10 +172,6 @@ class EmbroideryElement(object):
@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.
@@ -202,22 +199,32 @@ 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 = self.path
-
- # start with the identity transform
- transform = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]
+ return cubicsuperpath.parsePath(self.node.get("d"))
- # combine this node's transform with all parent groups' transforms
- transform = simpletransform.composeParents(self.node, transform)
+ @cache
+ def parse_path(self):
+ return apply_transforms(self.path, self.node)
- # add in the transform implied by the viewBox
- viewbox_transform = get_viewbox_transform(self.node.getroottree().getroot())
- transform = simpletransform.composeTransform(viewbox_transform, transform)
+ @property
+ @cache
+ def commands(self):
+ return find_commands(self.node)
- # apply the combined transform to this node's path
- simpletransform.applyTransformToPath(transform, path)
+ @cache
+ def get_commands(self, command):
+ return [c for c in self.commands if c.command == command]
- return path
+ @cache
+ def get_command(self, command):
+ commands = self.get_commands(command)
+
+ if len(commands) == 1:
+ return commands[0]
+ elif len(commands) > 1:
+ raise ValueError(_("%(id)s has more than one command of type '%(command)s' linked to it") %
+ dict(id=self.node.get(id), command=command))
+ else:
+ return None
def strip_control_points(self, subpath):
return [point for control_before, point, control_after in subpath]
diff --git a/lib/elements/stroke.py b/lib/elements/stroke.py
index 5239f978..eca9e0ba 100644
--- a/lib/elements/stroke.py
+++ b/lib/elements/stroke.py
@@ -4,6 +4,7 @@ from .element import param, EmbroideryElement, Patch
from ..i18n import _
from ..utils import cache, Point
from ..stitches import running_stitch
+from ..svg import parse_length_with_units
warned_about_legacy_running_stitch = False
@@ -57,7 +58,7 @@ class Stroke(EmbroideryElement):
def is_running_stitch(self):
# using stroke width <= 0.5 pixels to indicate running stitch is deprecated in favor of dashed lines
- stroke_width = float(self.get_style("stroke-width", 1))
+ stroke_width, units = parse_length_with_units(self.get_style("stroke-width", "1"))
if self.dashed:
return True
diff --git a/lib/extensions/__init__.py b/lib/extensions/__init__.py
index 6d3e00d8..b8951e12 100644
--- a/lib/extensions/__init__.py
+++ b/lib/extensions/__init__.py
@@ -1,5 +1,5 @@
from embroider import Embroider
-from palettes import Palettes
+from install import Install
from params import Params
from print_pdf import Print
from simulate import Simulate
diff --git a/lib/extensions/base.py b/lib/extensions/base.py
index 831b6dc6..78f75cf1 100644
--- a/lib/extensions/base.py
+++ b/lib/extensions/base.py
@@ -7,6 +7,7 @@ from collections import MutableMapping
from ..svg.tags import *
from ..elements import AutoFill, Fill, Stroke, SatinColumn, Polyline, EmbroideryElement
from ..utils import cache
+from ..commands import is_command
SVG_METADATA_TAG = inkex.addNS("metadata", "svg")
@@ -165,7 +166,8 @@ class InkstitchExtension(inkex.Effect):
classes.append(Fill)
if element.get_style("stroke"):
- classes.append(Stroke)
+ if not is_command(element.node):
+ classes.append(Stroke)
if element.get_boolean_param("stroke_first", False):
classes.reverse()
diff --git a/lib/extensions/palettes.py b/lib/extensions/install.py
index f7a6c7a5..d55b96d0 100644
--- a/lib/extensions/palettes.py
+++ b/lib/extensions/install.py
@@ -1,3 +1,5 @@
+# -*- coding: UTF-8 -*-
+
import sys
import traceback
import os
@@ -14,26 +16,26 @@ import inkex
from ..utils import guess_inkscape_config_path
-class InstallPalettesFrame(wx.Frame):
+class InstallerFrame(wx.Frame):
def __init__(self, *args, **kwargs):
wx.Frame.__init__(self, *args, **kwargs)
- default_path = os.path.join(guess_inkscape_config_path(), "palettes")
+ self.path = guess_inkscape_config_path()
panel = wx.Panel(self)
sizer = wx.BoxSizer(wx.VERTICAL)
- text = wx.StaticText(panel, label=_("Directory in which to install palettes:"))
- font = wx.Font(12, wx.DEFAULT, wx.NORMAL, wx.NORMAL)
- text.SetFont(font)
- sizer.Add(text, proportion=0, flag=wx.ALL|wx.EXPAND, border=10)
+ text_sizer = wx.BoxSizer(wx.HORIZONTAL)
- path_sizer = wx.BoxSizer(wx.HORIZONTAL)
- self.path_input = wx.TextCtrl(panel, wx.ID_ANY, value=default_path)
- path_sizer.Add(self.path_input, proportion=3, flag=wx.RIGHT|wx.EXPAND, border=20)
- chooser_button = wx.Button(panel, wx.ID_OPEN, _('Choose another directory...'))
- path_sizer.Add(chooser_button, proportion=1, flag=wx.EXPAND)
- sizer.Add(path_sizer, proportion=0, flag=wx.ALL|wx.EXPAND, border=10)
+ text = _('Ink/Stitch can install files ("add-ons") that make it easier to use Inkscape to create machine embroidery designs. These add-ons will be installed:') + \
+ "\n\n • " + _("thread manufacturer color palettes") + \
+ "\n • " + _("Ink/Stitch visual commands (Object -> Symbols...)")
+
+ static_text = wx.StaticText(panel, label=text)
+ font = wx.Font(12, wx.DEFAULT, wx.NORMAL, wx.NORMAL)
+ static_text.SetFont(font)
+ text_sizer.Add(static_text, proportion=0, flag=wx.ALL|wx.EXPAND, border=10)
+ sizer.Add(text_sizer, proportion=3, flag=wx.ALL|wx.EXPAND, border=0)
buttons_sizer = wx.BoxSizer(wx.HORIZONTAL)
install_button = wx.Button(panel, wx.ID_ANY, _("Install"))
@@ -41,15 +43,11 @@ class InstallPalettesFrame(wx.Frame):
buttons_sizer.Add(install_button, proportion=0, flag=wx.ALIGN_RIGHT|wx.ALL, border=5)
cancel_button = wx.Button(panel, wx.ID_CANCEL, _("Cancel"))
buttons_sizer.Add(cancel_button, proportion=0, flag=wx.ALIGN_RIGHT|wx.ALL, border=5)
- sizer.Add(buttons_sizer, proportion=0, flag=wx.ALIGN_RIGHT)
-
- outer_sizer = wx.BoxSizer(wx.HORIZONTAL)
- outer_sizer.Add(sizer, proportion=0, flag=wx.ALIGN_CENTER_VERTICAL)
+ sizer.Add(buttons_sizer, proportion=1, flag=wx.ALIGN_RIGHT|wx.ALIGN_BOTTOM)
- panel.SetSizer(outer_sizer)
+ panel.SetSizer(sizer)
panel.Layout()
- chooser_button.Bind(wx.EVT_BUTTON, self.chooser_button_clicked)
cancel_button.Bind(wx.EVT_BUTTON, self.cancel_button_clicked)
install_button.Bind(wx.EVT_BUTTON, self.install_button_clicked)
@@ -57,36 +55,37 @@ class InstallPalettesFrame(wx.Frame):
self.Destroy()
def chooser_button_clicked(self, event):
- dialog = wx.DirDialog(self, _("Choose Inkscape palettes directory"))
+ dialog = wx.DirDialog(self, _("Choose Inkscape directory"))
if dialog.ShowModal() != wx.ID_CANCEL:
self.path_input.SetValue(dialog.GetPath())
def install_button_clicked(self, event):
try:
- self.install_palettes()
+ self.install_addons('palettes')
+ self.install_addons('symbols')
except Exception, e:
wx.MessageDialog(self,
- _('Thread palette installation failed') + ': \n' + traceback.format_exc(),
+ _('Inkscape add-on installation failed') + ': \n' + traceback.format_exc(),
_('Installation Failed'),
wx.OK).ShowModal()
else:
wx.MessageDialog(self,
- _('Thread palette files have been installed. Please restart Inkscape to load the new palettes.'),
+ _('Inkscape add-on files have been installed. Please restart Inkscape to load the new add-ons.'),
_('Installation Completed'),
wx.OK).ShowModal()
self.Destroy()
- def install_palettes(self):
- path = self.path_input.GetValue()
- palettes_dir = self.get_bundled_palettes_dir()
- self.copy_files(glob(os.path.join(palettes_dir, "*")), path)
+ def install_addons(self, type):
+ path = os.path.join(self.path, type)
+ src_dir = self.get_bundled_dir(type)
+ self.copy_files(glob(os.path.join(src_dir, "*")), path)
- def get_bundled_palettes_dir(self):
+ def get_bundled_dir(self, name):
if getattr(sys, 'frozen', None) is not None:
- return realpath(os.path.join(sys._MEIPASS, '..', 'palettes'))
+ return realpath(os.path.join(sys._MEIPASS, '..', name))
else:
- return os.path.join(dirname(realpath(__file__)), 'palettes')
+ return realpath(os.path.join(dirname(realpath(__file__)), '..', '..', name))
if (sys.platform == "win32"):
# If we try to just use shutil.copy it says the operation requires elevation.
@@ -104,9 +103,9 @@ class InstallPalettesFrame(wx.Frame):
for palette_file in files:
shutil.copy(palette_file, dest)
-class Palettes(inkex.Effect):
+class Install(inkex.Effect):
def effect(self):
app = wx.App()
- installer_frame = InstallPalettesFrame(None, title=_("Ink/Stitch Thread Palette Installer"), size=(450, 200))
+ installer_frame = InstallerFrame(None, title=_("Ink/Stitch Add-ons Installer"), size=(550, 250))
installer_frame.Show()
app.MainLoop()
diff --git a/lib/extensions/params.py b/lib/extensions/params.py
index 9d8de41b..58fedd6b 100644
--- a/lib/extensions/params.py
+++ b/lib/extensions/params.py
@@ -19,6 +19,7 @@ from ..stitch_plan import patches_to_stitch_plan
from ..elements import EmbroideryElement, Fill, AutoFill, Stroke, SatinColumn
from ..utils import save_stderr, restore_stderr
from ..simulator import EmbroiderySimulator
+from ..commands import is_command
def presets_path():
@@ -655,7 +656,7 @@ class Params(InkstitchExtension):
classes.append(AutoFill)
classes.append(Fill)
- if element.get_style("stroke"):
+ if element.get_style("stroke") and not is_command(node):
classes.append(Stroke)
if element.get_style("stroke-dasharray") is None:
diff --git a/lib/stitches/auto_fill.py b/lib/stitches/auto_fill.py
index 518a2812..6326ced2 100644
--- a/lib/stitches/auto_fill.py
+++ b/lib/stitches/auto_fill.py
@@ -2,7 +2,7 @@ import sys
import shapely
import networkx
import math
-from itertools import groupby
+from itertools import groupby, izip
from collections import deque
from .fill import intersect_region_with_grating, row_num, stitch_row
@@ -14,18 +14,38 @@ from ..utils.geometry import Point as InkstitchPoint
class MaxQueueLengthExceeded(Exception):
pass
+class PathEdge(object):
+ OUTLINE_KEYS = ("outline", "extra", "initial")
+ SEGMENT_KEY = "segment"
-def auto_fill(shape, angle, row_spacing, end_row_spacing, max_stitch_length, running_stitch_length, staggers, starting_point=None):
+ def __init__(self, nodes, key):
+ self.nodes = nodes
+ self._sorted_nodes = tuple(sorted(self.nodes))
+ self.key = key
+
+ def __getitem__(self, item):
+ return self.nodes[item]
+
+ def __hash__(self):
+ return hash((self._sorted_nodes, self.key))
+
+ def __eq__(self, other):
+ return self._sorted_nodes == other._sorted_nodes and self.key == other.key
+
+ def is_outline(self):
+ return self.key in self.OUTLINE_KEYS
+
+ def is_segment(self):
+ return self.key == self.SEGMENT_KEY
+
+def auto_fill(shape, angle, row_spacing, end_row_spacing, max_stitch_length, running_stitch_length, staggers, starting_point, ending_point=None):
stitches = []
rows_of_segments = intersect_region_with_grating(shape, angle, row_spacing, end_row_spacing)
segments = [segment for row in rows_of_segments for segment in row]
graph = build_graph(shape, segments, angle, row_spacing)
- path = find_stitch_path(graph, segments)
-
- if starting_point:
- stitches.extend(connect_points(shape, starting_point, path[0][0], running_stitch_length))
+ path = find_stitch_path(graph, segments, starting_point, ending_point)
stitches.extend(path_to_stitches(graph, path, shape, angle, row_spacing, max_stitch_length, running_stitch_length, staggers))
@@ -134,8 +154,6 @@ def build_graph(shape, segments, angle, row_spacing):
else:
edge_set = 1
- #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]])):
graph.add_edge(node1, node2, key="outline")
@@ -157,14 +175,20 @@ def node_list_to_edge_list(node_list):
def bfs_for_loop(graph, starting_node, max_queue_length=2000):
to_search = deque()
- to_search.appendleft(([starting_node], set(), 0))
+ to_search.append((None, set()))
while to_search:
if len(to_search) > max_queue_length:
raise MaxQueueLengthExceeded()
- path, visited_edges, visited_segments = to_search.pop()
- ending_node = path[-1]
+ path, visited_edges = to_search.pop()
+
+ if path is None:
+ # This is the very first time through the loop, so initialize.
+ path = []
+ ending_node = starting_node
+ else:
+ ending_node = path[-1][-1]
# get a list of neighbors paired with the key of the edge I can follow to get there
neighbors = [
@@ -178,26 +202,21 @@ def bfs_for_loop(graph, starting_node, max_queue_length=2000):
for next_node, key in neighbors:
# skip if I've already followed this edge
- edge = (tuple(sorted((ending_node, next_node))), key)
+ edge = PathEdge((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
+ new_path = path + [edge]
if next_node == starting_node:
# ignore trivial loops (down and back a doubled edge)
if len(new_path) > 3:
- return node_list_to_edge_list(new_path), new_visited_segments
+ return new_path
new_visited_edges = visited_edges.copy()
new_visited_edges.add(edge)
- to_search.appendleft((new_path, new_visited_edges, new_visited_segments))
+ to_search.appendleft((new_path, new_visited_edges))
def find_loop(graph, starting_nodes):
@@ -216,14 +235,6 @@ def find_loop(graph, starting_nodes):
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
@@ -231,7 +242,6 @@ def find_loop(graph, starting_nodes):
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
@@ -240,12 +250,7 @@ def find_loop(graph, starting_nodes):
# case we discard that node and try the next.
loop = 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.
@@ -272,7 +277,7 @@ def insert_loop(path, loop):
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))
+ ((p1, p2), (p2, p3), (p3, p4), ...)
path will be modified in place.
"""
@@ -282,11 +287,59 @@ def insert_loop(path, loop):
for i, (start, end) in enumerate(path):
if start == loop_start:
break
+ else:
+ # if we didn't find the start of the loop in the list at all, it must
+ # be the endpoint of the last segment
+ i += 1
path[i:i] = loop
+def nearest_node_on_outline(graph, point, outline_index=0):
+ point = shapely.geometry.Point(*point)
+ outline_nodes = [node for node, data in graph.nodes(data=True) if data['index'] == outline_index]
+ nearest = min(outline_nodes, key=lambda node: shapely.geometry.Point(*node).distance(point))
-def find_stitch_path(graph, segments):
+ return nearest
+
+def get_outline_nodes(graph, outline_index=0):
+ outline_nodes = [(node, data['projection']) \
+ for node, data \
+ in graph.nodes(data=True) \
+ if data['index'] == outline_index]
+ outline_nodes.sort(key=lambda (node, projection): projection)
+ outline_nodes = [node for node, data in outline_nodes]
+
+ return outline_nodes
+
+def find_initial_path(graph, starting_point, ending_point=None):
+ starting_node = nearest_node_on_outline(graph, starting_point)
+
+ if ending_point is None:
+ # If they didn't give an ending point, pick either neighboring node
+ # along the outline -- doesn't matter which. We do this because
+ # the algorithm requires we start with _some_ path.
+ neighbors = [n for n, keys in graph.adj[starting_node].iteritems() if 'outline' in keys]
+ return [PathEdge((starting_node, neighbors[0]), "initial")]
+ else:
+ ending_node = nearest_node_on_outline(graph, ending_point)
+ outline_nodes = get_outline_nodes(graph)
+
+ # Multiply the outline_nodes list by 2 (duplicate it) because
+ # the ending_node may occur first.
+ outline_nodes *= 2
+ start_index = outline_nodes.index(starting_node)
+ end_index = outline_nodes.index(ending_node, start_index)
+ nodes = outline_nodes[start_index:end_index + 1]
+
+ # we have a series of sequential points, but we need to
+ # turn it into an edge list
+ path = []
+ for start, end in izip(nodes[:-1], nodes[1:]):
+ path.append(PathEdge((start, end), "initial"))
+
+ return path
+
+def find_stitch_path(graph, segments, starting_point=None, ending_point=None):
"""find a path that visits every grating segment exactly once
Theoretically, we just need to find an Eulerian Path in the graph.
@@ -294,13 +347,14 @@ def find_stitch_path(graph, segments):
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.
+ We'll build a Eulerian Path using Hierholzer's algorithm. A true
+ Eulerian Path would visit every single edge (including all the extras
+ we inserted in build_graph()),but we'll stop short once we've visited
+ every grating segment since that's all we really care about.
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
+ the starting node 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
@@ -313,40 +367,42 @@ def find_stitch_path(graph, 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]))]
+ if starting_point is None:
+ starting_point = segments[0][0]
+
+ path = find_initial_path(graph, starting_point, ending_point)
- graph.remove_edges_from(path)
+ # Our graph is Eulerian: every node has an even degree. An Eulerian graph
+ # must have an Eulerian Circuit which visits every edge and ends where it
+ # starts.
+ #
+ # However, we're starting with a path and _not_ removing the edges of that
+ # path from the graph. By doing this, we're implicitly adding those edges
+ # to the graph, after which the starting and ending point (and only those
+ # two) will now have odd degree. A graph that's Eulerian except for two
+ # nodes must have an Eulerian Path that starts and ends at those two nodes.
+ # That's how we force the starting and ending point.
- segments_visited += 1
- nodes_visited.extend(segments[0])
+ nodes_visited.append(path[0][0])
while segments_visited < num_segments:
- result = find_loop(graph, nodes_visited)
+ loop = find_loop(graph, nodes_visited)
- if not result:
+ if not loop:
print >> sys.stderr, _("Unexpected error while generating fill stitches. Please send your SVG file 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]
+ segments_visited += sum(1 for edge in loop if edge.is_segment())
+ nodes_visited.extend(edge[0] for edge in loop)
graph.remove_edges_from(loop)
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()
+ if ending_point is None:
+ # If they didn't specify an ending point, then the end of the path travels
+ # around the outline back to the start (see find_initial_path()). This
+ # isn't necessary, so remove it.
+ trim_end(path)
return path
@@ -363,10 +419,10 @@ def collapse_sequential_outline_edges(graph, path):
new_path = []
for edge in path:
- if graph.has_edge(*edge, key="segment"):
+ if edge.is_segment():
if start_of_run:
# close off the last run
- new_path.append((start_of_run, edge[0]))
+ new_path.append(PathEdge((start_of_run, edge[0]), "collapsed"))
start_of_run = None
new_path.append(edge)
@@ -376,7 +432,7 @@ def collapse_sequential_outline_edges(graph, path):
if start_of_run:
# if we were still in a run, close it off
- new_path.append((start_of_run, edge[1]))
+ new_path.append(PathEdge((start_of_run, edge[1]), "collapsed"))
return new_path
@@ -416,9 +472,6 @@ def connect_points(shape, start, end, running_stitch_length):
direction = math.copysign(1.0, distance)
one_stitch = running_stitch_length * direction
- #print >> dbg, "connect_points:", outline_index, start, end, distance, stitches, direction
- #dbg.flush()
-
stitches = [InkstitchPoint(*outline.interpolate(pos).coords[0])]
for i in xrange(num_stitches):
@@ -430,11 +483,11 @@ def connect_points(shape, start, end, running_stitch_length):
if (end - stitches[-1]).length() > 0.1 * PIXELS_PER_MM:
stitches.append(end)
- #print >> dbg, "end connect_points"
- #dbg.flush()
-
return stitches
+def trim_end(path):
+ while path and path[-1].is_outline():
+ path.pop()
def path_to_stitches(graph, path, shape, angle, row_spacing, max_stitch_length, running_stitch_length, staggers):
path = collapse_sequential_outline_edges(graph, path)
@@ -442,7 +495,7 @@ def path_to_stitches(graph, path, shape, angle, row_spacing, max_stitch_length,
stitches = []
for edge in path:
- if graph.has_edge(*edge, key="segment"):
+ if edge.is_segment():
stitch_row(stitches, edge[0], edge[1], angle, row_spacing, max_stitch_length, staggers)
else:
stitches.extend(connect_points(shape, edge[0], edge[1], running_stitch_length))
diff --git a/lib/svg/__init__.py b/lib/svg/__init__.py
index 1895bba4..50543b1b 100644
--- a/lib/svg/__init__.py
+++ b/lib/svg/__init__.py
@@ -1,2 +1,3 @@
from .svg import color_block_to_point_lists, render_stitch_plan
from .units import *
+from .path import apply_transforms
diff --git a/lib/svg/path.py b/lib/svg/path.py
new file mode 100644
index 00000000..a8012774
--- /dev/null
+++ b/lib/svg/path.py
@@ -0,0 +1,20 @@
+import simpletransform
+import cubicsuperpath
+
+from .units import get_viewbox_transform
+
+def apply_transforms(path, node):
+ # 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(node, transform)
+
+ # add in the transform implied by the viewBox
+ viewbox_transform = get_viewbox_transform(node.getroottree().getroot())
+ transform = simpletransform.composeTransform(viewbox_transform, transform)
+
+ # apply the combined transform to this node's path
+ simpletransform.applyTransformToPath(transform, path)
+
+ return path
diff --git a/lib/svg/tags.py b/lib/svg/tags.py
index fee59957..5488608c 100644
--- a/lib/svg/tags.py
+++ b/lib/svg/tags.py
@@ -5,8 +5,13 @@ 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')
+SVG_SYMBOL_TAG = inkex.addNS('symbol', 'svg')
+SVG_USE_TAG = inkex.addNS('use', 'svg')
INKSCAPE_LABEL = inkex.addNS('label', 'inkscape')
INKSCAPE_GROUPMODE = inkex.addNS('groupmode', 'inkscape')
+CONNECTION_START = inkex.addNS('connection-start', 'inkscape')
+CONNECTION_END = inkex.addNS('connection-end', 'inkscape')
+XLINK_HREF = inkex.addNS('href', 'xlink')
EMBROIDERABLE_TAGS = (SVG_PATH_TAG, SVG_POLYLINE_TAG)