summaryrefslogtreecommitdiff
path: root/lib/elements
diff options
context:
space:
mode:
Diffstat (limited to 'lib/elements')
-rw-r--r--lib/elements/element.py18
-rw-r--r--lib/elements/satin_column.py292
-rw-r--r--lib/elements/stroke.py2
3 files changed, 218 insertions, 94 deletions
diff --git a/lib/elements/element.py b/lib/elements/element.py
index 436423a4..75f9fc10 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..e4d3ba94 100644
--- a/lib/elements/satin_column.py
+++ b/lib/elements/satin_column.py
@@ -4,18 +4,21 @@
# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details.
from copy import deepcopy
+import itertools
from itertools import chain
-import numpy as np
+import typing
+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
-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
@@ -84,13 +87,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_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.'),
+ default=0, type='float', unit="% (each side)", sort_index=91)
+ @cache
+ def random_width_decrease(self):
+ return self.get_split_float_param("random_width_decrease_percent", (0, 0)) / 100
+
+ @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.'),
+ default=0, type='float', unit="% (each side)", sort_index=90)
+ @cache
+ def random_width_increase(self):
+ return self.get_split_float_param("random_width_increase_percent", (0, 0)) / 100
+
+ @property
+ @param('random_zigzag_spacing_percent',
+ _('Random zig-zag spacing percentage'),
+ 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
+ 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=96)
+ 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=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_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',
_('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):
@@ -99,7 +157,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):
@@ -497,6 +555,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):
@@ -545,7 +619,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))
@@ -633,7 +707,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):
@@ -696,19 +770,23 @@ 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
+ ) -> 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().
- 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
+ offset_range = (self.random_width_increase + self.random_width_decrease)
+ spacing_sigma = spacing * self.random_zigzag_spacing
- 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
@@ -771,23 +849,32 @@ class SatinColumn(EmbroideryElement):
old_center = new_center
if to_travel <= 0:
- add_pair(pos0, pos1)
- to_travel = spacing
+ if use_random:
+ roll = prng.uniformFloats(seed, cycle)
+ offset_prop = offset_proportional_min + roll[0:2] * offset_range
+ to_travel = spacing + ((roll[2] - 0.5) * 2 * spacing_sigma)
+ 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,
@@ -801,16 +888,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,
@@ -830,27 +917,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)
@@ -869,28 +955,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_jitter
+ 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):
@@ -903,15 +1008,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.any() and self.random_width_increase.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)
@@ -922,49 +1028,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))
- for i in range(split_count):
- line = shgeo.LineString((left, right))
- split_point = line.interpolate((i+1)/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
diff --git a/lib/elements/stroke.py b/lib/elements/stroke.py
index 00e069b2..a95edf70 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