summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKaalleen <36401965+kaalleen@users.noreply.github.com>2025-01-26 07:37:21 +0100
committerGitHub <noreply@github.com>2025-01-26 07:37:21 +0100
commit160ef32d43f5fc0d7229dbec6e7daf638a811d15 (patch)
tree694256bd5b8534c4756c5c32fac49a769b1658c1
parentc08e17b1f8290959b01fada161f0ee339454f8ea (diff)
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
-rw-r--r--lib/elements/stroke.py71
-rw-r--r--lib/extensions/__init__.py2
-rw-r--r--lib/extensions/selection_to_anchor_line.py26
-rw-r--r--lib/marker.py2
-rw-r--r--lib/stitches/ripple_stitch.py103
-rw-r--r--lib/svg/tags.py3
-rw-r--r--symbols/marker.svg19
-rw-r--r--templates/selection_to_anchor_line.xml19
8 files changed, 217 insertions, 28 deletions
diff --git a/lib/elements/stroke.py b/lib/elements/stroke.py
index 8c3d226f..7c1c39fd 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,12 +368,24 @@ 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'),
tooltip=_('Scale axis for satin guided ripple stitches.'),
@@ -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 94e0b4bb..353d3894 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 00000000..fe9442f1
--- /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 dd0a27bf..ac19fe74 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 5ebd531b..58189817 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 99027571..f965a4f4 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 1e9dab09..340935c9 100644
--- a/symbols/marker.svg
+++ b/symbols/marker.svg
@@ -20,6 +20,25 @@
refX="10"
refY="5"
orient="auto"
+ id="inkstitch-anchor-line-marker"
+ markerUnits="userSpaceOnUse"
+ markerWidth="0.5"
+ viewBox="0 0 1 1">
+ <g
+ id="inkstitch-anchor-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-anchor-marker-circle" />
+ <path
+ id="inkstitch-anchor-marker-anchor"
+ d="M 5.2906995,1.4202597 C 4.8679006,1.4083964 4.4823552,1.8090635 4.5397442,2.2343062 4.568792,2.5148029 4.7793023,2.7587002 5.0437779,2.8466079 5.0437779,2.9192332 5.0437779,2.9918586 5.0437779,3.0644837 4.757712,3.0652868 4.4715658,3.0628316 4.1855481,3.0656999 3.983847,3.0756357 3.8853404,3.3543656 4.0348752,3.489474 4.1478426,3.6002592 4.31397,3.5512227 4.4554139,3.5621911 4.6515344,3.5621911 4.8476573,3.5621911 5.0437779,3.5621911 5.0437779,5.0224311 5.0437779,6.4826482 5.0437779,7.9428881 5.0096543,8.024302 4.9683829,8.1027557 4.9314507,8.1830222 4.5485281,8.0564269 4.1615026,7.9023189 3.8538855,7.6348557 3.7475473,7.5519503 3.6239924,7.3164515 3.6794837,7.2402006 3.7811339,7.2804256 3.8827819,7.3206736 3.9844344,7.3609216 3.7194356,6.8548158 3.4544369,6.34871 3.1894404,5.8425813 3.018173,6.3946946 2.9111692,6.9738617 2.9541363,7.5536254 3.0251487,7.4780859 3.0961587,7.4025463 3.1671686,7.3270297 3.210932,7.7306569 3.5076174,8.0547518 3.8487157,8.2481441 4.2154106,8.4697145 4.6368556,8.6024135 4.9665311,8.8827497 5.0675274,8.9663434 5.0899483,9.1360779 5.2401555,9.1568903 5.3638962,9.1876385 5.4871069,9.1042742 5.5285504,8.9923876 5.6758342,8.77302 5.9336002,8.6689352 6.1579449,8.546149 6.558449,8.3439683 7.0105734,8.1629441 7.2590462,7.7657878 7.3472866,7.6408676 7.3925713,7.4561491 7.4223649,7.3336154 7.4913096,7.406952 7.5602521,7.4802887 7.6291968,7.5536254 7.6721663,6.9738617 7.5651626,6.3946946 7.3938953,5.8425813 7.1288988,6.34871 6.8639,6.8548158 6.5989012,7.3609216 6.7089682,7.3173464 6.8190352,7.2737711 6.9290999,7.2301959 6.9168144,7.4798528 6.728156,7.6694589 6.5254544,7.7910518 6.2545561,7.9625073 5.9565673,8.0857983 5.6518827,8.1830222 5.6172818,8.0962619 5.5584541,8.0156053 5.5395578,7.9247834 5.5395578,6.4705783 5.5395578,5.0163962 5.5395578,3.5621911 5.8256237,3.561365 6.1117698,3.5638203 6.3977852,3.560952 6.5994887,3.5510392 6.6979976,3.2723093 6.5484582,3.1372009 6.4354885,3.0263929 6.2693634,3.0754521 6.1279195,3.0644837 5.9318012,3.0644837 5.7356784,3.0644837 5.5395578,3.0644837 5.5395578,2.9918586 5.5395578,2.9192332 5.5395578,2.8466079 5.8943848,2.7361898 6.1301614,2.3326085 6.023401,1.9714093 5.9391303,1.6494025 5.6214856,1.4158999 5.2906995,1.4202597 Z M 5.2906995,1.9179901 C 5.457747,1.9056908 5.6057444,2.0890325 5.5308427,2.247202 5.4408929,2.4597086 5.0701226,2.4287081 5.036871,2.1916259 5.0098769,2.043025 5.1461509,1.9119093 5.2906995,1.9179901 Z" />
+ </g>
+ </marker>
+ <marker
+ refX="10"
+ refY="5"
+ orient="auto"
id="inkstitch-pattern-marker"
markerUnits="userSpaceOnUse"
markerWidth="0.5"
diff --git a/templates/selection_to_anchor_line.xml b/templates/selection_to_anchor_line.xml
new file mode 100644
index 00000000..7f174e35
--- /dev/null
+++ b/templates/selection_to_anchor_line.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<inkscape-extension translationdomain="inkstitch" xmlns="http://www.inkscape.org/namespace/inkscape/extension">
+ <name>Selection to anchor line</name>
+ <id>org.{{ id_inkstitch }}.selection_to_anchor_line</id>
+ <param name="extension" type="string" gui-hidden="true">selection_to_anchor_line</param>
+ <effect>
+ <object-type>all</object-type>
+ <icon>{{ icon_path }}inx/anchor_line.svg</icon>
+ <menu-tip>Marks selected elements as anchor lines</menu-tip>
+ <effects-menu>
+ <submenu name="{{ menu_inkstitch }}" translatable="no">
+ <submenu name="Edit" />
+ </submenu>
+ </effects-menu>
+ </effect>
+ <script>
+ {{ command_tag | safe }}
+ </script>
+</inkscape-extension>