kopia lustrzana https://github.com/inkstitch/inkstitch
new extension: Auto-Route Satin Columns (#330)
**video demo:** https://www.youtube.com/watch?v=tbghtqziB1g This branch adds a new extension, Auto-Route Satin Columns, implementing #214! This is a huge new feature that opens the door wide for exciting stuff like lettering (#142). To use it, select some satin columns and run the extension. After a few seconds, it will replace your satins with a new set with a logical stitching order. Under-pathing and jump-stitches will be added as necessary, and satins will be broken to facilitate jumps. The resulting satins will retain all of the parameters you had set on the original satins, including underlay, zig-zag spacing, etc. By default, it will choose the left-most extreme as the starting point and the right-most extreme as the ending point (even if these occur partway through a satin such as the left edge of a letter "o"). You can override this by attaching the new "Auto-route satin stitch starting/ending position" commands. There's also an option to add trims instead of jump stitches. Any jump stitch over 1mm is trimmed. I might make this configurable in the future but in my tests it seems to do a good job. Trim commands are added to the SVG, so it's easy enough to modify/delete as you see fit.pull/347/head
rodzic
d9525968a2
commit
be833f898f
47
inkstitch.py
47
inkstitch.py
|
@ -1,12 +1,24 @@
|
|||
import os
|
||||
import sys
|
||||
import logging
|
||||
import traceback
|
||||
from cStringIO import StringIO
|
||||
from argparse import ArgumentParser
|
||||
|
||||
from lib import extensions
|
||||
from lib.utils import save_stderr, restore_stderr
|
||||
|
||||
|
||||
logger = logging.getLogger('shapely.geos')
|
||||
logger.setLevel(logging.DEBUG)
|
||||
shapely_errors = StringIO()
|
||||
ch = logging.StreamHandler(shapely_errors)
|
||||
ch.setLevel(logging.DEBUG)
|
||||
formatter = logging.Formatter('%(name)s - %(levelname)s - %(message)s')
|
||||
ch.setFormatter(formatter)
|
||||
logger.addHandler(ch)
|
||||
|
||||
|
||||
parser = ArgumentParser()
|
||||
parser.add_argument("--extension")
|
||||
my_args, remaining_args = parser.parse_known_args()
|
||||
|
@ -31,20 +43,25 @@ extension_class_name = extension_name.title().replace("_", "")
|
|||
extension_class = getattr(extensions, extension_class_name)
|
||||
extension = extension_class()
|
||||
|
||||
exception = None
|
||||
|
||||
save_stderr()
|
||||
try:
|
||||
if hasattr(sys, 'gettrace') and sys.gettrace():
|
||||
extension.affect(args=remaining_args)
|
||||
except (SystemExit, KeyboardInterrupt):
|
||||
raise
|
||||
except Exception:
|
||||
exception = traceback.format_exc()
|
||||
finally:
|
||||
restore_stderr()
|
||||
|
||||
if exception:
|
||||
print >> sys.stderr, exception
|
||||
sys.exit(1)
|
||||
else:
|
||||
sys.exit(0)
|
||||
save_stderr()
|
||||
exception = None
|
||||
try:
|
||||
extension.affect(args=remaining_args)
|
||||
except (SystemExit, KeyboardInterrupt):
|
||||
raise
|
||||
except Exception:
|
||||
exception = traceback.format_exc()
|
||||
finally:
|
||||
restore_stderr()
|
||||
|
||||
if shapely_errors.tell():
|
||||
print >> sys.stderr, shapely_errors.getvalue()
|
||||
|
||||
if exception:
|
||||
print >> sys.stderr, exception
|
||||
sys.exit(1)
|
||||
else:
|
||||
sys.exit(0)
|
||||
|
|
|
@ -15,6 +15,12 @@ COMMANDS = {
|
|||
# L10N command attached to an object
|
||||
N_("fill_end"): N_("Fill stitch ending position"),
|
||||
|
||||
# L10N command attached to an object
|
||||
N_("satin_start"): N_("Auto-route satin stitch starting position"),
|
||||
|
||||
# L10N command attached to an object
|
||||
N_("satin_end"): N_("Auto-route satin stitch ending position"),
|
||||
|
||||
# L10N command attached to an object
|
||||
N_("stop"): N_("Stop (pause machine) after sewing this object"),
|
||||
|
||||
|
@ -38,7 +44,7 @@ COMMANDS = {
|
|||
N_("stop_position"): N_("Jump destination for Stop commands (a.k.a. \"Frame Out position\")."),
|
||||
}
|
||||
|
||||
OBJECT_COMMANDS = ["fill_start", "fill_end", "stop", "trim", "ignore_object", "satin_cut_point"]
|
||||
OBJECT_COMMANDS = ["fill_start", "fill_end", "satin_start", "satin_end", "stop", "trim", "ignore_object", "satin_cut_point"]
|
||||
LAYER_COMMANDS = ["ignore_layer"]
|
||||
GLOBAL_COMMANDS = ["origin", "stop_position"]
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
from auto_fill import AutoFill
|
||||
from fill import Fill
|
||||
from stroke import Stroke
|
||||
from satin_column import SatinColumn
|
||||
from element import EmbroideryElement
|
||||
from satin_column import SatinColumn
|
||||
from stroke import Stroke
|
||||
from polyline import Polyline
|
||||
from fill import Fill
|
||||
from auto_fill import AutoFill
|
||||
|
|
|
@ -5,8 +5,8 @@ import cubicsuperpath
|
|||
|
||||
from .element import param, EmbroideryElement, Patch
|
||||
from ..i18n import _
|
||||
from ..utils import cache, Point, cut
|
||||
from ..svg import line_strings_to_csp, get_correction_transform
|
||||
from ..utils import cache, Point, cut, collapse_duplicate_point
|
||||
from ..svg import line_strings_to_csp, point_lists_to_csp
|
||||
|
||||
|
||||
class SatinColumn(EmbroideryElement):
|
||||
|
@ -245,10 +245,17 @@ class SatinColumn(EmbroideryElement):
|
|||
|
||||
intersection = rail_segment.intersection(rung)
|
||||
|
||||
# If there are duplicate points in a rung-less satin, then
|
||||
# intersection will be a GeometryCollection of multiple copies
|
||||
# of the same point. This reduces it that to a single point.
|
||||
intersection = collapse_duplicate_point(intersection)
|
||||
|
||||
if not intersection.is_empty:
|
||||
if isinstance(intersection, shgeo.MultiLineString):
|
||||
intersections += len(intersection)
|
||||
break
|
||||
elif not isinstance(intersection, shgeo.Point):
|
||||
self.fatal("intersection is a: %s %s" % (intersection, intersection.geoms))
|
||||
else:
|
||||
intersections += 1
|
||||
|
||||
|
@ -321,6 +328,35 @@ class SatinColumn(EmbroideryElement):
|
|||
self.fatal(_("satin column: object %(id)s has two paths with an unequal number of points (%(length1)d and %(length2)d)") %
|
||||
dict(id=node_id, length1=len(self.rails[0]), length2=len(self.rails[1])))
|
||||
|
||||
def reverse(self):
|
||||
"""Return a new SatinColumn like this one but in the opposite direction.
|
||||
|
||||
The path will be flattened and the new satin will contain a new XML
|
||||
node that is not yet in the SVG.
|
||||
"""
|
||||
# flatten the path because you can't just reverse a CSP subpath's elements (I think)
|
||||
point_lists = []
|
||||
|
||||
for rail in self.rails:
|
||||
point_lists.append(list(reversed(self.flatten_subpath(rail))))
|
||||
|
||||
# reverse the order of the rails because we're sewing in the opposite direction
|
||||
point_lists.reverse()
|
||||
|
||||
for rung in self.rungs:
|
||||
point_lists.append(self.flatten_subpath(rung))
|
||||
|
||||
return self._csp_to_satin(point_lists_to_csp(point_lists))
|
||||
|
||||
def apply_transform(self):
|
||||
"""Return a new SatinColumn like this one but with transforms applied.
|
||||
|
||||
This node's and all ancestor nodes' transforms will be applied. The
|
||||
new SatinColumn's node will not be in the SVG document.
|
||||
"""
|
||||
|
||||
return self._csp_to_satin(self.csp)
|
||||
|
||||
def split(self, split_point):
|
||||
"""Split a satin into two satins at the specified point
|
||||
|
||||
|
@ -328,6 +364,9 @@ class SatinColumn(EmbroideryElement):
|
|||
ends. Finds corresponding point on the other rail (taking into account
|
||||
the rungs) and breaks the rails at these points.
|
||||
|
||||
split_point can also be a noramlized projection of a distance along the
|
||||
satin, in the range 0.0 to 1.0.
|
||||
|
||||
Returns two new SatinColumn instances: the part before and the part
|
||||
after the split point. All parameters are copied over to the new
|
||||
SatinColumn instances.
|
||||
|
@ -337,7 +376,7 @@ class SatinColumn(EmbroideryElement):
|
|||
path_lists = self._cut_rails(cut_points)
|
||||
self._assign_rungs_to_split_rails(path_lists)
|
||||
self._add_rungs_if_necessary(path_lists)
|
||||
return self._path_lists_to_satins(path_lists)
|
||||
return [self._path_list_to_satins(path_list) for path_list in path_lists]
|
||||
|
||||
def _find_cut_points(self, split_point):
|
||||
"""Find the points on each satin corresponding to the split point.
|
||||
|
@ -347,22 +386,30 @@ class SatinColumn(EmbroideryElement):
|
|||
for that rail. A corresponding cut point will be chosen on the other
|
||||
rail, taking into account the satin's rungs to choose a matching point.
|
||||
|
||||
split_point can instead be a number in [0.0, 1.0] indicating a
|
||||
a fractional distance down the satin to cut at.
|
||||
|
||||
Returns: a list of two Point objects corresponding to the selected
|
||||
cut points.
|
||||
"""
|
||||
|
||||
split_point = Point(*split_point)
|
||||
patch = self.do_satin()
|
||||
index_of_closest_stitch = min(range(len(patch)), key=lambda index: split_point.distance(patch.stitches[index]))
|
||||
# like in do_satin()
|
||||
points = list(chain.from_iterable(izip(*self.walk_paths(self.zigzag_spacing, 0))))
|
||||
|
||||
if isinstance(split_point, float):
|
||||
index_of_closest_stitch = int(round(len(points) * split_point))
|
||||
else:
|
||||
split_point = Point(*split_point)
|
||||
index_of_closest_stitch = min(range(len(points)), key=lambda index: split_point.distance(points[index]))
|
||||
|
||||
if index_of_closest_stitch % 2 == 0:
|
||||
# split point is on the first rail
|
||||
return (patch.stitches[index_of_closest_stitch],
|
||||
patch.stitches[index_of_closest_stitch + 1])
|
||||
return (points[index_of_closest_stitch],
|
||||
points[index_of_closest_stitch + 1])
|
||||
else:
|
||||
# split point is on the second rail
|
||||
return (patch.stitches[index_of_closest_stitch - 1],
|
||||
patch.stitches[index_of_closest_stitch])
|
||||
return (points[index_of_closest_stitch - 1],
|
||||
points[index_of_closest_stitch])
|
||||
|
||||
def _cut_rails(self, cut_points):
|
||||
"""Cut the rails of this satin at the specified points.
|
||||
|
@ -372,7 +419,7 @@ class SatinColumn(EmbroideryElement):
|
|||
|
||||
Returns: A list of two elements, corresponding two the two new sets of
|
||||
rails. Each element is a list of two rails of type LineString.
|
||||
"""
|
||||
"""
|
||||
|
||||
rails = [shgeo.LineString(self.flatten_subpath(rail)) for rail in self.rails]
|
||||
|
||||
|
@ -396,19 +443,22 @@ class SatinColumn(EmbroideryElement):
|
|||
path_list.extend(rung for rung in rungs if path_list[0].intersects(rung) and path_list[1].intersects(rung))
|
||||
|
||||
def _add_rungs_if_necessary(self, path_lists):
|
||||
"""Add an additional rung to each new satin if it ended up with none.
|
||||
"""Add an additional rung to each new satin if needed.
|
||||
|
||||
If the split point is between the end and the last rung, then one of
|
||||
the satins will have no rungs. Add one to make it stitch properly.
|
||||
Case #1: If the split point is between the end and the last rung, then
|
||||
one of the satins will have no rungs. It will be treated as an old-style
|
||||
satin, but it may not have an equal number of points in each rail. Adding
|
||||
a rung will make it stitch properly.
|
||||
|
||||
Case #2: If one of the satins ends up with exactly two rungs, it's
|
||||
ambiguous which of the subpaths are rails and which are rungs. Adding
|
||||
another rung disambiguates this case. See rail_indices() above for more
|
||||
information.
|
||||
"""
|
||||
|
||||
# no need to add rungs if there weren't any in the first place
|
||||
if not self.rungs:
|
||||
return
|
||||
|
||||
for path_list in path_lists:
|
||||
if len(path_list) == 2:
|
||||
# If a path has no rungs, it may be invalid. Add a rung at the start.
|
||||
if len(path_list) in (2, 4):
|
||||
# Add the rung just after the start of the satin.
|
||||
rung_start = path_list[0].interpolate(0.1)
|
||||
rung_end = path_list[1].interpolate(0.1)
|
||||
rung = shgeo.LineString((rung_start, rung_end))
|
||||
|
@ -418,18 +468,26 @@ class SatinColumn(EmbroideryElement):
|
|||
|
||||
path_list.append(rung)
|
||||
|
||||
def _path_lists_to_satins(self, path_lists):
|
||||
transform = get_correction_transform(self.node)
|
||||
satins = []
|
||||
for path_list in path_lists:
|
||||
node = deepcopy(self.node)
|
||||
csp = line_strings_to_csp(path_list)
|
||||
d = cubicsuperpath.formatPath(csp)
|
||||
node.set("d", d)
|
||||
node.set("transform", transform)
|
||||
satins.append(SatinColumn(node))
|
||||
def _path_list_to_satins(self, path_list):
|
||||
return self._csp_to_satin(line_strings_to_csp(path_list))
|
||||
|
||||
return satins
|
||||
def _csp_to_satin(self, csp):
|
||||
node = deepcopy(self.node)
|
||||
d = cubicsuperpath.formatPath(csp)
|
||||
node.set("d", d)
|
||||
|
||||
# we've already applied the transform, so get rid of it
|
||||
if node.get("transform"):
|
||||
del node.attrib["transform"]
|
||||
|
||||
return SatinColumn(node)
|
||||
|
||||
@property
|
||||
@cache
|
||||
def center_line(self):
|
||||
# similar technique to do_center_walk()
|
||||
center_walk, _ = self.walk_paths(self.zigzag_spacing, -100000)
|
||||
return shgeo.LineString(center_walk)
|
||||
|
||||
def offset_points(self, pos1, pos2, offset_px):
|
||||
# Expand or contract two points about their midpoint. This is
|
||||
|
|
|
@ -12,6 +12,7 @@ from layer_commands import LayerCommands
|
|||
from global_commands import GlobalCommands
|
||||
from convert_to_satin import ConvertToSatin
|
||||
from cut_satin import CutSatin
|
||||
from auto_satin import AutoSatin
|
||||
|
||||
__all__ = extensions = [Embroider,
|
||||
Install,
|
||||
|
@ -26,4 +27,5 @@ __all__ = extensions = [Embroider,
|
|||
LayerCommands,
|
||||
GlobalCommands,
|
||||
ConvertToSatin,
|
||||
CutSatin]
|
||||
CutSatin,
|
||||
AutoSatin]
|
||||
|
|
|
@ -0,0 +1,108 @@
|
|||
import sys
|
||||
|
||||
import inkex
|
||||
|
||||
from ..elements import SatinColumn
|
||||
from ..i18n import _
|
||||
from ..stitches.auto_satin import auto_satin
|
||||
from ..svg import get_correction_transform
|
||||
from ..svg.tags import SVG_GROUP_TAG, INKSCAPE_LABEL
|
||||
from .commands import CommandsExtension
|
||||
|
||||
|
||||
class AutoSatin(CommandsExtension):
|
||||
COMMANDS = ["trim"]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
CommandsExtension.__init__(self, *args, **kwargs)
|
||||
|
||||
self.OptionParser.add_option("-p", "--preserve_order", dest="preserve_order", type="inkbool", default=False)
|
||||
|
||||
def get_starting_point(self):
|
||||
return self.get_point("satin_start")
|
||||
|
||||
def get_ending_point(self):
|
||||
return self.get_point("satin_end")
|
||||
|
||||
def get_point(self, command_type):
|
||||
command = None
|
||||
for satin in self.elements:
|
||||
this_command = satin.get_command(command_type)
|
||||
if command is not None and this_command:
|
||||
inkex.errormsg(_("Please ensure that at most one start and end command is attached to the selected satin columns."))
|
||||
sys.exit(0)
|
||||
elif this_command:
|
||||
command = this_command
|
||||
|
||||
if command is not None:
|
||||
return command.target_point
|
||||
|
||||
def effect(self):
|
||||
if not self.check_selection():
|
||||
return
|
||||
|
||||
group = self.create_group()
|
||||
new_elements, trim_indices = self.auto_satin()
|
||||
|
||||
# The ordering is careful here. Some of the original satins may have
|
||||
# been used unmodified. That's why we remove all of the original
|
||||
# satins _first_ before adding new_elements back into the SVG.
|
||||
self.remove_original_satins()
|
||||
self.add_elements(group, new_elements)
|
||||
|
||||
self.add_trims(new_elements, trim_indices)
|
||||
|
||||
def check_selection(self):
|
||||
if not self.get_elements():
|
||||
return
|
||||
|
||||
if not self.selected:
|
||||
# L10N auto-route satin columns extension
|
||||
inkex.errormsg(_("Please select one or more satin columns."))
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def create_group(self):
|
||||
first = self.elements[0].node
|
||||
parent = first.getparent()
|
||||
insert_index = parent.index(first)
|
||||
group = inkex.etree.Element(SVG_GROUP_TAG, {
|
||||
"transform": get_correction_transform(parent, child=True)
|
||||
})
|
||||
parent.insert(insert_index, group)
|
||||
|
||||
return group
|
||||
|
||||
def auto_satin(self):
|
||||
starting_point = self.get_starting_point()
|
||||
ending_point = self.get_ending_point()
|
||||
return auto_satin(self.elements, self.options.preserve_order, starting_point, ending_point)
|
||||
|
||||
def remove_original_satins(self):
|
||||
for element in self.elements:
|
||||
for command in element.commands:
|
||||
command.connector.getparent().remove(command.connector)
|
||||
command.use.getparent().remove(command.use)
|
||||
element.node.getparent().remove(element.node)
|
||||
|
||||
def add_elements(self, group, new_elements):
|
||||
for i, element in enumerate(new_elements):
|
||||
if isinstance(element, SatinColumn):
|
||||
element.node.set("id", self.uniqueId("autosatin"))
|
||||
|
||||
# L10N Label for a satin column created by Auto-Route Satin Columns extension
|
||||
element.node.set(INKSCAPE_LABEL, _("AutoSatin %d") % (i + 1))
|
||||
else:
|
||||
element.node.set("id", self.uniqueId("autosatinrun"))
|
||||
|
||||
# L10N Label for running stitch (underpathing) created by Auto-Route Satin Columns extension
|
||||
element.node.set(INKSCAPE_LABEL, _("AutoSatin Running Stitch %d") % (i + 1))
|
||||
|
||||
group.append(element.node)
|
||||
|
||||
def add_trims(self, new_elements, trim_indices):
|
||||
if self.options.trim and trim_indices:
|
||||
self.ensure_symbol("trim")
|
||||
for i in trim_indices:
|
||||
self.add_commands(new_elements[i], ["trim"])
|
|
@ -1,14 +1,15 @@
|
|||
import inkex
|
||||
import re
|
||||
import json
|
||||
from copy import deepcopy
|
||||
from collections import MutableMapping
|
||||
from copy import deepcopy
|
||||
import json
|
||||
import re
|
||||
|
||||
import inkex
|
||||
from stringcase import snakecase
|
||||
|
||||
from ..svg.tags import SVG_GROUP_TAG, INKSCAPE_GROUPMODE, SVG_DEFS_TAG, EMBROIDERABLE_TAGS, SVG_POLYLINE_TAG
|
||||
from ..elements import AutoFill, Fill, Stroke, SatinColumn, Polyline, EmbroideryElement
|
||||
from ..commands import is_command, layer_commands
|
||||
from ..elements import AutoFill, Fill, Stroke, SatinColumn, Polyline, EmbroideryElement
|
||||
from ..i18n import _
|
||||
from ..svg.tags import SVG_GROUP_TAG, INKSCAPE_GROUPMODE, SVG_DEFS_TAG, EMBROIDERABLE_TAGS, SVG_POLYLINE_TAG
|
||||
|
||||
|
||||
SVG_METADATA_TAG = inkex.addNS("metadata", "svg")
|
||||
|
@ -21,7 +22,7 @@ def strip_namespace(tag):
|
|||
<<< namedview
|
||||
"""
|
||||
|
||||
match = re.match('^\{[^}]+\}(.+)$', tag)
|
||||
match = re.match(r'^\{[^}]+\}(.+)$', tag)
|
||||
|
||||
if match:
|
||||
return match.group(1)
|
||||
|
@ -211,8 +212,20 @@ class InkstitchExtension(inkex.Effect):
|
|||
|
||||
return svg_filename
|
||||
|
||||
def uniqueId(self, prefix, make_new_id=True):
|
||||
"""Override inkex.Effect.uniqueId with a nicer naming scheme."""
|
||||
i = 1
|
||||
while True:
|
||||
new_id = "%s%d" % (prefix, i)
|
||||
if new_id not in self.doc_ids:
|
||||
break
|
||||
i += 1
|
||||
self.doc_ids[new_id] = 1
|
||||
|
||||
return new_id
|
||||
|
||||
def parse(self):
|
||||
"""Override inkex.Effect to add Ink/Stitch xml namespace"""
|
||||
"""Override inkex.Effect.parse to add Ink/Stitch xml namespace"""
|
||||
|
||||
# SVG parsers don't actually look for anything at this URL. They just
|
||||
# care that it's unique. That defines a "namespace" of element and
|
||||
|
|
|
@ -1,13 +1,21 @@
|
|||
import os
|
||||
import inkex
|
||||
from copy import deepcopy
|
||||
from random import random
|
||||
|
||||
|
||||
from .base import InkstitchExtension
|
||||
from ..utils import get_bundled_dir, cache
|
||||
from ..svg.tags import SVG_DEFS_TAG
|
||||
from ..commands import get_command_description
|
||||
from ..i18n import _
|
||||
from ..svg.tags import SVG_DEFS_TAG, SVG_PATH_TAG, CONNECTION_START, CONNECTION_END, \
|
||||
CONNECTOR_TYPE, INKSCAPE_LABEL, SVG_GROUP_TAG, SVG_USE_TAG, XLINK_HREF
|
||||
from ..svg import get_correction_transform
|
||||
|
||||
|
||||
class CommandsExtension(InkstitchExtension):
|
||||
"""Base class for extensions that manipulate commands."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
InkstitchExtension.__init__(self, *args, **kwargs)
|
||||
for command in self.COMMANDS:
|
||||
|
@ -37,3 +45,84 @@ class CommandsExtension(InkstitchExtension):
|
|||
path = "./*[@id='inkstitch_%s']" % command
|
||||
if self.defs.find(path) is None:
|
||||
self.defs.append(deepcopy(self.symbol_defs.find(path)))
|
||||
|
||||
def add_connector(self, symbol, element):
|
||||
# I'd like it if I could position the connector endpoint nicely but inkscape just
|
||||
# moves it to the element's center immediately after the extension runs.
|
||||
start_pos = (symbol.get('x'), symbol.get('y'))
|
||||
end_pos = element.shape.centroid
|
||||
|
||||
path = inkex.etree.Element(SVG_PATH_TAG,
|
||||
{
|
||||
"id": self.uniqueId("connector"),
|
||||
"d": "M %s,%s %s,%s" % (start_pos[0], start_pos[1], end_pos.x, end_pos.y),
|
||||
"style": "stroke:#000000;stroke-width:1px;stroke-opacity:0.5;fill:none;",
|
||||
CONNECTION_START: "#%s" % symbol.get('id'),
|
||||
CONNECTION_END: "#%s" % element.node.get('id'),
|
||||
CONNECTOR_TYPE: "polyline",
|
||||
|
||||
# l10n: the name of the line that connects a command to the object it applies to
|
||||
INKSCAPE_LABEL: _("connector")
|
||||
}
|
||||
)
|
||||
|
||||
symbol.getparent().insert(0, path)
|
||||
|
||||
def get_command_pos(self, element, index, total):
|
||||
# Put command symbols 30 pixels out from the shape, spaced evenly around it.
|
||||
|
||||
# get a line running 30 pixels out from the shape
|
||||
outline = element.shape.buffer(30).exterior
|
||||
|
||||
# pick this item's spot arond the outline and perturb it a bit to avoid
|
||||
# stacking up commands if they run the extension multiple times
|
||||
position = index / float(total)
|
||||
position += random() * 0.1
|
||||
|
||||
return outline.interpolate(position, normalized=True)
|
||||
|
||||
def remove_legacy_param(self, element, command):
|
||||
if command == "trim" or command == "stop":
|
||||
# If they had the old "TRIM after" or "STOP after" attributes set,
|
||||
# automatically delete them. THe new commands will do the same
|
||||
# thing.
|
||||
#
|
||||
# If we didn't delete these here, then things would get confusing.
|
||||
# If the user were to delete a "trim" symbol added by this extension
|
||||
# but the "embroider_trim_after" attribute is still set, then the
|
||||
# trim would keep happening.
|
||||
|
||||
attribute = "embroider_%s_after" % command
|
||||
|
||||
if attribute in element.node.attrib:
|
||||
del element.node.attrib[attribute]
|
||||
|
||||
def add_commands(self, element, commands):
|
||||
for i, command in enumerate(commands):
|
||||
self.remove_legacy_param(element, command)
|
||||
|
||||
pos = self.get_command_pos(element, i, len(commands))
|
||||
|
||||
group = inkex.etree.SubElement(element.node.getparent(), SVG_GROUP_TAG,
|
||||
{
|
||||
"id": self.uniqueId("group"),
|
||||
INKSCAPE_LABEL: _("Ink/Stitch Command") + ": %s" % get_command_description(command),
|
||||
"transform": get_correction_transform(element.node)
|
||||
}
|
||||
)
|
||||
|
||||
symbol = inkex.etree.SubElement(group, SVG_USE_TAG,
|
||||
{
|
||||
"id": self.uniqueId("use"),
|
||||
XLINK_HREF: "#inkstitch_%s" % command,
|
||||
"height": "100%",
|
||||
"width": "100%",
|
||||
"x": str(pos.x),
|
||||
"y": str(pos.y),
|
||||
|
||||
# l10n: the name of a command symbol (example: scissors icon for trim command)
|
||||
INKSCAPE_LABEL: _("command marker"),
|
||||
}
|
||||
)
|
||||
|
||||
self.add_connector(symbol, element)
|
||||
|
|
|
@ -3,6 +3,7 @@ import inkex
|
|||
from .base import InkstitchExtension
|
||||
from ..i18n import _
|
||||
from ..elements import SatinColumn
|
||||
from ..svg import get_correction_transform
|
||||
|
||||
|
||||
class CutSatin(InkstitchExtension):
|
||||
|
@ -29,9 +30,11 @@ class CutSatin(InkstitchExtension):
|
|||
command.connector.getparent().remove(command.connector)
|
||||
|
||||
new_satins = satin.split(split_point)
|
||||
transform = get_correction_transform(satin.node)
|
||||
parent = satin.node.getparent()
|
||||
index = parent.index(satin.node)
|
||||
parent.remove(satin.node)
|
||||
for new_satin in new_satins:
|
||||
new_satin.node.set('transform', transform)
|
||||
parent.insert(index, new_satin.node)
|
||||
index += 1
|
||||
|
|
|
@ -35,12 +35,12 @@ class LayerCommands(CommandsExtension):
|
|||
|
||||
inkex.etree.SubElement(self.current_layer, SVG_USE_TAG,
|
||||
{
|
||||
"id": self.uniqueId("use"),
|
||||
INKSCAPE_LABEL: _("Ink/Stitch Command") + ": %s" % get_command_description(command),
|
||||
XLINK_HREF: "#inkstitch_%s" % command,
|
||||
"height": "100%",
|
||||
"width": "100%",
|
||||
"x": str(i * 20),
|
||||
"y": "-10",
|
||||
"transform": correction_transform
|
||||
"id": self.uniqueId("use"),
|
||||
INKSCAPE_LABEL: _("Ink/Stitch Command") + ": %s" % get_command_description(command),
|
||||
XLINK_HREF: "#inkstitch_%s" % command,
|
||||
"height": "100%",
|
||||
"width": "100%",
|
||||
"x": str(i * 20),
|
||||
"y": "-10",
|
||||
"transform": correction_transform
|
||||
})
|
||||
|
|
|
@ -1,97 +1,13 @@
|
|||
import inkex
|
||||
from random import random
|
||||
|
||||
from .commands import CommandsExtension
|
||||
from ..commands import OBJECT_COMMANDS, get_command_description
|
||||
from ..commands import OBJECT_COMMANDS
|
||||
from ..i18n import _
|
||||
from ..svg.tags import SVG_PATH_TAG, CONNECTION_START, CONNECTION_END, CONNECTOR_TYPE, INKSCAPE_LABEL, SVG_GROUP_TAG, SVG_USE_TAG, XLINK_HREF
|
||||
from ..svg import get_correction_transform
|
||||
|
||||
|
||||
class ObjectCommands(CommandsExtension):
|
||||
COMMANDS = OBJECT_COMMANDS
|
||||
|
||||
def add_connector(self, symbol, element):
|
||||
# I'd like it if I could position the connector endpoint nicely but inkscape just
|
||||
# moves it to the element's center immediately after the extension runs.
|
||||
start_pos = (symbol.get('x'), symbol.get('y'))
|
||||
end_pos = element.shape.centroid
|
||||
|
||||
path = inkex.etree.Element(SVG_PATH_TAG,
|
||||
{
|
||||
"id": self.uniqueId("connector"),
|
||||
"d": "M %s,%s %s,%s" % (start_pos[0], start_pos[1], end_pos.x, end_pos.y),
|
||||
"style": "stroke:#000000;stroke-width:1px;stroke-opacity:0.5;fill:none;",
|
||||
CONNECTION_START: "#%s" % symbol.get('id'),
|
||||
CONNECTION_END: "#%s" % element.node.get('id'),
|
||||
CONNECTOR_TYPE: "polyline",
|
||||
|
||||
# l10n: the name of the line that connects a command to the object it applies to
|
||||
INKSCAPE_LABEL: _("connector")
|
||||
}
|
||||
)
|
||||
|
||||
symbol.getparent().insert(0, path)
|
||||
|
||||
def get_command_pos(self, element, index, total):
|
||||
# Put command symbols 30 pixels out from the shape, spaced evenly around it.
|
||||
|
||||
# get a line running 30 pixels out from the shape
|
||||
outline = element.shape.buffer(30).exterior
|
||||
|
||||
# pick this item's spot arond the outline and perturb it a bit to avoid
|
||||
# stacking up commands if they run the extension multiple times
|
||||
position = index / float(total)
|
||||
position += random() * 0.1
|
||||
|
||||
return outline.interpolate(position, normalized=True)
|
||||
|
||||
def remove_legacy_param(self, element, command):
|
||||
if command == "trim" or command == "stop":
|
||||
# If they had the old "TRIM after" or "STOP after" attributes set,
|
||||
# automatically delete them. THe new commands will do the same
|
||||
# thing.
|
||||
#
|
||||
# If we didn't delete these here, then things would get confusing.
|
||||
# If the user were to delete a "trim" symbol added by this extension
|
||||
# but the "embroider_trim_after" attribute is still set, then the
|
||||
# trim would keep happening.
|
||||
|
||||
attribute = "embroider_%s_after" % command
|
||||
|
||||
if attribute in element.node.attrib:
|
||||
del element.node.attrib[attribute]
|
||||
|
||||
def add_commands(self, element, commands):
|
||||
for i, command in enumerate(commands):
|
||||
self.remove_legacy_param(element, command)
|
||||
|
||||
pos = self.get_command_pos(element, i, len(commands))
|
||||
|
||||
group = inkex.etree.SubElement(element.node.getparent(), SVG_GROUP_TAG,
|
||||
{
|
||||
"id": self.uniqueId("group"),
|
||||
INKSCAPE_LABEL: _("Ink/Stitch Command") + ": %s" % get_command_description(command),
|
||||
"transform": get_correction_transform(element.node)
|
||||
}
|
||||
)
|
||||
|
||||
symbol = inkex.etree.SubElement(group, SVG_USE_TAG,
|
||||
{
|
||||
"id": self.uniqueId("use"),
|
||||
XLINK_HREF: "#inkstitch_%s" % command,
|
||||
"height": "100%",
|
||||
"width": "100%",
|
||||
"x": str(pos.x),
|
||||
"y": str(pos.y),
|
||||
|
||||
# l10n: the name of a command symbol (example: scissors icon for trim command)
|
||||
INKSCAPE_LABEL: _("command marker"),
|
||||
}
|
||||
)
|
||||
|
||||
self.add_connector(symbol, element)
|
||||
|
||||
def effect(self):
|
||||
if not self.get_elements():
|
||||
return
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
from running_stitch import *
|
||||
from auto_fill import auto_fill
|
||||
from fill import legacy_fill
|
||||
|
||||
# Can't put this here because we get a circular import :(
|
||||
#from auto_satin import auto_satin
|
||||
|
|
|
@ -0,0 +1,573 @@
|
|||
from itertools import chain
|
||||
import math
|
||||
|
||||
import cubicsuperpath
|
||||
import inkex
|
||||
from shapely import geometry as shgeo
|
||||
from shapely.geometry import Point as ShapelyPoint
|
||||
import simplestyle
|
||||
|
||||
import networkx as nx
|
||||
|
||||
from ..elements import Stroke
|
||||
from ..svg import PIXELS_PER_MM, line_strings_to_csp
|
||||
from ..svg.tags import SVG_PATH_TAG
|
||||
from ..utils import Point as InkstitchPoint, cut, cache
|
||||
|
||||
|
||||
class SatinSegment(object):
|
||||
"""A portion of SatinColumn.
|
||||
|
||||
Attributes:
|
||||
satin -- the SatinColumn instance
|
||||
start -- how far along the satin this graph edge starts (a float from 0.0 to 1.0)
|
||||
end -- how far along the satin this graph edge ends (a float from 0.0 to 1.0)
|
||||
reverse -- if True, reverse the direction of the satin
|
||||
"""
|
||||
|
||||
def __init__(self, satin, start=0.0, end=1.0, reverse=False):
|
||||
"""Initialize a SatinEdge.
|
||||
|
||||
Arguments:
|
||||
satin -- the SatinColumn instance
|
||||
start, end -- a tuple or Point falling somewhere on the
|
||||
satin column, OR a floating point specifying a
|
||||
normalized projection of a distance along the satin
|
||||
(0.0 to 1.0 inclusive)
|
||||
reverse -- boolean
|
||||
"""
|
||||
|
||||
self.satin = satin
|
||||
self.reverse = reverse
|
||||
|
||||
# start and end are stored as normalized projections
|
||||
self.start = self._parse_init_param(start)
|
||||
self.end = self._parse_init_param(end)
|
||||
|
||||
if self.start > self.end:
|
||||
self.end, self.start = self.start, self.end
|
||||
self.reverse = True
|
||||
|
||||
def _parse_init_param(self, param):
|
||||
if isinstance(param, (float, int)):
|
||||
return param
|
||||
elif isinstance(param, (tuple, InkstitchPoint, ShapelyPoint)):
|
||||
return self.satin.center.project(ShapelyPoint(param), normalized=True)
|
||||
|
||||
def to_satin(self):
|
||||
satin = self.satin
|
||||
|
||||
if self.start > 0.0:
|
||||
before, satin = satin.split(self.start)
|
||||
|
||||
if self.end < 1.0:
|
||||
satin, after = satin.split(
|
||||
(self.end - self.start) / (1.0 - self.start))
|
||||
|
||||
if self.reverse:
|
||||
satin = satin.reverse()
|
||||
|
||||
satin = satin.apply_transform()
|
||||
|
||||
return satin
|
||||
|
||||
to_element = to_satin
|
||||
|
||||
def to_running_stitch(self):
|
||||
return RunningStitch(self.center_line, self.satin)
|
||||
|
||||
def break_up(self, segment_size):
|
||||
"""Break this SatinSegment up into SatinSegments of the specified size."""
|
||||
|
||||
num_segments = int(math.ceil(self.center_line.length / segment_size))
|
||||
segments = []
|
||||
satin = self.to_satin()
|
||||
for i in xrange(num_segments):
|
||||
segments.append(SatinSegment(satin, float(
|
||||
i) / num_segments, float(i + 1) / num_segments, self.reverse))
|
||||
|
||||
if self.reverse:
|
||||
segments.reverse()
|
||||
|
||||
return segments
|
||||
|
||||
def reversed(self):
|
||||
"""Return a copy of this SatinSegment in the opposite direction."""
|
||||
return SatinSegment(self.satin, self.start, self.end, not self.reverse)
|
||||
|
||||
@property
|
||||
def center_line(self):
|
||||
center_line = self.satin.center_line
|
||||
|
||||
if self.start < 1.0:
|
||||
before, center_line = cut(center_line, self.start, normalized=True)
|
||||
|
||||
if self.end > 0.0:
|
||||
center_line, after = cut(
|
||||
center_line, (self.end - self.start) / (1.0 - self.start), normalized=True)
|
||||
|
||||
if self.reverse:
|
||||
center_line = shgeo.LineString(reversed(center_line.coords))
|
||||
|
||||
return center_line
|
||||
|
||||
@property
|
||||
@cache
|
||||
def start_point(self):
|
||||
return self.satin.center_line.interpolate(self.start, normalized=True)
|
||||
|
||||
@property
|
||||
@cache
|
||||
def end_point(self):
|
||||
return self.satin.center_line.interpolate(self.end, normalized=True)
|
||||
|
||||
def is_sequential(self, other):
|
||||
"""Check if a satin segment immediately follows this one on the same satin."""
|
||||
|
||||
if not isinstance(other, SatinSegment):
|
||||
return False
|
||||
|
||||
if self.satin is not other.satin:
|
||||
return False
|
||||
|
||||
if self.reverse != other.reverse:
|
||||
return False
|
||||
|
||||
if self.reverse:
|
||||
return self.start == other.end
|
||||
else:
|
||||
return self.end == other.start
|
||||
|
||||
def __add__(self, other):
|
||||
"""Combine two sequential SatinSegments.
|
||||
|
||||
If self.is_sequential(other) is not True then adding results in
|
||||
undefined behavior.
|
||||
"""
|
||||
if self.reverse:
|
||||
return SatinSegment(self.satin, other.start, self.end, reverse=True)
|
||||
else:
|
||||
return SatinSegment(self.satin, self.start, other.end)
|
||||
|
||||
def __eq__(self, other):
|
||||
# Two SatinSegments are equal if they refer to the same section of the same
|
||||
# satin (even if in opposite directions).
|
||||
return self.satin is other.satin and self.start == other.start and self.end == other.end
|
||||
|
||||
def __hash__(self):
|
||||
return hash((id(self.satin), self.start, self.end))
|
||||
|
||||
def __repr__(self):
|
||||
return "SatinSegment(%s, %s, %s, %s)" % (self.satin, self.start, self.end, self.reverse)
|
||||
|
||||
|
||||
class JumpStitch(object):
|
||||
"""A jump stitch between two points."""
|
||||
|
||||
def __init__(self, start, end):
|
||||
"""Initialize a JumpStitch.
|
||||
|
||||
Arguments:
|
||||
start, end -- instances of shgeo.Point
|
||||
"""
|
||||
|
||||
self.start = start
|
||||
self.end = end
|
||||
|
||||
def is_sequential(self, other):
|
||||
# Don't bother joining jump stitches.
|
||||
return False
|
||||
|
||||
@property
|
||||
@cache
|
||||
def length(self):
|
||||
return self.start.distance(self.end)
|
||||
|
||||
|
||||
class RunningStitch(object):
|
||||
"""Running stitch along a path."""
|
||||
|
||||
def __init__(self, path_or_stroke, original_element=None):
|
||||
if isinstance(path_or_stroke, Stroke):
|
||||
# Technically a Stroke object's underlying path could have multiple
|
||||
# subpaths. We don't have a particularly good way of dealing with
|
||||
# that so we'll just use the first one.
|
||||
self.path = path_or_stroke.shape.geoms[0]
|
||||
original_element = path_or_stroke
|
||||
else:
|
||||
self.path = path_or_stroke
|
||||
|
||||
self.original_element = original_element
|
||||
self.style = original_element.node.get('style', '')
|
||||
self.running_stitch_length = \
|
||||
original_element.node.get('embroider_running_stitch_length_mm', '') or \
|
||||
original_element.node.get('embroider_center_walk_underlay_stitch_length_mm', '') or \
|
||||
original_element.node.get('embroider_contour_underlay_stitch_length_mm', '')
|
||||
|
||||
def to_element(self):
|
||||
node = inkex.etree.Element(SVG_PATH_TAG)
|
||||
node.set("d", cubicsuperpath.formatPath(
|
||||
line_strings_to_csp([self.path])))
|
||||
|
||||
style = simplestyle.parseStyle(self.style)
|
||||
style['stroke-dasharray'] = "0.5,0.5"
|
||||
style = simplestyle.formatStyle(style)
|
||||
node.set("style", style)
|
||||
|
||||
node.set("embroider_running_stitch_length_mm", self.running_stitch_length)
|
||||
|
||||
return Stroke(node)
|
||||
|
||||
@property
|
||||
@cache
|
||||
def start_point(self):
|
||||
return self.path.interpolate(0.0, normalized=True)
|
||||
|
||||
@property
|
||||
@cache
|
||||
def end_point(self):
|
||||
return self.path.interpolate(1.0, normalized=True)
|
||||
|
||||
@cache
|
||||
def reversed(self):
|
||||
return RunningStitch(shgeo.LineString(reversed(self.path.coords)), self.style)
|
||||
|
||||
def is_sequential(self, other):
|
||||
if not isinstance(other, RunningStitch):
|
||||
return False
|
||||
|
||||
return self.path.distance(other.path) < 0.5
|
||||
|
||||
def __add__(self, other):
|
||||
new_path = shgeo.LineString(chain(self.path.coords, other.path.coords))
|
||||
return RunningStitch(new_path, self.original_element)
|
||||
|
||||
|
||||
def auto_satin(elements, preserve_order=False, starting_point=None, ending_point=None):
|
||||
"""Find an optimal order to stitch a list of SatinColumns.
|
||||
|
||||
Add running stitch and jump stitches as necessary to construct a stitch
|
||||
order. Cut satins as necessary to minimize jump stitch length.
|
||||
|
||||
For example, consider three satins making up the letters "PO":
|
||||
|
||||
* one vertical satin for the "P"
|
||||
* the loop of the "P"
|
||||
* the "O"
|
||||
|
||||
A good stitch path would be:
|
||||
|
||||
1. up the leg
|
||||
2. down through half of the loop
|
||||
3. running stitch to the bottom of the loop
|
||||
4. satin stitch back up to the middle of the loop
|
||||
5. jump to the closest point on the O
|
||||
6. satin stitch around the O
|
||||
|
||||
If passed, stitching will start from starting_point and end at
|
||||
ending_point. It is expected that the starting and ending points will
|
||||
fall on satin columns in the list. If they don't, the nearest
|
||||
point on a satin column in the list will be used.
|
||||
|
||||
If preserve_order is True, then the algorithm is constrained to keep the
|
||||
satins in the same order they were in the original list. It will only split
|
||||
them and add running stitch as necessary to achieve an optimal stitch path.
|
||||
|
||||
Elements should be primarily made up of SatinColumn instances. Some Stroke
|
||||
instances (that are running stitch) can be included to indicate how to travel
|
||||
between two SatinColumns. This works best when preserve_order is True.
|
||||
|
||||
Returns: a list of SVG path nodes making up the selected stitch order.
|
||||
Jumps between objects are implied if they are not right next to each
|
||||
other.
|
||||
"""
|
||||
|
||||
graph = build_graph(elements, preserve_order)
|
||||
add_jumps(graph, elements, preserve_order)
|
||||
starting_node, ending_node = get_starting_and_ending_nodes(
|
||||
graph, elements, preserve_order, starting_point, ending_point)
|
||||
path = find_path(graph, starting_node, ending_node)
|
||||
operations = path_to_operations(graph, path)
|
||||
operations = collapse_sequential_segments(operations)
|
||||
return operations_to_elements_and_trims(operations)
|
||||
|
||||
|
||||
def build_graph(elements, preserve_order=False):
|
||||
if preserve_order:
|
||||
graph = nx.DiGraph()
|
||||
else:
|
||||
graph = nx.Graph()
|
||||
|
||||
# Take each satin and dice it up into pieces 1mm long. This allows many
|
||||
# possible spots for jump-stitches between satins. NetworkX will find the
|
||||
# best spots for us.
|
||||
|
||||
for element in elements:
|
||||
segments = []
|
||||
if isinstance(element, Stroke):
|
||||
segments.append(RunningStitch(element))
|
||||
else:
|
||||
whole_satin = SatinSegment(element)
|
||||
segments = whole_satin.break_up(PIXELS_PER_MM)
|
||||
|
||||
for segment in segments:
|
||||
# This is necessary because shapely points aren't hashable and thus
|
||||
# can't be used as nodes directly.
|
||||
graph.add_node(str(segment.start_point), point=segment.start_point)
|
||||
graph.add_node(str(segment.end_point), point=segment.end_point)
|
||||
graph.add_edge(str(segment.start_point), str(
|
||||
segment.end_point), segment=segment, element=element)
|
||||
|
||||
if preserve_order:
|
||||
# The graph is a directed graph, but we want to allow travel in
|
||||
# any direction in a satin, so we add the edge in the opposite
|
||||
# direction too.
|
||||
graph.add_edge(str(segment.end_point), str(
|
||||
segment.start_point), segment=segment, element=element)
|
||||
|
||||
return graph
|
||||
|
||||
|
||||
def get_starting_and_ending_nodes(graph, elements, preserve_order, starting_point, ending_point):
|
||||
"""Find or choose the starting and ending graph nodes.
|
||||
|
||||
If points were passed, we'll find the nearest graph nodes. Since we split
|
||||
every satin up into 1mm-chunks, we'll be at most 1mm away which is good
|
||||
enough.
|
||||
|
||||
If we weren't given starting and ending points, we'll pic kthe far left and
|
||||
right nodes.
|
||||
|
||||
returns:
|
||||
(starting graph node, ending graph node)
|
||||
"""
|
||||
|
||||
nodes = []
|
||||
|
||||
nodes.append(find_node(graph, starting_point,
|
||||
min, preserve_order, elements[0]))
|
||||
nodes.append(find_node(graph, ending_point,
|
||||
max, preserve_order, elements[-1]))
|
||||
|
||||
return nodes
|
||||
|
||||
|
||||
def find_node(graph, point, extreme_function, constrain_to_satin=False, satin=None):
|
||||
if constrain_to_satin:
|
||||
nodes = get_nodes_on_element(graph, satin)
|
||||
else:
|
||||
nodes = graph.nodes()
|
||||
|
||||
if point is None:
|
||||
return extreme_function(nodes, key=lambda node: graph.nodes[node]['point'].x)
|
||||
else:
|
||||
point = shgeo.Point(*point)
|
||||
return min(nodes, key=lambda node: graph.nodes[node]['point'].distance(point))
|
||||
|
||||
|
||||
def get_nodes_on_element(graph, element):
|
||||
nodes = set()
|
||||
|
||||
for start_node, end_node, element_for_edge in graph.edges(data='element'):
|
||||
if element_for_edge is element:
|
||||
nodes.add(start_node)
|
||||
nodes.add(end_node)
|
||||
|
||||
return nodes
|
||||
|
||||
|
||||
def add_jumps(graph, elements, preserve_order):
|
||||
"""Add jump stitches between elements as necessary.
|
||||
|
||||
Jump stitches are added to ensure that all elements can be reached. Only the
|
||||
minimal number and length of jumps necessary will be added.
|
||||
"""
|
||||
|
||||
if preserve_order:
|
||||
# For each sequential pair of elements, find the shortest possible jump
|
||||
# stitch between them and add it. The directions of these new edges
|
||||
# will enforce stitching the satins in order.
|
||||
|
||||
for element1, element2 in zip(elements[:-1], elements[1:]):
|
||||
potential_edges = []
|
||||
|
||||
nodes1 = get_nodes_on_element(graph, element1)
|
||||
nodes2 = get_nodes_on_element(graph, element2)
|
||||
|
||||
for node1 in nodes1:
|
||||
for node2 in nodes2:
|
||||
point1 = graph.nodes[node1]['point']
|
||||
point2 = graph.nodes[node2]['point']
|
||||
potential_edges.append((point1, point2))
|
||||
|
||||
edge = min(potential_edges, key=lambda (p1, p2): p1.distance(p2))
|
||||
graph.add_edge(str(edge[0]), str(edge[1]), jump=True)
|
||||
else:
|
||||
# networkx makes this super-easy! k_edge_agumentation tells us what edges
|
||||
# we need to add to ensure that the graph is fully connected. We give it a
|
||||
# set of possible edges that it can consider adding (avail). Each edge has
|
||||
# a weight, which we'll set as the length of the jump stitch. The
|
||||
# algorithm will minimize the total length of jump stitches added.
|
||||
for jump in nx.k_edge_augmentation(graph, 1, avail=list(possible_jumps(graph))):
|
||||
graph.add_edge(*jump, jump=True)
|
||||
|
||||
|
||||
def possible_jumps(graph):
|
||||
"""All possible jump stitches in the graph with their lengths.
|
||||
|
||||
Returns: a generator of tuples: (node1, node2, length)
|
||||
"""
|
||||
|
||||
# We'll take the easy approach and list all edges that aren't already in
|
||||
# the graph. networkx's algorithm is pretty efficient at ignoring
|
||||
# pointless options like jumping between two points on the same satin.
|
||||
|
||||
for start, end in nx.complement(graph).edges():
|
||||
start_point = graph.nodes[start]['point']
|
||||
end_point = graph.nodes[end]['point']
|
||||
yield (start, end, start_point.distance(end_point))
|
||||
|
||||
|
||||
def find_path(graph, starting_node, ending_node):
|
||||
"""Find a path through the graph that sews every satin."""
|
||||
|
||||
# This is done in two steps. First, we find the shortest path from the
|
||||
# start to the end. We remove it from the graph, and proceed to step 2.
|
||||
#
|
||||
# Then, we traverse the path node by node. At each node, we follow any
|
||||
# branchings with a depth-first search. We travel down each branch of
|
||||
# the tree, inserting each seen branch into the tree. When the DFS
|
||||
# hits a dead-end, as it back-tracks, we also add the seen edges _again_.
|
||||
# Repeat until there are no more edges left in the graph.
|
||||
#
|
||||
# Visiting the edges again on the way back allows us to set up
|
||||
# "underpathing". As we stitch down each branch, we'll do running stitch.
|
||||
# Then when we come back up, we'll do satin stitch, covering the previous
|
||||
# running stitch.
|
||||
path = nx.shortest_path(graph, starting_node, ending_node)
|
||||
|
||||
# Copy the graph so that we can remove the edges as we visit them.
|
||||
# This also converts the directed graph into an undirected graph in the
|
||||
# case that "preserve_order" is set. This way we avoid going back and
|
||||
# forth on each satin twice due to the satin edges being in the graph
|
||||
# twice (forward and reverse).
|
||||
graph = nx.Graph(graph)
|
||||
graph.remove_edges_from(zip(path[:-1], path[1:]))
|
||||
|
||||
final_path = []
|
||||
prev = None
|
||||
for node in path:
|
||||
if prev is not None:
|
||||
final_path.append((prev, node))
|
||||
prev = node
|
||||
|
||||
for n1, n2, edge_type in list(nx.dfs_labeled_edges(graph, node)):
|
||||
if n1 == n2:
|
||||
# dfs_labeled_edges gives us (start, start, "forward") for
|
||||
# the starting node for some reason
|
||||
continue
|
||||
|
||||
if edge_type == "forward":
|
||||
final_path.append((n1, n2))
|
||||
graph.remove_edge(n1, n2)
|
||||
elif edge_type == "reverse":
|
||||
final_path.append((n2, n1))
|
||||
elif edge_type == "nontree":
|
||||
# a "nontree" happens when there exists an edge from n1 to n2
|
||||
# but n2 has already been visited. It's a dead-end that runs
|
||||
# into part of the graph that we've already traversed. We
|
||||
# do still need to make sure that satin is sewn, so we travel
|
||||
# down and back on this edge.
|
||||
#
|
||||
# It's possible for a given "nontree" edge to be listed more
|
||||
# than once so we'll deduplicate.
|
||||
if (n1, n2) in graph.edges:
|
||||
final_path.append((n1, n2))
|
||||
final_path.append((n2, n1))
|
||||
graph.remove_edge(n1, n2)
|
||||
|
||||
return final_path
|
||||
|
||||
|
||||
def reversed_path(path):
|
||||
"""Generator for a version of the path travelling in the opposite direction.
|
||||
|
||||
Example:
|
||||
|
||||
[(1, 2), (2, 3), (3, 4), (4, 5), (5, 1)] =>
|
||||
[(1, 5), (5, 4), (4, 3), (3, 2), (2, 1)]
|
||||
"""
|
||||
|
||||
for node1, node2 in reversed(path):
|
||||
yield (node2, node1)
|
||||
|
||||
|
||||
def path_to_operations(graph, path):
|
||||
"""Convert an edge path to a list of SatinSegment and JumpStitch instances."""
|
||||
|
||||
graph = nx.Graph(graph)
|
||||
|
||||
operations = []
|
||||
|
||||
for start, end in path:
|
||||
segment = graph[start][end].get('segment')
|
||||
if segment:
|
||||
start_point = graph.nodes[start]['point']
|
||||
if segment.start_point != start_point:
|
||||
segment = segment.reversed()
|
||||
operations.append(segment)
|
||||
else:
|
||||
operations.append(JumpStitch(graph.nodes[start]['point'], graph.nodes[end]['point']))
|
||||
|
||||
# find_path() will have duplicated some of the edges in the graph. We don't
|
||||
# want to sew the same satin twice. If a satin section appears twice in the
|
||||
# path, we'll sew the first occurrence as running stitch. It will later be
|
||||
# covered by the satin stitch.
|
||||
seen = set()
|
||||
|
||||
for i, item in reversed(list(enumerate(operations))):
|
||||
if isinstance(item, SatinSegment):
|
||||
if item in seen:
|
||||
operations[i] = item.to_running_stitch()
|
||||
else:
|
||||
seen.add(item)
|
||||
|
||||
return operations
|
||||
|
||||
|
||||
def collapse_sequential_segments(old_operations):
|
||||
old_operations = iter(old_operations)
|
||||
new_operations = [next(old_operations)]
|
||||
|
||||
for operation in old_operations:
|
||||
if new_operations[-1].is_sequential(operation):
|
||||
new_operations[-1] += operation
|
||||
else:
|
||||
new_operations.append(operation)
|
||||
|
||||
return new_operations
|
||||
|
||||
|
||||
def operations_to_elements_and_trims(operations):
|
||||
"""Convert a list of operations to Elements and locations of trims.
|
||||
|
||||
Returns:
|
||||
(nodes, trims)
|
||||
|
||||
element -- a list of Element instances
|
||||
trims -- indices of nodes after which the thread should be trimmed
|
||||
"""
|
||||
|
||||
elements = []
|
||||
trims = []
|
||||
|
||||
for operation in operations:
|
||||
# Ignore JumpStitch opertions. Jump stitches in Ink/Stitch are
|
||||
# implied and added by Embroider if needed.
|
||||
if isinstance(operation, (SatinSegment, RunningStitch)):
|
||||
elements.append(operation.to_element())
|
||||
elif isinstance(operation, (JumpStitch)):
|
||||
if elements and operation.length > PIXELS_PER_MM:
|
||||
trims.append(len(elements) - 1)
|
||||
|
||||
return elements, list(set(trims))
|
|
@ -1,3 +1,3 @@
|
|||
from .svg import color_block_to_point_lists, render_stitch_plan
|
||||
from .units import *
|
||||
from .path import apply_transforms, get_node_transform, get_correction_transform, line_strings_to_csp
|
||||
from .path import apply_transforms, get_node_transform, get_correction_transform, line_strings_to_csp, point_lists_to_csp
|
||||
|
|
|
@ -50,13 +50,19 @@ def get_correction_transform(node, child=False):
|
|||
|
||||
|
||||
def line_strings_to_csp(line_strings):
|
||||
return point_lists_to_csp(ls.coords for ls in line_strings)
|
||||
|
||||
|
||||
def point_lists_to_csp(point_lists):
|
||||
csp = []
|
||||
|
||||
for ls in line_strings:
|
||||
for point_list in point_lists:
|
||||
subpath = []
|
||||
for point in ls.coords:
|
||||
for point in point_list:
|
||||
# cubicsuperpath is very particular that these must be lists, not tuples
|
||||
point = list(point)
|
||||
# create a straight line as a degenerate bezier
|
||||
subpath.append((point, point, point))
|
||||
subpath.append([point, point, point])
|
||||
csp.append(subpath)
|
||||
|
||||
return csp
|
||||
|
|
|
@ -2,11 +2,14 @@ from shapely.geometry import LineString, Point as ShapelyPoint
|
|||
import math
|
||||
|
||||
|
||||
def cut(line, distance):
|
||||
def cut(line, distance, normalized=False):
|
||||
""" Cuts a LineString in two at a distance from its starting point.
|
||||
|
||||
This is an example in the Shapely documentation.
|
||||
"""
|
||||
if normalized:
|
||||
distance *= line.length
|
||||
|
||||
if distance <= 0.0:
|
||||
return [None, line]
|
||||
elif distance >= line.length:
|
||||
|
@ -47,6 +50,14 @@ def cut_path(points, length):
|
|||
return [Point(*point) for point in subpath.coords]
|
||||
|
||||
|
||||
def collapse_duplicate_point(geometry):
|
||||
if hasattr(geometry, 'geoms'):
|
||||
if geometry.area < 0.01:
|
||||
return geometry.representative_point()
|
||||
|
||||
return geometry
|
||||
|
||||
|
||||
class Point:
|
||||
def __init__(self, x, y):
|
||||
self.x = x
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
./pyembroidery
|
||||
backports.functools_lru_cache
|
||||
wxPython
|
||||
networkx
|
||||
networkx==2.2
|
||||
shapely
|
||||
lxml
|
||||
appdirs
|
||||
|
|
|
@ -25,9 +25,9 @@
|
|||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="7.9999996"
|
||||
inkscape:cx="306.93962"
|
||||
inkscape:cy="286.56855"
|
||||
inkscape:zoom="1.9999999"
|
||||
inkscape:cx="160.57712"
|
||||
inkscape:cy="279.26165"
|
||||
inkscape:document-units="px"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="true"
|
||||
|
@ -254,6 +254,48 @@
|
|||
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#242424;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2.4000001;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
|
||||
inkscape:connector-curvature="0" />
|
||||
</symbol>
|
||||
<symbol
|
||||
style="display:inline"
|
||||
id="inkstitch_satin_start">
|
||||
<title
|
||||
id="inkstitch_title9432-1">Satin column starting point</title>
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="inkstitch_circle13166-6-2"
|
||||
d="M 9.2465269,-4.9265995e-6 C 9.246525,5.1067241 5.1067022,9.2465451 -2.715882e-5,9.2465451 -5.106756,9.2465451 -9.2465782,5.1067241 -9.2465801,-4.9265995e-6 -9.2465799,-5.1067349 -5.1067572,-9.2465579 -2.715882e-5,-9.2465579 c 2.45233855882,0 4.80423565882,0.974187 6.53830095882,2.708252 1.7340652,1.734066 2.708253,4.085963 2.7082531,6.5383009734005 0,0 0,0 0,0"
|
||||
style="opacity:1;vector-effect:none;fill:#fafafa;fill-opacity:1;fill-rule:evenodd;stroke:#003399;stroke-width:1.06501234;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:3.19503705, 3.19503705;stroke-dashoffset:0;stroke-opacity:1" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
style="opacity:1;vector-effect:none;fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:2.74180555;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
d="m 6.5728129,0.00355507 c 0,0 -10.4514,6.03412003 -10.4514,6.03412003 0,0 0,-12.06823 0,-12.06823 0,0 10.4514,6.03410997 10.4514,6.03410997"
|
||||
id="inkstitch_path4183-7" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
style="display:inline;opacity:1;fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:0.2;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke"
|
||||
d="m -3.0312502,1.577739 c 0,0 0.25,-3.6875002 0.25,-3.6875002 0,0 1.0312501,3.8437502 1.0312501,3.8437502 0,0 0.34375,-3.6250002 0.34375,-3.6250002 0,0 0.96874997,3.5625002 0.96874997,3.5625002 0,0 0.28125,-3.3125002 0.28125,-3.3125002 0,0 0.9375001,3.3125002 0.9375001,3.3125002 0,0 0.21875,-3.0937502 0.21875,-3.0937502 0,0 1.03125013,2.9687502 1.03125013,2.9687502 0,0 0.0625,-2.7187501 0.0625,-2.7187501 0,0 0.96875,2.4687501 0.96875,2.4687501 0,0 0.15625,-2.18750023 0.15625,-2.18750023"
|
||||
id="path31975" />
|
||||
</symbol>
|
||||
<symbol
|
||||
id="inkstitch_satin_end"
|
||||
style="display:inline">
|
||||
<title
|
||||
id="inkstitch_title9427-3">Satin column ending point</title>
|
||||
<path
|
||||
id="inkstitch_circle13166-60"
|
||||
d="m 9.220113,0.0792309 c -1.9e-6,5.106729 -4.1398241,9.24655 -9.246553,9.24655 -5.1067293,0 -9.2465521,-4.139821 -9.246554,-9.24655 1e-7,-2.452338 0.9741879,-4.804235 2.7082531,-6.538301 1.7340653,-1.734065 4.0859624,-2.708252 6.5383009,-2.708252 5.1067301,0 9.2465528,4.139823 9.246553,9.246553 0,0 0,0 0,0"
|
||||
style="opacity:1;vector-effect:none;fill:#fafafa;fill-opacity:1;fill-rule:evenodd;stroke:#003399;stroke-width:1.06500006;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:3.19500017, 3.19500017;stroke-dashoffset:0;stroke-opacity:1"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2.27154255;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:4.81866985, 4.81866985;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke"
|
||||
d="m -4.570439,-4.5704391 c 0,0 9.140878,0 9.140878,0 0,0 0,9.14087 0,9.14087 0,0 -9.140878,0 -9.140878,0 0,0 0,-9.14087 0,-9.14087"
|
||||
id="inkstitch_rect5371-2-6"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
style="display:inline;opacity:1;fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:0.2;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke"
|
||||
d="m -3.2964153,1.5777408 0.25,-3.6875001 1.0312501,3.8437501 0.34375,-3.6250001 0.9687501,3.5625001 0.28125,-3.3125001 0.9375,3.3125001 0.21875,-3.0937501 1.0312499,2.9687501 0.0625,-2.7187501 0.96875,2.4687501 0.15625,-2.1875"
|
||||
id="path31975-0-2"
|
||||
inkscape:connector-curvature="0" />
|
||||
</symbol>
|
||||
</defs>
|
||||
<metadata
|
||||
id="metadata8380">
|
||||
|
@ -356,5 +398,21 @@
|
|||
width="100%"
|
||||
height="100%"
|
||||
transform="translate(340.1839,75.511321)" />
|
||||
<use
|
||||
transform="translate(113.29209,37.983158)"
|
||||
height="100%"
|
||||
width="100%"
|
||||
y="0"
|
||||
x="0"
|
||||
id="use32163"
|
||||
xlink:href="#inkstitch_satin_start" />
|
||||
<use
|
||||
xlink:href="#inkstitch_satin_end"
|
||||
id="use37077"
|
||||
x="0"
|
||||
y="0"
|
||||
width="100%"
|
||||
height="100%"
|
||||
transform="translate(37.966704,37.983156)" />
|
||||
</g>
|
||||
</svg>
|
||||
|
|
Przed Szerokość: | Wysokość: | Rozmiar: 39 KiB Po Szerokość: | Wysokość: | Rozmiar: 43 KiB |
|
@ -0,0 +1,23 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
|
||||
<name>{% trans %}Auto-Route Satin Columns{% endtrans %}</name>
|
||||
<id>org.inkstitch.auto_satin.{{ locale }}</id>
|
||||
<dependency type="executable" location="extensions">inkstitch.py</dependency>
|
||||
<dependency type="executable" location="extensions">inkex.py</dependency>
|
||||
<param name="trim" type="boolean" _gui-text="{% trans %}Trim jump stitches{% endtrans %}">true</param>
|
||||
<param name="preserve_order" type="boolean" _gui-text="{% trans %}Preserve order of satin columns{% endtrans %}">false</param>
|
||||
<param name="extension" type="string" gui-hidden="true">auto_satin</param>
|
||||
<effect>
|
||||
<object-type>all</object-type>
|
||||
<effects-menu>
|
||||
<submenu name="Ink/Stitch">
|
||||
<submenu name="{% trans %}English{% endtrans %}">
|
||||
<submenu name="{% trans %}Satin Tools{% endtrans %}" />
|
||||
</submenu>
|
||||
</submenu>
|
||||
</effects-menu>
|
||||
</effect>
|
||||
<script>
|
||||
<command reldir="extensions" interpreter="python">inkstitch.py</command>
|
||||
</script>
|
||||
</inkscape-extension>
|
|
@ -9,7 +9,9 @@
|
|||
<object-type>all</object-type>
|
||||
<effects-menu>
|
||||
<submenu name="Ink/Stitch">
|
||||
<submenu name="{% trans %}English{% endtrans %}" />
|
||||
<submenu name="{% trans %}English{% endtrans %}">
|
||||
<submenu name="{% trans %}Satin Tools{% endtrans %}" />
|
||||
</submenu>
|
||||
</submenu>
|
||||
</effects-menu>
|
||||
</effect>
|
||||
|
|
|
@ -9,7 +9,9 @@
|
|||
<object-type>all</object-type>
|
||||
<effects-menu>
|
||||
<submenu name="Ink/Stitch">
|
||||
<submenu name="{% trans %}English{% endtrans %}" />
|
||||
<submenu name="{% trans %}English{% endtrans %}">
|
||||
<submenu name="{% trans %}Satin Tools{% endtrans %}" />
|
||||
</submenu>
|
||||
</submenu>
|
||||
</effects-menu>
|
||||
</effect>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<inkscape-extension xmlns="http://www.inkscape.org/namespace/inkscape/extension">
|
||||
<name>{% trans %}Flip Satin Columns{% endtrans %}</name>
|
||||
<name>{% trans %}Flip Satin Column Rails{% endtrans %}</name>
|
||||
<id>org.inkstitch.flip_satins.{{ locale }}</id>
|
||||
<dependency type="executable" location="extensions">inkstitch.py</dependency>
|
||||
<dependency type="executable" location="extensions">inkex.py</dependency>
|
||||
|
@ -9,7 +9,9 @@
|
|||
<object-type>all</object-type>
|
||||
<effects-menu>
|
||||
<submenu name="Ink/Stitch">
|
||||
<submenu name="{% trans %}English{% endtrans %}" />
|
||||
<submenu name="{% trans %}English{% endtrans %}">
|
||||
<submenu name="{% trans %}Satin Tools{% endtrans %}" />
|
||||
</submenu>
|
||||
</submenu>
|
||||
</effects-menu>
|
||||
</effect>
|
||||
|
|
Ładowanie…
Reference in New Issue