From e884fb78db288c91e8183ef8e242840ba5d68db2 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Wed, 22 Jun 2022 09:26:37 -0400 Subject: [PATCH] add running stitch tolerance param (#1701) --- lib/elements/fill_stitch.py | 18 ++++++++++++++++++ lib/elements/stroke.py | 21 +++++++++++++++++---- lib/stitches/auto_fill.py | 21 ++++++++++++--------- lib/stitches/contour_fill.py | 16 ++++++++-------- lib/stitches/ripple_stitch.py | 8 +++++--- lib/stitches/running_stitch.py | 11 +++++------ lib/svg/tags.py | 1 + 7 files changed, 66 insertions(+), 30 deletions(-) diff --git a/lib/elements/fill_stitch.py b/lib/elements/fill_stitch.py index 245c67e04..dbd94fb45 100644 --- a/lib/elements/fill_stitch.py +++ b/lib/elements/fill_stitch.py @@ -366,6 +366,19 @@ class FillStitch(EmbroideryElement): def running_stitch_length(self): return max(self.get_float_param("running_stitch_length_mm", 1.5), 0.01) + @property + @param('running_stitch_tolerance_mm', + _('Running stitch tolerance'), + tooltip=_('All stitches must be within this distance of the path. ' + + 'A lower tolerance means stitches will be closer together. ' + + 'A higher tolerance means sharp corners may be rounded.'), + unit='mm', + type='float', + default=0.2, + sort_index=6) + def running_stitch_tolerance(self): + return max(self.get_float_param("running_stitch_tolerance_mm", 0.2), 0.01) + @property @param('fill_underlay', _('Underlay'), type='toggle', group=_('Fill Underlay'), default=True) def fill_underlay(self): @@ -560,6 +573,7 @@ class FillStitch(EmbroideryElement): self.fill_underlay_row_spacing, self.fill_underlay_max_stitch_length, self.running_stitch_length, + self.running_stitch_tolerance, self.staggers, self.fill_underlay_skip_last, starting_point, @@ -580,6 +594,7 @@ class FillStitch(EmbroideryElement): self.end_row_spacing, self.max_stitch_length, self.running_stitch_length, + self.running_stitch_tolerance, self.staggers, self.skip_last, starting_point, @@ -601,6 +616,7 @@ class FillStitch(EmbroideryElement): tree, self.row_spacing, self.max_stitch_length, + self.running_stitch_tolerance, starting_point, self.avoid_self_crossing ) @@ -608,12 +624,14 @@ class FillStitch(EmbroideryElement): stitches = contour_fill.single_spiral( tree, self.max_stitch_length, + self.running_stitch_tolerance, starting_point ) elif self.contour_strategy == 2: stitches = contour_fill.double_spiral( tree, self.max_stitch_length, + self.running_stitch_tolerance, starting_point ) diff --git a/lib/elements/stroke.py b/lib/elements/stroke.py index 6edd2c9ec..bf5e1d35b 100644 --- a/lib/elements/stroke.py +++ b/lib/elements/stroke.py @@ -111,6 +111,19 @@ class Stroke(EmbroideryElement): def running_stitch_length(self): return max(self.get_float_param("running_stitch_length_mm", 1.5), 0.01) + @property + @param('running_stitch_tolerance_mm', + _('Running stitch tolerance'), + tooltip=_('All stitches must be within this distance from the path. ' + + 'A lower tolerance means stitches will be closer together. ' + + 'A higher tolerance means sharp corners may be rounded.'), + unit='mm', + type='float', + default=0.2, + sort_index=4) + def running_stitch_tolerance(self): + return max(self.get_float_param("running_stitch_tolerance_mm", 0.2), 0.01) + @property @param('zigzag_spacing_mm', _('Zig-zag spacing (peak-to-peak)'), @@ -360,7 +373,7 @@ class Stroke(EmbroideryElement): # `self.zigzag_spacing` is the length for a zig and a zag # together (a V shape). Start with running stitch at half # that length: - patch = self.running_stitch(path, zigzag_spacing / 2.0) + patch = self.running_stitch(path, zigzag_spacing / 2.0, self.running_stitch_tolerance) # Now move the points left and right. Consider each pair # of points in turn, and move perpendicular to them, @@ -385,7 +398,7 @@ class Stroke(EmbroideryElement): return patch - def running_stitch(self, path, stitch_length): + def running_stitch(self, path, stitch_length, tolerance): repeated_path = [] # go back and forth along the path as specified by self.repeats @@ -398,7 +411,7 @@ class Stroke(EmbroideryElement): repeated_path.extend(this_path) - stitches = running_stitch(repeated_path, stitch_length) + stitches = running_stitch(repeated_path, stitch_length, tolerance) return StitchGroup(self.color, stitches) @@ -429,7 +442,7 @@ class Stroke(EmbroideryElement): patch = StitchGroup(color=self.color, stitches=path, stitch_as_is=True) # running stitch elif self.is_running_stitch(): - patch = self.running_stitch(path, self.running_stitch_length) + patch = self.running_stitch(path, self.running_stitch_length, self.running_stitch_tolerance) if self.bean_stitch_repeats > 0: patch.stitches = self.do_bean_repeats(patch.stitches) # simple satin diff --git a/lib/stitches/auto_fill.py b/lib/stitches/auto_fill.py index 65b1e06d0..ac1e477bf 100644 --- a/lib/stitches/auto_fill.py +++ b/lib/stitches/auto_fill.py @@ -53,6 +53,7 @@ def auto_fill(shape, end_row_spacing, max_stitch_length, running_stitch_length, + running_stitch_tolerance, staggers, skip_last, starting_point, @@ -64,15 +65,16 @@ def auto_fill(shape, fill_stitch_graph = build_fill_stitch_graph(shape, segments, starting_point, ending_point) except ValueError: # Small shapes will cause the graph to fail - min() arg is an empty sequence through insert node - return fallback(shape, running_stitch_length) + return fallback(shape, running_stitch_length, running_stitch_tolerance) if not graph_is_valid(fill_stitch_graph, shape, max_stitch_length): - return fallback(shape, running_stitch_length) + return fallback(shape, running_stitch_length, running_stitch_tolerance) travel_graph = build_travel_graph(fill_stitch_graph, shape, angle, underpath) path = find_stitch_path(fill_stitch_graph, travel_graph, starting_point, ending_point) result = path_to_stitches(path, travel_graph, fill_stitch_graph, angle, row_spacing, - max_stitch_length, running_stitch_length, staggers, skip_last) + max_stitch_length, running_stitch_length, running_stitch_tolerance, + staggers, skip_last) return result @@ -251,7 +253,7 @@ def graph_is_valid(graph, shape, max_stitch_length): return not networkx.is_empty(graph) and networkx.is_eulerian(graph) -def fallback(shape, running_stitch_length): +def fallback(shape, running_stitch_length, running_stitch_tolerance): """Generate stitches when the auto-fill algorithm fails. If graph_is_valid() returns False, we're not going to be able to run the @@ -263,7 +265,7 @@ def fallback(shape, running_stitch_length): boundary = ensure_multi_line_string(shape.boundary) outline = boundary.geoms[0] - return running_stitch(line_string_to_point_list(outline), running_stitch_length) + return running_stitch(line_string_to_point_list(outline), running_stitch_length, running_stitch_tolerance) @debug.time @@ -583,12 +585,12 @@ def collapse_sequential_outline_edges(path): return new_path -def travel(travel_graph, start, end, running_stitch_length, skip_last): +def travel(travel_graph, start, end, running_stitch_length, running_stitch_tolerance, skip_last): """Create stitches to get from one point on an outline of the shape to another.""" path = networkx.shortest_path(travel_graph, start, end, weight='weight') path = [Stitch(*p) for p in path] - stitches = running_stitch(path, running_stitch_length) + stitches = running_stitch(path, running_stitch_length, running_stitch_tolerance) for stitch in stitches: stitch.add_tag('auto_fill_travel') @@ -610,7 +612,8 @@ def travel(travel_graph, start, end, running_stitch_length, skip_last): @debug.time -def path_to_stitches(path, travel_graph, fill_stitch_graph, angle, row_spacing, max_stitch_length, running_stitch_length, staggers, skip_last): +def path_to_stitches(path, travel_graph, fill_stitch_graph, angle, row_spacing, max_stitch_length, running_stitch_length, running_stitch_tolerance, + staggers, skip_last): path = collapse_sequential_outline_edges(path) stitches = [] @@ -624,6 +627,6 @@ def path_to_stitches(path, travel_graph, fill_stitch_graph, angle, row_spacing, stitch_row(stitches, edge[0], edge[1], angle, row_spacing, max_stitch_length, staggers, skip_last) travel_graph.remove_edges_from(fill_stitch_graph[edge[0]][edge[1]]['segment'].get('underpath_edges', [])) else: - stitches.extend(travel(travel_graph, edge[0], edge[1], running_stitch_length, skip_last)) + stitches.extend(travel(travel_graph, edge[0], edge[1], running_stitch_length, running_stitch_tolerance, skip_last)) return stitches diff --git a/lib/stitches/contour_fill.py b/lib/stitches/contour_fill.py index c42cc6f2b..f2ab2aef4 100644 --- a/lib/stitches/contour_fill.py +++ b/lib/stitches/contour_fill.py @@ -391,12 +391,12 @@ def _find_path_inner_to_outer(tree, node, offset, starting_point, avoid_self_cro return LineString(result_coords) -def inner_to_outer(tree, offset, stitch_length, starting_point, avoid_self_crossing): +def inner_to_outer(tree, offset, stitch_length, tolerance, starting_point, avoid_self_crossing): """Fill a shape with spirals, from innermost to outermost.""" stitch_path = _find_path_inner_to_outer(tree, 'root', offset, starting_point, avoid_self_crossing) points = [Stitch(*point) for point in stitch_path.coords] - stitches = running_stitch(points, stitch_length) + stitches = running_stitch(points, stitch_length, tolerance) return stitches @@ -490,24 +490,24 @@ def _check_and_prepare_tree_for_valid_spiral(tree): return process_node('root') -def single_spiral(tree, stitch_length, starting_point): +def single_spiral(tree, stitch_length, tolerance, starting_point): """Fill a shape with a single spiral going from outside to center.""" - return _spiral_fill(tree, stitch_length, starting_point, _make_spiral) + return _spiral_fill(tree, stitch_length, tolerance, starting_point, _make_spiral) -def double_spiral(tree, stitch_length, starting_point): +def double_spiral(tree, stitch_length, tolerance, starting_point): """Fill a shape with a double spiral going from outside to center and back to outside. """ - return _spiral_fill(tree, stitch_length, starting_point, _make_fermat_spiral) + return _spiral_fill(tree, stitch_length, tolerance, starting_point, _make_fermat_spiral) -def _spiral_fill(tree, stitch_length, close_point, spiral_maker): +def _spiral_fill(tree, stitch_length, tolerance, close_point, spiral_maker): starting_point = close_point.coords[0] rings = _get_spiral_rings(tree) path = spiral_maker(rings, stitch_length, starting_point) path = [Stitch(*stitch) for stitch in path] - return running_stitch(path, stitch_length) + return running_stitch(path, stitch_length, tolerance) def _get_spiral_rings(tree): diff --git a/lib/stitches/ripple_stitch.py b/lib/stitches/ripple_stitch.py index 46fc5e07d..489666b00 100644 --- a/lib/stitches/ripple_stitch.py +++ b/lib/stitches/ripple_stitch.py @@ -30,7 +30,7 @@ def ripple_stitch(stroke): if stroke.grid_size != 0: ripple_points.extend(_do_grid(stroke, helper_lines)) - stitches = running_stitch(ripple_points, stroke.running_stitch_length) + stitches = running_stitch(ripple_points, stroke.running_stitch_length, stroke.running_stitch_tolerance) return _repeat_coords(stitches, stroke.repeats) @@ -57,7 +57,9 @@ def _get_helper_lines(stroke): if len(lines) > 1: return True, _get_satin_ripple_helper_lines(stroke) else: - outline = LineString(running_stitch(line_string_to_point_list(lines[0]), stroke.grid_size or stroke.running_stitch_length)) + outline = LineString(running_stitch(line_string_to_point_list(lines[0]), + stroke.grid_size or stroke.running_stitch_length, + stroke.running_stitch_tolerance)) if stroke.is_closed: return False, _get_circular_ripple_helper_lines(stroke, outline) @@ -146,7 +148,7 @@ def _get_guided_helper_lines(stroke, outline, max_distance): def _generate_guided_helper_lines(stroke, outline, max_distance, guide_line): # helper lines are generated by making copies of the outline alog the guide line line_point_dict = defaultdict(list) - outline = LineString(running_stitch(line_string_to_point_list(outline), max_distance)) + outline = LineString(running_stitch(line_string_to_point_list(outline), max_distance, stroke.running_stitch_tolerance)) center = outline.centroid center = InkstitchPoint(center.x, center.y) diff --git a/lib/stitches/running_stitch.py b/lib/stitches/running_stitch.py index 98d080bad..8c86eb7c5 100644 --- a/lib/stitches/running_stitch.py +++ b/lib/stitches/running_stitch.py @@ -3,16 +3,15 @@ # Copyright (c) 2010 Authors # Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details. -from ..debug import debug import math from copy import copy + from shapely.geometry import LineString """ Utility functions to produce running stitches. """ -@debug.time -def running_stitch(points, stitch_length): +def running_stitch(points, stitch_length, tolerance): """Generate running stitch along a path. Given a path and a stitch length, walk along the path in increments of the @@ -28,9 +27,9 @@ def running_stitch(points, stitch_length): return [] # simplify will remove as many points as possible while ensuring that the - # resulting path stays within 0.75 pixels (0.2mm) of the original path. + # resulting path stays within the specified tolerance of the original path. path = LineString(points) - simplified = path.simplify(0.75, preserve_topology=False) + simplified = path.simplify(tolerance, preserve_topology=False) # save the points that simplify picked and make sure we stitch them important_points = set(simplified.coords) @@ -50,7 +49,7 @@ def running_stitch(points, stitch_length): section_length = section_ls.length if section_length > stitch_length: # a fractional stitch needs to be rounded up, which will make all - # of the stitches shorter + # the stitches shorter num_stitches = math.ceil(section_length / stitch_length) actual_stitch_length = section_length / num_stitches diff --git a/lib/svg/tags.py b/lib/svg/tags.py index 98edb6049..2561667a5 100644 --- a/lib/svg/tags.py +++ b/lib/svg/tags.py @@ -95,6 +95,7 @@ inkstitch_attribs = [ 'bean_stitch_repeats', 'repeats', 'running_stitch_length_mm', + 'running_stitch_tolerance_mm', # satin column 'satin_column', 'running_stitch_length_mm',