Auto route for running stitch (#1638)

* add auto route for running stitch
* introduce free motion commands

Co-authored-by: Lex Neva <github.com@lexneva.name>
pull/1666/head
Kaalleen 2022-05-18 16:02:07 +02:00 zatwierdzone przez GitHub
rodzic bb0f3b8168
commit bc4f3b4699
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
23 zmienionych plików z 793 dodań i 299 usunięć

Wyświetl plik

@ -26,6 +26,12 @@ COMMANDS = {
# L10N command attached to an object
"fill_end": N_("Fill stitch ending position"),
# L10N command attached to an object
"run_start": N_("Auto-route running stitch starting position"),
# L10N command attached to an object
"run_end": N_("Auto-route running stitch ending position"),
# L10N command attached to an object
"satin_start": N_("Auto-route satin stitch starting position"),
@ -54,7 +60,8 @@ COMMANDS = {
"stop_position": N_("Jump destination for Stop commands (a.k.a. \"Frame Out position\")."),
}
OBJECT_COMMANDS = ["fill_start", "fill_end", "satin_start", "satin_end", "stop", "trim", "ignore_object", "satin_cut_point"]
OBJECT_COMMANDS = ["fill_start", "fill_end", "run_start", "run_end", "satin_start", "satin_end", "stop", "trim", "ignore_object", "satin_cut_point"]
FREE_MOVEMENT_OBJECT_COMMANDS = ["run_start", "run_end", "satin_start", "satin_end"]
LAYER_COMMANDS = ["ignore_layer"]
GLOBAL_COMMANDS = ["origin", "stop_position"]
@ -288,7 +295,7 @@ def add_group(document, node, command):
return group
def add_connector(document, symbol, element):
def add_connector(document, symbol, command, 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'))
@ -304,12 +311,14 @@ def add_connector(document, symbol, element):
"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")
})
if command not in FREE_MOVEMENT_OBJECT_COMMANDS:
path.attrib[CONNECTOR_TYPE] = "polyline"
symbol.getparent().insert(0, path)
@ -383,7 +392,7 @@ def add_commands(element, commands):
group = add_group(svg, element.node, command)
pos = get_command_pos(element, i, len(commands))
symbol = add_symbol(svg, group, command, pos)
add_connector(svg, symbol, element)
add_connector(svg, symbol, command, element)
def add_layer_commands(layer, commands):

Wyświetl plik

@ -10,24 +10,23 @@ from .element import EmbroideryElement
from .validation import ObjectTypeWarning
class PatternWarning(ObjectTypeWarning):
name = _("Pattern Element")
class MarkerWarning(ObjectTypeWarning):
name = _("Marker Element")
description = _("This element will not be embroidered. "
"It will appear as a pattern applied to objects in the same group as it. "
"Objects in sub-groups will be ignored.")
"It will be applied to objects in the same group. Objects in sub-groups will be ignored.")
steps_to_solve = [
_("To disable pattern mode, remove the pattern marker:"),
_("Turn back to normal embroidery element mode, remove the marker:"),
_('* Open the Fill and Stroke panel (Objects > Fill and Stroke)'),
_('* Go to the Stroke style tab'),
_('* Under "Markers" choose the first (empty) option in the first dropdown list.')
]
class PatternObject(EmbroideryElement):
class MarkerObject(EmbroideryElement):
def validation_warnings(self):
repr_point = next(inkex.Path(self.parse_path()).end_points)
yield PatternWarning(repr_point)
yield MarkerWarning(repr_point)
def to_stitch_groups(self, last_patch):
return []

Wyświetl plik

@ -216,10 +216,7 @@ class SatinColumn(EmbroideryElement):
# This isn't used for satins at all, but other parts of the code
# may need to know the general shape of a satin column.
flattened = self.flatten(self.parse_path())
line_strings = [shgeo.LineString(path) for path in flattened]
return shgeo.MultiLineString(line_strings)
return shgeo.MultiLineString(self.flattened_rails).convex_hull
@property
@cache

Wyświetl plik

