summaryrefslogtreecommitdiff
path: root/lib/stitches
diff options
context:
space:
mode:
authorGeorge Steel <george.steel@gmail.com>2024-05-05 13:55:33 -0400
committerGitHub <noreply@github.com>2024-05-05 13:55:33 -0400
commitd32a8fd4661331da0affb15623a2ec9a9eac5c44 (patch)
tree6ac6a11c099a5b6b5463c9ff46bc7fb87d6ba888 /lib/stitches
parentedbe382914bc45a3f953c6e0258ff1feb05d8c95 (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.py22
-rw-r--r--lib/stitches/circular_fill.py31
-rw-r--r--lib/stitches/contour_fill.py19
-rw-r--r--lib/stitches/fill.py81
-rw-r--r--lib/stitches/guided_fill.py25
-rw-r--r--lib/stitches/meander_fill.py6
-rw-r--r--lib/stitches/ripple_stitch.py60
-rw-r--r--lib/stitches/running_stitch.py68
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.