From 72b001e7c227131a01d3bf3ae8678b704127de77 Mon Sep 17 00:00:00 2001 From: Claudine Date: Sun, 6 Nov 2022 11:03:47 -0500 Subject: add random parameters to satin columns --- icons/randomize_20x20.png | Bin 0 -> 697 bytes lib/elements/satin_column.py | 103 +++++++++++++++++++++++++++++++++++++++---- lib/extensions/params.py | 25 +++++++++++ lib/svg/tags.py | 7 +++ 4 files changed, 127 insertions(+), 8 deletions(-) create mode 100644 icons/randomize_20x20.png diff --git a/icons/randomize_20x20.png b/icons/randomize_20x20.png new file mode 100644 index 00000000..40d8e88b Binary files /dev/null and b/icons/randomize_20x20.png differ diff --git a/lib/elements/satin_column.py b/lib/elements/satin_column.py index 772d1454..51ed43a4 100644 --- a/lib/elements/satin_column.py +++ b/lib/elements/satin_column.py @@ -2,16 +2,15 @@ # # Copyright (c) 2010 Authors # Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details. - +import random from copy import deepcopy from itertools import chain +from inkex import paths from shapely import affinity as shaffinity from shapely import geometry as shgeo from shapely.ops import nearest_points -from inkex import paths - from ..i18n import _ from ..stitch_plan import StitchGroup from ..svg import line_strings_to_csp, point_lists_to_csp @@ -99,6 +98,46 @@ class SatinColumn(EmbroideryElement): def max_stitch_length(self): return self.get_float_param("max_stitch_length_mm") or None + @property + @param('random_split_factor', + _('Random Split Factor'), + tooltip=_('randomize position for split stitches.'), + type='int', unit="%", sort_index=70) + def random_split_factor(self): + return min(max(self.get_int_param("random_split_factor", 0), 0), 100) + + @property + @param('random_first_rail_factor_in', + _('First Rail Random Factor inside'), + tooltip=_('shorten stitch around first rail at most this percent.'), + type='int', unit="%", sort_index=60) + def random_first_rail_factor_in(self): + return min(max(self.get_int_param("random_first_rail_factor_in", 0), 0), 100) + + @property + @param('random_first_rail_factor_out', + _('First Rail Random Factor outside'), + tooltip=_('lengthen stitch around first rail at most this percent.'), + type='int', unit="%", sort_index=61) + def random_first_rail_factor_out(self): + return max(self.get_int_param("random_first_rail_factor_out", 0), 0) + + @property + @param('random_second_rail_factor_in', + _('Second Rail Random Factor inside'), + tooltip=_('shorten stitch around second rail at most this percent.'), + type='int', unit="%", sort_index=62) + def random_second_rail_factor_in(self): + return min(max(self.get_int_param("random_second_rail_factor_in", 0), 0), 100) + + @property + @param('random_second_rail_factor_out', + _('Second Rail Random Factor outside'), + tooltip=_('lengthen stitch around second rail at most this percent.'), + type='int', unit="%", sort_index=63) + def random_second_rail_factor_out(self): + return max(self.get_int_param("random_second_rail_factor_out", 0), 0) + @property @param('short_stitch_inset', _('Short stitch inset'), @@ -132,6 +171,15 @@ class SatinColumn(EmbroideryElement): # peak-to-peak distance between zigzags return max(self.get_float_param("zigzag_spacing_mm", 0.4), 0.01) + @property + @param('random_zigzag_spacing', + _('Zig-zag spacing randomness(peak-to-peak)'), + tooltip=_('percentage of randomness of Peak-to-peak distance between zig-zags.'), + type='int', unit="%", sort_index=64) + def random_zigzag_spacing(self): + # peak-to-peak distance between zigzags + return max(self.get_int_param("random_zigzag_spacing", 0), 0) + @property @param( 'pull_compensation_percent', @@ -257,6 +305,10 @@ class SatinColumn(EmbroideryElement): def zigzag_underlay_max_stitch_length(self): return self.get_float_param("zigzag_underlay_max_stitch_length_mm") or None + @property + def use_seed(self): + return self.get_int_param("use_seed", 0) + @property @cache def shape(self): @@ -500,6 +552,22 @@ class SatinColumn(EmbroideryElement): for rung in self.rungs: point_lists.append(self.flatten_subpath(rung)) + # If originally there were only two subpaths (no rungs) with same number of rails, we may the rails may now + # have two rails with different number of points, and still no rungs, let's add one. + + if not self.rungs: + rails = [shgeo.LineString(reversed(self.flatten_subpath(rail))) for rail in self.rails] + rails.reverse() + path_list = rails + + rung_start = path_list[0].interpolate(0.1) + rung_end = path_list[1].interpolate(0.1) + rung = shgeo.LineString((rung_start, rung_end)) + # make it a bit bigger so that it definitely intersects + rung = shaffinity.scale(rung, 1.1, 1.1) + path_list.append(rung) + return (self._path_list_to_satins(path_list)) + return self._csp_to_satin(point_lists_to_csp(point_lists)) def apply_transform(self): @@ -789,8 +857,13 @@ class SatinColumn(EmbroideryElement): old_center = new_center if to_travel <= 0: - add_pair(pos0, pos1) - to_travel = spacing + + decalage0 = random.uniform(-self.random_first_rail_factor_in, self.random_first_rail_factor_out) / 100 + decalage1 = random.uniform(-self.random_second_rail_factor_in, self.random_second_rail_factor_out) / 100 + + add_pair(pos0 + (pos0 - pos1) * decalage0, pos1 + (pos1 - pos0) * decalage1) + + to_travel = spacing * (random.uniform(1, 1 + self.random_zigzag_spacing/100)) if to_travel > 0: add_pair(pos0, pos1) @@ -842,8 +915,7 @@ class SatinColumn(EmbroideryElement): patch = StitchGroup(color=self.color) - sides = self.plot_points_on_rails(self.zigzag_underlay_spacing / 2.0, - -self.zigzag_underlay_inset) + sides = self.plot_points_on_rails(self.zigzag_underlay_spacing / 2.0, -self.zigzag_underlay_inset) if self._center_walk_is_odd(): sides = [list(reversed(sides[0])), list(reversed(sides[1]))] @@ -953,7 +1025,12 @@ class SatinColumn(EmbroideryElement): split_count = count or int(-(-distance // max_stitch_length)) for i in range(split_count): line = shgeo.LineString((left, right)) - split_point = line.interpolate((i+1)/split_count, normalized=True) + + random_move = 0 + if self.random_split_factor and i != split_count-1: + random_move = random.uniform(-self.random_split_factor / 100, self.random_split_factor / 100) + + split_point = line.interpolate((i + 1 + random_move) / split_count, normalized=True) points.append(Point(split_point.x, split_point.y)) return [points, split_count] @@ -978,6 +1055,16 @@ class SatinColumn(EmbroideryElement): # beziers. The boundary points between beziers serve as "checkpoints", # allowing the user to control how the zigzags flow around corners. + # If no seed is defined, compute one randomly using time to seed, otherwise, use stored seed + + if self.use_seed == 0: + random.seed() + x = random.randint(1, 10000) + random.seed(x) + self.set_param("use_seed", x) + else: + random.seed(self.use_seed) + patch = StitchGroup(color=self.color) if self.center_walk_underlay: diff --git a/lib/extensions/params.py b/lib/extensions/params.py index df62128f..7d38b5ff 100644 --- a/lib/extensions/params.py +++ b/lib/extensions/params.py @@ -6,6 +6,7 @@ # -*- coding: UTF-8 -*- import os +import random import sys from collections import defaultdict from copy import copy @@ -78,6 +79,9 @@ class ParamsTab(ScrolledPanel): self.pencil_icon = wx.Image(os.path.join(get_resource_dir( "icons"), "pencil_20x20.png")).ConvertToBitmap() + self.randomize_icon = wx.Image(os.path.join(get_resource_dir( + "icons"), "randomize_20x20.png")).ConvertToBitmap() + self.__set_properties() self.__do_layout() @@ -187,6 +191,16 @@ class ParamsTab(ScrolledPanel): return values + def on_change_seed(self, event): + + for node in self.nodes: + random.seed() + new_seed = random.randint(1, 10000) + node.set_param("use_seed", new_seed) + if self.on_change_hook: + self.on_change_hook(self) + event.Skip() + def apply(self): values = self.get_values() for node in self.nodes: @@ -379,6 +393,17 @@ class ParamsTab(ScrolledPanel): self.inputs_to_params = {v: k for k, v in self.param_inputs.items()} box.Add(self.settings_grid, proportion=1, flag=wx.ALL, border=10) + + add_seed_button = False + for param in self.params: + if param.name[:6] == "random": + add_seed_button = True + if add_seed_button: + self.change_seed_button = wx.Button(self, wx.ID_ANY, _("Change Seed")) + self.change_seed_button.Bind(wx.EVT_BUTTON, self.on_change_seed) + self.change_seed_button.SetBitmap(self.randomize_icon) + box.Add(self.change_seed_button, proportion=0, flag=wx.ALIGN_CENTER_HORIZONTAL, border=10) + self.SetSizer(box) self.update_choice_widgets() diff --git a/lib/svg/tags.py b/lib/svg/tags.py index 06c402dc..a4dfa0ba 100644 --- a/lib/svg/tags.py +++ b/lib/svg/tags.py @@ -119,6 +119,13 @@ inkstitch_attribs = [ 'pull_compensation_percent', 'pull_compensation_rails', 'stroke_first', + 'random_split_factor', + 'random_first_rail_factor_in', + 'random_first_rail_factor_out', + 'random_second_rail_factor_in', + 'random_second_rail_factor_out', + 'random_zigzag_spacing', + 'use_seed', # stitch_plan 'invisible_layers', # Legacy -- cgit v1.2.3 From c4c22cac4f3a0bf89856982167cf17089498b517 Mon Sep 17 00:00:00 2001 From: Claudine <88194877+claudinepeyrat06@users.noreply.github.com> Date: Sun, 27 Nov 2022 18:14:41 +0100 Subject: use split parameters --- lib/elements/satin_column.py | 73 ++++++++++++++++++-------------------------- lib/svg/tags.py | 6 ++-- 2 files changed, 31 insertions(+), 48 deletions(-) diff --git a/lib/elements/satin_column.py b/lib/elements/satin_column.py index 3a183ab0..2021a73f 100644 --- a/lib/elements/satin_column.py +++ b/lib/elements/satin_column.py @@ -2,11 +2,12 @@ # # Copyright (c) 2010 Authors # Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details. + import random from copy import deepcopy from itertools import chain -import numpy as np +import numpy as np from inkex import paths from shapely import affinity as shaffinity from shapely import geometry as shgeo @@ -16,7 +17,7 @@ from ..i18n import _ from ..stitch_plan import StitchGroup from ..svg import line_strings_to_csp, point_lists_to_csp from ..utils import Point, cache, collapse_duplicate_point, cut -from .element import EmbroideryElement, param, PIXELS_PER_MM +from .element import PIXELS_PER_MM, EmbroideryElement, param from .validation import ValidationError, ValidationWarning @@ -108,36 +109,31 @@ class SatinColumn(EmbroideryElement): return min(max(self.get_int_param("random_split_factor", 0), 0), 100) @property - @param('random_first_rail_factor_in', - _('First Rail Random Factor inside'), - tooltip=_('shorten stitch around first rail at most this percent.'), - type='int', unit="%", sort_index=60) - def random_first_rail_factor_in(self): - return min(max(self.get_int_param("random_first_rail_factor_in", 0), 0), 100) - - @property - @param('random_first_rail_factor_out', - _('First Rail Random Factor outside'), - tooltip=_('lengthen stitch around first rail at most this percent.'), - type='int', unit="%", sort_index=61) - def random_first_rail_factor_out(self): - return max(self.get_int_param("random_first_rail_factor_out", 0), 0) + @param('random_zigzag_spacing', + _('Zig-zag spacing randomness(peak-to-peak)'), + tooltip=_('percentage of randomness of Peak-to-peak distance between zig-zags.'), + type='int', unit="%", sort_index=64) + def random_zigzag_spacing(self): + # peak-to-peak distance between zigzags + return max(self.get_int_param("random_zigzag_spacing", 0), 0) @property - @param('random_second_rail_factor_in', - _('Second Rail Random Factor inside'), - tooltip=_('shorten stitch around second rail at most this percent.'), - type='int', unit="%", sort_index=62) - def random_second_rail_factor_in(self): - return min(max(self.get_int_param("random_second_rail_factor_in", 0), 0), 100) + @param('random_width_decrease_percent', + _('Random percentage of satin width decrease'), + tooltip=_('shorten stitch across rails at most this percent.' + 'Two values separated by a space may be used for an aysmmetric effect.'), + type='int', unit="% (each side)", sort_index=60) + def random_width_decrease_percent(self): + return self.get_split_float_param("random_width_decrease_percent", (0, 0)) @property - @param('random_second_rail_factor_out', - _('Second Rail Random Factor outside'), - tooltip=_('lengthen stitch around second rail at most this percent.'), - type='int', unit="%", sort_index=63) - def random_second_rail_factor_out(self): - return max(self.get_int_param("random_second_rail_factor_out", 0), 0) + @param('random_width_increase_percent', + _('Random percentage of satin width increase'), + tooltip=_('lengthen stitch across rails at most this percent.' + 'Two values separated by a space may be used for an aysmmetric effect.'), + type='int', unit="% (each side)", sort_index=60) + def random_width_increase_percent(self): + return self.get_split_float_param("random_width_increase_percent", (0, 0)) @property @param('short_stitch_inset', @@ -172,15 +168,6 @@ class SatinColumn(EmbroideryElement): # peak-to-peak distance between zigzags return max(self.get_float_param("zigzag_spacing_mm", 0.4), 0.01) - @property - @param('random_zigzag_spacing', - _('Zig-zag spacing randomness(peak-to-peak)'), - tooltip=_('percentage of randomness of Peak-to-peak distance between zig-zags.'), - type='int', unit="%", sort_index=64) - def random_zigzag_spacing(self): - # peak-to-peak distance between zigzags - return max(self.get_int_param("random_zigzag_spacing", 0), 0) - @property @param( 'pull_compensation_percent', @@ -604,7 +591,7 @@ class SatinColumn(EmbroideryElement): for rung in self.rungs: point_lists.append(self.flatten_subpath(rung)) - # If originally there were only two subpaths (no rungs) with same number of rails, we may the rails may now + # If originally there were only two subpaths (no rungs) with same number of rails, the rails may now # have two rails with different number of points, and still no rungs, let's add one. if not self.rungs: @@ -899,11 +886,9 @@ class SatinColumn(EmbroideryElement): if to_travel <= 0: - decalage0 = random.uniform(-self.random_first_rail_factor_in, self.random_first_rail_factor_out) / 100 - decalage1 = random.uniform(-self.random_second_rail_factor_in, self.random_second_rail_factor_out) / 100 - - add_pair(pos0 + (pos0 - pos1) * decalage0, pos1 + (pos1 - pos0) * decalage1) - + mismatch0 = random.uniform(-self.random_width_decrease_percent[0], self.random_width_increase_percent[0]) / 100 + mismatch1 = random.uniform(-self.random_width_decrease_percent[1], self.random_width_increase_percent[1]) / 100 + add_pair(pos0 + (pos0 - pos1) * mismatch0, pos1 + (pos1 - pos0) * mismatch1) to_travel = spacing * (random.uniform(1, 1 + self.random_zigzag_spacing/100)) if to_travel > 0: @@ -1080,10 +1065,10 @@ class SatinColumn(EmbroideryElement): points = [] distance = left.distance(right) split_count = count or int(-(-distance // max_stitch_length)) + random_move = 0 for i in range(split_count): line = shgeo.LineString((left, right)) - random_move = 0 if self.random_split_factor and i != split_count-1: random_move = random.uniform(-self.random_split_factor / 100, self.random_split_factor / 100) diff --git a/lib/svg/tags.py b/lib/svg/tags.py index 4743438b..54400c4e 100644 --- a/lib/svg/tags.py +++ b/lib/svg/tags.py @@ -123,11 +123,9 @@ inkstitch_attribs = [ 'pull_compensation_mm', 'pull_compensation_percent', 'stroke_first', + 'random_width_decrease_percent', + 'random_width_increase_percent', 'random_split_factor', - 'random_first_rail_factor_in', - 'random_first_rail_factor_out', - 'random_second_rail_factor_in', - 'random_second_rail_factor_out', 'random_zigzag_spacing', 'use_seed', # stitch_plan -- cgit v1.2.3 From 903f724c888aea968e85b1e3f2817c66e64d7a02 Mon Sep 17 00:00:00 2001 From: George Steel Date: Sun, 11 Dec 2022 22:18:09 -0500 Subject: refine GUI for text-based random_seed parameter and add global param. --- lib/elements/element.py | 16 ++++++++++++++++ lib/extensions/base.py | 2 ++ lib/extensions/params.py | 46 +++++++++++++++++++++++----------------------- lib/svg/tags.py | 3 ++- 4 files changed, 43 insertions(+), 24 deletions(-) diff --git a/lib/elements/element.py b/lib/elements/element.py index 84141d4f..b8b25b27 100644 --- a/lib/elements/element.py +++ b/lib/elements/element.py @@ -250,6 +250,22 @@ class EmbroideryElement(object): def force_lock_stitches(self): return self.get_boolean_param('force_lock_stitches', False) + @property + @param('random_seed', + _('Random seed'), + tooltip=_('Use a specific seed for randomized attributes. Uses the element ID if empty.'), + 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 def path(self): # A CSP is a "cubic superpath". diff --git a/lib/extensions/base.py b/lib/extensions/base.py index cf94714c..c2f76b27 100644 --- a/lib/extensions/base.py +++ b/lib/extensions/base.py @@ -179,6 +179,8 @@ class InkstitchExtension(inkex.Effect): return nodes def get_nodes(self, troubleshoot=False): + # Postorder traversal of selected nodes and their descendants. + # Returns all nodes if there is no selection. return self.descendants(self.document.getroot(), troubleshoot=troubleshoot) def get_elements(self, troubleshoot=False): diff --git a/lib/extensions/params.py b/lib/extensions/params.py index 7d38b5ff..a568573f 100644 --- a/lib/extensions/params.py +++ b/lib/extensions/params.py @@ -6,11 +6,11 @@ # -*- coding: UTF-8 -*- import os -import random import sys from collections import defaultdict from copy import copy from itertools import groupby, zip_longest +from secrets import randbelow import wx from wx.lib.scrolledpanel import ScrolledPanel @@ -35,6 +35,8 @@ class ParamsTab(ScrolledPanel): def __init__(self, *args, **kwargs): self.params = kwargs.pop('params', []) self.name = kwargs.pop('name', None) + + # TODO: this is actually a list of embroidery elements, not DOM nodes, and needs to be renamed self.nodes = kwargs.pop('nodes') kwargs["style"] = wx.TAB_TRAVERSAL ScrolledPanel.__init__(self, *args, **kwargs) @@ -191,15 +193,21 @@ class ParamsTab(ScrolledPanel): return values - def on_change_seed(self, event): + def on_reroll(self, event): + if len(self.nodes) == 1: + new_seed = str(randbelow(int(1e8))) + input = self.param_inputs['random_seed'] + input.SetValue(new_seed) + self.changed_inputs.add(input) + else: + for node in self.nodes: + new_seed = str(randbelow(int(1e8))) + node.set_param('random_seed', new_seed) - for node in self.nodes: - random.seed() - new_seed = random.randint(1, 10000) - node.set_param("use_seed", new_seed) + self.enable_change_indicator('random_seed') + event.Skip() if self.on_change_hook: self.on_change_hook(self) - event.Skip() def apply(self): values = self.get_values() @@ -377,7 +385,12 @@ class ParamsTab(ScrolledPanel): self.param_inputs[param.name] = input - col4 = wx.StaticText(self, label=param.unit or "") + if param.type == 'random_seed': + col4 = wx.Button(self, wx.ID_ANY, _("Re-roll")) + col4.Bind(wx.EVT_BUTTON, self.on_reroll) + col4.SetBitmap(self.randomize_icon) + else: + col4 = wx.StaticText(self, label=param.unit or "") if param.select_items is not None: input.Hide() @@ -394,16 +407,6 @@ class ParamsTab(ScrolledPanel): box.Add(self.settings_grid, proportion=1, flag=wx.ALL, border=10) - add_seed_button = False - for param in self.params: - if param.name[:6] == "random": - add_seed_button = True - if add_seed_button: - self.change_seed_button = wx.Button(self, wx.ID_ANY, _("Change Seed")) - self.change_seed_button.Bind(wx.EVT_BUTTON, self.on_change_seed) - self.change_seed_button.SetBitmap(self.randomize_icon) - box.Add(self.change_seed_button, proportion=0, flag=wx.ALIGN_CENTER_HORIZONTAL, border=10) - self.SetSizer(box) self.update_choice_widgets() @@ -425,11 +428,6 @@ class ParamsTab(ScrolledPanel): self.param_change_indicators[param].SetToolTip( _('This parameter will be saved when you click "Apply and Quit"')) - self.changed_inputs.add(self.param_inputs[param]) - - if self.on_change_hook: - self.on_change_hook(self) - # end of class SatinPane @@ -636,6 +634,8 @@ class Params(InkstitchExtension): return classes def get_nodes_by_class(self): + # returns embroidery elements (not nodes) by class + # TODO: rename this nodes = self.get_nodes() nodes_by_class = defaultdict(list) diff --git a/lib/svg/tags.py b/lib/svg/tags.py index 54400c4e..6ae3b66a 100644 --- a/lib/svg/tags.py +++ b/lib/svg/tags.py @@ -130,9 +130,10 @@ inkstitch_attribs = [ 'use_seed', # stitch_plan 'invisible_layers', - # Legacy + # All elements 'trim_after', 'stop_after', + 'random_seed', 'manual_stitch', ] for attrib in inkstitch_attribs: -- cgit v1.2.3 From 54b0a3d6bf77b4c7b23f299ae1600399b80092df Mon Sep 17 00:00:00 2001 From: George Steel Date: Sun, 11 Dec 2022 22:29:29 -0500 Subject: add comment on set_param --- lib/elements/element.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/elements/element.py b/lib/elements/element.py index b8b25b27..bf87d517 100644 --- a/lib/elements/element.py +++ b/lib/elements/element.py @@ -163,6 +163,8 @@ class EmbroideryElement(object): return self.get_split_float_param(param, default) * PIXELS_PER_MM def set_param(self, name, value): + # Sets a param on the node backing this element. Used by params dialog. + # After calling, this element is invalid due to caching and must be re-created to use the new value. param = INKSTITCH_ATTRIBS[name] self.node.set(param, str(value)) -- cgit v1.2.3 From b63f19b2d0747769c60c8a2a52489ee30fa02a07 Mon Sep 17 00:00:00 2001 From: George Steel Date: Mon, 26 Dec 2022 20:10:10 -0500 Subject: fix seed param and error messages --- lib/elements/element.py | 2 +- lib/extensions/params.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/elements/element.py b/lib/elements/element.py index e7eb4dea..75f9fc10 100644 --- a/lib/elements/element.py +++ b/lib/elements/element.py @@ -261,7 +261,7 @@ class EmbroideryElement(object): sort_index=100) @cache def random_seed(self) -> str: - seed = self.get_param('random_seed') + 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, diff --git a/lib/extensions/params.py b/lib/extensions/params.py index a568573f..306e5e56 100644 --- a/lib/extensions/params.py +++ b/lib/extensions/params.py @@ -7,6 +7,7 @@ import os import sys +import traceback from collections import defaultdict from copy import copy from itertools import groupby, zip_longest @@ -511,9 +512,10 @@ class SettingsFrame(wx.Frame): except SystemExit: wx.CallAfter(self._show_warning) raise - except Exception: + except Exception as e: # Ignore errors. This can be things like incorrect paths for # satins or division by zero caused by incorrect param values. + traceback.print_exception(e, file=sys.stderr) pass return patches -- cgit v1.2.3 From e28ea888a9604052d6d7b94c8e29e34b74242994 Mon Sep 17 00:00:00 2001 From: George Steel Date: Mon, 26 Dec 2022 20:13:48 -0500 Subject: use random oracle for randomized satin columns and redo split stitches --- lib/elements/satin_column.py | 298 ++++++++++++++++++++++------------------ lib/elements/stroke.py | 2 +- lib/stitch_plan/stitch.py | 11 +- lib/stitch_plan/stitch_group.py | 8 +- lib/stitches/__init__.py | 1 - lib/stitches/ripple_stitch.py | 4 +- lib/stitches/running_stitch.py | 43 +++++- lib/svg/tags.py | 5 +- lib/utils/geometry.py | 2 +- lib/utils/prng.py | 44 ++++++ 10 files changed, 265 insertions(+), 153 deletions(-) create mode 100644 lib/utils/prng.py diff --git a/lib/elements/satin_column.py b/lib/elements/satin_column.py index 4d3bbdc3..bfb716af 100644 --- a/lib/elements/satin_column.py +++ b/lib/elements/satin_column.py @@ -3,8 +3,8 @@ # Copyright (c) 2010 Authors # Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details. -import random from copy import deepcopy +import itertools from itertools import chain import numpy as np @@ -16,7 +16,8 @@ from shapely.ops import nearest_points from ..i18n import _ from ..stitch_plan import StitchGroup from ..svg import line_strings_to_csp, point_lists_to_csp -from ..utils import Point, cache, cut, cut_multiple +from ..utils import Point, cache, cut, cut_multiple, prng +from ..stitches import running_stitch from .element import EmbroideryElement, param, PIXELS_PER_MM from .validation import ValidationError, ValidationWarning @@ -85,48 +86,68 @@ class SatinColumn(EmbroideryElement): _('Maximum stitch length'), tooltip=_('Maximum stitch length for split stitches.'), type='float', unit="mm") - def max_stitch_length(self): + def max_stitch_length_px(self): return self.get_float_param("max_stitch_length_mm") or None - @property - @param('random_split_factor', - _('Random Split Factor'), - tooltip=_('randomize position for split stitches.'), - type='int', unit="%", sort_index=70) - def random_split_factor(self): - return min(max(self.get_int_param("random_split_factor", 0), 0), 100) - - @property - @param('random_zigzag_spacing', - _('Zig-zag spacing randomness(peak-to-peak)'), - tooltip=_('percentage of randomness of Peak-to-peak distance between zig-zags.'), - type='int', unit="%", sort_index=64) - def random_zigzag_spacing(self): - # peak-to-peak distance between zigzags - return max(self.get_int_param("random_zigzag_spacing", 0), 0) - @property @param('random_width_decrease_percent', _('Random percentage of satin width decrease'), - tooltip=_('shorten stitch across rails at most this percent.' + tooltip=_('shorten stitch across rails at most this percent. ' 'Two values separated by a space may be used for an aysmmetric effect.'), - type='int', unit="% (each side)", sort_index=60) + default=0, type='float', unit="% (each side)", sort_index=91) + @cache def random_width_decrease_percent(self): return self.get_split_float_param("random_width_decrease_percent", (0, 0)) @property @param('random_width_increase_percent', _('Random percentage of satin width increase'), - tooltip=_('lengthen stitch across rails at most this percent.' + tooltip=_('lengthen stitch across rails at most this percent. ' 'Two values separated by a space may be used for an aysmmetric effect.'), - type='int', unit="% (each side)", sort_index=60) + default=0, type='float', unit="% (each side)", sort_index=90) + @cache def random_width_increase_percent(self): return self.get_split_float_param("random_width_increase_percent", (0, 0)) + @property + @param('random_zigzag_spacing', + _('Random zig-zag spacing percentage increase'), + tooltip=_('Percentage of stitch length added randomply Peak-to-peak distance between zig-zags.'), + default=0, type='float', unit="%", sort_index=92) + def random_zigzag_spacing(self): + # peak-to-peak distance between zigzags + return max(self.get_float_param("random_zigzag_spacing", 0), 0) + + @property + @param('random_split_phase', + _('Random phase for split stitches'), + tooltip=_('Controls whether split stitches are centered or with a random phase (which may increase stitch count).'), + default=False, type='boolean', sort_index=95) + def random_split_phase(self): + return self.get_boolean_param('random_split_phase') + + @property + @param('min_random_split_length_mm', + _('Minimum length for random-phase split.'), + tooltip=_('Defaults to maximum stitch length. Smaller values allow for a transition between single-stitch and split-stitch.'), + default='', type='float', unit='mm', sort_index=96) + def min_random_split_length_px(self): + if self.max_stitch_length_px is None: + return None + return min(self.max_stitch_length_px, self.get_float_param('min_random_split_length_mm', self.max_stitch_length_px)) + + @property + @param('random_split_length_percent', + _('Random jitter split stitch'), + tooltip=_('randomizes split stitch length if random phase is emabled, stitch position if disabled.'), + type='float', unit="%", sort_index=97) + def random_split_length(self): + return min(max(self.get_float_param("random_split_length_percent", 0), 0), 100) / 100 + @property @param('short_stitch_inset', _('Short stitch inset'), - tooltip=_('Stitches in areas with high density will be shortened by this amount.'), + tooltip=_('Stitches in areas with high density will be inset by this amount.'), type='float', unit="%", default=15) def short_stitch_inset(self): @@ -135,7 +156,7 @@ class SatinColumn(EmbroideryElement): @property @param('short_stitch_distance_mm', _('Short stitch distance'), - tooltip=_('Do short stitches if the distance between stitches is smaller than this.'), + tooltip=_('Inset stitches if the distance between stitches is smaller than this.'), type='float', unit="mm", default=0.25) def short_stitch_distance(self): @@ -328,10 +349,6 @@ class SatinColumn(EmbroideryElement): def zigzag_underlay_max_stitch_length(self): return self.get_float_param("zigzag_underlay_max_stitch_length_mm") or None - @property - def use_seed(self): - return self.get_int_param("use_seed", 0) - @property @cache def shape(self): @@ -601,7 +618,7 @@ class SatinColumn(EmbroideryElement): """ # like in do_satin() - points = list(chain.from_iterable(zip(*self.plot_points_on_rails(self.zigzag_spacing)))) + points = list(chain.from_iterable(self.plot_points_on_rails(self.zigzag_spacing))) if isinstance(split_point, float): index_of_closest_stitch = int(round(len(points) * split_point)) @@ -689,7 +706,7 @@ class SatinColumn(EmbroideryElement): @cache def center_line(self): # similar technique to do_center_walk() - center_walk, _ = self.plot_points_on_rails(self.zigzag_spacing, (0, 0), (-0.5, -0.5)) + center_walk = [p[0] for p in self.plot_points_on_rails(self.zigzag_spacing, (0, 0), (-0.5, -0.5))] return shgeo.LineString(center_walk) def offset_points(self, pos1, pos2, offset_px, offset_proportional): @@ -752,19 +769,22 @@ class SatinColumn(EmbroideryElement): distance_remaining -= segment_length pos = segment_end - def plot_points_on_rails(self, spacing, offset_px=(0, 0), offset_proportional=(0, 0)): + def plot_points_on_rails(self, spacing, offset_px=(0, 0), offset_proportional=(0, 0), use_random=False) -> list[tuple[Point, Point]]: # Take a section from each rail in turn, and plot out an equal number # of points on both rails. Return the points plotted. The points will # be contracted or expanded by offset using self.offset_points(). - def add_pair(pos0, pos1): - pos0, pos1 = self.offset_points(pos0, pos1, offset_px, offset_proportional) - points[0].append(pos0) - points[1].append(pos1) + # pre-cache ramdomised parameters to avoid property calls in loop + if use_random: + seed = prng.joinArgs(self.random_seed, "satin-points") + offset_proportional_min = np.array(offset_proportional) - self.random_width_decrease_percent/100 + offset_range = (self.random_width_increase_percent + self.random_width_decrease_percent) / 100 + spacing_range = spacing * self.random_zigzag_spacing / 100 - points = [[], []] + pairs = [] to_travel = 0 + cycle = 0 for section0, section1 in self.flattened_sections: # Take one section at a time, delineated by the rungs. For each @@ -827,26 +847,32 @@ class SatinColumn(EmbroideryElement): old_center = new_center if to_travel <= 0: - - mismatch0 = random.uniform(-self.random_width_decrease_percent[0], self.random_width_increase_percent[0]) / 100 - mismatch1 = random.uniform(-self.random_width_decrease_percent[1], self.random_width_increase_percent[1]) / 100 - add_pair(pos0 + (pos0 - pos1) * mismatch0, pos1 + (pos1 - pos0) * mismatch1) - to_travel = spacing * (random.uniform(1, 1 + self.random_zigzag_spacing/100)) + if use_random: + roll = prng.uniformFloats(seed, cycle) + offset_prop = offset_proportional_min + roll[0:2] * offset_range + to_travel = spacing + roll[2] * spacing_range + else: + offset_prop = offset_proportional + to_travel = spacing + + a, b = self.offset_points(pos0, pos1, offset_px, offset_prop) + pairs.append((a, b)) + cycle += 1 if to_travel > 0: - add_pair(pos0, pos1) + pairs.append((pos0, pos1)) - return points + return pairs def do_contour_underlay(self): # "contour walk" underlay: do stitches up one side and down the # other. - forward, back = self.plot_points_on_rails( + pairs = self.plot_points_on_rails( self.contour_underlay_stitch_length, -self.contour_underlay_inset_px, -self.contour_underlay_inset_percent/100) - stitches = (forward + list(reversed(back))) + stitches = [p[0] for p in pairs] + [p[1] for p in reversed(pairs)] if self._center_walk_is_odd(): - stitches = (list(reversed(back)) + forward) + stitches = list(reversed(stitches)) return StitchGroup( color=self.color, @@ -860,16 +886,16 @@ class SatinColumn(EmbroideryElement): inset_prop = -np.array([self.center_walk_underlay_position, 100-self.center_walk_underlay_position]) / 100 # Do it like contour underlay, but inset all the way to the center. - forward, back = self.plot_points_on_rails( + pairs = self.plot_points_on_rails( self.center_walk_underlay_stitch_length, (0, 0), inset_prop) stitches = [] for i in range(self.center_walk_underlay_repeats): if i % 2 == 0: - stitches += forward + stitches += [p[0] for p in pairs] else: - stitches += list(reversed(back)) + stitches += [p[1] for p in reversed(pairs)] return StitchGroup( color=self.color, @@ -889,27 +915,26 @@ class SatinColumn(EmbroideryElement): patch = StitchGroup(color=self.color) - sides = self.plot_points_on_rails(self.zigzag_underlay_spacing / 2.0, + pairs = self.plot_points_on_rails(self.zigzag_underlay_spacing / 2.0, -self.zigzag_underlay_inset_px, -self.zigzag_underlay_inset_percent/100) if self._center_walk_is_odd(): - sides = [list(reversed(sides[0])), list(reversed(sides[1]))] + pairs = list(reversed(pairs)) # This organizes the points in each side in the order that they'll be # visited. - sides = [sides[0][::2] + list(reversed(sides[0][1::2])), - sides[1][1::2] + list(reversed(sides[1][::2]))] + # take a points, from each side in turn, then go backed over the other points + points = [p[i % 2] for i, p in enumerate(pairs)] + list(reversed([p[i % 2] for i, p in enumerate(pairs, 1)])) - # This fancy bit of iterable magic just repeatedly takes a point - # from each side in turn. + max_len = self.zigzag_underlay_max_stitch_length last_point = None - for point in chain.from_iterable(zip(*sides)): - if last_point and self.zigzag_underlay_max_stitch_length: - if last_point.distance(point) > self.zigzag_underlay_max_stitch_length: - points, count = self._get_split_points(last_point, point, self.zigzag_underlay_max_stitch_length) - for point in points: - patch.add_stitch(point) + for point in points: + if last_point and max_len: + if last_point.distance(point) > max_len: + split_points = running_stitch.split_segment_even_dist(last_point, point, max_len) + for p in split_points: + patch.add_stitch(p) last_point = point patch.add_stitch(point) @@ -928,28 +953,47 @@ class SatinColumn(EmbroideryElement): patch = StitchGroup(color=self.color) # pull compensation is automatically converted from mm to pixels by get_float_param - sides = self.plot_points_on_rails( + pairs = self.plot_points_on_rails( self.zigzag_spacing, self.pull_compensation_px, - self.pull_compensation_percent/100 + self.pull_compensation_percent/100, + True, ) - if self.max_stitch_length: - return self.do_split_stitch(patch, sides) + max_stitch_length = self.max_stitch_length_px + length_sigma = self.random_split_length + random_phase = self.random_split_phase + min_split_length = self.min_random_split_length_px + seed = self.random_seed - # short stitches are not not included into the split stitch - # they would move the points in a maybe unwanted behaviour - if self.short_stitch_inset > 0: - self._do_short_stitches(sides) + short_pairs = self.inset_short_stitches_sawtooth(pairs) - # Like in zigzag_underlay(): take a point from each side in turn. - for point in chain.from_iterable(zip(*sides)): - patch.add_stitch(point) + last_point = None + last_short_point = None + last_count = None + for i, (a, b), (a_short, b_short) in zip(itertools.count(0), pairs, short_pairs): + if last_point is not None: + split_points, _ = self.get_split_points( + last_point, a, last_short_point, a_short, max_stitch_length, last_count, + length_sigma, random_phase, min_split_length, prng.joinArgs(seed, 'satin-split', 2*i)) + patch.add_stitches(split_points, ("satin_column", "satin_split_stitch")) + + patch.add_stitch(a_short) + patch.stitches[-1].add_tags(("satin_column", "satin_column_edge")) + + split_points, last_count = self.get_split_points( + a, b, a_short, b_short, max_stitch_length, None, + length_sigma, random_phase, min_split_length, prng.joinArgs(seed, 'satin-split', 2*i+1)) + patch.add_stitches(split_points, ("satin_column", "satin_split_stitch")) + + patch.add_stitch(b_short) + patch.stitches[-1].add_tags(("satin_column", "satin_column_edge")) + last_point = b + last_short_point = b_short if self._center_walk_is_odd(): patch.stitches = list(reversed(patch.stitches)) - patch.add_tags(("satin_column", "satin_column_edge")) return patch def do_e_stitch(self): @@ -962,15 +1006,16 @@ class SatinColumn(EmbroideryElement): patch = StitchGroup(color=self.color) - sides = self.plot_points_on_rails( + pairs = self.plot_points_on_rails( self.zigzag_spacing, self.pull_compensation_px, - self.pull_compensation_percent/100 + self.pull_compensation_percent/100, + self.random_width_decrease_percent.any() and self.random_width_increase_percent.any() and self.random_zigzag_spacing, ) # "left" and "right" here are kind of arbitrary designations meaning # a point from the first and second rail respectively - for left, right in zip(*sides): + for left, right in pairs: patch.add_stitch(left) patch.add_stitch(right) patch.add_stitch(left) @@ -981,54 +1026,49 @@ class SatinColumn(EmbroideryElement): patch.add_tags(("satin_column", "e_stitch")) return patch - def do_split_stitch(self, patch, sides): - # stitches exceeding the maximum stitch length will be divided into equal parts through additional stitches - for i, (left, right) in enumerate(zip(*sides)): - patch.add_stitch(left) - patch.stitches[-1].add_tags(("satin_column", "satin_column_edge")) - points, count = self._get_split_points(left, right, self.max_stitch_length) - for point in points: - patch.add_stitch(point) - patch.stitches[-1].add_tags(("satin_column", "satin_split_stitch")) - patch.add_stitch(right) - patch.stitches[-1].add_tags(("satin_column", "satin_column_edge")) - # it is possible that the way back has a different length from the first - # but it looks ugly if the points differ too much - # so let's make sure they have at least the same amount of divisions - if not i+1 >= len(sides[0]): - points, count = self._get_split_points(right, sides[0][i+1], self.max_stitch_length, count) - for point in points: - patch.add_stitch(point) - patch.stitches[-1].add_tags(("satin_column", "satin_split_stitch")) - if self._center_walk_is_odd(): - patch.stitches = list(reversed(patch.stitches)) - return patch - - def _get_split_points(self, left, right, max_stitch_length, count=None): - points = [] - distance = left.distance(right) - split_count = count or int(-(-distance // max_stitch_length)) - random_move = 0 - for i in range(split_count): - line = shgeo.LineString((left, right)) - - if self.random_split_factor and i != split_count-1: - random_move = random.uniform(-self.random_split_factor / 100, self.random_split_factor / 100) - - split_point = line.interpolate((i + 1 + random_move) / split_count, normalized=True) - points.append(Point(split_point.x, split_point.y)) - return [points, split_count] - - def _do_short_stitches(self, sides): - for i, (left, right) in enumerate(zip(*sides)): + def get_split_points(self, a, b, a_short, b_short, length, count=None, length_sigma=0.0, random_phase=False, min_split_length=None, seed=None): + if not length: + return ([], None) + if min_split_length is None: + min_split_length = length + distance = a.distance(b) + if distance <= min_split_length: + return ([], 1) + if random_phase: + points = running_stitch.split_segment_random_phase(a_short, b_short, length, length_sigma, seed) + return (points, None) + elif count is not None: + points = running_stitch.split_segment_even_n(a, b, count, length_sigma, seed) + return (points, count) + else: + points = running_stitch.split_segment_even_dist(a, b, length, length_sigma, seed) + return (points, len(points) + 1) + + def inset_short_stitches_sawtooth(self, pairs): + min_dist = self.short_stitch_distance + inset = min(self.short_stitch_inset, 0.5) + max_stitch_length = None if self.random_split_phase else self.max_stitch_length_px + if not min_dist or not inset: + return pairs + + shortened = [] + for i, (a, b) in enumerate(pairs): if i % 2 == 0: + shortened.append((a, b)) continue - if left.distance(sides[0][i-1]) < self.short_stitch_distance: - split_point = self._get_inset_point(left, right, self.short_stitch_inset) - sides[0][i] = Point(split_point.x, split_point.y) - if right.distance(sides[1][i-1]) < self.short_stitch_distance: - split_point = self._get_inset_point(right, left, self.short_stitch_inset) - sides[1][i] = Point(split_point.x, split_point.y) + dist = a.distance(b) + inset_px = inset * dist + if max_stitch_length and not self.random_split_phase: + # make sure inset is less than split etitch length + inset_px = min(inset_px, max_stitch_length / 3) + + offset_px = [0, 0] + if a.distance(pairs[i-1][0]) < min_dist: + offset_px[0] = -inset_px + if b.distance(pairs[i-1][0]) < min_dist: + offset_px[1] = -inset_px + shortened.append(self.offset_points(a, b, offset_px, (0, 0))) + return shortened def _get_inset_point(self, point1, point2, distance_fraction): return point1 * (1 - distance_fraction) + point2 * distance_fraction @@ -1040,16 +1080,6 @@ class SatinColumn(EmbroideryElement): # beziers. The boundary points between beziers serve as "checkpoints", # allowing the user to control how the zigzags flow around corners. - # If no seed is defined, compute one randomly using time to seed, otherwise, use stored seed - - if self.use_seed == 0: - random.seed() - x = random.randint(1, 10000) - random.seed(x) - self.set_param("use_seed", x) - else: - random.seed(self.use_seed) - patch = StitchGroup(color=self.color) if self.center_walk_underlay: diff --git a/lib/elements/stroke.py b/lib/elements/stroke.py index 2854adaf..6bd6a54a 100644 --- a/lib/elements/stroke.py +++ b/lib/elements/stroke.py @@ -12,7 +12,7 @@ from inkex import Transform from ..i18n import _ from ..marker import get_marker_elements from ..stitch_plan import StitchGroup -from ..stitches import bean_stitch, running_stitch +from ..stitches.running_stitch import bean_stitch, running_stitch from ..stitches.ripple_stitch import ripple_stitch from ..svg import get_node_transform, parse_length_with_units from ..utils import Point, cache diff --git a/lib/stitch_plan/stitch.py b/lib/stitch_plan/stitch.py index 0d46b85d..3bfa7075 100644 --- a/lib/stitch_plan/stitch.py +++ b/lib/stitch_plan/stitch.py @@ -4,6 +4,7 @@ # Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details. from ..utils.geometry import Point +from shapely import geometry as shgeo class Stitch(Point): @@ -17,13 +18,13 @@ class Stitch(Point): # Allow creating a Stitch from another Stitch. Attributes passed as # arguments will override any existing attributes. base_stitch = x - self.x = base_stitch.x - self.y = base_stitch.y - elif isinstance(x, Point): + self.x: float = base_stitch.x + self.y: float = base_stitch.y + elif isinstance(x, (Point, shgeo.Point)): # Allow creating a Stitch from a Point point = x - self.x = point.x - self.y = point.y + self.x: float = point.x + self.y: float = point.y else: Point.__init__(self, x, y) diff --git a/lib/stitch_plan/stitch_group.py b/lib/stitch_plan/stitch_group.py index 21beebe1..6dbeb9e5 100644 --- a/lib/stitch_plan/stitch_group.py +++ b/lib/stitch_plan/stitch_group.py @@ -43,14 +43,14 @@ class StitchGroup: # This method allows `len(patch)` and `if patch: return len(self.stitches) - def add_stitches(self, stitches): + def add_stitches(self, stitches, tags=None): for stitch in stitches: - self.add_stitch(stitch) + self.add_stitch(stitch, tags=tags) - def add_stitch(self, stitch): + def add_stitch(self, stitch, tags=None): if not isinstance(stitch, Stitch): # probably a Point - stitch = Stitch(stitch) + stitch = Stitch(stitch, tags=tags) self.stitches.append(stitch) diff --git a/lib/stitches/__init__.py b/lib/stitches/__init__.py index b0ff64fc..cfa05e51 100644 --- a/lib/stitches/__init__.py +++ b/lib/stitches/__init__.py @@ -6,7 +6,6 @@ from .auto_fill import auto_fill from .fill import legacy_fill from .guided_fill import guided_fill -from .running_stitch import * # Can't put this here because we get a circular import :( # from .auto_satin import auto_satin diff --git a/lib/stitches/ripple_stitch.py b/lib/stitches/ripple_stitch.py index 6b7ce6ca..a66eff74 100644 --- a/lib/stitches/ripple_stitch.py +++ b/lib/stitches/ripple_stitch.py @@ -74,12 +74,12 @@ def _get_satin_ripple_helper_lines(stroke): length = stroke.grid_size or stroke.running_stitch_length # use satin column points for satin like build ripple stitches - rail_points = SatinColumn(stroke.node).plot_points_on_rails(length) + rail_pairs = SatinColumn(stroke.node).plot_points_on_rails(length) steps = _get_steps(stroke.get_line_count(), exponent=stroke.exponent, flip=stroke.flip_exponent) helper_lines = [] - for point0, point1 in zip(*rail_points): + for point0, point1 in rail_pairs: helper_lines.append([]) helper_line = LineString((point0, point1)) for step in steps: diff --git a/lib/stitches/running_stitch.py b/lib/stitches/running_stitch.py index 8c86eb7c..fd6d4572 100644 --- a/lib/stitches/running_stitch.py +++ b/lib/stitches/running_stitch.py @@ -6,11 +6,48 @@ import math from copy import copy -from shapely.geometry import LineString +import numpy as np +from shapely import geometry as shgeo +from ..utils import prng """ Utility functions to produce running stitches. """ +def split_segment_even_n(a, b, segments: int, jitter_sigma: float = 0.0, random_seed: str | None = None) -> list[shgeo.Point]: + if segments <= 1: + return [] + line = shgeo.LineString((a, b)) + + splits = np.array(range(1, segments)) / segments + if random_seed is not None: + jitters = (prng.nUniformFloats(len(splits), random_seed) * 2) - 1 + splits = splits + jitters * (jitter_sigma / segments) + + # sort the splits in case a bad roll transposes any of them + return [line.interpolate(x, normalized=True) for x in sorted(splits)] + + +def split_segment_even_dist(a, b, max_length: float, jitter_sigma: float = 0.0, random_seed: str | None = None) -> list[shgeo.Point]: + distance = shgeo.Point(a).distance(shgeo.Point(b)) + segments = math.ceil(distance / max_length) + return split_segment_even_n(a, b, segments, jitter_sigma, random_seed) + + +def split_segment_random_phase(a, b, length: float, length_sigma: float, random_seed: str) -> list[shgeo.Point]: + line = shgeo.LineString([a, b]) + progress = length * prng.uniformFloats(random_seed, "phase")[0] + splits = [progress] + distance = line.length + if progress >= distance: + return [] + for x in prng.iterUniformFloats(random_seed): + progress += length * (1 + length_sigma * (x - 0.5) * 2) + if progress >= distance: + break + splits.append(progress) + return [line.interpolate(x, normalized=False) for x in splits] + + def running_stitch(points, stitch_length, tolerance): """Generate running stitch along a path. @@ -28,7 +65,7 @@ def running_stitch(points, stitch_length, tolerance): # simplify will remove as many points as possible while ensuring that the # resulting path stays within the specified tolerance of the original path. - path = LineString(points) + path = shgeo.LineString(points) simplified = path.simplify(tolerance, preserve_topology=False) # save the points that simplify picked and make sure we stitch them @@ -45,7 +82,7 @@ def running_stitch(points, stitch_length, tolerance): # Now split each section up evenly into stitches, each with a length no # greater than the specified stitch_length. - section_ls = LineString(section) + section_ls = shgeo.LineString(section) section_length = section_ls.length if section_length > stitch_length: # a fractional stitch needs to be rounded up, which will make all diff --git a/lib/svg/tags.py b/lib/svg/tags.py index 931c82c7..0d17ac46 100644 --- a/lib/svg/tags.py +++ b/lib/svg/tags.py @@ -126,9 +126,10 @@ inkstitch_attribs = [ 'stroke_first', 'random_width_decrease_percent', 'random_width_increase_percent', - 'random_split_factor', 'random_zigzag_spacing', - 'use_seed', + 'random_split_phase', + 'random_split_length_percent', + 'min_random_split_length_mm', # stitch_plan 'invisible_layers', # All elements diff --git a/lib/utils/geometry.py b/lib/utils/geometry.py index 98f40709..0ca13d8f 100644 --- a/lib/utils/geometry.py +++ b/lib/utils/geometry.py @@ -148,7 +148,7 @@ def cut_path(points, length): class Point: - def __init__(self, x, y): + def __init__(self, x: float, y: float): self.x = x self.y = y diff --git a/lib/utils/prng.py b/lib/utils/prng.py new file mode 100644 index 00000000..518049cb --- /dev/null +++ b/lib/utils/prng.py @@ -0,0 +1,44 @@ +from hashlib import blake2s +from math import ceil +from itertools import count, chain +import numpy as np + + +def joinArgs(*args): + # Stringifies parameters into a slash-separated string for use in hash keys. + # Idempotent and associative. + return "/".join([str(x) for x in args]) + + +MAX_UNIFORM_INT = 2 ** 32 - 1 + + +def uniformInts(*args): + # Single pseudo-random drawing determined by the joined parameters. + # Returns 4 uniformly random uint64. + s = joinArgs(*args) + h = blake2s(s.encode()).hexdigest() + nums = [] + for i in range(0, 64, 8): + nums.append(int(h[i:i+8], 16)) + return np.array(nums) + + +def uniformFloats(*args): + # returns an array of 8 floats in the range [0,1] + return uniformInts(*args) / MAX_UNIFORM_INT + + +def nUniformFloats(n: int, *args): + # returns a fixed number (which may exceed 8) of floats in the range [0,1] + seed = joinArgs(*args) + nBlocks = ceil(n/8) + blocks = [uniformFloats(seed, x) for x in range(nBlocks)] + return np.concatenate(blocks)[0:n] + + +def iterUniformFloats(*args): + # returns an infinite sequence of floats in the range [0,1] + seed = joinArgs(*args) + blocks = map(lambda x: list(uniformFloats(seed, x)), count(0)) + return chain.from_iterable(blocks) -- cgit v1.2.3 From 26c24d98ed37f5fc63436201539ec68023fce154 Mon Sep 17 00:00:00 2001 From: George Steel Date: Mon, 26 Dec 2022 20:42:13 -0500 Subject: python 3.8 fixes --- lib/elements/satin_column.py | 3 ++- lib/stitches/running_stitch.py | 7 ++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/elements/satin_column.py b/lib/elements/satin_column.py index bfb716af..00b468c1 100644 --- a/lib/elements/satin_column.py +++ b/lib/elements/satin_column.py @@ -6,6 +6,7 @@ from copy import deepcopy import itertools from itertools import chain +import typing import numpy as np from inkex import paths @@ -769,7 +770,7 @@ class SatinColumn(EmbroideryElement): distance_remaining -= segment_length pos = segment_end - def plot_points_on_rails(self, spacing, offset_px=(0, 0), offset_proportional=(0, 0), use_random=False) -> list[tuple[Point, Point]]: + def plot_points_on_rails(self, spacing, offset_px=(0, 0), offset_proportional=(0, 0), use_random=False) -> typing.List[typing.Tuple[Point, Point]]: # Take a section from each rail in turn, and plot out an equal number # of points on both rails. Return the points plotted. The points will # be contracted or expanded by offset using self.offset_points(). diff --git a/lib/stitches/running_stitch.py b/lib/stitches/running_stitch.py index fd6d4572..34729109 100644 --- a/lib/stitches/running_stitch.py +++ b/lib/stitches/running_stitch.py @@ -4,6 +4,7 @@ # Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details. import math +import typing from copy import copy import numpy as np @@ -13,7 +14,7 @@ from ..utils import prng """ Utility functions to produce running stitches. """ -def split_segment_even_n(a, b, segments: int, jitter_sigma: float = 0.0, random_seed: str | None = None) -> list[shgeo.Point]: +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 [] line = shgeo.LineString((a, b)) @@ -27,13 +28,13 @@ def split_segment_even_n(a, b, segments: int, jitter_sigma: float = 0.0, random_ return [line.interpolate(x, normalized=True) for x in sorted(splits)] -def split_segment_even_dist(a, b, max_length: float, jitter_sigma: float = 0.0, random_seed: str | None = None) -> list[shgeo.Point]: +def split_segment_even_dist(a, b, max_length: float, jitter_sigma: float = 0.0, random_seed=None) -> typing.List[shgeo.Point]: distance = shgeo.Point(a).distance(shgeo.Point(b)) segments = math.ceil(distance / max_length) return split_segment_even_n(a, b, segments, jitter_sigma, random_seed) -def split_segment_random_phase(a, b, length: float, length_sigma: float, random_seed: str) -> list[shgeo.Point]: +def split_segment_random_phase(a, b, length: float, length_sigma: float, random_seed: str) -> typing.List[shgeo.Point]: line = shgeo.LineString([a, b]) progress = length * prng.uniformFloats(random_seed, "phase")[0] splits = [progress] -- cgit v1.2.3 From 9e88534c271664c06a6515509f24a221634d18b8 Mon Sep 17 00:00:00 2001 From: George Steel Date: Tue, 27 Dec 2022 01:12:15 -0500 Subject: fix typos --- lib/elements/satin_column.py | 39 ++++++++++++++++++++------------------- lib/svg/tags.py | 4 ++-- 2 files changed, 22 insertions(+), 21 deletions(-) diff --git a/lib/elements/satin_column.py b/lib/elements/satin_column.py index 00b468c1..c5db1acc 100644 --- a/lib/elements/satin_column.py +++ b/lib/elements/satin_column.py @@ -97,8 +97,8 @@ class SatinColumn(EmbroideryElement): 'Two values separated by a space may be used for an aysmmetric effect.'), default=0, type='float', unit="% (each side)", sort_index=91) @cache - def random_width_decrease_percent(self): - return self.get_split_float_param("random_width_decrease_percent", (0, 0)) + def random_width_decrease(self): + return self.get_split_float_param("random_width_decrease_percent", (0, 0)) / 100 @property @param('random_width_increase_percent', @@ -107,23 +107,23 @@ class SatinColumn(EmbroideryElement): 'Two values separated by a space may be used for an aysmmetric effect.'), default=0, type='float', unit="% (each side)", sort_index=90) @cache - def random_width_increase_percent(self): - return self.get_split_float_param("random_width_increase_percent", (0, 0)) + def random_width_increase(self): + return self.get_split_float_param("random_width_increase_percent", (0, 0)) / 100 @property - @param('random_zigzag_spacing', + @param('random_zigzag_spacing_percent', _('Random zig-zag spacing percentage increase'), tooltip=_('Percentage of stitch length added randomply Peak-to-peak distance between zig-zags.'), default=0, type='float', unit="%", sort_index=92) def random_zigzag_spacing(self): # peak-to-peak distance between zigzags - return max(self.get_float_param("random_zigzag_spacing", 0), 0) + return max(self.get_float_param("random_zigzag_spacing_percent", 0), 0) / 100 @property @param('random_split_phase', _('Random phase for split stitches'), tooltip=_('Controls whether split stitches are centered or with a random phase (which may increase stitch count).'), - default=False, type='boolean', sort_index=95) + default=False, type='boolean', sort_index=96) def random_split_phase(self): return self.get_boolean_param('random_split_phase') @@ -131,19 +131,19 @@ class SatinColumn(EmbroideryElement): @param('min_random_split_length_mm', _('Minimum length for random-phase split.'), tooltip=_('Defaults to maximum stitch length. Smaller values allow for a transition between single-stitch and split-stitch.'), - default='', type='float', unit='mm', sort_index=96) + default='', type='float', unit='mm', sort_index=97) def min_random_split_length_px(self): if self.max_stitch_length_px is None: return None return min(self.max_stitch_length_px, self.get_float_param('min_random_split_length_mm', self.max_stitch_length_px)) @property - @param('random_split_length_percent', - _('Random jitter split stitch'), - tooltip=_('randomizes split stitch length if random phase is emabled, stitch position if disabled.'), - type='float', unit="%", sort_index=97) - def random_split_length(self): - return min(max(self.get_float_param("random_split_length_percent", 0), 0), 100) / 100 + @param('random_split_jitter_percent', + _('Random jitter for split stitches'), + tooltip=_('Randomizes split stitch length if random phase is enabled, stitch position if disabled.'), + default=0, type='float', unit="%", sort_index=95) + def random_split_jitter(self): + return min(max(self.get_float_param("random_split_jitter_percent", 0), 0), 100) / 100 @property @param('short_stitch_inset', @@ -770,7 +770,8 @@ class SatinColumn(EmbroideryElement): distance_remaining -= segment_length pos = segment_end - def plot_points_on_rails(self, spacing, offset_px=(0, 0), offset_proportional=(0, 0), use_random=False) -> typing.List[typing.Tuple[Point, Point]]: + def plot_points_on_rails(self, spacing, offset_px=(0, 0), offset_proportional=(0, 0), use_random=False + ) -> typing.List[typing.Tuple[Point, Point]]: # Take a section from each rail in turn, and plot out an equal number # of points on both rails. Return the points plotted. The points will # be contracted or expanded by offset using self.offset_points(). @@ -778,8 +779,8 @@ class SatinColumn(EmbroideryElement): # pre-cache ramdomised parameters to avoid property calls in loop if use_random: seed = prng.joinArgs(self.random_seed, "satin-points") - offset_proportional_min = np.array(offset_proportional) - self.random_width_decrease_percent/100 - offset_range = (self.random_width_increase_percent + self.random_width_decrease_percent) / 100 + offset_proportional_min = np.array(offset_proportional) - self.random_width_decrease + offset_range = (self.random_width_increase + self.random_width_decrease) spacing_range = spacing * self.random_zigzag_spacing / 100 pairs = [] @@ -962,7 +963,7 @@ class SatinColumn(EmbroideryElement): ) max_stitch_length = self.max_stitch_length_px - length_sigma = self.random_split_length + length_sigma = self.random_split_jitter random_phase = self.random_split_phase min_split_length = self.min_random_split_length_px seed = self.random_seed @@ -1011,7 +1012,7 @@ class SatinColumn(EmbroideryElement): self.zigzag_spacing, self.pull_compensation_px, self.pull_compensation_percent/100, - self.random_width_decrease_percent.any() and self.random_width_increase_percent.any() and self.random_zigzag_spacing, + self.random_width_decrease.any() and self.random_width_increase.any() and self.random_zigzag_spacing, ) # "left" and "right" here are kind of arbitrary designations meaning diff --git a/lib/svg/tags.py b/lib/svg/tags.py index 0d17ac46..ac61ab4c 100644 --- a/lib/svg/tags.py +++ b/lib/svg/tags.py @@ -126,9 +126,9 @@ inkstitch_attribs = [ 'stroke_first', 'random_width_decrease_percent', 'random_width_increase_percent', - 'random_zigzag_spacing', + 'random_zigzag_spacing_percent', 'random_split_phase', - 'random_split_length_percent', + 'random_split_jitter_percent', 'min_random_split_length_mm', # stitch_plan 'invisible_layers', -- cgit v1.2.3 From 28e6898e2e9857444ad9a54e8a234e3de20bcf91 Mon Sep 17 00:00:00 2001 From: George Steel Date: Wed, 28 Dec 2022 19:38:55 -0500 Subject: fix length ramdomization --- lib/elements/satin_column.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/elements/satin_column.py b/lib/elements/satin_column.py index c5db1acc..e7922a9e 100644 --- a/lib/elements/satin_column.py +++ b/lib/elements/satin_column.py @@ -112,9 +112,9 @@ class SatinColumn(EmbroideryElement): @property @param('random_zigzag_spacing_percent', - _('Random zig-zag spacing percentage increase'), - tooltip=_('Percentage of stitch length added randomply Peak-to-peak distance between zig-zags.'), - default=0, type='float', unit="%", sort_index=92) + _('Random zig-zag spacing percentage'), + tooltip=_('Afount of random jitter added to stitch length.'), + default=0, type='float', unit="± %", sort_index=92) def random_zigzag_spacing(self): # peak-to-peak distance between zigzags return max(self.get_float_param("random_zigzag_spacing_percent", 0), 0) / 100 @@ -141,7 +141,7 @@ class SatinColumn(EmbroideryElement): @param('random_split_jitter_percent', _('Random jitter for split stitches'), tooltip=_('Randomizes split stitch length if random phase is enabled, stitch position if disabled.'), - default=0, type='float', unit="%", sort_index=95) + default=0, type='float', unit="± %", sort_index=95) def random_split_jitter(self): return min(max(self.get_float_param("random_split_jitter_percent", 0), 0), 100) / 100 @@ -781,7 +781,7 @@ class SatinColumn(EmbroideryElement): seed = prng.joinArgs(self.random_seed, "satin-points") offset_proportional_min = np.array(offset_proportional) - self.random_width_decrease offset_range = (self.random_width_increase + self.random_width_decrease) - spacing_range = spacing * self.random_zigzag_spacing / 100 + spacing_sigma = spacing * self.random_zigzag_spacing pairs = [] @@ -852,7 +852,7 @@ class SatinColumn(EmbroideryElement): if use_random: roll = prng.uniformFloats(seed, cycle) offset_prop = offset_proportional_min + roll[0:2] * offset_range - to_travel = spacing + roll[2] * spacing_range + to_travel = spacing + ((roll[2] - 0.5) * 2 * spacing_sigma) else: offset_prop = offset_proportional to_travel = spacing -- cgit v1.2.3 From d416407f2bb447a655266ae926337be853e88217 Mon Sep 17 00:00:00 2001 From: George Steel Date: Mon, 2 Jan 2023 19:33:50 -0500 Subject: add comment to PRNG --- lib/utils/prng.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/lib/utils/prng.py b/lib/utils/prng.py index 518049cb..2ec037c6 100644 --- a/lib/utils/prng.py +++ b/lib/utils/prng.py @@ -3,6 +3,15 @@ from math import ceil from itertools import count, chain import numpy as np +# Framework for reproducable pseudo-random number generation. + +# Unlike python's random module (which uses a stateful generator based on global variables), +# a counter-mode PRNG like uniformFloats can be used to generate multiple, independent random streams +# by including an additional parameter before the loop counter. +# This allows different aspects of an embroidery element to not effect each other's rolls, +# making random generation resistant to small edits in the control paths or refactoring. +# Using multiple counters for n-dimentional random streams is also possible and is useful for grid-like structures. + def joinArgs(*args): # Stringifies parameters into a slash-separated string for use in hash keys. @@ -15,8 +24,11 @@ MAX_UNIFORM_INT = 2 ** 32 - 1 def uniformInts(*args): # Single pseudo-random drawing determined by the joined parameters. - # Returns 4 uniformly random uint64. + # To get a longer sequence of random numbers, call this loop with a counter as one of the parameters. + # Returns 8 uniformly random uint32. + s = joinArgs(*args) + # blake2s is python's fastest hash algorithm for small inputs and is designed to be usable as a PRNG. h = blake2s(s.encode()).hexdigest() nums = [] for i in range(0, 64, 8): @@ -25,7 +37,9 @@ def uniformInts(*args): def uniformFloats(*args): - # returns an array of 8 floats in the range [0,1] + # Single pseudo-random drawing determined by the joined parameters. + # To get a longer sequence of random numbers, call this loop with a counter as one of the parameters. + # Returns an array of 8 floats in the range [0,1] return uniformInts(*args) / MAX_UNIFORM_INT -- cgit v1.2.3 From 8d13e09bc41f9cfeaa0cd32cd75fa4da07afc03c Mon Sep 17 00:00:00 2001 From: George Steel Date: Sat, 7 Jan 2023 19:32:29 -0500 Subject: spelling --- lib/elements/satin_column.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/elements/satin_column.py b/lib/elements/satin_column.py index e7922a9e..e4d3ba94 100644 --- a/lib/elements/satin_column.py +++ b/lib/elements/satin_column.py @@ -113,7 +113,7 @@ class SatinColumn(EmbroideryElement): @property @param('random_zigzag_spacing_percent', _('Random zig-zag spacing percentage'), - tooltip=_('Afount of random jitter added to stitch length.'), + tooltip=_('Amount of random jitter added to stitch length.'), default=0, type='float', unit="± %", sort_index=92) def random_zigzag_spacing(self): # peak-to-peak distance between zigzags -- cgit v1.2.3