diff options
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/elements/satin_column.py | 298 | ||||
| -rw-r--r-- | lib/elements/stroke.py | 2 | ||||
| -rw-r--r-- | lib/stitch_plan/stitch.py | 11 | ||||
| -rw-r--r-- | lib/stitch_plan/stitch_group.py | 8 | ||||
| -rw-r--r-- | lib/stitches/__init__.py | 1 | ||||
| -rw-r--r-- | lib/stitches/ripple_stitch.py | 4 | ||||
| -rw-r--r-- | lib/stitches/running_stitch.py | 43 | ||||
| -rw-r--r-- | lib/svg/tags.py | 5 | ||||
| -rw-r--r-- | lib/utils/geometry.py | 2 | ||||
| -rw-r--r-- | lib/utils/prng.py | 44 |
10 files changed, 265 insertions, 153 deletions
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): @@ -329,10 +350,6 @@ 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 @@ -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) |
