From 612f18dc711c6a0fbf554465174b449604abb374 Mon Sep 17 00:00:00 2001 From: Kaalleen <36401965+kaalleen@users.noreply.github.com> Date: Sun, 31 Aug 2025 11:25:46 +0200 Subject: stroke to satin: ensure a good starting point for closed paths (#3944) starting point for the conversion, not necessarily the resulting column --- lib/elements/element.py | 4 +++ lib/elements/satin_column.py | 4 ++- lib/elements/utils/stroke_to_satin.py | 47 +++++++++++++++++++++++++++++------ 3 files changed, 46 insertions(+), 9 deletions(-) (limited to 'lib/elements') diff --git a/lib/elements/element.py b/lib/elements/element.py index d830012f..fdc0233f 100644 --- a/lib/elements/element.py +++ b/lib/elements/element.py @@ -467,6 +467,10 @@ class EmbroideryElement(object): return inkex.Path(d).to_superpath() + @property + def is_closed_path(self): + return isinstance(self.node.get_path()[-1], inkex.paths.ZoneClose) + @cache def parse_path(self): return apply_transforms(self.path, self.node) diff --git a/lib/elements/satin_column.py b/lib/elements/satin_column.py index 4951f8bf..26b4d534 100644 --- a/lib/elements/satin_column.py +++ b/lib/elements/satin_column.py @@ -26,7 +26,7 @@ from ..utils import Point, cache, cut, cut_multiple, offset_points, prng from ..utils.param import ParamOption from ..utils.threading import check_stop_flag from .element import PIXELS_PER_MM, EmbroideryElement, param -from .utils.stroke_to_satin import convert_path_to_satin +from .utils.stroke_to_satin import convert_path_to_satin, set_first_node from .validation import ValidationError, ValidationWarning @@ -625,6 +625,8 @@ class SatinColumn(EmbroideryElement): paths = [path for path in self.paths if len(path) > 1] if len(paths) == 1: style_args = get_join_style_args(self) + if self.is_closed_path: + set_first_node(paths, self.stroke_width) new_satin = convert_path_to_satin(paths[0], self.stroke_width, style_args, rungs_at_nodes=True) if new_satin: rails, rungs = new_satin diff --git a/lib/elements/utils/stroke_to_satin.py b/lib/elements/utils/stroke_to_satin.py index e75ca5d3..0b8ec819 100644 --- a/lib/elements/utils/stroke_to_satin.py +++ b/lib/elements/utils/stroke_to_satin.py @@ -3,18 +3,19 @@ # Copyright (c) 2025 Authors # Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details. -from numpy import zeros, convolve, int32, diff, setdiff1d, sign -from math import degrees, acos -from ...svg import PIXELS_PER_MM +import sys +from math import acos, degrees -from ...utils import Point -from shapely import geometry as shgeo from inkex import errormsg -from ...utils.geometry import remove_duplicate_points +from numpy import convolve, diff, int32, setdiff1d, sign, zeros +from shapely import geometry as shgeo +from shapely.affinity import rotate, scale from shapely.ops import substring -from shapely.affinity import scale + from ...i18n import _ -import sys +from ...svg import PIXELS_PER_MM +from ...utils import Point, roll_linear_ring +from ...utils.geometry import remove_duplicate_points class SelfIntersectionError(Exception): @@ -305,3 +306,33 @@ def _merge(section, other_section): rungs.extend(other_rungs) return (rails, rungs) + + +def set_first_node(paths, stroke_width): + """ + Rolls the first path in paths to a starting node which has no intersections and is not within a sharp corner + + paths is expected to be a list with only one closed path. + """ + path = paths[0] + + ring = shgeo.LinearRing(path) + buffered_ring = ring.buffer(stroke_width / 2).boundary + + for point1, point2 in zip(path[:-1], path[1:]): + line = shgeo.LineString([point1, point2]) + if line.length == 0: + continue + + # create a rung at the center of the line + # we know that the line (and therefore it's center) is always straight + scale_factor = (stroke_width + 0.001) / line.length + rung = rotate(line, 90) + rung = scale(rung, xfact=scale_factor, yfact=scale_factor) + + # when the rung intersects twice with the buffered ring, we assume a good starting point + intersection = rung.intersection(buffered_ring) + if isinstance(intersection, shgeo.MultiPoint) and len(intersection.geoms) == 2: + distance = ring.project(line.centroid) + paths[0] = list(roll_linear_ring(ring, distance).coords) + break -- cgit v1.2.3