summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--icons/randomize_20x20.pngbin0 -> 697 bytes
-rw-r--r--lib/elements/element.py18
-rw-r--r--lib/elements/satin_column.py86
-rw-r--r--lib/extensions/base.py2
-rw-r--r--lib/extensions/params.py37
-rw-r--r--lib/svg/tags.py8
6 files changed, 138 insertions, 13 deletions
diff --git a/icons/randomize_20x20.png b/icons/randomize_20x20.png
new file mode 100644
index 00000000..40d8e88b
--- /dev/null
+++ b/icons/randomize_20x20.png
Binary files differ
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: