diff options
| -rw-r--r-- | icons/randomize_20x20.png | bin | 0 -> 697 bytes | |||
| -rw-r--r-- | lib/elements/element.py | 18 | ||||
| -rw-r--r-- | lib/elements/satin_column.py | 86 | ||||
| -rw-r--r-- | lib/extensions/base.py | 2 | ||||
| -rw-r--r-- | lib/extensions/params.py | 37 | ||||
| -rw-r--r-- | lib/svg/tags.py | 8 |
6 files changed, 138 insertions, 13 deletions
diff --git a/icons/randomize_20x20.png b/icons/randomize_20x20.png Binary files differnew file mode 100644 index 00000000..40d8e88b --- /dev/null +++ b/icons/randomize_20x20.png diff --git a/lib/elements/element.py b/lib/elements/element.py index 436423a4..e7eb4dea 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)) @@ -251,6 +253,22 @@ class EmbroideryElement(object): 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/elements/satin_column.py b/lib/elements/satin_column.py index 26fc411d..4d3bbdc3 100644 --- a/lib/elements/satin_column.py +++ b/lib/elements/satin_column.py @@ -3,15 +3,16 @@ # 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 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 @@ -88,6 +89,41 @@ class SatinColumn(EmbroideryElement): 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.' + '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_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', _('Short stitch inset'), tooltip=_('Stitches in areas with high density will be shortened by this amount.'), @@ -293,6 +329,10 @@ class SatinColumn(EmbroideryElement): 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): # This isn't used for satins at all, but other parts of the code @@ -497,6 +537,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, 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): @@ -771,8 +827,11 @@ class SatinColumn(EmbroideryElement): old_center = new_center if to_travel <= 0: - add_pair(pos0, pos1) - to_travel = spacing + + 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: add_pair(pos0, pos1) @@ -949,9 +1008,14 @@ 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)) - split_point = line.interpolate((i+1)/split_count, normalized=True) + + 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] @@ -976,6 +1040,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/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 df62128f..a568573f 100644 --- a/lib/extensions/params.py +++ b/lib/extensions/params.py @@ -10,6 +10,7 @@ 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 @@ -34,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) @@ -78,6 +81,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 +193,22 @@ class ParamsTab(ScrolledPanel): return values + 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) + + self.enable_change_indicator('random_seed') + event.Skip() + if self.on_change_hook: + self.on_change_hook(self) + def apply(self): values = self.get_values() for node in self.nodes: @@ -363,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() @@ -379,6 +406,7 @@ 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) + self.SetSizer(box) self.update_choice_widgets() @@ -400,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 @@ -611,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 e2de7348..931c82c7 100644 --- a/lib/svg/tags.py +++ b/lib/svg/tags.py @@ -124,11 +124,17 @@ inkstitch_attribs = [ 'pull_compensation_mm', 'pull_compensation_percent', 'stroke_first', + 'random_width_decrease_percent', + 'random_width_increase_percent', + 'random_split_factor', + 'random_zigzag_spacing', + 'use_seed', # stitch_plan 'invisible_layers', - # Legacy + # All elements 'trim_after', 'stop_after', + 'random_seed', 'manual_stitch', ] for attrib in inkstitch_attribs: |
