kopia lustrzana https://github.com/inkstitch/inkstitch
197 wiersze
8.8 KiB
Python
197 wiersze
8.8 KiB
Python
# Authors: see git history
|
|
#
|
|
# Copyright (c) 2023 Authors
|
|
# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details.
|
|
|
|
from inkex import Boolean, DirectedLineSegment, Path, PathElement, Transform
|
|
|
|
from ..elements import Stroke
|
|
from ..svg import PIXELS_PER_MM, generate_unique_id, get_correction_transform
|
|
from ..svg.tags import INKSTITCH_ATTRIBS, SVG_GROUP_TAG
|
|
from .base import InkstitchExtension
|
|
|
|
|
|
class JumpToStroke(InkstitchExtension):
|
|
"""Adds a running stitch as a connection between two (or more) selected elements.
|
|
The elements must have the same color and a minimum distance (collapse_len)."""
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
InkstitchExtension.__init__(self, *args, **kwargs)
|
|
self.arg_parser.add_argument("--tab")
|
|
|
|
self.arg_parser.add_argument("-i", "--minimum-jump-length", type=float, default=3.0, dest="min_jump")
|
|
self.arg_parser.add_argument("-a", "--maximum-jump-length", type=float, default=0, dest="max_jump")
|
|
self.arg_parser.add_argument("--connect", type=str, default="all", dest="connect")
|
|
self.arg_parser.add_argument("--exclude-trim", type=Boolean, default=True, dest="exclude_trim")
|
|
self.arg_parser.add_argument("--exclude-stop", type=Boolean, default=True, dest="exclude_stop")
|
|
self.arg_parser.add_argument("--exclude-force-lock-stitch", type=Boolean, default=True, dest="exclude_forced_lock")
|
|
|
|
self.arg_parser.add_argument("-m", "--merge", type=Boolean, default=False, dest="merge")
|
|
self.arg_parser.add_argument("--merge_subpaths", type=Boolean, default=False, dest="merge_subpaths")
|
|
self.arg_parser.add_argument("-l", "--stitch-length", type=float, default=2.5, dest="running_stitch_length_mm")
|
|
self.arg_parser.add_argument("-t", "--tolerance", type=float, default=2.0, dest="running_stitch_tolerance_mm")
|
|
|
|
def effect(self):
|
|
self._set_selection()
|
|
self.get_elements()
|
|
|
|
if self.options.merge_subpaths:
|
|
# when we merge stroke elements we are going to replace original path elements
|
|
# which would be bad in the case that the element has more subpaths
|
|
self._split_stroke_elements_with_subpaths()
|
|
|
|
last_group = None
|
|
last_layer = None
|
|
last_element = None
|
|
last_stitch_group = None
|
|
next_elements = [None]
|
|
if len(self.elements) > 1:
|
|
next_elements = self.elements[1:] + next_elements
|
|
for element, next_element in zip(self.elements, next_elements):
|
|
layer, group = self._get_element_layer_and_group(element)
|
|
stitch_groups = element.to_stitch_groups(last_stitch_group, next_element)
|
|
multiple = not self.options.merge_subpaths and stitch_groups
|
|
|
|
if multiple:
|
|
ending_point = stitch_groups[0].stitches[0]
|
|
stitch_groups = [stitch_groups[-1]]
|
|
|
|
if (not stitch_groups or
|
|
last_element is None or
|
|
(self.options.connect == "layer" and last_layer != layer) or
|
|
(self.options.connect == "group" and last_group != group) or
|
|
(self.options.exclude_trim and (last_element.has_command("trim") or last_element.trim_after)) or
|
|
(self.options.exclude_stop and (last_element.has_command("stop") or last_element.stop_after)) or
|
|
(self.options.exclude_forced_lock and last_element.force_lock_stitches)):
|
|
last_layer = layer
|
|
last_group = group
|
|
last_element = element
|
|
if stitch_groups:
|
|
last_stitch_group = stitch_groups[-1]
|
|
continue
|
|
|
|
for stitch_group in stitch_groups:
|
|
if last_stitch_group is None or stitch_group.color != last_stitch_group.color:
|
|
last_layer = layer
|
|
last_group = group
|
|
last_stitch_group = stitch_group
|
|
continue
|
|
start = last_stitch_group.stitches[-1]
|
|
if multiple:
|
|
end = ending_point
|
|
else:
|
|
end = stitch_group.stitches[0]
|
|
self.generate_stroke(last_element, element, start, end)
|
|
last_stitch_group = stitch_group
|
|
|
|
last_group = group
|
|
last_layer = layer
|
|
last_element = element
|
|
|
|
def _set_selection(self):
|
|
if not self.svg.selection:
|
|
self.svg.selection.clear()
|
|
|
|
def _get_element_layer_and_group(self, element):
|
|
layer = None
|
|
group = None
|
|
for ancestor in element.node.iterancestors(SVG_GROUP_TAG):
|
|
if group is None:
|
|
group = ancestor
|
|
if ancestor.groupmode == "layer":
|
|
layer = ancestor
|
|
break
|
|
return layer, group
|
|
|
|
def _split_stroke_elements_with_subpaths(self):
|
|
elements = []
|
|
for element in self.elements:
|
|
if isinstance(element, Stroke) and len(element.paths) > 1:
|
|
if element.get_param('stroke_method', None) in ['ripple_stitch']:
|
|
elements.append(element)
|
|
continue
|
|
node = element.node
|
|
parent = node.getparent()
|
|
index = parent.index(node)
|
|
paths = node.get_path().break_apart()
|
|
|
|
block_ids = []
|
|
for path in paths:
|
|
subpath_element = node.copy()
|
|
subpath_id = generate_unique_id(node, f'{node.get_id()}_', block_ids)
|
|
subpath_element.set('id', subpath_id)
|
|
subpath_element.set('d', str(path))
|
|
block_ids.append(subpath_id)
|
|
parent.insert(index, subpath_element)
|
|
elements.append(Stroke(subpath_element))
|
|
parent.remove(node)
|
|
else:
|
|
elements.append(element)
|
|
self.elements = elements
|
|
|
|
def _is_mergable(self, element1, element2):
|
|
if not (isinstance(element1, Stroke)):
|
|
return False
|
|
if (self.options.merge_subpaths and
|
|
element1.node.get_id() not in self.svg.selection.ids and
|
|
element2.node.get_id() not in self.svg.selection.ids):
|
|
return True
|
|
if (self.options.merge and
|
|
element1.node.TAG == "path" and
|
|
element1.get_param('stroke_method', None) == element2.get_param('stroke_method', None) and
|
|
not element1.get_param('stroke_method', '') == 'ripple_stitch'):
|
|
return True
|
|
return False
|
|
|
|
def generate_stroke(self, last_element, element, start, end):
|
|
node = element.node
|
|
parent = node.getparent()
|
|
index = parent.index(node)
|
|
|
|
# do not add a running stitch if the distance is smaller than min_jump setting
|
|
line = DirectedLineSegment((start.x, start.y), (end.x, end.y))
|
|
if line.length < self.options.min_jump * PIXELS_PER_MM:
|
|
return
|
|
# do not add a running stitch if the distance is longer than max_jump setting
|
|
if self.options.max_jump > 0 and line.length > self.options.max_jump * PIXELS_PER_MM:
|
|
return
|
|
|
|
path = Path([(start.x, start.y), (end.x, end.y)])
|
|
# option: merge line with paths
|
|
merged = False
|
|
if self._is_mergable(last_element, element):
|
|
path.transform(Transform(get_correction_transform(last_element.node, True)), True)
|
|
path = last_element.node.get_path() + path[1:]
|
|
last_element.node.set('d', str(path))
|
|
path.transform(-Transform(get_correction_transform(last_element.node)), True)
|
|
merged = True
|
|
if self._is_mergable(element, last_element):
|
|
path.transform(Transform(get_correction_transform(node)), True)
|
|
path = path + node.get_path()[1:]
|
|
node.set('d', str(path))
|
|
if merged:
|
|
# remove last element (since it is merged)
|
|
last_parent = last_element.node.getparent()
|
|
last_parent.remove(last_element.node)
|
|
# remove parent group if empty
|
|
if len(last_parent) == 0:
|
|
last_parent.getparent().remove(last_parent)
|
|
return
|
|
|
|
if merged:
|
|
return
|
|
|
|
# add simple stroke to connect elements
|
|
path.transform(Transform(get_correction_transform(node)), True)
|
|
color = element.color
|
|
style = f'stroke:{color};stroke-width:{self.svg.viewport_to_unit("1px")};stroke-dasharray:3, 1;fill:none;'
|
|
|
|
line = PathElement(d=str(path), style=style)
|
|
line.set(INKSTITCH_ATTRIBS['running_stitch_length_mm'], self.options.running_stitch_length_mm)
|
|
line.set(INKSTITCH_ATTRIBS['running_stitch_tolerance_mm'], self.options.running_stitch_tolerance_mm)
|
|
parent.insert(index, line)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
JumpToStroke().run()
|