kopia lustrzana https://github.com/inkstitch/inkstitch
Merge 39b58c33ec
into ad2914284e
commit
a5f5c3f9c9
|
@ -374,7 +374,7 @@ class FillStitch(EmbroideryElement):
|
|||
tooltip=_('The last stitch in each row is quite close to the first stitch in the next row. '
|
||||
'Skipping it decreases stitch count and density.'),
|
||||
type='boolean',
|
||||
sort_index=26,
|
||||
sort_index=30,
|
||||
select_items=[('fill_method', 'auto_fill'),
|
||||
('fill_method', 'guided_fill'),
|
||||
('fill_method', 'linear_gradient_fill'),
|
||||
|
@ -390,7 +390,7 @@ class FillStitch(EmbroideryElement):
|
|||
tooltip=_('The flip option can help you with routing your stitch path. '
|
||||
'When you enable flip, stitching goes from right-to-left instead of left-to-right.'),
|
||||
type='boolean',
|
||||
sort_index=27,
|
||||
sort_index=31,
|
||||
select_items=[('fill_method', 'legacy_fill')],
|
||||
default=False)
|
||||
def flip(self):
|
||||
|
@ -402,7 +402,7 @@ class FillStitch(EmbroideryElement):
|
|||
_('Reverse fill'),
|
||||
tooltip=_('Reverses fill path.'),
|
||||
type='boolean',
|
||||
sort_index=28,
|
||||
sort_index=32,
|
||||
select_items=[('fill_method', 'legacy_fill')],
|
||||
default=False)
|
||||
def reverse(self):
|
||||
|
@ -415,7 +415,7 @@ class FillStitch(EmbroideryElement):
|
|||
tooltip=_('If this option is disabled, the ending point will only be used to define a general direction for '
|
||||
'stitch routing. When enabled the last section will end at the defined spot.'),
|
||||
type='boolean',
|
||||
sort_index=30,
|
||||
sort_index=33,
|
||||
select_items=[('fill_method', 'linear_gradient_fill')],
|
||||
default=False
|
||||
)
|
||||
|
@ -431,7 +431,7 @@ class FillStitch(EmbroideryElement):
|
|||
type='boolean',
|
||||
default=True,
|
||||
select_items=[('fill_method', 'auto_fill'), ('fill_method', 'guided_fill'), ('fill_method', 'circular_fill')],
|
||||
sort_index=30)
|
||||
sort_index=40)
|
||||
def underpath(self):
|
||||
return self.get_boolean_param('underpath', True)
|
||||
|
||||
|
@ -449,7 +449,7 @@ class FillStitch(EmbroideryElement):
|
|||
('fill_method', 'circular_fill'),
|
||||
('fill_method', 'linear_gradient_fill'),
|
||||
('fill_method', 'tartan_fill')],
|
||||
sort_index=31)
|
||||
sort_index=41)
|
||||
def running_stitch_length(self):
|
||||
return max(self.get_float_param("running_stitch_length_mm", 2.5), 0.01)
|
||||
|
||||
|
@ -461,10 +461,40 @@ class FillStitch(EmbroideryElement):
|
|||
unit='mm',
|
||||
type='float',
|
||||
default=0.1,
|
||||
sort_index=32)
|
||||
sort_index=43)
|
||||
def running_stitch_tolerance(self):
|
||||
return max(self.get_float_param("running_stitch_tolerance_mm", 0.2), 0.01)
|
||||
|
||||
@property
|
||||
@param('enable_random_stitches',
|
||||
_('Randomize stitches'),
|
||||
tooltip=_('Randomize stitch length and phase instead of dividing evenly or staggering. '
|
||||
'This is recommended for closely-spaced curved fills to avoid Moiré artefacts.'),
|
||||
type='boolean',
|
||||
select_items=[('fill_method', 'auto_fill'),
|
||||
('fill_method', 'contour_fill'),
|
||||
('fill_method', 'guided_fill'),
|
||||
('fill_method', 'circular_fill')],
|
||||
default=False,
|
||||
sort_index=44)
|
||||
def enable_random_stitches(self):
|
||||
return self.get_boolean_param('enable_random_stitches', False)
|
||||
|
||||
@property
|
||||
@param('random_stitch_length_jitter_percent',
|
||||
_('Random stitch length jitter'),
|
||||
tooltip=_('Amount to vary the length of each stitch by when randomizing.'),
|
||||
unit='± %',
|
||||
type='float',
|
||||
select_items=[('fill_method', 'auto_fill'),
|
||||
('fill_method', 'contour_fill'),
|
||||
('fill_method', 'guided_fill'),
|
||||
('fill_method', 'circular_fill')],
|
||||
default=10,
|
||||
sort_index=46)
|
||||
def random_stitch_length_jitter(self):
|
||||
return max(self.get_float_param("random_stitch_length_jitter_percent", 10), 0.0) / 100.0
|
||||
|
||||
@property
|
||||
@param('repeats',
|
||||
_('Repeats'),
|
||||
|
@ -473,7 +503,7 @@ class FillStitch(EmbroideryElement):
|
|||
default="1",
|
||||
select_items=[('fill_method', 'meander_fill'),
|
||||
('fill_method', 'circular_fill')],
|
||||
sort_index=33)
|
||||
sort_index=50)
|
||||
def repeats(self):
|
||||
return max(1, self.get_int_param("repeats", 1))
|
||||
|
||||
|
@ -489,7 +519,7 @@ class FillStitch(EmbroideryElement):
|
|||
('fill_method', 'circular_fill'),
|
||||
('fill_method', 'tartan_fill')],
|
||||
default=0,
|
||||
sort_index=34)
|
||||
sort_index=51)
|
||||
def bean_stitch_repeats(self):
|
||||
return self.get_multiple_int_param("bean_stitch_repeats", "0")
|
||||
|
||||
|
@ -501,7 +531,7 @@ class FillStitch(EmbroideryElement):
|
|||
type='float',
|
||||
select_items=[('fill_method', 'meander_fill')],
|
||||
default=0,
|
||||
sort_index=35)
|
||||
sort_index=60)
|
||||
@cache
|
||||
def zigzag_spacing(self):
|
||||
return self.get_float_param("zigzag_spacing_mm", 0)
|
||||
|
@ -514,7 +544,7 @@ class FillStitch(EmbroideryElement):
|
|||
type='float',
|
||||
select_items=[('fill_method', 'meander_fill')],
|
||||
default=3,
|
||||
sort_index=36)
|
||||
sort_index=61)
|
||||
@cache
|
||||
def zigzag_width(self):
|
||||
return self.get_float_param("zigzag_width_mm", 3)
|
||||
|
@ -527,7 +557,7 @@ class FillStitch(EmbroideryElement):
|
|||
type='int',
|
||||
default="2",
|
||||
select_items=[('fill_method', 'tartan_fill')],
|
||||
sort_index=35
|
||||
sort_index=62
|
||||
)
|
||||
def rows_per_thread(self):
|
||||
return max(1, self.get_int_param("rows_per_thread", 2))
|
||||
|
@ -540,7 +570,7 @@ class FillStitch(EmbroideryElement):
|
|||
type='int',
|
||||
default=0,
|
||||
select_items=[('fill_method', 'tartan_fill')],
|
||||
sort_index=36)
|
||||
sort_index=63)
|
||||
def herringbone_width(self):
|
||||
return self.get_float_param('herringbone_width_mm', 0)
|
||||
|
||||
|
@ -648,7 +678,11 @@ class FillStitch(EmbroideryElement):
|
|||
@param('random_seed',
|
||||
_('Random seed'),
|
||||
tooltip=_('Use a specific seed for randomized attributes. Uses the element ID if empty.'),
|
||||
select_items=[('fill_method', 'meander_fill')],
|
||||
select_items=[('fill_method', 'auto_fill'),
|
||||
('fill_method', 'contour_fill'),
|
||||
('fill_method', 'guided_fill'),
|
||||
('fill_method', 'circular_fill'),
|
||||
('fill_method', 'meander_fill')],
|
||||
type='random_seed',
|
||||
default='',
|
||||
sort_index=100)
|
||||
|
@ -963,7 +997,10 @@ class FillStitch(EmbroideryElement):
|
|||
self.skip_last,
|
||||
starting_point,
|
||||
ending_point,
|
||||
self.underpath
|
||||
self.underpath,
|
||||
self.enable_random_stitches,
|
||||
self.random_stitch_length_jitter,
|
||||
self.random_seed,
|
||||
)
|
||||
)
|
||||
return [stitch_group]
|
||||
|
@ -986,21 +1023,30 @@ class FillStitch(EmbroideryElement):
|
|||
self.running_stitch_tolerance,
|
||||
self.smoothness,
|
||||
starting_point,
|
||||
self.avoid_self_crossing
|
||||
self.avoid_self_crossing,
|
||||
self.enable_random_stitches,
|
||||
self.random_stitch_length_jitter,
|
||||
self.random_seed
|
||||
)
|
||||
elif self.contour_strategy == 1:
|
||||
stitches = contour_fill.single_spiral(
|
||||
tree,
|
||||
self.max_stitch_length,
|
||||
self.running_stitch_tolerance,
|
||||
starting_point
|
||||
starting_point,
|
||||
self.enable_random_stitches,
|
||||
self.random_stitch_length_jitter,
|
||||
self.random_seed
|
||||
)
|
||||
elif self.contour_strategy == 2:
|
||||
stitches = contour_fill.double_spiral(
|
||||
tree,
|
||||
self.max_stitch_length,
|
||||
self.running_stitch_tolerance,
|
||||
starting_point
|
||||
starting_point,
|
||||
self.enable_random_stitches,
|
||||
self.random_stitch_length_jitter,
|
||||
self.random_seed
|
||||
)
|
||||
|
||||
stitch_group = StitchGroup(
|
||||
|
@ -1038,7 +1084,10 @@ class FillStitch(EmbroideryElement):
|
|||
starting_point,
|
||||
ending_point,
|
||||
self.underpath,
|
||||
self.guided_fill_strategy
|
||||
self.guided_fill_strategy,
|
||||
self.enable_random_stitches,
|
||||
self.random_stitch_length_jitter,
|
||||
self.random_seed,
|
||||
)
|
||||
)
|
||||
return [stitch_group]
|
||||
|
@ -1089,7 +1138,10 @@ class FillStitch(EmbroideryElement):
|
|||
starting_point,
|
||||
ending_point,
|
||||
self.underpath,
|
||||
target
|
||||
target,
|
||||
self.enable_random_stitches,
|
||||
self.random_stitch_length_jitter,
|
||||
self.random_seed,
|
||||
)
|
||||
|
||||
stitch_group = StitchGroup(
|
||||
|
|
|
@ -372,10 +372,11 @@ class SatinColumn(EmbroideryElement):
|
|||
unit='mm',
|
||||
group=_('Contour Underlay'),
|
||||
type='float',
|
||||
default=0.2,
|
||||
)
|
||||
def contour_underlay_stitch_tolerance(self):
|
||||
tolerance = self.get_float_param("contour_underlay_stitch_tolerance_mm", self.contour_underlay_stitch_length)
|
||||
return max(tolerance, 0.01)
|
||||
tolerance = self.get_float_param("contour_underlay_stitch_tolerance_mm", 0.2)
|
||||
return max(tolerance, 0.01 * PIXELS_PER_MM) # sanity check to prevent crash from excessively-small values
|
||||
|
||||
@property
|
||||
@param('contour_underlay_inset_mm',
|
||||
|
@ -428,11 +429,12 @@ class SatinColumn(EmbroideryElement):
|
|||
),
|
||||
unit='mm',
|
||||
group=_('Center-Walk Underlay'),
|
||||
type='float'
|
||||
type='float',
|
||||
default=0.2
|
||||
)
|
||||
def center_walk_underlay_stitch_tolerance(self):
|
||||
tolerance = self.get_float_param("center_walk_underlay_stitch_tolerance_mm", self.contour_underlay_stitch_length)
|
||||
return max(tolerance, 0.01)
|
||||
tolerance = self.get_float_param("center_walk_underlay_stitch_tolerance_mm", 0.2)
|
||||
return max(tolerance, 0.01 * PIXELS_PER_MM)
|
||||
|
||||
@property
|
||||
@param('center_walk_underlay_repeats',
|
||||
|
@ -1171,12 +1173,12 @@ class SatinColumn(EmbroideryElement):
|
|||
self.contour_underlay_stitch_tolerance,
|
||||
-self.contour_underlay_inset_px, -self.contour_underlay_inset_percent/100)
|
||||
|
||||
first_side = running_stitch.running_stitch(
|
||||
first_side = running_stitch.even_running_stitch(
|
||||
[points[0] for points in pairs],
|
||||
self.contour_underlay_stitch_length,
|
||||
self.contour_underlay_stitch_tolerance
|
||||
)
|
||||
second_side = running_stitch.running_stitch(
|
||||
second_side = running_stitch.even_running_stitch(
|
||||
[points[1] for points in pairs],
|
||||
self.contour_underlay_stitch_length,
|
||||
self.contour_underlay_stitch_tolerance
|
||||
|
@ -1209,7 +1211,7 @@ class SatinColumn(EmbroideryElement):
|
|||
(0, 0), inset_prop)
|
||||
|
||||
points = [points[0] for points in pairs]
|
||||
stitches = running_stitch.running_stitch(points, self.center_walk_underlay_stitch_length, self.center_walk_underlay_stitch_tolerance)
|
||||
stitches = running_stitch.even_running_stitch(points, self.center_walk_underlay_stitch_length, self.center_walk_underlay_stitch_tolerance)
|
||||
|
||||
for i in range(self.center_walk_underlay_repeats - 1):
|
||||
if i % 2 == 0:
|
||||
|
|
|
@ -13,8 +13,7 @@ from ..i18n import _
|
|||
from ..marker import get_marker_elements
|
||||
from ..stitch_plan import StitchGroup
|
||||
from ..stitches.ripple_stitch import ripple_stitch
|
||||
from ..stitches.running_stitch import (bean_stitch, running_stitch,
|
||||
zigzag_stitch)
|
||||
from ..stitches.running_stitch import (bean_stitch, running_stitch, zigzag_stitch)
|
||||
from ..svg import get_node_transform, parse_length_with_units
|
||||
from ..svg.clip import get_clip_path
|
||||
from ..threads import ThreadColor
|
||||
|
@ -130,6 +129,30 @@ class Stroke(EmbroideryElement):
|
|||
def running_stitch_tolerance(self):
|
||||
return max(self.get_float_param("running_stitch_tolerance_mm", 0.2), 0.01)
|
||||
|
||||
@property
|
||||
@param('enable_random_stitches',
|
||||
_('Randomize stitches'),
|
||||
tooltip=_('Randomize stitch length and phase instead of dividing evenly or staggering. '
|
||||
'This is recommended for closely-spaced curved fills to avoid Moiré artefacts.'),
|
||||
type='boolean',
|
||||
select_items=[('stroke_method', 'running_stitch'), ('stroke_method', 'ripple_stitch')],
|
||||
default=False,
|
||||
sort_index=5)
|
||||
def enable_random_stitches(self):
|
||||
return self.get_boolean_param('enable_random_stitches', False)
|
||||
|
||||
@property
|
||||
@param('random_stitch_length_jitter_percent',
|
||||
_('Random stitch length jitter'),
|
||||
tooltip=_('Amount to vary the length of each stitch by when randomizing.'),
|
||||
unit='± %',
|
||||
type='float',
|
||||
select_items=[('stroke_method', 'running_stitch'), ('stroke_method', 'ripple_stitch')],
|
||||
default=10,
|
||||
sort_index=6)
|
||||
def random_stitch_length_jitter(self):
|
||||
return max(self.get_float_param("random_stitch_length_jitter_percent", 10), 0.0) / 100
|
||||
|
||||
@property
|
||||
@param('max_stitch_length_mm',
|
||||
_('Max stitch length'),
|
||||
|
@ -203,10 +226,11 @@ class Stroke(EmbroideryElement):
|
|||
_('Stagger lines this many times before repeating'),
|
||||
tooltip=_('Length of the cycle by which successive stitch lines are staggered. '
|
||||
'Fractional values are allowed and can have less visible diagonals than integer values. '
|
||||
'A value of 0 (default) disables staggering and instead stitches evenly.'
|
||||
'For linear ripples only.'),
|
||||
type='int',
|
||||
select_items=[('stroke_method', 'ripple_stitch')],
|
||||
default=1,
|
||||
default=0,
|
||||
sort_index=9)
|
||||
def staggers(self):
|
||||
return self.get_float_param("staggers", 1)
|
||||
|
@ -367,6 +391,24 @@ class Stroke(EmbroideryElement):
|
|||
def join_style(self):
|
||||
return self.get_int_param('join_style', 0)
|
||||
|
||||
@property
|
||||
@param('random_seed',
|
||||
_('Random seed'),
|
||||
tooltip=_('Use a specific seed for randomized attributes. Uses the element ID if empty.'),
|
||||
select_items=[('stroke_method', 'running_stitch'),
|
||||
('stroke_method', 'ripple_stitch')],
|
||||
type='random_seed',
|
||||
default='',
|
||||
sort_index=100)
|
||||
@cache
|
||||
def random_seed(self) -> str:
|
||||
seed = self.get_param('random_seed', '')
|
||||
if not seed:
|
||||
seed = self.node.get_id() or ''
|
||||
# TODO(#1696): When inplementing grouped clones, join this with the IDs of any shadow roots,
|
||||
# letting each instance without a specified seed get a different default.
|
||||
return seed
|
||||
|
||||
@property
|
||||
@cache
|
||||
def is_closed(self):
|
||||
|
@ -443,13 +485,14 @@ 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:
|
||||
stitch_group = self.running_stitch(path, zigzag_spacing / 2.0, self.running_stitch_tolerance)
|
||||
stitch_group = self.running_stitch(path, zigzag_spacing / 2.0, self.running_stitch_tolerance, False, 0, "")
|
||||
stitch_group.stitches = zigzag_stitch(stitch_group.stitches, zigzag_spacing, stroke_width, pull_compensation)
|
||||
|
||||
return stitch_group
|
||||
|
||||
def running_stitch(self, path, stitch_length, tolerance):
|
||||
stitches = running_stitch(path, stitch_length, tolerance)
|
||||
def running_stitch(self, path, stitch_length, tolerance, enable_random, random_sigma, random_seed):
|
||||
# running stitch with repeats
|
||||
stitches = running_stitch(path, stitch_length, tolerance, enable_random, random_sigma, random_seed)
|
||||
|
||||
repeated_stitches = []
|
||||
# go back and forth along the path as specified by self.repeats
|
||||
|
@ -529,7 +572,8 @@ class Stroke(EmbroideryElement):
|
|||
stitch_group = self.simple_satin(path, self.zigzag_spacing, self.stroke_width, self.pull_compensation)
|
||||
# running stitch
|
||||
else:
|
||||
stitch_group = self.running_stitch(path, self.running_stitch_length, self.running_stitch_tolerance)
|
||||
stitch_group = self.running_stitch(path, self.running_stitch_length, self.running_stitch_tolerance,
|
||||
self.enable_random_stitches, self.random_stitch_length_jitter, self.random_seed)
|
||||
# bean stitch
|
||||
if any(self.bean_stitch_repeats):
|
||||
stitch_group.stitches = self.do_bean_repeats(stitch_group.stitches)
|
||||
|
|
|
@ -10,7 +10,7 @@ from shapely.ops import linemerge, nearest_points, split, voronoi_diagram
|
|||
|
||||
from ..elements import FillStitch, Stroke
|
||||
from ..i18n import _
|
||||
from ..stitches.running_stitch import running_stitch
|
||||
from ..stitches.running_stitch import even_running_stitch
|
||||
from ..svg import get_correction_transform
|
||||
from ..utils import ensure_multi_line_string
|
||||
from ..utils.geometry import Point as InkstitchPoint
|
||||
|
@ -110,11 +110,11 @@ class FillToStroke(InkstitchExtension):
|
|||
|
||||
def _get_high_res_polygon(self, polygon):
|
||||
# use running stitch method
|
||||
runs = [running_stitch(line_string_to_point_list(polygon.exterior), 1, 0.1)]
|
||||
runs = [even_running_stitch(line_string_to_point_list(polygon.exterior), 1, 0.1)]
|
||||
if len(runs[0]) < 3:
|
||||
return
|
||||
for interior in polygon.interiors:
|
||||
shape = running_stitch(line_string_to_point_list(interior), 1, 0.1)
|
||||
shape = even_running_stitch(line_string_to_point_list(interior), 1, 0.1)
|
||||
if len(shape) >= 3:
|
||||
runs.append(shape)
|
||||
return MultiPolygon([(runs[0], runs[1:])])
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
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().
|
||||
|
||||
# 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
|
||||
row_direction = (end - beg).unit()
|
||||
segment_length = (end - beg).length()
|
||||
|
||||
offset = (first_stitch - beg).length()
|
||||
stitches.append(beg)
|
||||
|
||||
while offset < segment_length:
|
||||
stitches.append(Stitch(beg + offset * row_direction, tags=('fill_row',)))
|
||||
offset += max_stitch_length
|
||||
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
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -106,6 +106,8 @@ inkstitch_attribs = [
|
|||
'rows_per_thread',
|
||||
'herringbone_width_mm',
|
||||
'tartan_angle',
|
||||
'enable_random_stitches',
|
||||
'random_stitch_length_jitter_percent',
|
||||
# stroke
|
||||
'stroke_method',
|
||||
'bean_stitch_repeats',
|
||||
|
|
|
@ -2,7 +2,7 @@ import numpy as np
|
|||
from scipy.interpolate import splprep, splev
|
||||
|
||||
from .geometry import Point, coordinate_list_to_point_list
|
||||
from ..stitches.running_stitch import running_stitch
|
||||
from ..stitches.running_stitch import even_running_stitch
|
||||
|
||||
|
||||
def _remove_duplicate_coordinates(coords_array):
|
||||
|
@ -48,7 +48,7 @@ def smooth_path(path, smoothness=1.0):
|
|||
#
|
||||
# Fortunately, we can convert the path to segments that are mostly the same
|
||||
# length by using the running stitch algorithm.
|
||||
path = running_stitch(coordinate_list_to_point_list(path), 5 * smoothness, smoothness / 2)
|
||||
path = even_running_stitch(coordinate_list_to_point_list(path), 5 * smoothness, smoothness / 2)
|
||||
|
||||
# splprep blows up on duplicated consecutive points with "Invalid inputs"
|
||||
coords = _remove_duplicate_coordinates(np.array(path))
|
||||
|
|
Ładowanie…
Reference in New Issue