summaryrefslogtreecommitdiff
path: root/lib/stitches
diff options
context:
space:
mode:
authorKaalleen <36401965+kaalleen@users.noreply.github.com>2025-11-07 17:10:03 +0100
committerGitHub <noreply@github.com>2025-11-07 17:10:03 +0100
commit5a9ae03dc14ea5b68a99581c21a5d8085f1a3243 (patch)
tree4b729fbdc8656f3ba75b7dfdd2ccfecdd8718fee /lib/stitches
parent383f164b6d90c0819d49f4fb16deb9efa3e11df2 (diff)
Running stitch length sequence (#4034)
* allow running stitch length sequences * contour fill: fix error message for long stitch length * satin: fix center underlay stitch length
Diffstat (limited to 'lib/stitches')
-rw-r--r--lib/stitches/auto_fill.py4
-rw-r--r--lib/stitches/circular_fill.py4
-rw-r--r--lib/stitches/contour_fill.py7
-rw-r--r--lib/stitches/fill.py3
-rw-r--r--lib/stitches/guided_fill.py7
-rw-r--r--lib/stitches/linear_gradient_fill.py6
-rw-r--r--lib/stitches/meander_fill.py4
-rw-r--r--lib/stitches/ripple_stitch.py41
-rw-r--r--lib/stitches/running_stitch.py47
-rw-r--r--lib/stitches/tartan_fill.py4
10 files changed, 90 insertions, 37 deletions
diff --git a/lib/stitches/auto_fill.py b/lib/stitches/auto_fill.py
index bcde63e7..2573bf88 100644
--- a/lib/stitches/auto_fill.py
+++ b/lib/stitches/auto_fill.py
@@ -391,7 +391,7 @@ def fallback(shape, running_stitch_length, running_stitch_tolerance):
boundary = ensure_multi_line_string(shape.boundary)
outline = boundary.geoms[0]
- return even_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
@@ -941,7 +941,7 @@ def path_to_stitches(shape, path, travel_graph, fill_stitch_graph, angle, row_sp
if fill_stitch_graph.has_edge(edge[0], edge[1], key='segment'):
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))
+ stitches.extend(travel(shape, travel_graph, edge, [running_stitch_length], running_stitch_tolerance, skip_last, underpath))
check_stop_flag()
diff --git a/lib/stitches/circular_fill.py b/lib/stitches/circular_fill.py
index 26b48f24..fb6303ea 100644
--- a/lib/stitches/circular_fill.py
+++ b/lib/stitches/circular_fill.py
@@ -79,7 +79,7 @@ def circular_fill(shape,
if isinstance(line, shgeo.LineString):
# use running stitch here to adjust the stitch length
coords = running_stitch([Point(*point) for point in line.coords],
- running_stitch_length,
+ [running_stitch_length],
running_stitch_tolerance,
enable_random_stitch_length,
running_stitch_length_jitter,
@@ -162,6 +162,6 @@ def path_to_stitches(shape, path, travel_graph, fill_stitch_graph, running_stitc
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))
+ stitches.extend(travel(shape, travel_graph, edge, [running_stitch_length], running_stitch_tolerance, skip_last, underpath))
return stitches
diff --git a/lib/stitches/contour_fill.py b/lib/stitches/contour_fill.py
index 0665aacb..6a3ed4ff 100644
--- a/lib/stitches/contour_fill.py
+++ b/lib/stitches/contour_fill.py
@@ -422,7 +422,7 @@ def inner_to_outer(tree, polygon, offset,
smoothed = smooth_path(points, smoothness)
points = clamp_path_to_polygon(smoothed, polygon)
- stitches = running_stitch(points, stitch_length, tolerance, enable_random_stitch_length, random_sigma, random_seed)
+ stitches = running_stitch(points, [stitch_length], tolerance, enable_random_stitch_length, random_sigma, random_seed)
return stitches
@@ -460,6 +460,9 @@ def _interpolate_linear_rings(ring1, ring2, max_stitch_length, start=None):
# orders of magnitude faster because we're not building and querying a KDTree.
num_points = int(20 * ring1.length / max_stitch_length)
+ if num_points <= 1:
+ return LineString()
+
ring1_resampled = trimesh.path.traversal.resample_path(np.array(ring1.coords), count=num_points)
ring2_resampled = trimesh.path.traversal.resample_path(np.array(ring2.coords), count=num_points)
@@ -535,7 +538,7 @@ def _spiral_fill(tree, stitch_length, tolerance, close_point, enable_random_stit
path = spiral_maker(rings, stitch_length, starting_point)
path = [Stitch(*stitch) for stitch in path]
- return running_stitch(path, stitch_length, tolerance, enable_random_stitch_length, random_sigma, random_seed)
+ return running_stitch(path, [stitch_length], tolerance, enable_random_stitch_length, random_sigma, random_seed)
def _get_spiral_rings(tree):
diff --git a/lib/stitches/fill.py b/lib/stitches/fill.py
index ad082b2d..e50c4007 100644
--- a/lib/stitches/fill.py
+++ b/lib/stitches/fill.py
@@ -63,7 +63,8 @@ def stitch_row(stitches, beg, end, angle, row_spacing, max_stitch_length, stagge
stitches.append(beg)
if enable_random_stitch_length:
- stitches += split_segment_random_phase(beg, end, max_stitch_length, random_sigma, random_seed)
+ stitched_line = split_segment_random_phase(beg, end, max_stitch_length, random_sigma, random_seed)
+ stitches.extend([Stitch(stitch, tags=('fill_row',)) for stitch in stitched_line])
else:
# We want our stitches to look like this:
#
diff --git a/lib/stitches/guided_fill.py b/lib/stitches/guided_fill.py
index 16f0f2e8..84d7600e 100644
--- a/lib/stitches/guided_fill.py
+++ b/lib/stitches/guided_fill.py
@@ -102,7 +102,7 @@ def path_to_stitches(shape, path, travel_graph, fill_stitch_graph,
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))
+ stitches.extend(travel(shape, travel_graph, edge, [running_stitch_length], running_stitch_tolerance, skip_last, underpath))
return stitches
@@ -166,6 +166,7 @@ def take_only_line_strings(thing):
def apply_stitches(line, max_stitch_length, num_staggers, row_spacing, row_num, threshold=None) -> shgeo.LineString:
if num_staggers == 0:
num_staggers = 1 # sanity check to avoid division by zero.
+ max_stitch_length = max_stitch_length[0]
start = ((row_num / num_staggers) % 1) * max_stitch_length
projections = np.arange(start, line.length, max_stitch_length)
points = np.array([line.interpolate(projection).coords[0] for projection in projections])
@@ -275,9 +276,9 @@ def intersect_region_with_grating_guideline(shape, line, row_spacing, num_stagge
if enable_random_stitch_length:
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)))
+ 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)
+ 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/linear_gradient_fill.py b/lib/stitches/linear_gradient_fill.py
index 589a1f23..4f2f3f74 100644
--- a/lib/stitches/linear_gradient_fill.py
+++ b/lib/stitches/linear_gradient_fill.py
@@ -127,9 +127,11 @@ def _get_lines(fill, shape, bounding_box, angle):
if fill.enable_random_stitch_length:
points = [InkstitchPoint(*x) for x in line]
staggered_line = LineString(random_running_stitch(
- points, fill.max_stitch_length, fill.running_stitch_tolerance, fill.random_stitch_length_jitter, prng.join_args(fill.random_seed, i)))
+ points,
+ [fill.max_stitch_length], fill.running_stitch_tolerance, fill.random_stitch_length_jitter, prng.join_args(fill.random_seed, i))
+ )
else:
- staggered_line = apply_stitches(LineString(line), fill.max_stitch_length, fill.staggers, fill.row_spacing, i)
+ staggered_line = apply_stitches(LineString(line), [fill.max_stitch_length], fill.staggers, fill.row_spacing, i)
staggered_lines.append(staggered_line)
return staggered_lines, bottom_line
diff --git a/lib/stitches/meander_fill.py b/lib/stitches/meander_fill.py
index 87c52e17..1883c97f 100644
--- a/lib/stitches/meander_fill.py
+++ b/lib/stitches/meander_fill.py
@@ -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 = even_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, 0))
else:
- stitches = even_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:
# the stitch path may have self intersections
diff --git a/lib/stitches/ripple_stitch.py b/lib/stitches/ripple_stitch.py
index 804e5f15..2abb5963 100644
--- a/lib/stitches/ripple_stitch.py
+++ b/lib/stitches/ripple_stitch.py
@@ -4,6 +4,7 @@ from math import atan2, ceil
import numpy as np
from shapely.affinity import rotate, scale, translate
from shapely.geometry import LineString, Point
+from shapely.ops import substring
from ..elements import SatinColumn
from ..utils import Point as InkstitchPoint
@@ -80,7 +81,7 @@ def _get_staggered_stitches(stroke, lines, skip_start):
stitches = []
stitch_length = stroke.running_stitch_length
tolerance = stroke.running_stitch_tolerance
- enable_random_stitch_length = stroke.enable_random_stitch_length
+ is_random = stroke.enable_random_stitch_length
length_sigma = stroke.random_stitch_length_jitter
random_seed = stroke.random_seed
last_point = None
@@ -98,14 +99,18 @@ def _get_staggered_stitches(stroke, lines, skip_start):
elif stroke.join_style == 1:
should_reverse = (i + skip_start) % 2 == 1
- if enable_random_stitch_length or stroke.staggers == 0:
+ if stroke.staggers == 0:
if should_reverse and stroke.flip_copies:
line.reverse()
- points = running_stitch(line, stitch_length, tolerance, enable_random_stitch_length, length_sigma, prng.join_args(random_seed, i))
- stitched_line = connector + points
+ stitched_line = running_stitch(line, stitch_length, tolerance, is_random, length_sigma, prng.join_args(random_seed, i))
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)
+ if len(stitch_length) > 1:
+ points = list(
+ apply_stagger(line, stitch_length, stroke.staggers, i, tolerance, is_random, length_sigma, prng.join_args(random_seed, i)).coords
+ )
+ 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)
# simplifying the path in apply_stitches could have removed the start or end point
# we can simply add it again, the minimum stitch length value will take care to remove possible duplicates
@@ -114,13 +119,33 @@ def _get_staggered_stitches(stroke, lines, skip_start):
stitched_line = [InkstitchPoint(*point) for point in points]
if should_reverse and stroke.flip_copies:
stitched_line.reverse()
- stitched_line = connector + stitched_line
+
+ stitched_line = connector + stitched_line
last_point = stitched_line[-1]
stitches.extend(stitched_line)
return stitches
+def apply_stagger(line, stitch_length, num_staggers, row_num, tolerance, is_random, stitch_length_sigma, random_seed):
+ if num_staggers == 0:
+ num_staggers = 1 # sanity check to avoid division by zero.
+ start = ((row_num / num_staggers) % 1) * sum(stitch_length)
+ first_segment = LineString(line[:2])
+ segment_length = max(first_segment.length, 0.1)
+ target_length = segment_length + 2 * start
+ scale_factor = target_length / segment_length
+ extended_line = scale(first_segment, scale_factor, scale_factor)
+
+ line = [InkstitchPoint(*extended_line.coords[0])] + line
+ stitched_row = running_stitch(line, stitch_length, tolerance, is_random, stitch_length_sigma, random_seed)
+ if len(stitched_row) <= 1:
+ return LineString()
+
+ stitched_line = LineString(stitched_row)
+ return substring(stitched_line, start, stitched_line.length)
+
+
def _adjust_skip(stroke, num_lines, skip):
if stroke.skip_start + stroke.skip_end >= num_lines:
return 0
@@ -202,7 +227,7 @@ def _get_helper_lines(stroke):
def _get_satin_ripple_helper_lines(stroke):
# if grid_size has a number use this, otherwise use running_stitch_length
- length = stroke.grid_size or stroke.running_stitch_length
+ length = stroke.grid_size or min(stroke.running_stitch_length)
# use satin column points for satin like build ripple stitches
rail_pairs = SatinColumn(stroke.node).plot_points_on_rails(length)
diff --git a/lib/stitches/running_stitch.py b/lib/stitches/running_stitch.py
index 8b8add7e..058ee565 100644
--- a/lib/stitches/running_stitch.py
+++ b/lib/stitches/running_stitch.py
@@ -210,11 +210,12 @@ def take_stitch(start: Point, points: typing.Sequence[Point], idx: int, stitch_l
return points[-1], None
-def stitch_curve_evenly(points: typing.Sequence[Point], stitch_length: float, tolerance: float) -> typing.List[Point]:
+def stitch_curve_evenly(points: typing.Sequence[Point], stitch_length: typing.List[float], tolerance: float, stitch_length_pos: int = 0) -> \
+ typing.Tuple[typing.List[Point], int]:
# Will split a straight line into even-length stitches while still handling curves correctly.
# Includes end point but not start point.
if len(points) < 2:
- return []
+ return [], stitch_length_pos
distLeft = [0] * len(points)
for j in reversed(range(0, len(points) - 1)):
distLeft[j] = distLeft[j + 1] + points[j].distance(points[j+1])
@@ -225,26 +226,33 @@ def stitch_curve_evenly(points: typing.Sequence[Point], stitch_length: float, to
while i is not None and i < len(points):
d = last.distance(points[i]) + distLeft[i]
if d == 0:
- return stitches
- stitch_len = d / math.ceil(d / stitch_length) + 0.000001 # correction for rounding error
+ return stitches, stitch_length_pos
+ stitch_len = d / math.ceil(d / stitch_length[stitch_length_pos]) + 0.000001 # correction for rounding error
stitch, newidx = take_stitch(last, points, i, stitch_len, tolerance)
i = newidx
if stitch is not None:
stitches.append(stitch)
last = stitch
- return stitches
+ stitch_length_pos += 1
+ if stitch_length_pos > len(stitch_length) - 1:
+ stitch_length_pos = 0
+ return stitches, stitch_length_pos
+
+
+def stitch_curve_randomly(
+ points: typing.Sequence[Point],
+ stitch_length: typing.List[float], tolerance: float, stitch_length_sigma: float,
+ random_seed: str, stitch_length_pos: int = 0) -> typing.Tuple[typing.List[Point], int]:
+ min_stitch_length = max(0, stitch_length[stitch_length_pos] * (1 - stitch_length_sigma))
+ max_stitch_length = stitch_length[stitch_length_pos] * (1 + stitch_length_sigma)
-def stitch_curve_randomly(points: typing.Sequence[Point], stitch_length: float, tolerance: float, stitch_length_sigma: float, random_seed: str) ->\
- typing.List[Point]:
- 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 []
+ return [], stitch_length_pos
i: typing.Optional[int] = 1
last = points[0]
@@ -252,6 +260,13 @@ def stitch_curve_randomly(points: typing.Sequence[Point], stitch_length: float,
stitches = []
rand_iter = iter(prng.iter_uniform_floats(random_seed))
while i is not None and i < len(points):
+ if len(stitch_length) > 1:
+ min_stitch_length = max(0, stitch_length[stitch_length_pos] * (1 - stitch_length_sigma))
+ max_stitch_length = stitch_length[stitch_length_pos] * (1 + stitch_length_sigma)
+ stitch_length_pos += 1
+ if stitch_length_pos > len(stitch_length) - 1:
+ stitch_length_pos = 0
+
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.
@@ -263,7 +278,7 @@ def stitch_curve_randomly(points: typing.Sequence[Point], stitch_length: float,
stitches.append(stitch)
last_shortened = min(last.distance(stitch) / stitch_len, 1.0)
last = stitch
- return stitches
+ return stitches, stitch_length_pos
def path_to_curves(points: typing.List[Point], min_len: float):
@@ -309,10 +324,12 @@ def even_running_stitch(points, stitch_length, tolerance):
if not points:
return
stitches = [points[0]]
+ last_stitch_length_pos = 0
for curve in 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_evenly(curve, stitch_length, tolerance))
+ stitched_curve, last_stitch_length_pos = stitch_curve_evenly(curve, stitch_length, tolerance, last_stitch_length_pos)
+ stitches.extend(stitched_curve)
return stitches
@@ -323,10 +340,14 @@ def random_running_stitch(points, stitch_length, tolerance, stitch_length_sigma,
if not points:
return
stitches = [points[0]]
+ last_stitch_length_pos = 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)))
+ stitched_curve, last_stitch_length_pos = stitch_curve_randomly(
+ curve, stitch_length, tolerance, stitch_length_sigma, prng.join_args(random_seed, i), last_stitch_length_pos
+ )
+ stitches.extend(stitched_curve)
return stitches
diff --git a/lib/stitches/tartan_fill.py b/lib/stitches/tartan_fill.py
index c25bb435..521919a0 100644
--- a/lib/stitches/tartan_fill.py
+++ b/lib/stitches/tartan_fill.py
@@ -155,7 +155,7 @@ def _generate_herringbone_lines(
staggered_lines = []
for i, line in enumerate(lines):
linestring = LineString(line)
- staggered_line = apply_stitches(linestring, fill.max_stitch_length, fill.staggers, fill.row_spacing, i)
+ staggered_line = apply_stitches(linestring, [fill.max_stitch_length], fill.staggers, fill.row_spacing, i)
# make sure we do not ommit the very first or very last point (it would confuse our sorting algorithm)
staggered_line = LineString([linestring.coords[0]] + list(staggered_line.coords) + [linestring.coords[-1]])
staggered_lines.append(staggered_line)
@@ -205,7 +205,7 @@ def _generate_tartan_lines(
staggered_lines = []
for i, line in enumerate(lines):
linestring = LineString(line)
- staggered_line = apply_stitches(linestring, fill.max_stitch_length, fill.staggers, fill.row_spacing, i)
+ staggered_line = apply_stitches(linestring, [fill.max_stitch_length], fill.staggers, fill.row_spacing, i)
# make sure we do not ommit the very first or very last point (it would confuse our sorting algorithm)
staggered_line = LineString([linestring.coords[0]] + list(staggered_line.coords) + [linestring.coords[-1]])
staggered_lines.append(staggered_line)