From 160ef32d43f5fc0d7229dbec6e7daf638a811d15 Mon Sep 17 00:00:00 2001 From: Kaalleen <36401965+kaalleen@users.noreply.github.com> Date: Sun, 26 Jan 2025 07:37:21 +0100 Subject: [PATCH] Improve satin guided ripple stitch and add stitch grid first option (#3436) * ripple stitch: add stitch grid first option * introduce an anchor line to fine tune satin guided ripples --- lib/elements/stroke.py | 71 +++++++++----- lib/extensions/__init__.py | 2 + lib/extensions/selection_to_anchor_line.py | 26 ++++++ lib/marker.py | 2 +- lib/stitches/ripple_stitch.py | 103 ++++++++++++++++++++- lib/svg/tags.py | 3 +- symbols/marker.svg | 19 ++++ templates/selection_to_anchor_line.xml | 19 ++++ 8 files changed, 217 insertions(+), 28 deletions(-) create mode 100644 lib/extensions/selection_to_anchor_line.py create mode 100644 templates/selection_to_anchor_line.xml diff --git a/lib/elements/stroke.py b/lib/elements/stroke.py index 8c3d226fa..7c1c39fd8 100644 --- a/lib/elements/stroke.py +++ b/lib/elements/stroke.py @@ -236,16 +236,23 @@ class Stroke(EmbroideryElement): return return max(min_dist, 0.01) + _satin_guided_pattern_options = [ + ParamOption('default', _('Line count / Minimum line distance')), + ParamOption('render_at_rungs', _('Render at rungs')), + ParamOption('adaptive', _('Adaptive + minimum line distance')), + ] + @property - @param('render_at_rungs', - _('Render at rungs'), - tooltip=_('Position satin guided pattern at rungs.'), - type='boolean', + @param('satin_guide_pattern_position', + _('Pattern position'), + tooltip=_('Pattern position for satin guided ripples.'), + type='combo', + options=_satin_guided_pattern_options, + default='default', select_items=[('stroke_method', 'ripple_stitch')], - default=False, sort_index=9) - def render_at_rungs(self): - return self.get_boolean_param('render_at_rungs', False) + def satin_guide_pattern_position(self): + return self.get_param('satin_guide_pattern_position', 'line_count') @property @param('staggers', @@ -257,7 +264,7 @@ class Stroke(EmbroideryElement): type='int', select_items=[('stroke_method', 'ripple_stitch')], default=0, - sort_index=9) + sort_index=15) def staggers(self): return self.get_float_param("staggers", 1) @@ -268,7 +275,7 @@ class Stroke(EmbroideryElement): type='int', default=0, select_items=[('stroke_method', 'ripple_stitch')], - sort_index=10) + sort_index=16) @cache def skip_start(self): return abs(self.get_int_param("skip_start", 0)) @@ -280,7 +287,7 @@ class Stroke(EmbroideryElement): type='int', default=0, select_items=[('stroke_method', 'ripple_stitch')], - sort_index=11) + sort_index=17) @cache def skip_end(self): return abs(self.get_int_param("skip_end", 0)) @@ -292,7 +299,7 @@ class Stroke(EmbroideryElement): type='boolean', select_items=[('stroke_method', 'ripple_stitch')], default=True, - sort_index=12) + sort_index=18) def flip_copies(self): return self.get_boolean_param('flip_copies', True) @@ -303,7 +310,7 @@ class Stroke(EmbroideryElement): type='float', default=1, select_items=[('stroke_method', 'ripple_stitch')], - sort_index=13) + sort_index=19) @cache def exponent(self): return max(self.get_float_param("exponent", 1), 0.1) @@ -315,7 +322,7 @@ class Stroke(EmbroideryElement): type='boolean', default=False, select_items=[('stroke_method', 'ripple_stitch')], - sort_index=14) + sort_index=20) @cache def flip_exponent(self): return self.get_boolean_param("flip_exponent", False) @@ -327,7 +334,7 @@ class Stroke(EmbroideryElement): type='boolean', default=False, select_items=[('stroke_method', 'ripple_stitch')], - sort_index=15) + sort_index=21) @cache def reverse(self): return self.get_boolean_param("reverse", False) @@ -349,7 +356,7 @@ class Stroke(EmbroideryElement): options=_reverse_rails_options, default='automatic', select_items=[('stroke_method', 'ripple_stitch')], - sort_index=16) + sort_index=22) def reverse_rails(self): return self.get_param('reverse_rails', 'automatic') @@ -361,11 +368,23 @@ class Stroke(EmbroideryElement): default=0, unit='mm', select_items=[('stroke_method', 'ripple_stitch')], - sort_index=16) + sort_index=23) @cache def grid_size(self): return abs(self.get_float_param("grid_size_mm", 0)) + @property + @param('grid_first', + _('Stitch grid first'), + tooltip=_('Reverse the stitch paths, so that the grid will be stitched first'), + type='boolean', + default=False, + select_items=[('stroke_method', 'ripple_stitch')], + sort_index=24) + @cache + def grid_first(self): + return self.get_boolean_param("grid_first", False) + @property @param('scale_axis', _('Scale axis'), @@ -375,7 +394,7 @@ class Stroke(EmbroideryElement): # 0: xy, 1: x, 2: y, 3: none options=["X Y", "X", "Y", _("None")], select_items=[('stroke_method', 'ripple_stitch')], - sort_index=18) + sort_index=25) def scale_axis(self): return self.get_int_param('scale_axis', 0) @@ -387,7 +406,7 @@ class Stroke(EmbroideryElement): unit='%', default=100, select_items=[('stroke_method', 'ripple_stitch')], - sort_index=18) + sort_index=26) def scale_start(self): return self.get_float_param('scale_start', 100.0) @@ -399,7 +418,7 @@ class Stroke(EmbroideryElement): unit='%', default=0.0, select_items=[('stroke_method', 'ripple_stitch')], - sort_index=19) + sort_index=27) def scale_end(self): return self.get_float_param('scale_end', 0.0) @@ -410,7 +429,7 @@ class Stroke(EmbroideryElement): type='boolean', default=True, select_items=[('stroke_method', 'ripple_stitch')], - sort_index=20) + sort_index=30) @cache def rotate_ripples(self): return self.get_boolean_param("rotate_ripples", True) @@ -423,7 +442,7 @@ class Stroke(EmbroideryElement): default=0, options=(_("flat"), _("point")), select_items=[('stroke_method', 'ripple_stitch')], - sort_index=21) + sort_index=31) @cache def join_style(self): return self.get_int_param('join_style', 0) @@ -651,6 +670,16 @@ class Stroke(EmbroideryElement): return guide_lines['satin'][0] return guide_lines['stroke'][0] + @cache + def get_anchor_line(self): + anchor_lines = get_marker_elements(self.node, "anchor-line", False, True, False) + # No or empty guide line + if not anchor_lines or not anchor_lines['stroke']: + return None + + # ignore multiple anchor lines + return anchor_lines['stroke'][0].geoms[0] + def _representative_point(self): # if we just take the center of a line string we could end up on some point far away from the actual line try: diff --git a/lib/extensions/__init__.py b/lib/extensions/__init__.py index 94e0b4bb6..353d38942 100644 --- a/lib/extensions/__init__.py +++ b/lib/extensions/__init__.py @@ -58,6 +58,7 @@ from .remove_embroidery_settings import RemoveEmbroiderySettings from .reorder import Reorder from .satin_multicolor import SatinMulticolor from .select_elements import SelectElements +from .selection_to_anchor_line import SelectionToAnchorLine from .selection_to_guide_line import SelectionToGuideLine from .selection_to_pattern import SelectionToPattern from .simulator import Simulator @@ -128,6 +129,7 @@ __all__ = extensions = [About, Reorder, SatinMulticolor, SelectElements, + SelectionToAnchorLine, SelectionToGuideLine, SelectionToPattern, Simulator, diff --git a/lib/extensions/selection_to_anchor_line.py b/lib/extensions/selection_to_anchor_line.py new file mode 100644 index 000000000..fe9442f16 --- /dev/null +++ b/lib/extensions/selection_to_anchor_line.py @@ -0,0 +1,26 @@ +# 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 ..marker import set_marker +from ..svg.tags import EMBROIDERABLE_TAGS +from .base import InkstitchExtension + + +class SelectionToAnchorLine(InkstitchExtension): + + def effect(self): + if not self.get_elements(): + return + + if not self.svg.selected: + inkex.errormsg(_("Please select at least one object to be marked as a anchor line.")) + return + + for pattern in self.get_nodes(): + if pattern.tag in EMBROIDERABLE_TAGS: + set_marker(pattern, 'start', 'anchor-line') diff --git a/lib/marker.py b/lib/marker.py index dd0a27bfb..ac19fe747 100644 --- a/lib/marker.py +++ b/lib/marker.py @@ -12,7 +12,7 @@ from shapely import geometry as shgeo from .svg.tags import EMBROIDERABLE_TAGS from .utils import cache, get_bundled_dir -MARKER = ['pattern', 'guide-line'] +MARKER = ['anchor-line', 'pattern', 'guide-line'] def ensure_marker(svg, marker): diff --git a/lib/stitches/ripple_stitch.py b/lib/stitches/ripple_stitch.py index 5ebd531b5..58189817d 100644 --- a/lib/stitches/ripple_stitch.py +++ b/lib/stitches/ripple_stitch.py @@ -41,6 +41,8 @@ def ripple_stitch(stroke): if stitches and stroke.grid_size != 0: stitches.extend(_do_grid(stroke, helper_lines, skip_start, skip_end, is_linear, stitches[-1])) + if stroke.grid_first: + stitches = stitches[::-1] return _repeat_coords(stitches, stroke.repeats) @@ -306,7 +308,7 @@ def _get_guided_helper_lines(stroke, outline, max_distance): guide_line = stroke.get_guide_line() if isinstance(guide_line, SatinColumn): # satin type guide line - return _generate_satin_guide_helper_lines(stroke, outline, guide_line) + return generate_satin_guide_helper_lines(stroke, outline, guide_line) else: # simple guide line return _generate_guided_helper_lines(stroke, outline, max_distance, guide_line.geoms[0]) @@ -325,7 +327,7 @@ def _generate_guided_helper_lines(stroke, outline, max_distance, guide_line): center = outline.centroid center = InkstitchPoint(center.x, center.y) - if stroke.render_at_rungs: + if stroke.satin_guide_pattern_position == "render_at_rungs": count = len(guide_line.coords) else: count = _get_guided_line_count(stroke, guide_line) @@ -340,7 +342,7 @@ def _generate_guided_helper_lines(stroke, outline, max_distance, guide_line): for i in range(count): check_stop_flag() - if stroke.render_at_rungs: + if stroke.satin_guide_pattern_position == "render_at_rungs": # Requires the guide line to be defined as manual stitch guide_point = InkstitchPoint(*guide_line.coords[i]) else: @@ -369,11 +371,38 @@ def _get_start_rotation(line): return atan2(point1.y - point0.y, point1.x - point0.x) -def _generate_satin_guide_helper_lines(stroke, outline, guide_line): +def generate_satin_guide_helper_lines(stroke, outline, guide_line): + anchor_line = stroke.get_anchor_line() + if anchor_line: + # position, rotation and scale defined by anchor line + outline0 = InkstitchPoint(*anchor_line.coords[0]) + outline1 = InkstitchPoint(*anchor_line.coords[-1]) + else: + # position rotation and scale defined by line end points + outline_coords = outline.coords + outline0 = InkstitchPoint(*outline_coords[0]) + outline1 = InkstitchPoint(*outline_coords[-1]) + if outline0 == outline1: + return _generate_simple_satin_guide_helper_lines(stroke, outline, guide_line) + + outline_width = (outline1 - outline0).length() + outline_rotation = atan2(outline1.y - outline0.y, outline1.x - outline0.x) + + if stroke.satin_guide_pattern_position == "adaptive": + return _generate_satin_guide_helper_lines_with_varying_pattern_distance( + stroke, guide_line, outline, outline0, outline_width, outline_rotation + ) + else: + return _generate_satin_guide_helper_lines_with_constant_pattern_distance( + stroke, guide_line, outline, outline0, outline_width, outline_rotation + ) + + +def _generate_simple_satin_guide_helper_lines(stroke, outline, guide_line): count = _get_guided_line_count(stroke, guide_line.center_line) spacing = guide_line.center_line.length / max(1, count - 1) - if stroke.render_at_rungs: + if stroke.satin_guide_pattern_position == "render_at_rungs": sections = guide_line.flattened_sections pairs = [] for (rail0, rail1) in sections: @@ -413,6 +442,70 @@ def _generate_satin_guide_helper_lines(stroke, outline, guide_line): return _point_dict_to_helper_lines(len(outline.coords), line_point_dict) +def _generate_satin_guide_helper_lines_with_constant_pattern_distance(stroke, guide_line, outline, outline0, outline_width, outline_rotation): + # add scaled and rotated outlines along the satin column guide line + if stroke.satin_guide_pattern_position == "render_at_rungs": + sections = guide_line.flattened_sections + pairs = [] + for (rail0, rail1) in sections: + pairs.append((rail0[-1], rail1[-1])) + else: + count = _get_guided_line_count(stroke, guide_line.center_line) + spacing = guide_line.center_line.length / max(1, count - 1) + pairs = guide_line.plot_points_on_rails(spacing) + + if pairs[0] == pairs[-1]: + pairs = pairs[:-1] + + line_point_dict = defaultdict(list) + for i, (point0, point1) in enumerate(pairs): + check_stop_flag() + + # move to point0, rotate and scale so the other point hits point1 + scaling = (point1 - point0).length() / outline_width + rotation = atan2(point1.y - point0.y, point1.x - point0.x) + rotation = rotation - outline_rotation + translation = point0 - outline0 + transformed_outline = _transform_outline(translation, rotation, scaling, outline, Point(point0), 0) + + # outline to helper line points + for j, point in enumerate(transformed_outline.coords): + line_point_dict[j].append(InkstitchPoint(point[0], point[1])) + + return _point_dict_to_helper_lines(len(outline.coords), line_point_dict) + + +def _generate_satin_guide_helper_lines_with_varying_pattern_distance(stroke, guide_line, outline, outline0, outline_width, outline_rotation): + # rotate pattern and get the pattern width + minx, miny, maxx, maxy = _transform_outline(Point([0, 0]), outline_rotation, 1, outline, Point(outline0), 0).bounds + pattern_width = maxx - minx + + distance = 0 + line_point_dict = defaultdict(list) + while True: + if distance > guide_line.center_line.length: + break + check_stop_flag() + cut_point = guide_line.center_line.interpolate(distance) + point0, point1 = guide_line.find_cut_points(*cut_point.coords) + + # move to point0, rotate and scale so the other point hits point1 + scaling = (point1 - point0).length() / outline_width + rotation = atan2(point1.y - point0.y, point1.x - point0.x) + rotation = rotation - outline_rotation + translation = point0 - outline0 + transformed_outline = _transform_outline(translation, rotation, scaling, outline, Point(point0), 0) + + min_distance = stroke.min_line_dist or 0 + distance += max(1, (pattern_width * scaling) + min_distance) + + # outline to helper line points + for j, point in enumerate(transformed_outline.coords): + line_point_dict[j].append(InkstitchPoint(point[0], point[1])) + + return _point_dict_to_helper_lines(len(outline.coords), line_point_dict) + + def _transform_outline(translation, rotation, scaling, outline, origin, scale_axis): # transform transformed_outline = translate(outline, translation.x, translation.y) diff --git a/lib/svg/tags.py b/lib/svg/tags.py index 99027571b..f965a4f40 100644 --- a/lib/svg/tags.py +++ b/lib/svg/tags.py @@ -122,7 +122,7 @@ inkstitch_attribs = [ 'flip_copies', 'line_count', 'min_line_dist_mm', - 'render_at_rungs', + 'satin_guide_pattern_position', 'exponent', 'flip_exponent', 'skip_start', @@ -132,6 +132,7 @@ inkstitch_attribs = [ 'scale_end', 'rotate_ripples', 'grid_size_mm', + 'grid_first', # satin column 'satin_column', 'satin_method', diff --git a/symbols/marker.svg b/symbols/marker.svg index 1e9dab094..340935c96 100644 --- a/symbols/marker.svg +++ b/symbols/marker.svg @@ -16,6 +16,25 @@ xmlns:inkstitch="http://inkstitch.org/namespace"> + + + + + + + + Selection to anchor line + org.{{ id_inkstitch }}.selection_to_anchor_line + selection_to_anchor_line + + all + {{ icon_path }}inx/anchor_line.svg + Marks selected elements as anchor lines + + + + + + + +