inkstitch/lib/extensions/jump_to_stroke.py

194 wiersze
8.6 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
for element in self.elements:
layer, group = self._get_element_layer_and_group(element)
stitch_groups = element.to_stitch_groups(last_stitch_group)
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)
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: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()