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
Lex Neva 2018-10-30 17:43:21 -06:00 zatwierdzone przez GitHub
rodzic d9525968a2
commit be833f898f
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
22 zmienionych plików z 1059 dodań i 167 usunięć

Wyświetl plik

@ -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)

Wyświetl plik

@ -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"]

Wyświetl plik

@ -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

Wyświetl plik

@ -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

Wyświetl plik

@ -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]

Wyświetl plik

@ -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"])

Wyświetl plik

@ -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

Wyświetl plik

@ -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)

Wyświetl plik

@ -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

Wyświetl plik

@ -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
})

Wyświetl plik

@ -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

Wyświetl plik

@ -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

Wyświetl plik

@ -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))

Wyświetl plik

@ -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

Wyświetl plik

@ -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

Wyświetl plik

@ -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

Wyświetl plik

@ -1,7 +1,7 @@
./pyembroidery
backports.functools_lru_cache
wxPython
networkx
networkx==2.2
shapely
lxml
appdirs

Wyświetl plik

@ -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

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>