kopia lustrzana https://github.com/inkstitch/inkstitch
use random oracle for randomized satin columns and redo split stitches
rodzic
b63f19b2d0
commit
e28ea888a9
|
@ -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:
|
||||
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
|
||||
|
||||
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))
|
||||
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, 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 _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))
|
||||
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
|
||||
|
||||
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)):
|
||||
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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
Ładowanie…
Reference in New Issue