kopia lustrzana https://github.com/inkstitch/inkstitch
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
rodzic
bb0f3b8168
commit
bc4f3b4699
|
@ -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):
|
||||
|
|
|
@ -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 []
|
|
@ -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
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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"])
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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)
|
|
@ -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 |
|
@ -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 |
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
Ładowanie…
Reference in New Issue