@ -87,7 +87,7 @@ class Stroke(EmbroideryElement):
# manipulate invalid path
if len(flattened[0]) == 1:
return [[[flattened[0][0][0], flattened[0][0][1]], [flattened[0][0][0]+1.0, flattened[0][0][1]]]]
return [[[flattened[0][0][0], flattened[0][0][1]], [flattened[0][0][0] + 1.0, flattened[0][0][1]]]]
if self.manual_stitch_mode:
return [self.strip_control_points(subpath) for subpath in path]
@ -97,12 +97,13 @@ class Stroke(EmbroideryElement):
@property
@cache
def shape(self):
return self.as_multi_line_string().convex_hull
@cache
def as_multi_line_string(self):
line_strings = [shapely.geometry.LineString(path) for path in self.paths]
# Using convex_hull here is an important optimization. Otherwise
# complex paths cause operations on the shape to take a long time.
# This especially happens when importing machine embroidery files.
return shapely.geometry.MultiLineString(line_strings).convex_hull
return shapely.geometry.MultiLineString(line_strings)
@property
@param('manual_stitch',

Wyświetl plik

@ -4,7 +4,7 @@
# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details.
from ..commands import is_command
from ..patterns import is_pattern
from ..marker import has_marker
from ..svg.tags import (EMBROIDERABLE_TAGS, SVG_IMAGE_TAG, SVG_PATH_TAG,
SVG_POLYLINE_TAG, SVG_TEXT_TAG)
from .auto_fill import AutoFill
@ -13,7 +13,7 @@ from .element import EmbroideryElement
from .empty_d_object import EmptyDObject
from .fill import Fill
from .image import ImageObject
from .pattern import PatternObject
from .marker import MarkerObject
from .polyline import Polyline
from .satin_column import SatinColumn
from .stroke import Stroke
@ -30,8 +30,8 @@ def node_to_elements(node): # noqa: C901
elif node.tag == SVG_PATH_TAG and not node.get('d', ''):
return [EmptyDObject(node)]
elif is_pattern(node):
return [PatternObject(node)]
elif has_marker(node):
return [MarkerObject(node)]
elif node.tag in EMBROIDERABLE_TAGS:
element = EmbroideryElement(node)

Wyświetl plik

@ -6,6 +6,7 @@
from lib.extensions.troubleshoot import Troubleshoot
from .apply_threadlist import ApplyThreadlist
from .auto_run import AutoRun
from .auto_satin import AutoSatin
from .break_apart import BreakApart
from .cleanup import Cleanup
@ -61,6 +62,7 @@ __all__ = extensions = [StitchPlanPreview,
ConvertToStroke,
CutSatin,
AutoSatin,
AutoRun,
Lettering,
LetteringGenerateJson,
LetteringRemoveKerning,

Wyświetl plik

@ -0,0 +1,65 @@
# Authors: see git history
#
# Copyright (c) 2010 Authors
# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details.
import inkex
from ..elements import Stroke
from ..i18n import _
from ..stitches.auto_run import autorun
from .commands import CommandsExtension
class AutoRun(CommandsExtension):
COMMANDS = ["trim"]
def __init__(self, *args, **kwargs):
CommandsExtension.__init__(self, *args, **kwargs)
self.arg_parser.add_argument("-b", "--break_up", dest="break_up", type=inkex.Boolean, default=True)
self.arg_parser.add_argument("-p", "--preserve_order", dest="preserve_order", type=inkex.Boolean, default=False)
self.arg_parser.add_argument("-o", "--options", dest="options", type=str, default="")
self.arg_parser.add_argument("-i", "--info", dest="help", type=str, default="")
def effect(self):
elements = self.check_selection()
if not elements:
return
starting_point = self.get_starting_point()
ending_point = self.get_ending_point()
break_up = self.options.break_up
autorun(elements, self.options.preserve_order, break_up, starting_point, ending_point, self.options.trim)
def get_starting_point(self):
return self.get_command_point("run_start")
def get_ending_point(self):
return self.get_command_point("run_end")
def get_command_point(self, command_type):
command = None
for stroke in self.elements:
command = stroke.get_command(command_type)
# return the first occurence directly
if command:
return command.target_point
def check_selection(self):
if not self.get_elements():
return
if not self.svg.selection:
# L10N auto-route running stitch columns extension
inkex.errormsg(_("Please select one or more stroke elements."))
return False
elements = [element for element in self.elements if isinstance(element, Stroke)]
if len(elements) == 0:
inkex.errormsg(_("Please select at least one stroke element."))
return False
return elements

Wyświetl plik

@ -17,7 +17,7 @@ from ..commands import is_command, layer_commands
from ..elements import EmbroideryElement, nodes_to_elements
from ..elements.clone import is_clone
from ..i18n import _
from ..patterns import is_pattern
from ..marker import has_marker
from ..svg import generate_unique_id
from ..svg.tags import (CONNECTOR_TYPE, EMBROIDERABLE_TAGS, INKSCAPE_GROUPMODE,
NOT_EMBROIDERABLE_TAGS, SVG_CLIPPATH_TAG, SVG_DEFS_TAG,
@ -169,10 +169,10 @@ class InkstitchExtension(inkex.Effect):
if selected:
if node.tag == SVG_GROUP_TAG:
pass
elif (node.tag in EMBROIDERABLE_TAGS or is_clone(node)) and not is_pattern(node):
elif (node.tag in EMBROIDERABLE_TAGS or is_clone(node)) and not has_marker(node):
nodes.append(node)
# add images, text and patterns for the troubleshoot extension
elif troubleshoot and (node.tag in NOT_EMBROIDERABLE_TAGS or is_pattern(node)):
# add images, text and elements with a marker for the troubleshoot extension
elif troubleshoot and (node.tag in NOT_EMBROIDERABLE_TAGS or has_marker(node)):
nodes.append(node)
return nodes

Wyświetl plik

@ -17,7 +17,7 @@ class Reorder(InkstitchExtension):
objects = self.svg.selection
if not objects:
errormsg(_("Please select at least to elements to reorder."))
errormsg(_("Please select at least two elements to reorder."))
return
for obj in objects:

Wyświetl plik

@ -7,10 +7,12 @@ from copy import deepcopy
from os import path
import inkex
from shapely import geometry as shgeo
from .svg.tags import EMBROIDERABLE_TAGS
from .utils import cache, get_bundled_dir
MARKER = ['pattern']
MARKER = ['pattern', 'guide-line']
def ensure_marker(svg, marker):
@ -33,5 +35,45 @@ def set_marker(node, position, marker):
style = node.get('style') or ''
style = style.split(";")
style = [i for i in style if not i.startswith('marker-%s' % position)]
style.append('marker-%s:url(#inkstitch-pattern-marker)' % position)
style.append('marker-%s:url(#inkstitch-%s-marker)' % (position, marker))
node.set('style', ";".join(style))
def get_marker_elements(node, marker, get_fills=True, get_strokes=True):
from .elements import EmbroideryElement
from .elements.stroke import Stroke
fills = []
strokes = []
xpath = "./parent::svg:g/*[contains(@style, 'marker-start:url(#inkstitch-%s-marker)')]" % marker
markers = node.xpath(xpath, namespaces=inkex.NSS)
for marker in markers:
if marker.tag not in EMBROIDERABLE_TAGS:
continue
element = EmbroideryElement(marker)
fill = element.get_style('fill')
stroke = element.get_style('stroke')
if get_fills and fill is not None:
fill = Stroke(marker).paths
linear_rings = [shgeo.LinearRing(path) for path in fill]
for ring in linear_rings:
fills.append(shgeo.Polygon(ring))
if get_strokes and stroke is not None:
stroke = Stroke(marker).paths
line_strings = [shgeo.LineString(path) for path in stroke]
strokes.append(shgeo.MultiLineString(line_strings))
return {'fill': fills, 'stroke': strokes}
def has_marker(node, marker=list()):
if not marker:
marker = MARKER
for m in marker:
style = node.get('style') or ''
if "marker-start:url(#inkstitch-%s-marker)" % m in style:
return True
return False

Wyświetl plik

@ -3,25 +3,17 @@
# Copyright (c) 2010 Authors
# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details.
import inkex
from shapely import geometry as shgeo
from .marker import get_marker_elements
from .stitch_plan import Stitch
from .svg.tags import EMBROIDERABLE_TAGS
from .utils import Point
def is_pattern(node):
if node.tag not in EMBROIDERABLE_TAGS:
return False
style = node.get('style') or ''
return "marker-start:url(#inkstitch-pattern-marker)" in style
def apply_patterns(patches, node):
patterns = _get_patterns(node)
_apply_fill_patterns(patterns['fill_patterns'], patches)
_apply_stroke_patterns(patterns['stroke_patterns'], patches)
patterns = get_marker_elements(node, "pattern")
_apply_fill_patterns(patterns['fill'], patches)
_apply_stroke_patterns(patterns['stroke'], patches)
def _apply_stroke_patterns(patterns, patches):
@ -64,43 +56,13 @@ def _apply_fill_patterns(patterns, patches):
patch.stitches = patch_points
def _get_patterns(node):
from .elements import EmbroideryElement
from .elements.stroke import Stroke
fills = []
strokes = []
xpath = "./parent::svg:g/*[contains(@style, 'marker-start:url(#inkstitch-pattern-marker)')]"
patterns = node.xpath(xpath, namespaces=inkex.NSS)
for pattern in patterns:
if pattern.tag not in EMBROIDERABLE_TAGS:
continue
element = EmbroideryElement(pattern)
fill = element.get_style('fill')
stroke = element.get_style('stroke')
if fill is not None:
fill_pattern = Stroke(pattern).paths
linear_rings = [shgeo.LinearRing(path) for path in fill_pattern]
for ring in linear_rings:
fills.append(shgeo.Polygon(ring))
if stroke is not None:
stroke_pattern = Stroke(pattern).paths
line_strings = [shgeo.LineString(path) for path in stroke_pattern]
strokes.append(shgeo.MultiLineString(line_strings))
return {'fill_patterns': fills, 'stroke_patterns': strokes}
def _get_pattern_points(first, second, pattern):
points = []
intersection = shgeo.LineString([first, second]).intersection(pattern)
if isinstance(intersection, shgeo.Point):
points.append(Point(intersection.x, intersection.y))
if isinstance(intersection, shgeo.MultiPoint):
for point in intersection:
for point in intersection.geoms:
points.append(Point(point.x, point.y))
# sort points after their distance to first
points.sort(key=lambda point: point.distance(first))

Wyświetl plik

@ -0,0 +1,284 @@
# Authors: see git history
#
# Copyright (c) 2022 Authors
# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details.
from collections import defaultdict
import networkx as nx
from shapely.geometry import LineString, MultiLineString, MultiPoint, Point
from shapely.ops import nearest_points, substring, unary_union
import inkex
from ..commands import add_commands
from ..elements import Stroke
from ..i18n import _
from ..svg import PIXELS_PER_MM, generate_unique_id
from ..svg.tags import INKSCAPE_LABEL, INKSTITCH_ATTRIBS
from .utils.autoroute import (add_elements_to_group, add_jumps,
create_new_group, find_path,
get_starting_and_ending_nodes,
preserve_original_groups,
remove_original_elements)
class LineSegments:
'''
Takes elements and splits them into segments.
Attributes:
_lines -- a list of LineStrings from the subpaths of the Stroke elements
_elements -- a list of Stroke elements for each corresponding line in _lines
_intersection_points -- a dictionary with intersection points {line_index: [intersection_points]}
segments -- (public) a list of segments and corresponding elements [[segment, element], ...]
'''
def __init__(self, elements):
self._lines = []
self._elements = []
self._intersection_points = defaultdict(list)
self.segments = []
self._process_elements(elements)
self._get_intersection_points()
self._get_segments()
def _process_elements(self, elements):
for element in elements:
lines = element.as_multi_line_string().geoms
for line in lines:
# split at self-intersections if necessary
unary_lines = unary_union(line)
if isinstance(unary_lines, MultiLineString):
for unary_line in unary_lines.geoms:
self._lines.append(unary_line)
self._elements.append(element)
else:
self._lines.append(line)
self._elements.append(element)
def _get_intersection_points(self):
for i, line1 in enumerate(self._lines):
for j in range(i + 1, len(self._lines)):
line2 = self._lines[j]
distance = line1.distance(line2)
if distance > 50:
continue
if not distance == 0:
# add nearest points
near = nearest_points(line1, line2)
self._add_point(i, near[0])
self._add_point(j, near[1])
# add intersections
intersections = line1.intersection(line2)
if isinstance(intersections, Point):
self._add_point(i, intersections)
self._add_point(j, intersections)
elif isinstance(intersections, MultiPoint):
for point in intersections.geoms:
self._add_point(i, point)
self._add_point(j, point)
elif isinstance(intersections, LineString):
for point in intersections.coords:
self._add_point(i, Point(*point))
self._add_point(j, Point(*point))
def _add_point(self, element, point):
self._intersection_points[element].append(point)
def _get_segments(self):
'''
Splits elements into segments at intersection and "almost intersecions".
The split method would make this very easy (it can split a MultiString with
MultiPoints) but sadly it fails too often, while snap moves the points away
from where we want them. So we need to calculate the distance along the line
and finally split it into segments with shapelys substring method.
'''
self.segments = []
for i, line in enumerate(self._lines):
length = line.length
points = self._intersection_points[i]
distances = [0, length]
for point in points:
distances.append(line.project(point))
distances = sorted(set(distances))
for j in range(len(distances) - 1):
start = distances[j]
end = distances[j + 1]
if end - start > 0.1:
seg = substring(line, start, end)
self.segments.append([seg, self._elements[i]])
def autorun(elements, preserve_order=False, break_up=None, starting_point=None, ending_point=None, trim=False):
graph = build_graph(elements, preserve_order, break_up)
graph = add_jumps(graph, elements, preserve_order)
starting_point, ending_point = get_starting_and_ending_nodes(
graph, elements, preserve_order, starting_point, ending_point)
path = find_path(graph, starting_point, ending_point)
path = add_path_attribs(path)
new_elements, trims, original_parents = path_to_elements(graph, path, trim)
if preserve_order:
preserve_original_groups(new_elements, original_parents)
else:
parent = elements[0].node.getparent()
insert_index = parent.index(elements[0].node)
group = create_new_group(parent, insert_index, _("Auto-Run"))
add_elements_to_group(new_elements, group)
if trim:
add_trims(new_elements, trims)
remove_original_elements(elements)
def build_graph(elements, preserve_order, break_up):
if preserve_order:
graph = nx.DiGraph()
else:
graph = nx.Graph()
if not break_up:
segments = []
for element in elements:
line_strings = [[line, element] for line in element.as_multi_line_string().geoms]
segments.extend(line_strings)
else:
segments = LineSegments(elements).segments
for segment, element in segments:
for c1, c2 in zip(segment.coords[:-1], segment.coords[1:]):
start = Point(*c1)
end = Point(*c2)
graph.add_node(str(start), point=start)
graph.add_node(str(end), point=end)
graph.add_edge(str(start), str(end), element=element)
if preserve_order:
# The graph is a directed graph, but we want to allow travel in
# any direction, so we add the edge in the opposite direction too.
graph.add_edge(str(end), str(start), element=element)
return graph
def add_path_attribs(path):
# find_path() will have duplicated some of the edges in the graph. We don't
# want to sew the same running stitch twice. If a running stitch section appears
# twice in the path, we'll sew the first occurrence as a simple running stitch without
# the original running stitch repetitions and bean stitch settings.
seen = set()
for i, point in reversed(list(enumerate(path))):
if point in seen:
path[i] = (*point, "underpath")
else:
path[i] = (*point, "autorun")
seen.add(point)
seen.add((point[1], point[0]))
return path
def path_to_elements(graph, path, trim): # noqa: C901
element_list = []
original_parents = []
trims = []
d = ""
position = 0
path_direction = "autorun"
just_trimmed = False
el = None
for start, end, direction in path:
element = graph[start][end].get('element')
start_coord = graph.nodes[start]['point']
end_coord = graph.nodes[end]['point']
if element:
el = element
if just_trimmed:
if direction == "underpath":
# no sense in doing underpath after we trim
continue
else:
just_trimmed = False
# create a new element if direction (purpose) changes
if direction != path_direction:
if d:
element_list.append(create_element(d, position, path_direction, el))
original_parents.append(el.node.getparent())
d = ""
position += 1
path_direction = direction
if d == "":
d = "M %s %s, %s %s" % (start_coord.x, start_coord.y, end_coord.x, end_coord.y)
else:
d += ", %s %s" % (end_coord.x, end_coord.y)
elif el and d:
# this is a jump, so complete the element whose path we've been building
element_list.append(create_element(d, position, path_direction, el))
original_parents.append(el.node.getparent())
d = ""
if trim and start_coord.distance(end_coord) > 0.75 * PIXELS_PER_MM:
trims.append(position)
just_trimmed = True
position += 1
if d:
element_list.append(create_element(d, position, path_direction, el))
original_parents.append(el.node.getparent())
return element_list, trims, original_parents
def create_element(path, position, direction, element):
if not path:
return
style = inkex.Style(element.node.get("style"))
style = style + inkex.Style("stroke-dasharray:0.5,0.5;fill:none;")
el_id = "%s_%s_" % (direction, position)
index = position + 1
if direction == "autorun":
label = _("AutoRun %d") % index
else:
label = _("AutoRun Underpath %d") % index
stitch_length = element.node.get(INKSTITCH_ATTRIBS['running_stitch_length_mm'], '')
bean = element.node.get(INKSTITCH_ATTRIBS['bean_stitch_repeats'], 0)
repeats = int(element.node.get(INKSTITCH_ATTRIBS['repeats'], 1))
if repeats % 2 == 0:
repeats -= 1
node = inkex.PathElement()
node.set("id", generate_unique_id(element.node, el_id))
node.set(INKSCAPE_LABEL, label)
node.set("d", path)
node.set("style", str(style))
if stitch_length:
node.set(INKSTITCH_ATTRIBS['running_stitch_length_mm'], stitch_length)
if direction == "autorun":
node.set(INKSTITCH_ATTRIBS['repeats'], str(repeats))
if bean:
node.set(INKSTITCH_ATTRIBS['bean_stitch_repeats'], bean)
return Stroke(node)
def add_trims(elements, trim_indices):
for i in trim_indices:
add_commands(elements[i], ["trim"])

Wyświetl plik

@ -6,19 +6,24 @@
import math
from itertools import chain
import inkex
import networkx as nx
from shapely import geometry as shgeo
from shapely.geometry import Point as ShapelyPoint
import inkex
from ..commands import add_commands
from ..elements import SatinColumn, Stroke
from ..i18n import _
from ..svg import (PIXELS_PER_MM, generate_unique_id, get_correction_transform,
line_strings_to_csp)
from ..svg.tags import (INKSCAPE_LABEL, INKSTITCH_ATTRIBS)
from ..svg import PIXELS_PER_MM, generate_unique_id, line_strings_to_csp
from ..svg.tags import INKSCAPE_LABEL, INKSTITCH_ATTRIBS
from ..utils import Point as InkstitchPoint
from ..utils import cache, cut
from .utils.autoroute import (add_elements_to_group, add_jumps,
create_new_group, find_path,
get_starting_and_ending_nodes,
preserve_original_groups,
remove_original_elements)
class SatinSegment(object):
@ -177,7 +182,7 @@ class SatinSegment(object):
class JumpStitch(object):
"""A jump stitch between two points."""
def __init__(self, start, end):
def __init__(self, start, end, source_element, destination_element):
"""Initialize a JumpStitch.
Arguments:
@ -186,6 +191,8 @@ class JumpStitch(object):
self.start = start
self.end = end
self.source_element = source_element
self.destination_element = destination_element
def is_sequential(self, other):
# Don't bother joining jump stitches.
@ -196,6 +203,15 @@ class JumpStitch(object):
def length(self):
return self.start.distance(self.end)
def as_line_string(self):
return shgeo.LineString((self.start, self.end))
def should_trim(self):
actual_jump = self.as_line_string().difference(self.source_element.shape)
actual_jump = actual_jump.difference(self.destination_element.shape)
return actual_jump.length > PIXELS_PER_MM
class RunningStitch(object):
"""Running stitch along a path."""
@ -326,7 +342,7 @@ def auto_satin(elements, preserve_order=False, starting_point=None, ending_point
if preserve_order:
preserve_original_groups(new_elements, original_parents)
else:
group = create_new_group(parent, index)
group = create_new_group(parent, index, _("Auto-Route"))
add_elements_to_group(new_elements, group)
name_elements(new_elements, preserve_order)
@ -358,8 +374,8 @@ def build_graph(elements, preserve_order=False):
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_node(str(segment.start_point), point=segment.start_point, element=element)
graph.add_node(str(segment.end_point), point=segment.end_point, element=element)
graph.add_edge(str(segment.start_point), str(
segment.end_point), segment=segment, element=element)
@ -373,168 +389,6 @@ def build_graph(elements, preserve_order=False):
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))
if potential_edges:
edge = min(potential_edges, key=lambda p1_p2: p1_p2[0].distance(p1_p2[1]))
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(list(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.
@ -563,7 +417,10 @@ def path_to_operations(graph, path):
segment = segment.reversed()
operations.append(segment)
else:
operations.append(JumpStitch(graph.nodes[start]['point'], graph.nodes[end]['point']))
operations.append(JumpStitch(graph.nodes[start]['point'],
graph.nodes[end]['point'],
graph.nodes[start]['element'],
graph.nodes[end]['element']))
# 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
@ -616,59 +473,12 @@ def operations_to_elements_and_trims(operations, preserve_order):
elements.append(operation.to_element())
original_parent_nodes.append(operation.original_node.getparent())
elif isinstance(operation, (JumpStitch)):
if elements and operation.length > 0.75 * PIXELS_PER_MM:
if elements and operation.should_trim():
trims.append(len(elements) - 1)
return elements, list(set(trims)), original_parent_nodes
def remove_original_elements(elements):
for element in elements:
for command in element.commands:
command_group = command.use.getparent()
if command_group is not None and command_group.get('id').startswith('command_group'):
remove_from_parent(command_group)
else:
remove_from_parent(command.connector)
remove_from_parent(command.use)
remove_from_parent(element.node)
def remove_from_parent(node):
if node.getparent() is not None:
node.getparent().remove(node)
def preserve_original_groups(elements, original_parent_nodes):
"""Ensure that all elements are contained in the original SVG group elements.
When preserve_order is True, no SatinColumn or Stroke elements will be
reordered in the XML tree. This makes it possible to preserve original SVG
group membership. We'll ensure that each newly-created Element is added
to the group that contained the original SatinColumn that spawned it.
"""
for element, parent in zip(elements, original_parent_nodes):
if parent is not None:
parent.append(element.node)
element.node.set('transform', get_correction_transform(parent, child=True))
def create_new_group(parent, insert_index):
group = inkex.Group(attrib={
INKSCAPE_LABEL: _("Auto-Satin"),
"transform": get_correction_transform(parent, child=True)
})
parent.insert(insert_index, group)
return group
def add_elements_to_group(elements, group):
for element in elements:
group.append(element.node)
def name_elements(new_elements, preserve_order):
"""Give the newly-created SVG objects useful names.

Wyświetl plik

@ -0,0 +1,221 @@
# Authors: see git history
#
# Copyright (c) 2010 Authors
# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details.
from itertools import combinations
import networkx as nx
from shapely.geometry import Point, MultiPoint
from shapely.ops import nearest_points
import inkex
from ...svg import get_correction_transform
from ...svg.tags import INKSCAPE_LABEL
def find_path(graph, starting_node, ending_node):
"""Find a path through the graph that sews every edge."""
# 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".
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.
graph = nx.Graph(graph)
graph.remove_edges_from(list(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 edge 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 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 elements 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))
if potential_edges:
edge = min(potential_edges, key=lambda p1_p2: p1_p2[0].distance(p1_p2[1]))
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)
return graph
def possible_jumps(graph):
"""All possible jump stitches in the graph with their lengths.
Returns: a generator of tuples: (node1, node2, length)
"""
for component1, component2 in combinations(nx.connected_components(graph), 2):
points1 = MultiPoint([graph.nodes[node]['point'] for node in component1])
points2 = MultiPoint([graph.nodes[node]['point'] for node in component2])
start_point, end_point = nearest_points(points1, points2)
yield (str(start_point), str(end_point), start_point.distance(end_point))
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 path 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 = 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 remove_original_elements(elements):
for element in elements:
for command in element.commands:
command_group = command.use.getparent()
if command_group is not None and command_group.get('id').startswith('command_group'):
remove_from_parent(command_group)
else:
remove_from_parent(command.connector)
remove_from_parent(command.use)
remove_from_parent(element.node)
def remove_from_parent(node):
if node.getparent() is not None:
node.getparent().remove(node)
def create_new_group(parent, insert_index, label):
group = inkex.Group(attrib={
INKSCAPE_LABEL: label,
"transform": get_correction_transform(parent, child=True)
})
parent.insert(insert_index, group)
return group
def preserve_original_groups(elements, original_parent_nodes):
"""Ensure that all elements are contained in the original SVG group elements.
When preserve_order is True, no elements will be reordered in the XML tree.
This makes it possible to preserve original SVG group membership. We'll
ensure that each newly-created element is added to the group that contained
the original element that spawned it.
"""
for element, parent in zip(elements, original_parent_nodes):
if parent is not None:
parent.append(element.node)
element.node.set('transform', get_correction_transform(parent, child=True))
def add_elements_to_group(elements, group):
for element in elements:
group.append(element.node)

Wyświetl plik

@ -296,6 +296,34 @@
id="path31975-0-2"
inkscape:connector-curvature="0" />
</symbol>
<symbol
id="inkstitch_run_start"
style="display:inline">
<title
id="inkstitch_title9427-3">Running stitch starting point</title>
<path
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"
d="M 9.2201108,0.05852023 A 9.2576327,9.2122261 0 0 1 -0.03752532,9.2707454 9.2576327,9.2122261 0 0 1 -9.2951565,0.05852023 9.2576327,9.2122261 0 0 1 -0.03752532,-9.1537082 9.2576327,9.2122261 0 0 1 9.2201108,0.05852023 Z"
id="inkstitch-autorun_start-circle" />
<path
style="fill:#000000;"
id="inkstitch-autorun_start-path"
d="M -3.8159516,-5.6184574 c -0.3237427,0.020819 -0.5868063,0.2896236 -0.5907925,0.6167873 -0.00301,0.2444103 0.1384108,0.4575508 0.3447536,0.5613983 l -0.084402,6.772037 c -0.1244867,-0.03501 -0.2442949,-0.092709 -0.3519231,-0.1734001 -0.018852,-0.014824 -0.04551,-0.014824 -0.064361,0 -0.018491,0.013861 -0.025495,0.038228 -0.017164,0.05968 l 0.6437207,1.8106215 c 0.016803,0.044746 0.080472,0.044746 0.097275,0 l 0.6923582,-1.7949729 c 0.01924,-0.049558 -0.04036,-0.092132 -0.081524,-0.058277 -0.1120849,0.077886 -0.2339971,0.131597 -0.3605046,0.1634443 l 0.078701,-6.7706068 c 0.2092494,-0.097934 0.3546084,-0.3086274 0.3576257,-0.5542753 0.00418,-0.34328 -0.2710359,-0.6282831 -0.6165645,-0.632436 -0.016194,-1.924e-4 -0.031281,-0.00102 -0.047198,-3e-7 z m 0.9498575,1.1369602 c -0.042382,0.00503 -0.060846,0.055939 -0.031474,0.086686 l 1.2703061,1.4510604 c 0.035433,0.033525 0.093621,0.00416 0.087254,-0.044058 -0.016443,-0.1368774 -0.00712,-0.2718022 0.025745,-0.3993565 l 5.8279214,3.19201934 c -0.00955,0.23010929 0.1105352,0.45752327 0.3261511,0.57558932 0.3026489,0.1656996 0.6815068,0.0590468 0.8482917,-0.24160502 0.1667849,-0.30067931 0.059461,-0.68276459 -0.2431877,-0.84846414 -0.216862,-0.11872613 -0.4758284,-0.0980447 -0.6666138,0.0341025 l -5.8250435,-3.1905893 c 0.090438,-0.093452 0.1986471,-0.1708699 0.32186037,-0.2274139 0.044292,-0.022937 0.033578,-0.088721 -0.015724,-0.096642 L -2.8489315,-4.4800672 c -0.00556,-0.00146 -0.011405,-0.00189 -0.017162,-0.00146 z m 6.9651605,5.10354566 -1.909736,0.3126702 c -0.047392,0.011523 -0.054063,0.075768 -0.01,0.096642 0.1244589,0.057067 0.2344955,0.1342923 0.3261511,0.2273865 L -3.4211257,4.5999927 C -3.6139873,4.4704034 -3.8733966,4.4493919 -4.0877673,4.5701537 -4.38834,4.7394835 -4.4899335,5.1199736 -4.3194944,5.4186179 c 0.170439,0.2986167 0.5534215,0.4066446 0.8540219,0.2373147 0.2139276,-0.1204863 0.3297497,-0.3481203 0.3175695,-0.5769918 L 2.778711,1.7376949 c 0.03333,0.1260415 0.043323,0.2590963 0.027184,0.3936635 -0.00791,0.053024 0.060763,0.081379 0.092985,0.038365 L 4.1477039,0.70304186 c 0.026048,-0.036523 -0.00388,-0.086439 -0.048637,-0.0809934 Z" />
</symbol>
<symbol
id="inkstitch_run_end"
style="display:inline">
<title
id="inkstitch_title9427-3">Running stitch ending point</title>
<path
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"
d="M 9.2201108,0.05852023 A 9.2576327,9.2122261 0 0 1 -0.03752532,9.2707454 9.2576327,9.2122261 0 0 1 -9.2951565,0.05852023 9.2576327,9.2122261 0 0 1 -0.03752532,-9.1537082 9.2576327,9.2122261 0 0 1 9.2201108,0.05852023 Z"
id="inkstitch-autorun_end-circle" />
<path
style="fill:#000000;"
id="inkstitch-autorun_end-path"
d="M -1.5977169,-4.8605484 c -0.00319,1.523e-4 -0.00643,6.284e-4 -0.00955,0.00139 l -1.738218,0.6114515 c -0.044131,0.013274 -0.04771,0.07406 -0.00546,0.092381 l 1.7286718,0.6522048 c 0.042543,0.011464 0.077832,-0.033916 0.055977,-0.072004 -0.075631,-0.1076723 -0.1279528,-0.2256474 -0.1583813,-0.3478501 l 5.3580083,0.0693 C 3.7283489,-3.6574881 3.9289854,-3.5209456 4.163113,-3.51807 4.4929661,-3.51396 4.7679522,-3.7755203 4.7721242,-4.1036793 4.7763142,-4.4318766 4.5079699,-4.70559 4.178155,-4.7096844 3.9431472,-4.7126044 3.7385104,-4.5784741 3.6388043,-4.38223 l -5.3566689,-0.065225 c 0.033509,-0.1204507 0.089391,-0.2361787 0.1679695,-0.3396805 0.019712,-0.034678 -0.00793,-0.077126 -0.047786,-0.073375 z m -2.6203001,0.2119742 c -0.3227531,0.0041 -0.5844387,0.2625539 -0.5884958,0.5856093 -0.00295,0.2329983 0.129063,0.4383262 0.3236143,0.5394287 l -0.065546,5.2964025 c -0.1188053,-0.03344 -0.233209,-0.088553 -0.3359005,-0.1657553 -0.018028,-0.014169 -0.043442,-0.014169 -0.061451,0 -0.016535,0.013426 -0.022582,0.035878 -0.015004,0.055721 l 0.6130875,1.7310434 c 0.013338,0.043896 0.074426,0.047476 0.092855,0.00543 l 0.6608737,-1.7201931 c 0.021243,-0.048142 -0.037912,-0.091505 -0.077832,-0.057054 -0.1085476,0.075489 -0.2269701,0.1273635 -0.3495646,0.1576047 l 0.069641,-5.2950505 c 0.1995845,-0.095713 0.3397855,-0.2989653 0.3427327,-0.5326301 0.00412,-0.3281781 -0.2641734,-0.5964641 -0.5939692,-0.6005585 -0.00519,-7.62e-5 -0.00991,-7.62e-5 -0.015023,0 z m 8.4425639,1.3302136 c -0.020458,-1.333e-4 -0.038925,0.012283 -0.046428,0.03125 l -0.6608735,1.714742 c -0.021243,0.04818 0.037931,0.091524 0.077851,0.057054 0.1072273,-0.074556 0.2244441,-0.1271727 0.3454502,-0.1576044 L 3.8750009,3.6234841 C 3.6760287,3.7194257 3.5366315,3.9202022 3.5336461,4.15341 3.529436,4.4816071 3.7964416,4.7512453 4.1262565,4.7553397 4.4560331,4.759449 4.726962,4.4979085 4.731134,4.1697112 4.7341354,3.934523 4.5996979,3.7276145 4.4020654,3.6275594 L 4.4703294,-1.66612 c 0.1197239,0.033383 0.2338597,0.08802 0.3372785,0.1657745 0.039309,0.032355 0.096166,-0.00834 0.077832,-0.055721 L 4.2709744,-3.2871101 c -0.00748,-0.018949 -0.02595,-0.031384 -0.046428,-0.031251 z M 1.5114302,3.6180947 c -0.037451,6.856e-4 -0.060627,0.040868 -0.042351,0.073356 0.075823,0.1079391 0.1280103,0.2267139 0.1584006,0.3492023 L -3.7305473,3.971372 c -0.096108,-0.1988532 -0.2989079,-0.338138 -0.5338966,-0.3410517 -0.3297958,-0.0041 -0.5994042,0.2615256 -0.6035188,0.5896847 -0.00413,0.3281972 0.2627956,0.6019488 0.5926104,0.6060241 0.2345678,0.00292 0.4394534,-0.1304295 0.5393508,-0.3261024 l 5.3580084,0.063873 C 1.5885549,4.684593 1.5314859,4.7996924 1.4526979,4.90348 1.4299049,4.942272 1.4668789,4.9887 1.5100529,4.975503 L 3.2496085,4.3640511 c 0.043002,-0.015959 0.043002,-0.076441 0,-0.0924 L 1.5250561,3.6194659 c -0.00446,-0.00112 -0.00905,-0.00154 -0.013665,-0.00139 Z" />
</symbol>
</defs>
<metadata
id="metadata8380">
@ -414,5 +442,21 @@
width="100%"
height="100%"
transform="translate(37.966704,37.983156)" />
<use
xlink:href="#inkstitch_run_start"
id="use37078"
x="0"
y="0"
width="100%"
height="100%"
transform="translate(113.4214,113.28861)" />
<use
xlink:href="#inkstitch_run_end"
id="use37079"
x="0"
y="0"
width="100%"
height="100%"
transform="translate(37.830849,113.28861)" />
</g>
</svg>

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 43 KiB

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 50 KiB

Wyświetl plik

@ -33,5 +33,22 @@
d="M 4.9673651,5.7245662 C 4.7549848,5.7646159 4.6247356,5.522384 4.6430021,5.3419847 4.6765851,5.0103151 5.036231,4.835347 5.3381858,4.8987426 5.7863901,4.9928495 6.0126802,5.4853625 5.9002872,5.9065088 5.7495249,6.4714237 5.1195537,6.7504036 4.5799191,6.5874894 3.898118,6.3816539 3.5659013,5.6122905 3.7800789,4.9545192 4.0402258,4.1556558 4.9498996,3.7699484 5.7256318,4.035839 6.6416744,4.3498087 7.0810483,5.4003986 6.7631909,6.2939744 6.395633,7.3272552 5.2038143,7.8204128 4.1924535,7.4503931 3.0418762,7.0294421 2.4948761,5.6961604 2.9171752,4.567073 3.3914021,3.2991406 4.8663228,2.6982592 6.1130974,3.1729158 7.4983851,3.7003207 8.1531869,5.3169977 7.6260947,6.6814205 7.0456093,8.1841025 5.2870784,8.8928844 3.8050073,8.3132966 2.1849115,7.6797506 1.4221671,5.7793073 2.0542715,4.1796074 2.7408201,2.4420977 4.7832541,1.6253548 6.5005435,2.310012 8.3554869,3.0495434 9.2262638,5.2339874 8.4890181,7.0688861 8.4256397,7.2266036 8.3515789,7.379984 8.2675333,7.5277183" />
</g>
</marker>
<marker
refX="10"
refY="5"
orient="auto"
id="inkstitch-guide-line-marker">
<g
id="inkstitch-guide-line-group">
<path
style="fill:#fafafa;stroke:#ff5500;stroke-width:0.5;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:1, 1;stroke-dashoffset:0;stroke-opacity:1;fill-opacity:0.8;"
d="M 10.12911,5.2916678 A 4.8374424,4.8374426 0 0 1 5.2916656,10.12911 4.8374424,4.8374426 0 0 1 0.45422399,5.2916678 4.8374424,4.8374426 0 0 1 5.2916656,0.45422399 4.8374424,4.8374426 0 0 1 10.12911,5.2916678 Z"
id="inkstitch-guide-line-marker-circle" />
<path
style="fill:none;stroke:#000000;stroke-width:0.4;stroke-linecap:round;stroke-miterlimit:4;"
id="inkstitch-guide-line-marker-spiral"
d="M 1.7506092,6.2728168 3.4558825,5.2833684 7.1038696,7.400035 8.8193256,6.4046783 M 1.7963222,4.129626 3.4558825,3.1667016 7.1038696,5.2833682 8.8475785,4.2716184 M 1.6318889,5.2833683 3.4558825,4.225035 7.1038696,6.3417016 8.9558408,5.2677331" />
</g>
</marker>
</defs>
</svg>

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 2.4 KiB

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 3.5 KiB

Wyświetl plik

@ -0,0 +1,41 @@
<?xml version="1.0" encoding="UTF-8"?>
<inkscape-extension translationdomain="inkstitch" xmlns="http://www.inkscape.org/namespace/inkscape/extension">
<name>Autoroute Running Stitch</name>
<id>org.inkstitch.auto_run</id>
<param name="extension" type="string" gui-hidden="true">auto_run</param>
<effect>
<object-type>all</object-type>
<effects-menu>
<submenu name="Ink/Stitch" translatable="no">
<submenu name="Tools: Stroke" />
</submenu>
</effects-menu>
</effect>
<param name="options" type="notebook">
<page name="options" gui-text="Autoroute Running Stitch Options">
<spacer />
<param name="break_up" type="boolean" gui-text="Add nodes at intersections">true</param>
<spacer />
<param name="preserve_order" type="boolean" gui-text="Preserve order of running stitches">false</param>
<spacer />
<param name="trim" type="boolean" gui-text="Trim jump stitches">false</param>
</page>
<page name="help" gui-text="Help">
<label appearance="header">Autoroute Running Stitch</label>
<label>Add nodes at intersections:</label>
<spacer />
<label indent="1">- Enabled (automatic). Ink/Stitch will add some nodes for better routing. This is the default setting.</label>
<spacer />
<label indent="1">- Disabled (manual). Choose this option if you have manually set nodes at crucial spots.</label>
<spacer />
<separator />
<label>Use Start- end end commands to define where auto-routing for running stitch should start and end.</label>
<separator />
<label>More info on our website:</label>
<label appearance="url">https://inkstitch.org/docs/stroke-tools#auto-route-running-stitch</label>
</page>
</param>
<script>
{{ command_tag | safe }}
</script>
</inkscape-extension>

Wyświetl plik

@ -9,7 +9,7 @@
<object-type>all</object-type>
<effects-menu>
<submenu name="Ink/Stitch" translatable="no">
<submenu name="Satin Tools" />
<submenu name="Tools: Satin" />
</submenu>
</effects-menu>
</effect>

Wyświetl plik

@ -7,7 +7,7 @@
<object-type>all</object-type>
<effects-menu>
<submenu name="Ink/Stitch" translatable="no">
<submenu name="Fill Tools" />
<submenu name="Tools: Fill" />
</submenu>
</effects-menu>
</effect>

Wyświetl plik

@ -7,7 +7,7 @@
<object-type>all</object-type>
<effects-menu>
<submenu name="Ink/Stitch" translatable="no">
<submenu name="Satin Tools" />
<submenu name="Tools: Satin" />
</submenu>
</effects-menu>
</effect>

Wyświetl plik

@ -10,7 +10,7 @@
<object-type>all</object-type>
<effects-menu>
<submenu name="Ink/Stitch" translatable="no">
<submenu name="Satin Tools" />
<submenu name="Tools: Satin" />
</submenu>
</effects-menu>
</effect>

Wyświetl plik

@ -7,7 +7,7 @@
<object-type>all</object-type>
<effects-menu>
<submenu name="Ink/Stitch" translatable="no">
<submenu name="Satin Tools" />
<submenu name="Tools: Satin" />
</submenu>
</effects-menu>
</effect>

Wyświetl plik

@ -7,7 +7,7 @@
<object-type>all</object-type>
<effects-menu>
<submenu name="Ink/Stitch" translatable="no">
<submenu name="Satin Tools" />
<submenu name="Tools: Satin" />
</submenu>
</effects-menu>
</effect>