diff options
| author | George Steel <george.steel@gmail.com> | 2024-05-05 13:55:33 -0400 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2024-05-05 13:55:33 -0400 |
| commit | d32a8fd4661331da0affb15623a2ec9a9eac5c44 (patch) | |
| tree | 6ac6a11c099a5b6b5463c9ff46bc7fb87d6ba888 /lib/elements | |
| parent | edbe382914bc45a3f953c6e0258ff1feb05d8c95 (diff) | |
Add randomized running and fill stitches (#2830)
Add a mode to running stitch that uses randomized phase and stitch length instead of even spacing. This greatly reduces moire effects when stitching closely-spaced curves in running-stitch-based fills.
Add option for randomized running stitch to:
ripple stitch
circular fill
contour fill
guided fill
auto-fill
When is randomization is not selected, ripple stitch will use even running stitch when staggers are set to 0 (default) and the stagger algorithm from guided fill (which does not look nice with a stagger period of 0) when staggers is nonzero.
Also includes fix for satin contour underlays (missing tolerance default) mentioned in #2814. This sets the default tolerance to 0.2mm, which is the largest tolerance guaranteed to be backwards-compatible with existing designs using the default inset of 0.4mm.
Original commits:
* fix satin underlay tolerance default
* Add randomized running stitch, make available in ripple stitch, circular, and contour
* add randomized guided fill
* make ripple stitch use even stitching when not staggering or randomizing.
* add random auto-fill and switch jitter parameter to a percentage (matches satin)
* fix comments
Diffstat (limited to 'lib/elements')
| -rw-r--r-- | lib/elements/fill_stitch.py | 92 | ||||
| -rw-r--r-- | lib/elements/satin_column.py | 18 | ||||
| -rw-r--r-- | lib/elements/stroke.py | 58 |
3 files changed, 133 insertions, 35 deletions
diff --git a/lib/elements/fill_stitch.py b/lib/elements/fill_stitch.py index cb1d5225..56b9888d 100644 --- a/lib/elements/fill_stitch.py +++ b/lib/elements/fill_stitch.py @@ -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,11 +461,41 @@ 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'), tooltip=_('Defines how many times to run down and back along the path.'), @@ -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( diff --git a/lib/elements/satin_column.py b/lib/elements/satin_column.py index a33afb8b..9cf7bc73 100644 --- a/lib/elements/satin_column.py +++ b/lib/elements/satin_column.py @@ -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: diff --git a/lib/elements/stroke.py b/lib/elements/stroke.py index 2ce02dbd..0bb18ce8 100644 --- a/lib/elements/stroke.py +++ b/lib/elements/stroke.py @@ -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 @@ -131,6 +130,30 @@ class Stroke(EmbroideryElement): 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'), tooltip=_('Split stitches longer than this.'), @@ -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) @@ -368,6 +392,24 @@ class Stroke(EmbroideryElement): 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): # returns true if the outline of a single line stroke is a closed shape @@ -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) |
