diff --git a/lib/elements/satin_column.py b/lib/elements/satin_column.py index 778fc88a4..3f5f05e55 100644 --- a/lib/elements/satin_column.py +++ b/lib/elements/satin_column.py @@ -12,7 +12,8 @@ from shapely import geometry as shgeo from shapely.ops import nearest_points from ..i18n import _ -from ..svg import line_strings_to_csp, point_lists_to_csp +from ..svg import (PIXELS_PER_MM, apply_transforms, line_strings_to_csp, + point_lists_to_csp) from ..utils import Point, cache, collapse_duplicate_point, cut from .element import EmbroideryElement, Patch, param from .validation import ValidationError, ValidationWarning @@ -74,12 +75,31 @@ class SatinColumn(EmbroideryElement): def satin_column(self): return self.get_boolean_param("satin_column") + # I18N: Split stitch divides a satin column into equal with parts if the maximum stitch length is exceeded + @property + @param('split_stitch', + _('Split stitch'), + tooltip=_('Sets additional stitches if the satin column exceeds the maximum stitch length.'), + type='boolean', + default='false') + def split_stitch(self): + return self.get_boolean_param("split_stitch") + # I18N: "E" stitch is so named because it looks like the letter E. @property @param('e_stitch', _('"E" stitch'), type='boolean', default='false') def e_stitch(self): return self.get_boolean_param("e_stitch") + @property + @param('max_stitch_length_mm', + _('Maximum stitch length'), + tooltip=_('Maximum stitch length for split stitches.'), + type='float', unit="mm", + default=12.1) + def max_stitch_length(self): + return max(self.get_float_param("max_stitch_length_mm", 12.4), 0.1 * PIXELS_PER_MM) + @property def color(self): return self.get_style("stroke") @@ -556,6 +576,20 @@ class SatinColumn(EmbroideryElement): return SatinColumn(node) + def get_patterns(self): + xpath = ".//*[@inkstitch:pattern='%(id)s']" % dict(id=self.node.get('id')) + patterns = self.node.getroottree().getroot().xpath(xpath) + line_strings = [] + for pattern in patterns: + d = pattern.get_path() + path = paths.Path(d).to_superpath() + path = apply_transforms(path, pattern) + path = self.flatten(path) + lines = [shgeo.LineString(p) for p in path] + for line in lines: + line_strings.append(line) + return shgeo.MultiLineString(line_strings) + @property @cache def center_line(self): @@ -787,6 +821,63 @@ class SatinColumn(EmbroideryElement): return patch + def do_pattern_satin(self, patterns): + # elements with the attribute 'inkstitch:pattern' set to this elements id will cause extra stitches to be added + patch = Patch(color=self.color) + sides = self.plot_points_on_rails(self.zigzag_spacing, self.pull_compensation) + for i, (left, right) in enumerate(zip(*sides)): + patch.add_stitch(left) + for point in self._get_pattern_points(left, right, patterns): + patch.add_stitch(point) + patch.add_stitch(right) + if not i+1 >= len(sides[0]): + for point in self._get_pattern_points(right, sides[0][i+1], patterns): + patch.add_stitch(point) + return patch + + def do_split_stitch(self): + # stitches exceeding the maximum stitch length will be divided into equal parts through additional stitches + patch = Patch(color=self.color) + sides = self.plot_points_on_rails(self.zigzag_spacing, self.pull_compensation) + for i, (left, right) in enumerate(zip(*sides)): + patch.add_stitch(left) + points, count = self._get_split_points(left, right) + for point in points: + patch.add_stitch(point) + patch.add_stitch(right) + # it is possible that the way back has a different length from the first + # but it looks ugly if the points differ too much + # so let's make sure they have at least the same amount of divisions + if not i+1 >= len(sides[0]): + points, count = self._get_split_points(right, sides[0][i+1], count) + for point in points: + patch.add_stitch(point) + + return patch + + def _get_pattern_points(self, left, right, patterns): + points = [] + for pattern in patterns: + intersection = shgeo.LineString([left, right]).intersection(pattern) + if isinstance(intersection, shgeo.Point): + points.append(Point(intersection.x, intersection.y)) + if isinstance(intersection, shgeo.MultiPoint): + for point in intersection: + points.append(Point(point.x, point.y)) + # sort points after their distance to left + points.sort(key=lambda point: point.distance(left)) + return points + + def _get_split_points(self, left, right, count=None): + points = [] + distance = left.distance(right) + split_count = count or int(distance / self.max_stitch_length) + for i in range(split_count): + line = shgeo.LineString((left, right)) + split_point = line.interpolate((i+1)/split_count, normalized=True) + points.append(Point(split_point.x, split_point.y)) + return [points, split_count] + def to_patches(self, last_patch): # Stitch a variable-width satin column, zig-zagging between two paths. @@ -807,8 +898,13 @@ class SatinColumn(EmbroideryElement): # zigzags sit on the contour walk underlay like rail ties on rails. patch += self.do_zigzag_underlay() + patterns = self.get_patterns() if self.e_stitch: patch += self.do_e_stitch() + elif self.split_stitch: + patch += self.do_split_stitch() + elif self.get_patterns(): + patch += self.do_pattern_satin(patterns) else: patch += self.do_satin() diff --git a/lib/extensions/__init__.py b/lib/extensions/__init__.py index 25f835c33..70df7c373 100644 --- a/lib/extensions/__init__.py +++ b/lib/extensions/__init__.py @@ -5,6 +5,7 @@ from lib.extensions.troubleshoot import Troubleshoot +from .apply_satin_pattern import ApplySatinPattern from .auto_satin import AutoSatin from .break_apart import BreakApart from .cleanup import Cleanup @@ -45,6 +46,7 @@ __all__ = extensions = [StitchPlanPreview, GlobalCommands, ConvertToSatin, CutSatin, + ApplySatinPattern, AutoSatin, Lettering, LetteringGenerateJson, diff --git a/lib/extensions/apply_satin_pattern.py b/lib/extensions/apply_satin_pattern.py new file mode 100644 index 000000000..9da810752 --- /dev/null +++ b/lib/extensions/apply_satin_pattern.py @@ -0,0 +1,39 @@ +# Authors: see git history +# +# Copyright (c) 2021 Authors +# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details. + +import inkex + +from ..i18n import _ +from ..svg.tags import INKSTITCH_ATTRIBS +from .base import InkstitchExtension +from ..elements import SatinColumn + + +class ApplySatinPattern(InkstitchExtension): + # Add inkstitch:pattern attribute to selected patterns. The patterns will be projected on a satin column, which must be in the selection too + + def effect(self): + if not self.get_elements(): + return + + if not self.svg.selected or not any(isinstance(item, SatinColumn) for item in self.elements) or len(self.svg.selected) < 2: + inkex.errormsg(_("Please select at least one satin column and a pattern.")) + return + + if sum(isinstance(item, SatinColumn) for item in self.elements) > 1: + inkex.errormsg(_("Please select only one satin column.")) + return + + satin_id = self.get_satin_column().node.get('id', None) + patterns = self.get_patterns() + + for pattern in patterns: + pattern.node.set(INKSTITCH_ATTRIBS['pattern'], satin_id) + + def get_satin_column(self): + return list(filter(lambda satin: isinstance(satin, SatinColumn), self.elements))[0] + + def get_patterns(self): + return list(filter(lambda satin: not isinstance(satin, SatinColumn), self.elements)) diff --git a/lib/extensions/base.py b/lib/extensions/base.py index 70ca47011..f23ec5e2d 100644 --- a/lib/extensions/base.py +++ b/lib/extensions/base.py @@ -18,7 +18,8 @@ from ..elements.clone import is_clone from ..i18n import _ from ..svg import generate_unique_id from ..svg.tags import (CONNECTOR_TYPE, EMBROIDERABLE_TAGS, INKSCAPE_GROUPMODE, - NOT_EMBROIDERABLE_TAGS, SVG_DEFS_TAG, SVG_GROUP_TAG) + INKSTITCH_ATTRIBS, NOT_EMBROIDERABLE_TAGS, + SVG_DEFS_TAG, SVG_GROUP_TAG) SVG_METADATA_TAG = inkex.addNS("metadata", "svg") @@ -170,9 +171,9 @@ class InkstitchExtension(inkex.Effect): if selected: if node.tag == SVG_GROUP_TAG: pass - elif getattr(node, "get_path", None): + elif (node.tag in EMBROIDERABLE_TAGS or is_clone(node)) and not node.get(INKSTITCH_ATTRIBS['pattern']): nodes.append(node) - elif troubleshoot and (node.tag in NOT_EMBROIDERABLE_TAGS or node.tag in EMBROIDERABLE_TAGS or is_clone(node)): + elif troubleshoot and node.tag in NOT_EMBROIDERABLE_TAGS: nodes.append(node) return nodes diff --git a/lib/svg/tags.py b/lib/svg/tags.py index 5c1d892a3..c8e9b67e5 100644 --- a/lib/svg/tags.py +++ b/lib/svg/tags.py @@ -86,6 +86,8 @@ inkstitch_attribs = [ 'zigzag_underlay_inset_mm', 'zigzag_underlay_spacing_mm', 'e_stitch', + 'pattern', + 'split_stitch', 'pull_compensation_mm', 'stroke_first', # Legacy diff --git a/templates/apply_satin_pattern.xml b/templates/apply_satin_pattern.xml new file mode 100644 index 000000000..e52fb1a41 --- /dev/null +++ b/templates/apply_satin_pattern.xml @@ -0,0 +1,17 @@ + + + {% trans %}Apply Satin Pattern{% endtrans %} + org.inkstitch.apply_satin_pattern.{{ locale }} + apply_satin_pattern + + all + + + + + + + +