diff options
| author | George Steel <george.steel@gmail.com> | 2024-05-05 13:55:33 -0400 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2024-05-05 13:55:33 -0400 |
| commit | d32a8fd4661331da0affb15623a2ec9a9eac5c44 (patch) | |
| tree | 6ac6a11c099a5b6b5463c9ff46bc7fb87d6ba888 /lib/stitches | |
| parent | edbe382914bc45a3f953c6e0258ff1feb05d8c95 (diff) | |
Add randomized running and fill stitches (#2830)
Add a mode to running stitch that uses randomized phase and stitch length instead of even spacing. This greatly reduces moire effects when stitching closely-spaced curves in running-stitch-based fills.
Add option for randomized running stitch to:
ripple stitch
circular fill
contour fill
guided fill
auto-fill
When is randomization is not selected, ripple stitch will use even running stitch when staggers are set to 0 (default) and the stagger algorithm from guided fill (which does not look nice with a stagger period of 0) when staggers is nonzero.
Also includes fix for satin contour underlays (missing tolerance default) mentioned in #2814. This sets the default tolerance to 0.2mm, which is the largest tolerance guaranteed to be backwards-compatible with existing designs using the default inset of 0.4mm.
Original commits:
* fix satin underlay tolerance default
* Add randomized running stitch, make available in ripple stitch, circular, and contour
* add randomized guided fill
* make ripple stitch use even stitching when not staggering or randomizing.
* add random auto-fill and switch jitter parameter to a percentage (matches satin)
* fix comments
Diffstat (limited to 'lib/stitches')
| -rw-r--r-- | lib/stitches/auto_fill.py | 22 | ||||
| -rw-r--r-- | lib/stitches/circular_fill.py | 31 | ||||
| -rw-r--r-- | lib/stitches/contour_fill.py | 19 | ||||
| -rw-r--r-- | lib/stitches/fill.py | 81 | ||||
| -rw-r--r-- | lib/stitches/guided_fill.py | 25 | ||||
| -rw-r--r-- | lib/stitches/meander_fill.py | 6 | ||||
| -rw-r--r-- | lib/stitches/ripple_stitch.py | 60 | ||||
| -rw-r--r-- | lib/stitches/running_stitch.py | 68 |
8 files changed, 212 insertions, 100 deletions
diff --git a/lib/stitches/auto_fill.py b/lib/stitches/auto_fill.py index e90093ce..159a869b 100644 --- a/lib/stitches/auto_fill.py +++ b/lib/stitches/auto_fill.py @@ -15,6 +15,7 @@ from shapely import segmentize from shapely.ops import snap from shapely.strtree import STRtree + from ..debug import debug from ..stitch_plan import Stitch from ..svg import PIXELS_PER_MM @@ -25,8 +26,9 @@ from ..utils.geometry import (ensure_multi_line_string, line_string_to_point_list) from ..utils.smoothing import smooth_path from ..utils.threading import check_stop_flag +from ..utils.prng import join_args from .fill import intersect_region_with_grating, stitch_row -from .running_stitch import running_stitch +from .running_stitch import even_running_stitch class NoGratingsError(Exception): @@ -77,7 +79,10 @@ def auto_fill(shape, skip_last, starting_point, ending_point=None, - underpath=True): + underpath=True, + enable_random=False, + random_sigma=0.0, + random_seed=""): rows = intersect_region_with_grating(shape, angle, row_spacing, end_row_spacing) if not rows: # Small shapes may not intersect with the grating at all. @@ -102,7 +107,7 @@ def auto_fill(shape, path = find_stitch_path(fill_stitch_graph, travel_graph, starting_point, ending_point) result = path_to_stitches(shape, path, travel_graph, fill_stitch_graph, angle, row_spacing, max_stitch_length, running_stitch_length, running_stitch_tolerance, - staggers, skip_last, underpath) + staggers, skip_last, underpath, enable_random, random_sigma, random_seed) return result @@ -335,7 +340,7 @@ def fallback(shape, running_stitch_length, running_stitch_tolerance): boundary = ensure_multi_line_string(shape.boundary) outline = boundary.geoms[0] - return running_stitch(line_string_to_point_list(outline), running_stitch_length, running_stitch_tolerance) + return even_running_stitch(line_string_to_point_list(outline), running_stitch_length, running_stitch_tolerance) @debug.time @@ -694,7 +699,7 @@ def travel(shape, travel_graph, edge, running_stitch_length, running_stitch_tole if len(path) > 1: path = clamp_path_to_polygon(path, shape) - points = running_stitch(path, running_stitch_length, running_stitch_tolerance) + points = even_running_stitch(path, running_stitch_length, running_stitch_tolerance) stitches = [Stitch(point) for point in points] for stitch in stitches: @@ -718,7 +723,7 @@ def travel(shape, travel_graph, edge, running_stitch_length, running_stitch_tole @debug.time def path_to_stitches(shape, path, travel_graph, fill_stitch_graph, angle, row_spacing, max_stitch_length, running_stitch_length, - running_stitch_tolerance, staggers, skip_last, underpath): + running_stitch_tolerance, staggers, skip_last, underpath, enable_random, random_sigma, random_seed): path = collapse_sequential_outline_edges(path, fill_stitch_graph) stitches = [] @@ -727,9 +732,10 @@ def path_to_stitches(shape, path, travel_graph, fill_stitch_graph, angle, row_sp if not path[0].is_segment(): stitches.append(Stitch(*path[0].nodes[0])) - for edge in path: + for i, edge in enumerate(path): if edge.is_segment(): - stitch_row(stitches, edge[0], edge[1], angle, row_spacing, max_stitch_length, staggers, skip_last) + stitch_row(stitches, edge[0], edge[1], angle, row_spacing, max_stitch_length, staggers, skip_last, + enable_random, random_sigma, join_args(random_seed, i)) travel_graph.remove_edges_from(fill_stitch_graph[edge[0]][edge[1]]['segment'].get('underpath_edges', [])) else: stitches.extend(travel(shape, travel_graph, edge, running_stitch_length, running_stitch_tolerance, skip_last, underpath)) diff --git a/lib/stitches/circular_fill.py b/lib/stitches/circular_fill.py index 28346dd9..11fd0432 100644 --- a/lib/stitches/circular_fill.py +++ b/lib/stitches/circular_fill.py @@ -2,13 +2,15 @@ from networkx import is_empty from shapely import geometry as shgeo from shapely.ops import substring +from lib.utils import prng + from ..stitch_plan import Stitch -from ..utils.geometry import reverse_line_string +from ..utils.geometry import Point, reverse_line_string from .auto_fill import (build_fill_stitch_graph, build_travel_graph, collapse_sequential_outline_edges, fallback, find_stitch_path, graph_make_valid, travel) from .contour_fill import _make_fermat_spiral -from .running_stitch import bean_stitch, running_stitch +from .running_stitch import bean_stitch, even_running_stitch, running_stitch def circular_fill(shape, @@ -24,7 +26,10 @@ def circular_fill(shape, starting_point, ending_point, underpath, - target + target, + use_random, + running_stitch_length_jitter, + random_seed, ): # get furthest distance of the target point to a shape border @@ -35,8 +40,8 @@ def circular_fill(shape, if radius > distance: # if the shape is smaller than row_spacing, return a simple circle in the size of row_spacing - stitches = running_stitch([Stitch(*point) for point in center.buffer(radius).exterior.coords], - running_stitch_length, running_stitch_tolerance) + stitches = even_running_stitch([Stitch(*point) for point in center.buffer(radius).exterior.coords], + running_stitch_length, running_stitch_tolerance) return _apply_bean_stitch_and_repeats(stitches, repeats, bean_stitch_repeats) circles = [] @@ -61,16 +66,24 @@ def circular_fill(shape, # if we get a single linestrig (original shape is a circle), apply start and end commands and return path path = list(intersection.coords) path = _apply_start_end_commands(shape, path, starting_point, ending_point) - stitches = running_stitch([Stitch(*point) for point in path], running_stitch_length, running_stitch_tolerance) + stitches = running_stitch([Stitch(*point) for point in path], + running_stitch_length, + running_stitch_tolerance, + use_random, + running_stitch_length_jitter, + random_seed) return _apply_bean_stitch_and_repeats(stitches, repeats, bean_stitch_repeats) segments = [] - for line in intersection.geoms: + for n, line in enumerate(intersection.geoms): if isinstance(line, shgeo.LineString): # use running stitch here to adjust the stitch length - coords = running_stitch([Stitch(point[0], point[1]) for point in line.coords], + coords = running_stitch([Point(*point) for point in line.coords], running_stitch_length, - running_stitch_tolerance) + running_stitch_tolerance, + use_random, + running_stitch_length_jitter, + prng.join_args(random_seed, n)) segments.append([(point.x, point.y) for point in coords]) fill_stitch_graph = build_fill_stitch_graph(shape, segments, starting_point, ending_point) diff --git a/lib/stitches/contour_fill.py b/lib/stitches/contour_fill.py index e19e1aad..9eea90ab 100644 --- a/lib/stitches/contour_fill.py +++ b/lib/stitches/contour_fill.py @@ -409,7 +409,10 @@ def _find_path_inner_to_outer(tree, node, offset, starting_point, avoid_self_cro return LineString(result_coords) -def inner_to_outer(tree, polygon, offset, stitch_length, tolerance, smoothness, starting_point, avoid_self_crossing): +def inner_to_outer(tree, polygon, offset, + stitch_length, tolerance, smoothness, + starting_point, avoid_self_crossing, + enable_random, random_sigma, random_seed): """Fill a shape with spirals, from innermost to outermost.""" stitch_path = _find_path_inner_to_outer(tree, 'root', offset, starting_point, avoid_self_crossing) @@ -419,7 +422,7 @@ def inner_to_outer(tree, polygon, offset, stitch_length, tolerance, smoothness, smoothed = smooth_path(points, smoothness) points = clamp_path_to_polygon(smoothed, polygon) - stitches = running_stitch(points, stitch_length, tolerance) + stitches = running_stitch(points, stitch_length, tolerance, enable_random, random_sigma, random_seed) return stitches @@ -515,24 +518,24 @@ def _check_and_prepare_tree_for_valid_spiral(tree): return process_node('root') -def single_spiral(tree, stitch_length, tolerance, starting_point): +def single_spiral(tree, stitch_length, tolerance, starting_point, enable_random, random_sigma, random_seed): """Fill a shape with a single spiral going from outside to center.""" - return _spiral_fill(tree, stitch_length, tolerance, starting_point, _make_spiral) + return _spiral_fill(tree, stitch_length, tolerance, starting_point, enable_random, random_sigma, random_seed, _make_spiral) -def double_spiral(tree, stitch_length, tolerance, starting_point): +def double_spiral(tree, stitch_length, tolerance, starting_point, enable_random, random_sigma, random_seed): """Fill a shape with a double spiral going from outside to center and back to outside. """ - return _spiral_fill(tree, stitch_length, tolerance, starting_point, _make_fermat_spiral) + return _spiral_fill(tree, stitch_length, tolerance, starting_point, enable_random, random_sigma, random_seed, _make_fermat_spiral) -def _spiral_fill(tree, stitch_length, tolerance, close_point, spiral_maker): +def _spiral_fill(tree, stitch_length, tolerance, close_point, enable_random, random_sigma, random_seed, 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, tolerance) + return running_stitch(path, stitch_length, tolerance, enable_random, random_sigma, random_seed) def _get_spiral_rings(tree): diff --git a/lib/stitches/fill.py b/lib/stitches/fill.py index c492a629..59501a9e 100644 --- a/lib/stitches/fill.py +++ b/lib/stitches/fill.py @@ -12,6 +12,7 @@ from ..svg import PIXELS_PER_MM from ..utils import Point as InkstitchPoint from ..utils import cache from ..utils.threading import check_stop_flag +from .running_stitch import split_segment_random_phase def legacy_fill(shape, angle, row_spacing, end_row_spacing, max_stitch_length, flip, reverse, staggers, skip_last): @@ -51,46 +52,50 @@ def adjust_stagger(stitch, angle, row_spacing, max_stitch_length, staggers): return stitch - offset * east(angle) -def stitch_row(stitches, beg, end, angle, row_spacing, max_stitch_length, staggers, skip_last=False): - # We want our stitches to look like this: - # - # ---*-----------*----------- - # ------*-----------*-------- - # ---------*-----------*----- - # ------------*-----------*-- - # ---*-----------*----------- - # - # Each successive row of stitches will be staggered, with - # num_staggers rows before the pattern repeats. A value of - # 4 gives a nice fill while hiding the needle holes. The - # first row is offset 0%, the second 25%, the third 50%, and - # the fourth 75%. - # - # Actually, instead of just starting at an offset of 0, we - # can calculate a row's offset relative to the origin. This - # way if we have two abutting fill regions, they'll perfectly - # tile with each other. That's important because we often get - # abutting fill regions from pull_runs(). - +def stitch_row(stitches, beg, end, angle, row_spacing, max_stitch_length, staggers, skip_last, enable_random, random_sigma, random_seed): beg = Stitch(*beg, tags=('fill_row_start',)) - end = Stitch(*end, tags=('fill_row_end',)) - - row_direction = (end - beg).unit() - segment_length = (end - beg).length() - + end = Stitch(*end, tags=('fill_row_start',)) stitches.append(beg) - first_stitch = adjust_stagger(beg, angle, row_spacing, max_stitch_length, staggers) - - # we might have chosen our first stitch just outside this row, so move back in - if (first_stitch - beg) * row_direction < 0: - first_stitch += row_direction * max_stitch_length - - offset = (first_stitch - beg).length() - - while offset < segment_length: - stitches.append(Stitch(beg + offset * row_direction, tags=('fill_row',))) - offset += max_stitch_length + if enable_random: + stitches += split_segment_random_phase(beg, end, max_stitch_length, random_sigma, random_seed) + else: + # We want our stitches to look like this: + # + # ---*-----------*----------- + # ------*-----------*-------- + # ---------*-----------*----- + # ------------*-----------*-- + # ---*-----------*----------- + # + # Each successive row of stitches will be staggered, with + # num_staggers rows before the pattern repeats. A value of + # 4 gives a nice fill while hiding the needle holes. The + # first row is offset 0%, the second 25%, the third 50%, and + # the fourth 75%. + # + # Actually, instead of just starting at an offset of 0, we + # can calculate a row's offset relative to the origin. This + # way if we have two abutting fill regions, they'll perfectly + # tile with each other. That's important because we often get + # abutting fill regions from pull_runs(). + + row_direction = (end - beg).unit() + segment_length = (end - beg).length() + + stitches.append(beg) + + first_stitch = adjust_stagger(beg, angle, row_spacing, max_stitch_length, staggers) + + # we might have chosen our first stitch just outside this row, so move back in + if (first_stitch - beg) * row_direction < 0: + first_stitch += row_direction * max_stitch_length + + offset = (first_stitch - beg).length() + + while offset < segment_length: + stitches.append(Stitch(beg + offset * row_direction, tags=('fill_row',))) + offset += max_stitch_length if (end - stitches[-1]).length() > 0.1 * PIXELS_PER_MM and not skip_last: stitches.append(end) @@ -189,7 +194,7 @@ def section_to_stitches(group_of_segments, angle, row_spacing, max_stitch_length if (swap): (beg, end) = (end, beg) - stitch_row(stitches, beg, end, angle, row_spacing, max_stitch_length, staggers, skip_last) + stitch_row(stitches, beg, end, angle, row_spacing, max_stitch_length, staggers, skip_last, False, 0.0, "") swap = not swap diff --git a/lib/stitches/guided_fill.py b/lib/stitches/guided_fill.py index bc7a3ab2..1b564bcb 100644 --- a/lib/stitches/guided_fill.py +++ b/lib/stitches/guided_fill.py @@ -8,12 +8,15 @@ from shapely import geometry as shgeo from shapely.affinity import translate from shapely.ops import linemerge, nearest_points, unary_union +from lib.utils import prng + from ..debug import debug from ..stitch_plan import Stitch from ..utils.geometry import Point as InkstitchPoint from ..utils.geometry import (ensure_geometry_collection, ensure_multi_line_string, reverse_line_string) from ..utils.threading import check_stop_flag +from .running_stitch import random_running_stitch from .auto_fill import (auto_fill, build_fill_stitch_graph, build_travel_graph, collapse_sequential_outline_edges, find_stitch_path, graph_make_valid, travel) @@ -31,9 +34,13 @@ def guided_fill(shape, starting_point, ending_point, underpath, - strategy + strategy, + enable_random, + random_sigma, + random_seed, ): - segments = intersect_region_with_grating_guideline(shape, guideline, row_spacing, num_staggers, max_stitch_length, strategy) + segments = intersect_region_with_grating_guideline(shape, guideline, row_spacing, num_staggers, max_stitch_length, strategy, + enable_random, running_stitch_tolerance, random_sigma, random_seed,) if not segments: return fallback(shape, guideline, row_spacing, max_stitch_length, running_stitch_length, running_stitch_tolerance, num_staggers, skip_last, starting_point, ending_point, underpath) @@ -231,7 +238,8 @@ def _get_start_row(line, shape, row_spacing, line_direction): return copysign(row, shape_direction * line_direction) -def intersect_region_with_grating_guideline(shape, line, row_spacing, num_staggers, max_stitch_length, strategy): +def intersect_region_with_grating_guideline(shape, line, row_spacing, num_staggers, max_stitch_length, strategy, + enable_random, tolerance, random_sigma, random_seed): line = prepare_guide_line(line, shape) debug.log_line_string(shape.exterior, "guided fill shape") @@ -261,13 +269,14 @@ def intersect_region_with_grating_guideline(shape, line, row_spacing, num_stagge offset_line = clean_offset_line(offset_line) - if strategy == 1 and direction == -1: - # negative parallel offsets are reversed, so we need to compensate - offset_line = reverse_line_string(offset_line) - debug.log_line_string(offset_line, f"offset {row}") - stitched_line = apply_stitches(offset_line, max_stitch_length, num_staggers, row_spacing, row) + if enable_random: + points = [InkstitchPoint(*x) for x in offset_line.coords] + stitched_line = shgeo.LineString(random_running_stitch( + points, max_stitch_length, tolerance, random_sigma, prng.join_args(random_seed, row))) + else: + stitched_line = apply_stitches(offset_line, max_stitch_length, num_staggers, row_spacing, row) intersection = shape.intersection(stitched_line) if not intersection.is_empty and shape_envelope.intersects(stitched_line): diff --git a/lib/stitches/meander_fill.py b/lib/stitches/meander_fill.py index 16510dde..94e38e1f 100644 --- a/lib/stitches/meander_fill.py +++ b/lib/stitches/meander_fill.py @@ -14,7 +14,7 @@ from ..utils.list import poprandom from ..utils.prng import iter_uniform_floats from ..utils.smoothing import smooth_path from ..utils.threading import check_stop_flag -from .running_stitch import bean_stitch, running_stitch, zigzag_stitch +from .running_stitch import bean_stitch, even_running_stitch, zigzag_stitch def meander_fill(fill, shape, original_shape, shape_index, starting_point, ending_point): @@ -179,10 +179,10 @@ def post_process(points, shape, original_shape, fill): smoothed_points = [InkStitchPoint.from_tuple(point) for point in smoothed_points] if fill.zigzag_spacing > 0: - stitches = running_stitch(smoothed_points, fill.zigzag_spacing / 2, fill.running_stitch_tolerance) + stitches = even_running_stitch(smoothed_points, fill.zigzag_spacing / 2, fill.running_stitch_tolerance) stitches = zigzag_stitch(stitches, fill.zigzag_spacing, fill.zigzag_width, 0) else: - stitches = running_stitch(smoothed_points, fill.running_stitch_length, fill.running_stitch_tolerance) + stitches = even_running_stitch(smoothed_points, fill.running_stitch_length, fill.running_stitch_tolerance) if fill.clip: stitches = clamp_path_to_polygon(stitches, original_shape) diff --git a/lib/stitches/ripple_stitch.py b/lib/stitches/ripple_stitch.py index 156059e9..02dbdeb2 100644 --- a/lib/stitches/ripple_stitch.py +++ b/lib/stitches/ripple_stitch.py @@ -7,10 +7,11 @@ from shapely.geometry import LineString, Point from ..elements import SatinColumn from ..utils import Point as InkstitchPoint +from ..utils import prng from ..utils.geometry import line_string_to_point_list from ..utils.threading import check_stop_flag from .guided_fill import apply_stitches -from .running_stitch import running_stitch +from .running_stitch import even_running_stitch, running_stitch def ripple_stitch(stroke): @@ -46,33 +47,50 @@ def _get_stitches(stroke, is_linear, lines, skip_start): return _get_staggered_stitches(stroke, lines, skip_start) else: points = [point for line in lines for point in line] - return running_stitch(points, stroke.running_stitch_length, stroke.running_stitch_tolerance) + return running_stitch(points, + stroke.running_stitch_length, + stroke.running_stitch_tolerance, + stroke.enable_random_stitches, + stroke.random_stitch_length_jitter, + stroke.random_seed) def _get_staggered_stitches(stroke, lines, skip_start): stitches = [] + stitch_length = stroke.running_stitch_length + tolerance = stroke.running_stitch_tolerance + enable_random = stroke.enable_random_stitches + length_sigma = stroke.random_stitch_length_jitter + random_seed = stroke.random_seed + last_point = None for i, line in enumerate(lines): - stitched_line = [] connector = [] if i != 0 and stroke.join_style == 0: if i % 2 == 0: - last_point = lines[i-1][0] first_point = line[0] else: - last_point = lines[i-1][-1] first_point = line[-1] - connector = running_stitch([InkstitchPoint(*last_point), InkstitchPoint(*first_point)], - stroke.running_stitch_length, - stroke.running_stitch_tolerance) - points = list(apply_stitches(LineString(line), stroke.running_stitch_length, stroke.staggers, 0.5, i, stroke.running_stitch_tolerance).coords) - stitched_line.extend([InkstitchPoint(*point) for point in points]) - if i % 2 == 1 and stroke.join_style == 0: - # reverse every other row in linear ripple - stitched_line.reverse() - if (stroke.join_style == 1 and ((i % 2 == 1 and skip_start % 2 == 0) or - (i % 2 == 0 and skip_start % 2 == 1))): - stitched_line.reverse() - stitched_line = connector + stitched_line + connector = even_running_stitch([last_point, first_point], + stitch_length, tolerance)[1:-1] + if stroke.join_style == 0: + should_reverse = i % 2 == 1 + elif stroke.join_style == 1: + should_reverse = (i + skip_start) % 2 == 1 + + if enable_random or stroke.staggers == 0: + if should_reverse: + line.reverse() + points = running_stitch(line, stitch_length, tolerance, enable_random, length_sigma, prng.join_args(random_seed, i)) + stitched_line = connector + points + else: + # uses the guided fill alforithm to stagger rows of stitches + points = list(apply_stitches(LineString(line), stitch_length, stroke.staggers, 0.5, i, tolerance).coords) + stitched_line = [InkstitchPoint(*point) for point in points] + if should_reverse: + stitched_line.reverse() + stitched_line = connector + stitched_line + + last_point = stitched_line[-1] stitches.extend(stitched_line) return stitches @@ -137,9 +155,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, - stroke.running_stitch_tolerance)) + outline = LineString(even_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) @@ -278,7 +296,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 along the guide line line_point_dict = defaultdict(list) - outline = LineString(running_stitch(line_string_to_point_list(outline), max_distance, stroke.running_stitch_tolerance)) + outline = LineString(even_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 139a2006..6144a977 100644 --- a/lib/stitches/running_stitch.py +++ b/lib/stitches/running_stitch.py @@ -18,6 +18,10 @@ from ..utils.threading import check_stop_flag """ Utility functions to produce running stitches. """ +def lerp(a, b, t: float) -> float: + return (1 - t) * a + t * b + + def split_segment_even_n(a, b, segments: int, jitter_sigma: float = 0.0, random_seed=None) -> typing.List[shgeo.Point]: if segments <= 1: return [] @@ -216,8 +220,6 @@ def stitch_curve_evenly(points: typing.Sequence[Point], stitch_length: float, to last = points[0] stitches = [] while i is not None and i < len(points): - check_stop_flag() - d = last.distance(points[i]) + distLeft[i] if d == 0: return stitches @@ -231,6 +233,35 @@ def stitch_curve_evenly(points: typing.Sequence[Point], stitch_length: float, to return stitches +def stitch_curve_randomly(points: typing.Sequence[Point], stitch_length: float, tolerance: float, stitch_length_sigma: float, random_seed: str): + min_stitch_length = max(0, stitch_length * (1 - stitch_length_sigma)) + max_stitch_length = stitch_length * (1 + stitch_length_sigma) + # Will split a straight line into stitches of random length within the range. + # Attempts to randomize phase so that the distribution of outputs does not depend on direction. + # Includes end point but not start point. + if len(points) < 2: + return [] + + i = 1 + last = points[0] + last_shortened = 0.0 + stitches = [] + rand_iter = iter(prng.iter_uniform_floats(random_seed)) + while i is not None and i < len(points): + r = next(rand_iter) + # If the last stitch was shortened due to tolerance (or this is the first stitch), + # reduce the lower length limit to randomize the phase. This prevents moiré and asymmetry. + stitch_len = lerp(last_shortened, 1.0, r) * lerp(min_stitch_length, max_stitch_length, r) + + stitch, newidx = take_stitch(last, points, i, stitch_len, tolerance) + i = newidx + if stitch is not None: + stitches.append(stitch) + last_shortened = min(last.distance(stitch) / stitch_len, 1.0) + last = stitch + return stitches + + def path_to_curves(points: typing.List[Point], min_len: float): # split a path at obvious corner points so that they get stitched exactly # min_len controls the minimum length after splitting for which it won't split again, @@ -265,17 +296,44 @@ def path_to_curves(points: typing.List[Point], min_len: float): return curves -def running_stitch(points, stitch_length, tolerance): - # Turn a continuous path into a running stitch. +def even_running_stitch(points, stitch_length, tolerance): + # Turn a continuous path into a running stitch with as close to even stitch length as possible + # (including the first and last segments), keeping it within the tolerance of the path. + # This should not be used for stitching tightly-spaced parallel curves + # as it tends to produce ugly moiré effects in those situations. + # In these situations, random_running_stitch sould be used even if the maximum stitch length range is a single value. if not points: return stitches = [points[0]] for curve in path_to_curves(points, 2 * tolerance): - # segments longer than twice the tollerance will usually be forced by it, so set that as the minimum for corner detection + # segments longer than twice the tolerance will usually be forced by it, so set that as the minimum for corner detection + check_stop_flag() stitches.extend(stitch_curve_evenly(curve, stitch_length, tolerance)) return stitches +def random_running_stitch(points, stitch_length, tolerance, stitch_length_sigma, random_seed): + # Turn a continuous path into a running stitch with randomized phase and stitch length, + # keeping it within the tolerance of the path. + # This is suitable for tightly-spaced parallel curves. + if not points: + return + stitches = [points[0]] + for i, curve in enumerate(path_to_curves(points, 2 * tolerance)): + # segments longer than twice the tolerance will usually be forced by it, so set that as the minimum for corner detection + check_stop_flag() + stitches.extend(stitch_curve_randomly(curve, stitch_length, tolerance, stitch_length_sigma, prng.join_args(random_seed, i))) + return stitches + + +def running_stitch(points, stitch_length, tolerance, is_random, stitch_length_sigma, random_seed): + # running stitch with a choice of algorithm + if is_random: + return random_running_stitch(points, stitch_length, tolerance, stitch_length_sigma, random_seed) + else: + return even_running_stitch(points, stitch_length, tolerance) + + def bean_stitch(stitches, repeats, tags_to_ignore=None): """Generate bean stitch from a set of stitches. |
