summaryrefslogtreecommitdiff
path: root/lib/stitches/ripple_stitch.py
diff options
context:
space:
mode:
Diffstat (limited to 'lib/stitches/ripple_stitch.py')
-rw-r--r--lib/stitches/ripple_stitch.py330
1 files changed, 210 insertions, 120 deletions
diff --git a/lib/stitches/ripple_stitch.py b/lib/stitches/ripple_stitch.py
index 88d1b8d0..46fc5e07 100644
--- a/lib/stitches/ripple_stitch.py
+++ b/lib/stitches/ripple_stitch.py
@@ -1,12 +1,17 @@
from collections import defaultdict
+from math import atan2
+import numpy as np
+from shapely.affinity import rotate, scale, translate
from shapely.geometry import LineString, Point
-from ..utils.geometry import line_string_to_point_list
from .running_stitch import running_stitch
+from ..elements import SatinColumn
+from ..utils import Point as InkstitchPoint
+from ..utils.geometry import line_string_to_point_list
-def ripple_stitch(lines, target, line_count, points, max_stitch_length, repeats, flip, skip_start, skip_end, render_grid, exponent):
+def ripple_stitch(stroke):
'''
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
@@ -16,151 +21,236 @@ def ripple_stitch(lines, target, line_count, points, max_stitch_length, repeats,
If more sublines are present interpolation will take place between the first two.
'''
- # sort geoms by size
- lines = sorted(lines.geoms, key=lambda linestring: linestring.length, reverse=True)
- outline = lines[0]
+ 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))
+
+ stitches = running_stitch(ripple_points, stroke.running_stitch_length)
+
+ return _repeat_coords(stitches, stroke.repeats)
+
- # ignore skip_start and skip_end if both toghether are greater or equal to line_count
- if skip_start + skip_end >= line_count:
- skip_start = skip_end = 0
+def _do_ripple(stroke, helper_lines, is_linear):
+ points = []
- if is_closed(outline):
- rippled_line = do_circular_ripple(outline, target, line_count, repeats, flip, max_stitch_length, skip_start, skip_end, exponent)
+ 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)
else:
- rippled_line = do_linear_ripple(lines, points, target, line_count - 1, repeats, flip, skip_start, skip_end, render_grid, exponent)
+ outline = LineString(running_stitch(line_string_to_point_list(lines[0]), stroke.grid_size or stroke.running_stitch_length))
+
+ if stroke.is_closed:
+ return False, _get_circular_ripple_helper_lines(stroke, outline)
+ 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
+ rail_points = SatinColumn(stroke.node).plot_points_on_rails(length, 0)
+
+ steps = _get_steps(stroke.line_count, exponent=stroke.exponent, flip=stroke.flip_exponent)
+
+ 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)))
+
+ return helper_lines
+
+
+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.
+ 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):
+ points.append(line[j] * (1 - steps[i]) + line[j + 1] * steps[i])
+ helper_lines[i] = points
+
+ return helper_lines
+
- return running_stitch(line_string_to_point_list(rippled_line), max_stitch_length)
+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)
-def do_circular_ripple(outline, target, line_count, repeats, flip, max_stitch_length, skip_start, skip_end, exponent):
- # for each point generate a line going to the target point
- lines = target_point_lines_normalized_distances(outline, target, flip, max_stitch_length)
+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])
- # create a list of points for each line
- points = get_interpolation_points(lines, line_count, exponent, "circular")
+ for step in steps:
+ helper_lines[i].append(InkstitchPoint.from_shapely_point(line.interpolate(step, normalized=True)))
- # connect the lines to a spiral towards the target
- coords = []
- for i in range(skip_start, line_count - skip_end):
- for j in range(len(lines)):
- coords.append(Point(points[j][i].x, points[j][i].y))
+ return helper_lines
- coords = repeat_coords(coords, repeats)
- return LineString(coords)
+def _do_grid(stroke, helper_lines):
+ for i, helper in enumerate(helper_lines):
+ start = stroke.get_skip_start()
+ end = len(helper) - stroke.get_skip_end()
+ points = helper[start:end]
+ if i % 2 == 0:
+ points.reverse()
+ yield from points
-def do_linear_ripple(lines, points, target, line_count, repeats, flip, skip_start, skip_end, render_grid, exponent):
- if len(lines) == 1:
- helper_lines = target_point_lines(lines[0], target, flip)
+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)
else:
- helper_lines = []
- for start, end in zip(points[0], points[1]):
- if flip:
- helper_lines.append(LineString([end, start]))
- else:
- helper_lines.append(LineString([start, end]))
-
- # get linear points along the lines
- points = get_interpolation_points(helper_lines, line_count, exponent)
-
- # go back and forth along the lines - flip direction of every second line
- coords = []
- for i in range(skip_start, len(points[0]) - skip_end):
- for j in range(len(helper_lines)):
- k = j
- if i % 2 != 0:
- k = len(helper_lines) - j - 1
- coords.append(Point(points[k][i].x, points[k][i].y))
-
- # add helper lines as a grid
- # for now only add this to satin type ripples, otherwise it could become to dense at the target point
- if len(lines) > 1 and render_grid:
- coords.extend(do_grid(helper_lines, line_count - skip_end))
-
- coords = repeat_coords(coords, repeats)
-
- return LineString(coords)
-
-
-def do_grid(lines, num_lines):
- coords = []
- if num_lines % 2 == 0:
- lines = reversed(lines)
- for i, line in enumerate(lines):
- line_coords = list(line.coords)
- if (i % 2 == 0 and num_lines % 2 == 0) or (i % 2 != 0 and num_lines % 2 != 0):
- coords.extend(reversed(line_coords))
+ # 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):
+ # helper lines are generated by making copies of the outline alog the guide line
+ line_point_dict = defaultdict(list)
+ outline = LineString(running_stitch(line_string_to_point_list(outline), max_distance))
+
+ 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
else:
- coords.extend(line_coords)
- return coords
+ rotation = 0
+ transformed_outline = _transform_outline(translation, rotation, scaling, outline, Point(guide_point), stroke.scale_axis)
+ for j, point in enumerate(transformed_outline.coords):
+ line_point_dict[j].append(InkstitchPoint(point[0], point[1]))
-def line_length(line):
- return line.length
+ previous_guide_point = guide_point
+ return _point_dict_to_helper_lines(len(outline.coords), line_point_dict)
-def is_closed(line):
- coords = line.coords
- return Point(*coords[0]).distance(Point(*coords[-1])) < 0.05
+def _get_start_rotation(line):
+ point0 = line.interpolate(0)
+ point1 = line.interpolate(0.1)
-def target_point_lines(outline, target, flip):
- lines = []
- for point in outline.coords:
- if flip:
- lines.append(LineString([point, target]))
+ 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)
+ rail_points = guide_line.plot_points_on_rails(spacing, 0)
+
+ 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
else:
- lines.append(LineString([target, point]))
- return lines
+ 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
-def target_point_lines_normalized_distances(outline, target, flip, max_stitch_length):
+def _point_dict_to_helper_lines(line_count, point_dict):
lines = []
- outline = running_stitch(line_string_to_point_list(outline), max_stitch_length)
- for point in outline:
- if flip:
- lines.append(LineString([target, point]))
- else:
- lines.append(LineString([point, target]))
+ for i in range(line_count):
+ points = point_dict[i]
+ lines.append(points)
return lines
-def get_interpolation_points(lines, line_count, exponent, method="linear"):
- new_points = defaultdict(list)
- count = len(lines) - 1
- for i, line in enumerate(lines):
- steps = get_steps(line, line_count, exponent)
- distance = -1
- points = []
- for j in range(line_count):
- length = line.length * steps[j]
- if method == "circular":
- if distance == -1:
- # the first line makes sure, it is going to be a spiral
- distance = (line.length * steps[j+1]) * (i / count)
- else:
- distance += length - (line.length * steps[j-1])
- else:
- distance = line.length * steps[j]
- points.append(line.interpolate(distance))
- if method == "linear":
- points.append(Point(*line.coords[-1]))
- new_points[i] = points
- return new_points
-
-
-def get_steps(line, total_lines, exponent):
- # get_steps is scribbled from the inkscape interpolate extension
- # (https://gitlab.com/inkscape/extensions/-/blob/master/interp.py)
- steps = [
- ((i + 1) / (total_lines)) ** exponent
- for i in range(total_lines - 1)
- ]
- return [0] + steps + [1]
-
-
-def repeat_coords(coords, repeats):
+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):
final_coords = []
for i in range(repeats):
if i % 2 == 1: