2022-05-24 17:40:30 +00:00
|
|
|
from collections import defaultdict
|
2022-06-10 14:25:30 +00:00
|
|
|
from math import atan2
|
2022-05-24 17:40:30 +00:00
|
|
|
|
2022-06-10 14:25:30 +00:00
|
|
|
import numpy as np
|
|
|
|
from shapely.affinity import rotate, scale, translate
|
2022-05-24 17:40:30 +00:00
|
|
|
from shapely.geometry import LineString, Point
|
|
|
|
|
|
|
|
from .running_stitch import running_stitch
|
2022-06-10 14:25:30 +00:00
|
|
|
from ..elements import SatinColumn
|
|
|
|
from ..utils import Point as InkstitchPoint
|
|
|
|
from ..utils.geometry import line_string_to_point_list
|
2022-05-24 17:40:30 +00:00
|
|
|
|
|
|
|
|
2022-06-10 14:25:30 +00:00
|
|
|
def ripple_stitch(stroke):
|
2022-05-24 17:40:30 +00:00
|
|
|
'''
|
|
|
|
Ripple stitch is allowed to cross itself and doesn't care about an equal distance of lines
|
|
|
|
It is meant to be used with light (not dense) stitching
|
|
|
|
It will ignore holes in a closed shape. Closed shapes will be filled with a spiral
|
|
|
|
Open shapes will be stitched back and forth.
|
|
|
|
If there is only one (open) line or a closed shape the target point will be used.
|
|
|
|
If more sublines are present interpolation will take place between the first two.
|
|
|
|
'''
|
|
|
|
|
2022-06-10 14:25:30 +00:00
|
|
|
is_linear, helper_lines = _get_helper_lines(stroke)
|
|
|
|
ripple_points = _do_ripple(stroke, helper_lines, is_linear)
|
|
|
|
|
|
|
|
if stroke.reverse:
|
|
|
|
ripple_points.reverse()
|
|
|
|
|
|
|
|
if stroke.grid_size != 0:
|
|
|
|
ripple_points.extend(_do_grid(stroke, helper_lines))
|
|
|
|
|
2022-06-22 13:26:37 +00:00
|
|
|
stitches = running_stitch(ripple_points, stroke.running_stitch_length, stroke.running_stitch_tolerance)
|
2022-06-10 14:25:30 +00:00
|
|
|
|
|
|
|
return _repeat_coords(stitches, stroke.repeats)
|
|
|
|
|
2022-05-24 17:40:30 +00:00
|
|
|
|
2022-06-10 14:25:30 +00:00
|
|
|
def _do_ripple(stroke, helper_lines, is_linear):
|
|
|
|
points = []
|
2022-05-24 17:40:30 +00:00
|
|
|
|
2022-06-10 14:25:30 +00:00
|
|
|
for point_num in range(stroke.get_skip_start(), len(helper_lines[0]) - stroke.get_skip_end()):
|
|
|
|
row = []
|
|
|
|
for line_num in range(len(helper_lines)):
|
|
|
|
row.append(helper_lines[line_num][point_num])
|
|
|
|
|
|
|
|
if is_linear and point_num % 2 == 1:
|
|
|
|
# reverse every other row in linear ripple
|
|
|
|
row.reverse()
|
|
|
|
|
|
|
|
points.extend(row)
|
|
|
|
|
|
|
|
return points
|
|
|
|
|
|
|
|
|
|
|
|
def _get_helper_lines(stroke):
|
|
|
|
lines = stroke.as_multi_line_string().geoms
|
|
|
|
if len(lines) > 1:
|
|
|
|
return True, _get_satin_ripple_helper_lines(stroke)
|
2022-05-24 17:40:30 +00:00
|
|
|
else:
|
2022-06-22 13:26:37 +00:00
|
|
|
outline = LineString(running_stitch(line_string_to_point_list(lines[0]),
|
|
|
|
stroke.grid_size or stroke.running_stitch_length,
|
|
|
|
stroke.running_stitch_tolerance))
|
2022-06-10 14:25:30 +00:00
|
|
|
|
|
|
|
if stroke.is_closed:
|
|
|
|
return False, _get_circular_ripple_helper_lines(stroke, outline)
|
2022-06-30 17:22:33 +00:00
|
|
|
elif stroke.join_style == 1:
|
|
|
|
return True, _get_point_style_linear_helper_lines(stroke, outline)
|
2022-06-10 14:25:30 +00:00
|
|
|
else:
|
|
|
|
return True, _get_linear_ripple_helper_lines(stroke, outline)
|
|
|
|
|
|
|
|
|
|
|
|
def _get_satin_ripple_helper_lines(stroke):
|
|
|
|
# if grid_size has a number use this, otherwise use running_stitch_length
|
|
|
|
length = stroke.grid_size or stroke.running_stitch_length
|
|
|
|
|
|
|
|
# use satin column points for satin like build ripple stitches
|
2022-11-17 06:51:33 +00:00
|
|
|
rail_points = SatinColumn(stroke.node).plot_points_on_rails(length)
|
2022-06-10 14:25:30 +00:00
|
|
|
|
2022-06-30 17:22:33 +00:00
|
|
|
steps = _get_steps(stroke.get_line_count(), exponent=stroke.exponent, flip=stroke.flip_exponent)
|
2022-06-10 14:25:30 +00:00
|
|
|
|
|
|
|
helper_lines = []
|
|
|
|
for point0, point1 in zip(*rail_points):
|
|
|
|
helper_lines.append([])
|
|
|
|
helper_line = LineString((point0, point1))
|
|
|
|
for step in steps:
|
|
|
|
helper_lines[-1].append(InkstitchPoint.from_shapely_point(helper_line.interpolate(step, normalized=True)))
|
|
|
|
|
2022-06-30 17:22:33 +00:00
|
|
|
if stroke.join_style == 1:
|
|
|
|
helper_lines = _converge_helper_line_points(helper_lines, True)
|
2022-06-10 14:25:30 +00:00
|
|
|
|
2022-06-30 17:22:33 +00:00
|
|
|
return helper_lines
|
2022-06-10 14:25:30 +00:00
|
|
|
|
|
|
|
|
2022-06-30 17:22:33 +00:00
|
|
|
def _converge_helper_line_points(helper_lines, point_edge=False):
|
2022-06-10 14:25:30 +00:00
|
|
|
num_lines = len(helper_lines)
|
|
|
|
steps = _get_steps(num_lines)
|
|
|
|
for i, line in enumerate(helper_lines):
|
|
|
|
points = []
|
|
|
|
for j in range(len(line) - 1):
|
2022-06-30 17:22:33 +00:00
|
|
|
if point_edge and j % 2 == 1:
|
|
|
|
k = num_lines - 1 - i
|
|
|
|
points.append(line[j] * (1 - steps[k]) + line[j + 1] * steps[k])
|
|
|
|
else:
|
|
|
|
points.append(line[j] * (1 - steps[i]) + line[j + 1] * steps[i])
|
2022-06-10 14:25:30 +00:00
|
|
|
helper_lines[i] = points
|
|
|
|
|
|
|
|
return helper_lines
|
|
|
|
|
2022-05-24 17:40:30 +00:00
|
|
|
|
2022-06-30 17:22:33 +00:00
|
|
|
def _get_circular_ripple_helper_lines(stroke, outline):
|
|
|
|
helper_lines = _get_linear_ripple_helper_lines(stroke, outline)
|
|
|
|
# Now we want to adjust the helper lines to make a spiral.
|
|
|
|
return _converge_helper_line_points(helper_lines)
|
|
|
|
|
|
|
|
|
|
|
|
def _get_point_style_linear_helper_lines(stroke, outline):
|
|
|
|
helper_lines = _get_linear_ripple_helper_lines(stroke, outline)
|
|
|
|
return _converge_helper_line_points(helper_lines, True)
|
|
|
|
|
|
|
|
|
2022-06-10 14:25:30 +00:00
|
|
|
def _get_linear_ripple_helper_lines(stroke, outline):
|
|
|
|
guide_line = stroke.get_guide_line()
|
|
|
|
max_dist = stroke.grid_size or stroke.running_stitch_length
|
|
|
|
|
|
|
|
if guide_line:
|
|
|
|
return _get_guided_helper_lines(stroke, outline, max_dist)
|
|
|
|
else:
|
|
|
|
return _target_point_helper_lines(stroke, outline)
|
2022-05-24 17:40:30 +00:00
|
|
|
|
|
|
|
|
2022-06-10 14:25:30 +00:00
|
|
|
def _target_point_helper_lines(stroke, outline):
|
|
|
|
helper_lines = [[] for i in range(len(outline.coords))]
|
|
|
|
target = stroke.get_ripple_target()
|
|
|
|
steps = _get_steps(stroke.get_line_count(), exponent=stroke.exponent, flip=stroke.flip_exponent)
|
|
|
|
for i, point in enumerate(outline.coords):
|
|
|
|
line = LineString([point, target])
|
2022-05-24 17:40:30 +00:00
|
|
|
|
2022-06-10 14:25:30 +00:00
|
|
|
for step in steps:
|
|
|
|
helper_lines[i].append(InkstitchPoint.from_shapely_point(line.interpolate(step, normalized=True)))
|
2022-05-24 17:40:30 +00:00
|
|
|
|
2022-06-10 14:25:30 +00:00
|
|
|
return helper_lines
|
2022-05-24 17:40:30 +00:00
|
|
|
|
|
|
|
|
2022-06-30 17:22:33 +00:00
|
|
|
def _adjust_helper_lines_for_grid(stroke, helper_lines):
|
|
|
|
num_lines = stroke.line_count - stroke.skip_end
|
|
|
|
if stroke.reverse:
|
|
|
|
helper_lines = [helper_line[::-1] for helper_line in helper_lines]
|
|
|
|
num_lines = stroke.skip_start
|
|
|
|
if (num_lines % 2 != 0 and not stroke.is_closed) or (stroke.is_closed and not stroke.reverse):
|
|
|
|
helper_lines.reverse()
|
|
|
|
|
|
|
|
return helper_lines
|
|
|
|
|
|
|
|
|
2022-06-10 14:25:30 +00:00
|
|
|
def _do_grid(stroke, helper_lines):
|
2022-06-30 17:22:33 +00:00
|
|
|
helper_lines = _adjust_helper_lines_for_grid(stroke, helper_lines)
|
|
|
|
start = stroke.get_skip_start()
|
|
|
|
skip_end = stroke.get_skip_end()
|
|
|
|
if stroke.reverse:
|
|
|
|
start, skip_end = skip_end, start
|
2022-06-10 14:25:30 +00:00
|
|
|
for i, helper in enumerate(helper_lines):
|
2022-06-30 17:22:33 +00:00
|
|
|
end = len(helper) - skip_end
|
2022-06-10 14:25:30 +00:00
|
|
|
points = helper[start:end]
|
|
|
|
if i % 2 == 0:
|
|
|
|
points.reverse()
|
|
|
|
yield from points
|
2022-05-24 17:40:30 +00:00
|
|
|
|
|
|
|
|
2022-06-10 14:25:30 +00:00
|
|
|
def _get_guided_helper_lines(stroke, outline, max_distance):
|
|
|
|
# for each point generate a line going along and pointing to the guide line
|
|
|
|
guide_line = stroke.get_guide_line()
|
|
|
|
if isinstance(guide_line, SatinColumn):
|
|
|
|
# satin type guide line
|
|
|
|
return _generate_satin_guide_helper_lines(stroke, outline, guide_line)
|
2022-05-24 17:40:30 +00:00
|
|
|
else:
|
2022-06-10 14:25:30 +00:00
|
|
|
# simple guide line
|
|
|
|
return _generate_guided_helper_lines(stroke, outline, max_distance, guide_line.geoms[0])
|
|
|
|
|
|
|
|
|
|
|
|
def _generate_guided_helper_lines(stroke, outline, max_distance, guide_line):
|
2022-06-30 17:22:33 +00:00
|
|
|
# helper lines are generated by making copies of the outline along the guide line
|
2022-06-10 14:25:30 +00:00
|
|
|
line_point_dict = defaultdict(list)
|
2022-06-22 13:26:37 +00:00
|
|
|
outline = LineString(running_stitch(line_string_to_point_list(outline), max_distance, stroke.running_stitch_tolerance))
|
2022-06-10 14:25:30 +00:00
|
|
|
|
|
|
|
center = outline.centroid
|
|
|
|
center = InkstitchPoint(center.x, center.y)
|
|
|
|
|
|
|
|
outline_steps = _get_steps(stroke.get_line_count(), exponent=stroke.exponent, flip=stroke.flip_exponent)
|
|
|
|
scale_steps = _get_steps(stroke.get_line_count(), start=stroke.scale_start / 100.0, end=stroke.scale_end / 100.0)
|
|
|
|
|
|
|
|
start_point = InkstitchPoint(*(guide_line.coords[0]))
|
|
|
|
start_rotation = _get_start_rotation(guide_line)
|
|
|
|
|
|
|
|
previous_guide_point = None
|
|
|
|
for i in range(stroke.get_line_count()):
|
|
|
|
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:
|
|
|
|
rotation = atan2(guide_point.y - previous_guide_point.y, guide_point.x - previous_guide_point.x)
|
|
|
|
rotation = rotation - start_rotation
|
2022-05-24 17:40:30 +00:00
|
|
|
else:
|
2022-06-10 14:25:30 +00:00
|
|
|
rotation = 0
|
|
|
|
transformed_outline = _transform_outline(translation, rotation, scaling, outline, Point(guide_point), stroke.scale_axis)
|
2022-05-24 17:40:30 +00:00
|
|
|
|
2022-06-10 14:25:30 +00:00
|
|
|
for j, point in enumerate(transformed_outline.coords):
|
|
|
|
line_point_dict[j].append(InkstitchPoint(point[0], point[1]))
|
2022-05-24 17:40:30 +00:00
|
|
|
|
2022-06-10 14:25:30 +00:00
|
|
|
previous_guide_point = guide_point
|
2022-05-24 17:40:30 +00:00
|
|
|
|
2022-06-10 14:25:30 +00:00
|
|
|
return _point_dict_to_helper_lines(len(outline.coords), line_point_dict)
|
2022-05-24 17:40:30 +00:00
|
|
|
|
|
|
|
|
2022-06-10 14:25:30 +00:00
|
|
|
def _get_start_rotation(line):
|
|
|
|
point0 = line.interpolate(0)
|
|
|
|
point1 = line.interpolate(0.1)
|
2022-05-24 17:40:30 +00:00
|
|
|
|
2022-06-10 14:25:30 +00:00
|
|
|
return atan2(point1.y - point0.y, point1.x - point0.x)
|
|
|
|
|
|
|
|
|
|
|
|
def _generate_satin_guide_helper_lines(stroke, outline, guide_line):
|
|
|
|
spacing = guide_line.center_line.length / (stroke.get_line_count() - 1)
|
2022-11-27 13:56:03 +00:00
|
|
|
rail_points = guide_line.plot_points_on_rails(spacing)
|
2022-06-10 14:25:30 +00:00
|
|
|
|
|
|
|
point0 = rail_points[0][0]
|
|
|
|
point1 = rail_points[1][0]
|
|
|
|
start_rotation = atan2(point1.y - point0.y, point1.x - point0.x)
|
|
|
|
start_scale = (point1 - point0).length()
|
|
|
|
outline_center = InkstitchPoint.from_shapely_point(outline.centroid)
|
|
|
|
|
|
|
|
line_point_dict = defaultdict(list)
|
|
|
|
|
|
|
|
# add scaled and rotated outlines along the satin column guide line
|
|
|
|
for i, (point0, point1) in enumerate(zip(*rail_points)):
|
|
|
|
guide_center = (point0 + point1) / 2
|
|
|
|
translation = guide_center - outline_center
|
|
|
|
if stroke.rotate_ripples:
|
|
|
|
rotation = atan2(point1.y - point0.y, point1.x - point0.x)
|
|
|
|
rotation = rotation - start_rotation
|
2022-05-24 17:40:30 +00:00
|
|
|
else:
|
2022-06-10 14:25:30 +00:00
|
|
|
rotation = 0
|
|
|
|
scaling = (point1 - point0).length() / start_scale
|
|
|
|
|
|
|
|
transformed_outline = _transform_outline(translation, rotation, scaling, outline, Point(guide_center), stroke.scale_axis)
|
|
|
|
|
|
|
|
# outline to helper line points
|
|
|
|
for j, point in enumerate(transformed_outline.coords):
|
|
|
|
line_point_dict[j].append(InkstitchPoint(point[0], point[1]))
|
|
|
|
|
|
|
|
return _point_dict_to_helper_lines(len(outline.coords), line_point_dict)
|
|
|
|
|
|
|
|
|
|
|
|
def _transform_outline(translation, rotation, scaling, outline, origin, scale_axis):
|
|
|
|
# transform
|
|
|
|
transformed_outline = translate(outline, translation.x, translation.y)
|
|
|
|
# rotate
|
|
|
|
if rotation != 0:
|
|
|
|
transformed_outline = rotate(transformed_outline, rotation, use_radians=True, origin=origin)
|
|
|
|
# scale | scale_axis => 0: xy, 1: x, 2: y, 3: none
|
|
|
|
scale_x = scale_y = scaling
|
|
|
|
if scale_axis in [2, 3]:
|
|
|
|
scale_x = 1
|
|
|
|
if scale_axis in [1, 3]:
|
|
|
|
scale_y = 1
|
|
|
|
transformed_outline = scale(transformed_outline, scale_x, scale_y, origin=origin)
|
|
|
|
return transformed_outline
|
2022-05-24 17:40:30 +00:00
|
|
|
|
|
|
|
|
2022-06-10 14:25:30 +00:00
|
|
|
def _point_dict_to_helper_lines(line_count, point_dict):
|
2022-05-24 17:40:30 +00:00
|
|
|
lines = []
|
2022-06-10 14:25:30 +00:00
|
|
|
for i in range(line_count):
|
|
|
|
points = point_dict[i]
|
|
|
|
lines.append(points)
|
2022-05-24 17:40:30 +00:00
|
|
|
return lines
|
|
|
|
|
|
|
|
|
2022-06-10 14:25:30 +00:00
|
|
|
def _get_steps(num_steps, start=0.0, end=1.0, exponent=1, flip=False):
|
|
|
|
steps = np.linspace(start, end, num_steps)
|
|
|
|
steps = steps ** exponent
|
|
|
|
|
|
|
|
if flip:
|
|
|
|
steps = 1.0 - np.flip(steps)
|
|
|
|
|
|
|
|
return list(steps)
|
|
|
|
|
|
|
|
|
|
|
|
def _repeat_coords(coords, repeats):
|
2022-05-24 17:40:30 +00:00
|
|
|
final_coords = []
|
|
|
|
for i in range(repeats):
|
|
|
|
if i % 2 == 1:
|
|
|
|
# reverse every other pass
|
|
|
|
this_coords = coords[::-1]
|
|
|
|
else:
|
|
|
|
this_coords = coords[:]
|
|
|
|
|
|
|
|
final_coords.extend(this_coords)
|
|
|
|
return final_coords
|