Manual ripple pattern (#3256)

* ripple stitch: add manual pattern placement option
* add flip copies option
* rewrite adjust grid
* more control for satin guided ripples: render at rungs
pull/3288/head
Kaalleen 2024-11-18 11:54:14 +01:00 zatwierdzone przez GitHub
rodzic d15b1f40c7
commit dde0444ac2
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: B5690EEEBB952194
4 zmienionych plików z 127 dodań i 50 usunięć

Wyświetl plik

@ -103,6 +103,18 @@ class Stroke(EmbroideryElement):
def bean_stitch_repeats(self):
return self.get_multiple_int_param("bean_stitch_repeats", "0")
@property
@param('manual_pattern_placement',
_('Manual stitch placement'),
tooltip=_('No extra stitches will be added to the original ripple pattern '
'and the running stitch length value will be ignored.'),
type='boolean',
select_items=[('stroke_method', 'ripple_stitch')],
default=False,
sort_index=3)
def manual_pattern_placement(self):
return self.get_boolean_param('manual_pattern_placement', False)
@property
@param('running_stitch_length_mm',
_('Running stitch length'),
@ -222,6 +234,17 @@ class Stroke(EmbroideryElement):
return
return max(min_dist, 0.01)
@property
@param('render_at_rungs',
_('Render at rungs'),
tooltip=_('Position satin guided pattern at rungs.'),
type='boolean',
select_items=[('stroke_method', 'ripple_stitch')],
default=False,
sort_index=9)
def render_at_rungs(self):
return self.get_boolean_param('render_at_rungs', False)
@property
@param('staggers',
_('Stagger lines this many times before repeating'),
@ -260,6 +283,17 @@ class Stroke(EmbroideryElement):
def skip_end(self):
return abs(self.get_int_param("skip_end", 0))
@property
@param('flip_copies',
_('Flip every second line'),
tooltip=_('Linear ripple: wether to flip the pattern every second line or not.'),
type='boolean',
select_items=[('stroke_method', 'ripple_stitch')],
default=True,
sort_index=12)
def flip_copies(self):
return self.get_boolean_param('flip_copies', True)
@property
@param('exponent',
_('Line distance exponent'),
@ -267,7 +301,7 @@ class Stroke(EmbroideryElement):
type='float',
default=1,
select_items=[('stroke_method', 'ripple_stitch')],
sort_index=12)
sort_index=13)
@cache
def exponent(self):
return max(self.get_float_param("exponent", 1), 0.1)
@ -279,7 +313,7 @@ class Stroke(EmbroideryElement):
type='boolean',
default=False,
select_items=[('stroke_method', 'ripple_stitch')],
sort_index=13)
sort_index=14)
@cache
def flip_exponent(self):
return self.get_boolean_param("flip_exponent", False)
@ -291,7 +325,7 @@ class Stroke(EmbroideryElement):
type='boolean',
default=False,
select_items=[('stroke_method', 'ripple_stitch')],
sort_index=14)
sort_index=15)
@cache
def reverse(self):
return self.get_boolean_param("reverse", False)
@ -313,7 +347,7 @@ class Stroke(EmbroideryElement):
options=_reverse_rails_options,
default='automatic',
select_items=[('stroke_method', 'ripple_stitch')],
sort_index=15)
sort_index=16)
def reverse_rails(self):
return self.get_param('reverse_rails', 'automatic')
@ -339,7 +373,7 @@ class Stroke(EmbroideryElement):
# 0: xy, 1: x, 2: y, 3: none
options=["X Y", "X", "Y", _("None")],
select_items=[('stroke_method', 'ripple_stitch')],
sort_index=17)
sort_index=18)
def scale_axis(self):
return self.get_int_param('scale_axis', 0)

Wyświetl plik

@ -171,7 +171,8 @@ def apply_stitches(line, max_stitch_length, num_staggers, row_spacing, row_num,
points = np.array([line.interpolate(projection).coords[0] for projection in projections])
if len(points) < 2:
return line
coords = line.coords
points = [coords[0], coords[-1]]
stitched_line = shgeo.LineString(points)

Wyświetl plik

@ -33,20 +33,30 @@ def ripple_stitch(stroke):
skip_start = _adjust_skip(stroke, num_lines, stroke.skip_start)
skip_end = _adjust_skip(stroke, num_lines, stroke.skip_end)
lines = _get_ripple_lines(stroke, helper_lines, is_linear, skip_start, skip_end)
lines = _get_ripple_lines(helper_lines, is_linear, skip_start, skip_end)
stitches = _get_stitches(stroke, is_linear, lines, skip_start)
if stroke.reverse:
stitches.reverse()
if stroke.grid_size != 0:
stitches.extend(_do_grid(stroke, helper_lines, skip_start, skip_end, is_linear))
if stitches and stroke.grid_size != 0:
stitches.extend(_do_grid(stroke, helper_lines, skip_start, skip_end, is_linear, stitches[-1]))
return _repeat_coords(stitches, stroke.repeats)
def _get_stitches(stroke, is_linear, lines, skip_start):
if is_linear:
if stroke.manual_pattern_placement:
if stroke.flip_copies:
stitches = []
for i, line in enumerate(lines):
if i % 2 == 0:
stitches.extend(line[::-1])
else:
stitches.extend(line)
return stitches
return [point for line in lines for point in line]
if is_linear and stroke.flip_copies:
return _get_staggered_stitches(stroke, lines, skip_start)
else:
points = [point for line in lines for point in line]
@ -69,7 +79,7 @@ def _get_staggered_stitches(stroke, lines, skip_start):
for i, line in enumerate(lines):
connector = []
if i != 0 and stroke.join_style == 0:
if i % 2 == 0:
if i % 2 == 0 or not stroke.flip_copies:
first_point = line[0]
else:
first_point = line[-1]
@ -81,7 +91,7 @@ def _get_staggered_stitches(stroke, lines, skip_start):
should_reverse = (i + skip_start) % 2 == 1
if enable_random_stitch_length or stroke.staggers == 0:
if should_reverse:
if should_reverse and stroke.flip_copies:
line.reverse()
points = running_stitch(line, stitch_length, tolerance, enable_random_stitch_length, length_sigma, prng.join_args(random_seed, i))
stitched_line = connector + points
@ -89,7 +99,7 @@ def _get_staggered_stitches(stroke, lines, skip_start):
# uses the guided fill alforithm to stagger rows of stitches
points = list(apply_stitches(LineString(line), stitch_length, stroke.staggers, 0.5, i, tolerance).coords)
stitched_line = [InkstitchPoint(*point) for point in points]
if should_reverse:
if should_reverse and stroke.flip_copies:
stitched_line.reverse()
stitched_line = connector + stitched_line
@ -104,7 +114,7 @@ def _adjust_skip(stroke, num_lines, skip):
return skip
def _get_ripple_lines(stroke, helper_lines, is_linear, skip_start, skip_end):
def _get_ripple_lines(helper_lines, is_linear, skip_start, skip_end):
lines = []
for point_num in range(skip_start, len(helper_lines[0]) - skip_end):
row = []
@ -158,9 +168,16 @@ def _get_helper_lines(stroke):
if len(lines) > 1:
return True, _get_satin_ripple_helper_lines(stroke)
else:
outline = LineString(even_running_stitch(line_string_to_point_list(lines[0]),
stroke.grid_size or stroke.running_stitch_length,
stroke.running_stitch_tolerance))
if stroke.manual_pattern_placement:
path = stroke.parse_path()
path = [stroke.strip_control_points(subpath) for subpath in path][0]
outline = LineString(path)
else:
outline = LineString(even_running_stitch(
line_string_to_point_list(lines[0]),
stroke.grid_size or stroke.running_stitch_length,
stroke.running_stitch_tolerance)
)
if stroke.is_closed:
return False, _get_circular_ripple_helper_lines(stroke, outline)
@ -189,13 +206,13 @@ def _get_satin_ripple_helper_lines(stroke):
for step in steps:
helper_lines[-1].append(InkstitchPoint.from_shapely_point(helper_line.interpolate(step, normalized=True)))
if stroke.join_style == 1:
helper_lines = _converge_helper_line_points(helper_lines, True)
if stroke.join_style == 1 or not stroke.flip_copies:
helper_lines = _converge_helper_line_points(helper_lines, True, stroke.flip_copies)
return helper_lines
def _converge_helper_line_points(helper_lines, point_edge=False):
def _converge_helper_line_points(helper_lines, point_edge=False, flip_copies=True):
num_lines = len(helper_lines)
steps = _get_steps(num_lines)
for i, line in enumerate(helper_lines):
@ -203,7 +220,7 @@ def _converge_helper_line_points(helper_lines, point_edge=False):
points = []
for j in range(len(line) - 1):
if point_edge and j % 2 == 1:
if point_edge and j % 2 == 1 and flip_copies:
k = num_lines - 1 - i
points.append(line[j] * (1 - steps[k]) + line[j + 1] * steps[k])
else:
@ -250,37 +267,36 @@ def _target_point_helper_lines(stroke, outline):
return helper_lines
def _adjust_helper_lines_for_grid(stroke, helper_lines, skip_start, skip_end, is_linear):
num_lines = len(helper_lines[0])
count = num_lines - skip_start - skip_end
if stroke.join_style == 0 and (stroke.reverse and count % 2 != 0):
count += 1
elif (stroke.join_style == 1 and ((stroke.reverse and (count + skip_start) % 2 != 0) or
(not stroke.reverse and skip_start % 2 != 0))):
count += 1
if not is_linear:
count = 1
if stroke.reverse:
count = 0
if count % 2 != 0:
helper_lines.reverse()
return helper_lines
def _adjust_helper_lines_for_grid(grid, last_stitch):
# check which end of the grid is nearest to the last stitch
# and adjust grid accordingly
last_stitch = Point(last_stitch)
distances = [
Point(grid[0][0]).distance(last_stitch),
Point(grid[0][-1]).distance(last_stitch),
Point(grid[-1][0]).distance(last_stitch),
Point(grid[-1][-1]).distance(last_stitch)
]
nearest = distances.index(min(distances))
if nearest in [1, 3]:
adjusted_grid = []
for line in grid:
adjusted_grid.append(line[::-1])
grid = adjusted_grid
if nearest in [2, 3]:
grid.reverse()
return grid
def _do_grid(stroke, helper_lines, skip_start, skip_end, is_linear):
helper_lines = _adjust_helper_lines_for_grid(stroke, helper_lines, skip_start, skip_end, is_linear)
def _do_grid(stroke, helper_lines, skip_start, skip_end, is_linear, last_stitch):
grid = []
for i, helper in enumerate(helper_lines):
end = len(helper) - skip_end
points = helper[skip_start:end]
if stroke.reverse:
points.reverse()
if len(helper_lines) - skip_start - skip_end % 2 != 0:
if i % 2 != 0 and is_linear and not stroke.flip_copies and stroke.join_style == 0:
points.reverse()
grid.append(points)
grid = _adjust_helper_lines_for_grid(grid, last_stitch)
grid = _get_staggered_stitches(stroke, grid, 0)
return grid
@ -299,14 +315,22 @@ def _get_guided_helper_lines(stroke, outline, max_distance):
def _generate_guided_helper_lines(stroke, outline, max_distance, guide_line):
# helper lines are generated by making copies of the outline along the guide line
line_point_dict = defaultdict(list)
outline = LineString(even_running_stitch(line_string_to_point_list(outline), max_distance, stroke.running_stitch_tolerance))
if not stroke.manual_pattern_placement:
outline = LineString(even_running_stitch(
line_string_to_point_list(outline),
max_distance,
stroke.running_stitch_tolerance
))
center = outline.centroid
center = InkstitchPoint(center.x, center.y)
count = _get_guided_line_count(stroke, guide_line)
if stroke.render_at_rungs:
count = len(guide_line.coords)
else:
count = _get_guided_line_count(stroke, guide_line)
outline_steps = _get_steps(count, exponent=stroke.exponent, flip=stroke.flip_exponent)
outline_steps = _get_steps(count, exponent=stroke.exponent, flip=stroke.flip_exponent)
scale_steps = _get_steps(count, start=stroke.scale_start / 100.0, end=stroke.scale_end / 100.0)
start_point = InkstitchPoint(*(guide_line.coords[0]))
@ -316,7 +340,11 @@ def _generate_guided_helper_lines(stroke, outline, max_distance, guide_line):
for i in range(count):
check_stop_flag()
guide_point = InkstitchPoint.from_shapely_point(guide_line.interpolate(outline_steps[i], normalized=True))
if stroke.render_at_rungs:
# Requires the guide line to be defined as manual stitch
guide_point = InkstitchPoint(*guide_line.coords[i])
else:
guide_point = InkstitchPoint.from_shapely_point(guide_line.interpolate(outline_steps[i], normalized=True))
translation = guide_point - start_point
scaling = scale_steps[i]
if stroke.rotate_ripples and previous_guide_point:
@ -343,11 +371,20 @@ def _get_start_rotation(line):
def _generate_satin_guide_helper_lines(stroke, outline, guide_line):
count = _get_guided_line_count(stroke, guide_line.center_line)
spacing = guide_line.center_line.length / (count - 1)
pairs = guide_line.plot_points_on_rails(spacing)
spacing = guide_line.center_line.length / max(1, count - 1)
if stroke.render_at_rungs:
sections = guide_line.flattened_sections
pairs = []
for (rail0, rail1) in sections:
pairs.append((rail0[-1], rail1[-1]))
pairs = pairs[:-1]
else:
pairs = guide_line.plot_points_on_rails(spacing)
point0 = pairs[0][0]
point1 = pairs[0][1]
start_rotation = atan2(point1.y - point0.y, point1.x - point0.x)
start_scale = (point1 - point0).length()
outline_center = InkstitchPoint.from_shapely_point(outline.centroid)
@ -411,6 +448,8 @@ def _get_steps(num_steps, start=0.0, end=1.0, exponent=1, flip=False):
def _repeat_coords(coords, repeats):
if not coords:
return coords
final_coords = []
for i in range(repeats):
if i % 2 == 1:

Wyświetl plik

@ -118,8 +118,11 @@ inkstitch_attribs = [
'cutwork_needle',
'zigzag_width_mm',
# ripples
'manual_pattern_placement',
'flip_copies',
'line_count',
'min_line_dist_mm',
'render_at_rungs',
'exponent',
'flip_exponent',
'skip_start',