summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorKaalleen <36401965+kaalleen@users.noreply.github.com>2022-06-10 16:25:30 +0200
committerGitHub <noreply@github.com>2022-06-10 16:25:30 +0200
commit2fde596272b339ebb9b63ceebd66c5e7a0c641f2 (patch)
tree8beb5a62880fc66026551fca7fe4676de456029a /lib
parent68deec88a8a8c4e469191d8c00641a1077c2508a (diff)
Guided ripple stitch (#1675)
Co-authored-by: @lexelby
Diffstat (limited to 'lib')
-rw-r--r--lib/elements/element.py4
-rw-r--r--lib/elements/stroke.py212
-rw-r--r--lib/lettering/font.py2
-rw-r--r--lib/marker.py14
-rw-r--r--lib/stitches/ripple_stitch.py330
-rw-r--r--lib/stitches/running_stitch.py6
-rw-r--r--lib/svg/tags.py8
-rw-r--r--lib/utils/geometry.py9
8 files changed, 400 insertions, 185 deletions
diff --git a/lib/elements/element.py b/lib/elements/element.py
index 3648760b..75d22580 100644
--- a/lib/elements/element.py
+++ b/lib/elements/element.py
@@ -206,7 +206,7 @@ class EmbroideryElement(object):
# L10N options to allow lock stitch before and after objects
options=[_("Both"), _("Before"), _("After"), _("Neither")],
default=0,
- sort_index=10)
+ sort_index=50)
@cache
def ties(self):
return self.get_int_param("ties", 0)
@@ -218,7 +218,7 @@ class EmbroideryElement(object):
'even if the distance to the next object is shorter than defined by the collapse length value in the Ink/Stitch preferences.'),
type='boolean',
default=False,
- sort_index=10)
+ sort_index=51)
@cache
def force_lock_stitches(self):
return self.get_boolean_param('force_lock_stitches', False)
diff --git a/lib/elements/stroke.py b/lib/elements/stroke.py
index 40741caa..6edd2c9e 100644
--- a/lib/elements/stroke.py
+++ b/lib/elements/stroke.py
@@ -10,13 +10,13 @@ import shapely.geometry
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.ripple_stitch import ripple_stitch
from ..svg import get_node_transform, parse_length_with_units
from ..utils import Point, cache
from .element import EmbroideryElement, param
-from .satin_column import SatinColumn
from .validation import ValidationWarning
warned_about_legacy_running_stitch = False
@@ -26,8 +26,16 @@ class IgnoreSkipValues(ValidationWarning):
name = _("Ignore skip")
description = _("Skip values are ignored, because there was no line left to embroider.")
steps_to_solve = [
- _('* Reduce values of Skip first and last lines or'),
- _('* Increase number of lines accordinly in the params dialog.'),
+ _('* Open the params dialog with this object selected'),
+ _('* Reduce Skip values or increase number of lines'),
+ ]
+
+
+class MultipleGuideLineWarning(ValidationWarning):
+ name = _("Multiple Guide Lines")
+ description = _("This object has multiple guide lines, but only the first one will be used.")
+ steps_to_solve = [
+ _("* Remove all guide lines, except for one.")
]
@@ -85,7 +93,7 @@ class Stroke(EmbroideryElement):
_('Bean stitch number of repeats'),
tooltip=_('Backtrack each stitch this many times. '
'A value of 1 would triple each stitch (forward, back, forward). '
- 'A value of 2 would quintuple each stitch, etc. Only applies to running stitch.'),
+ 'A value of 2 would quintuple each stitch, etc.'),
type='int',
default=0,
sort_index=3)
@@ -104,6 +112,19 @@ class Stroke(EmbroideryElement):
return max(self.get_float_param("running_stitch_length_mm", 1.5), 0.01)
@property
+ @param('zigzag_spacing_mm',
+ _('Zig-zag spacing (peak-to-peak)'),
+ tooltip=_('Length of stitches in zig-zag mode.'),
+ unit='mm',
+ type='float',
+ default=0.4,
+ select_items=[('stroke_method', 0)],
+ sort_index=5)
+ @cache
+ def zigzag_spacing(self):
+ return max(self.get_float_param("zigzag_spacing_mm", 0.4), 0.01)
+
+ @property
@param('line_count',
_('Number of lines'),
tooltip=_('Number of lines from start to finish'),
@@ -115,6 +136,11 @@ class Stroke(EmbroideryElement):
def line_count(self):
return max(self.get_int_param("line_count", 10), 1)
+ def get_line_count(self):
+ if self.is_closed:
+ return self.line_count + 1
+ return self.line_count
+
@property
@param('skip_start',
_('Skip first lines'),
@@ -139,54 +165,124 @@ class Stroke(EmbroideryElement):
def skip_end(self):
return abs(self.get_int_param("skip_end", 0))
+ def _adjust_skip(self, skip):
+ if self.skip_start + self.skip_end >= self.line_count:
+ return 0
+ else:
+ return skip
+
+ def get_skip_start(self):
+ return self._adjust_skip(self.skip_start)
+
+ def get_skip_end(self):
+ return self._adjust_skip(self.skip_end)
+
@property
- @param('flip',
- _('Flip'),
- tooltip=_('Flip outer to inner'),
+ @param('exponent',
+ _('Line distance exponent'),
+ tooltip=_('Increase density towards one side.'),
+ type='float',
+ default=1,
+ select_items=[('stroke_method', 1)],
+ sort_index=8)
+ @cache
+ def exponent(self):
+ return max(self.get_float_param("exponent", 1), 0.1)
+
+ @property
+ @param('flip_exponent',
+ _('Flip exponent'),
+ tooltip=_('Reverse exponent effect.'),
type='boolean',
default=False,
select_items=[('stroke_method', 1)],
- sort_index=8)
+ sort_index=9)
@cache
- def flip(self):
- return self.get_boolean_param("flip", False)
+ def flip_exponent(self):
+ return self.get_boolean_param("flip_exponent", False)
@property
- @param('render_grid',
- _('Grid distance'),
- tooltip=_('Render as grid. Works only with satin type ripple stitches.'),
+ @param('reverse',
+ _('Reverse'),
+ tooltip=_('Flip start and end point'),
+ type='boolean',
+ default=False,
+ select_items=[('stroke_method', 1)],
+ sort_index=10)
+ @cache
+ def reverse(self):
+ return self.get_boolean_param("reverse", False)
+
+ @property
+ @param('grid_size',
+ _('Grid size'),
+ tooltip=_('Render as grid. Use with care and watch your stitch density.'),
type='float',
default=0,
+ unit='mm',
select_items=[('stroke_method', 1)],
- sort_index=8)
+ sort_index=11)
@cache
- def render_grid(self):
- return abs(self.get_float_param("render_grid", 0))
+ def grid_size(self):
+ return abs(self.get_float_param("grid_size", 0))
@property
- @param('exponent',
- _('Line distance exponent'),
- tooltip=_('Increse density towards one side.'),
+ @param('scale_axis',
+ _('Scale axis'),
+ tooltip=_('Scale axis for satin guided ripple stitches.'),
+ type='dropdown',
+ default=0,
+ # 0: xy, 1: x, 2: y, 3: none
+ options=[_("X Y"), _("X"), _("Y"), _("None")],
+ select_items=[('stroke_method', 1)],
+ sort_index=12)
+ def scale_axis(self):
+ return self.get_int_param('scale_axis', 0)
+
+ @property
+ @param('scale_start',
+ _('Starting scale'),
+ tooltip=_('How big the first copy of the line should be, in percent.') + " " + _('Used only for ripple stitch with a guide line.'),
type='float',
- default=1,
+ default=100,
select_items=[('stroke_method', 1)],
- sort_index=9)
- @cache
- def exponent(self):
- return max(self.get_float_param("exponent", 1), 0.1)
+ sort_index=13)
+ def scale_start(self):
+ return self.get_float_param('scale_start', 100.0)
@property
- @param('zigzag_spacing_mm',
- _('Zig-zag spacing (peak-to-peak)'),
- tooltip=_('Length of stitches in zig-zag mode.'),
- unit='mm',
+ @param('scale_end',
+ _('Ending scale'),
+ tooltip=_('How big the last copy of the line should be, in percent.') + " " + _('Used only for ripple stitch with a guide line.'),
type='float',
- default=0.4,
- select_items=[('stroke_method', 0)],
- sort_index=5)
+ default=0.0,
+ select_items=[('stroke_method', 1)],
+ sort_index=14)
+ def scale_end(self):
+ return self.get_float_param('scale_end', 0.0)
+
+ @property
+ @param('rotate_ripples',
+ _('Rotate'),
+ tooltip=_('Rotate satin guided ripple stitches'),
+ type='boolean',
+ default=True,
+ select_items=[('stroke_method', 1)],
+ sort_index=15)
@cache
- def zigzag_spacing(self):
- return max(self.get_float_param("zigzag_spacing_mm", 0.4), 0.01)
+ def rotate_ripples(self):
+ return self.get_boolean_param("rotate_ripples", True)
+
+ @property
+ @cache
+ def is_closed(self):
+ # returns true if the outline of a single line stroke is a closed shape
+ # (with a small tolerance)
+ lines = self.as_multi_line_string().geoms
+ if len(lines) == 1:
+ coords = lines[0].coords
+ return Point(*coords[0]).distance(Point(*coords[-1])) < 0.05
+ return False
@property
def paths(self):
@@ -306,6 +402,12 @@ class Stroke(EmbroideryElement):
return StitchGroup(self.color, stitches)
+ def ripple_stitch(self):
+ return StitchGroup(
+ color=self.color,
+ tags=["ripple_stitch"],
+ stitches=ripple_stitch(self))
+
def do_bean_repeats(self, stitches):
return bean_stitch(stitches, self.bean_stitch_repeats)
@@ -314,29 +416,7 @@ class Stroke(EmbroideryElement):
# ripple stitch
if self.stroke_method == 1:
- lines = self.as_multi_line_string()
- points = []
- if len(lines.geoms) > 1:
- # if render_grid has a number use this, otherwise use running_stitch_length
- length = self.render_grid or self.running_stitch_length
- # use satin column points for satin like build ripple stitches
- points = SatinColumn(self.node).plot_points_on_rails(length, 0)
- point_target = self.get_ripple_target()
- patch = StitchGroup(
- color=self.color,
- tags=["ripple_stitch"],
- stitches=ripple_stitch(
- self.as_multi_line_string(),
- point_target,
- self.line_count,
- points,
- self.running_stitch_length,
- self.repeats,
- self.flip,
- self.skip_start,
- self.skip_end,
- self.render_grid,
- self.exponent))
+ patch = self.ripple_stitch()
if patch:
if self.bean_stitch_repeats > 0:
patch.stitches = self.do_bean_repeats(patch.stitches)
@@ -361,6 +441,26 @@ class Stroke(EmbroideryElement):
return patches
+ @cache
+ def get_guide_line(self):
+ guide_lines = get_marker_elements(self.node, "guide-line", False, True, True)
+ # No or empty guide line
+ # if there is a satin guide line, it will also be in stroke, so no need to check for satin here
+ if not guide_lines or not guide_lines['stroke']:
+ return None
+
+ # use the satin guide line if there is one, else use stroke
+ # ignore multiple guide lines
+ if len(guide_lines['satin']) >= 1:
+ return guide_lines['satin'][0]
+ return guide_lines['stroke'][0]
+
def validation_warnings(self):
if self.stroke_method == 1 and self.skip_start + self.skip_end >= self.line_count:
yield IgnoreSkipValues(self.shape.centroid)
+
+ # guided fill warnings
+ if self.stroke_method == 1:
+ guide_lines = get_marker_elements(self.node, "guide-line", False, True, True)
+ if sum(len(x) for x in guide_lines.values()) > 1:
+ yield MultipleGuideLineWarning(self.shape.centroid)
diff --git a/lib/lettering/font.py b/lib/lettering/font.py
index 24ca86bf..3e601472 100644
--- a/lib/lettering/font.py
+++ b/lib/lettering/font.py
@@ -352,6 +352,8 @@ class Font(object):
satin operation. Any nested svg:g elements will be removed.
"""
+ # TODO: trim option for non-auto-route
+
elements = nodes_to_elements(group.iterdescendants(SVG_PATH_TAG))
if elements:
diff --git a/lib/marker.py b/lib/marker.py
index 56a43c3b..17d4bebd 100644
--- a/lib/marker.py
+++ b/lib/marker.py
@@ -6,9 +6,10 @@
from copy import deepcopy
from os import path
-import inkex
from shapely import geometry as shgeo
+import inkex
+
from .svg.tags import EMBROIDERABLE_TAGS
from .utils import cache, get_bundled_dir
@@ -39,12 +40,14 @@ def set_marker(node, position, marker):
node.set('style', ";".join(style))
-def get_marker_elements(node, marker, get_fills=True, get_strokes=True):
+def get_marker_elements(node, marker, get_fills=True, get_strokes=True, get_satins=False):
from .elements import EmbroideryElement
+ from .elements.satin_column import SatinColumn
from .elements.stroke import Stroke
fills = []
strokes = []
+ satins = []
xpath = "./parent::svg:g/*[contains(@style, 'marker-start:url(#inkstitch-%s-marker)')]" % marker
markers = node.xpath(xpath, namespaces=inkex.NSS)
for marker in markers:
@@ -66,7 +69,12 @@ def get_marker_elements(node, marker, get_fills=True, get_strokes=True):
line_strings = [shgeo.LineString(path) for path in stroke]
strokes.append(shgeo.MultiLineString(line_strings))
- return {'fill': fills, 'stroke': strokes}
+ if get_satins and stroke is not None:
+ satin = SatinColumn(marker)
+ if len(satin.rails) == 2:
+ satins.append(satin)
+
+ return {'fill': fills, 'stroke': strokes, 'satin': satins}
def has_marker(node, marker=list()):
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:
diff --git a/lib/stitches/running_stitch.py b/lib/stitches/running_stitch.py
index cb8acf68..98d080ba 100644
--- a/lib/stitches/running_stitch.py
+++ b/lib/stitches/running_stitch.py
@@ -41,7 +41,8 @@ def running_stitch(points, stitch_length):
# consider sections of the original path, each one starting and ending
# with an important point
section = points[start:end + 1]
- output.append(section[0])
+ if not output or output[-1] != section[0]:
+ output.append(section[0])
# Now split each section up evenly into stitches, each with a length no
# greater than the specified stitch_length.
@@ -70,7 +71,8 @@ def running_stitch(points, stitch_length):
distance -= segment_length
segment_start = segment_end
- output.append(points[-1])
+ if points[-1] != output[-1]:
+ output.append(points[-1])
return output
diff --git a/lib/svg/tags.py b/lib/svg/tags.py
index 4ff23249..f0dc69bc 100644
--- a/lib/svg/tags.py
+++ b/lib/svg/tags.py
@@ -66,8 +66,14 @@ inkstitch_attribs = [
'line_count',
'skip_start',
'skip_end',
- 'render_grid',
+ 'grid_size',
+ 'reverse',
'exponent',
+ 'flip_exponent',
+ 'scale_axis',
+ 'scale_start',
+ 'scale_end',
+ 'rotate_ripples',
'expand_mm',
'fill_underlay',
'fill_underlay_angle',
diff --git a/lib/utils/geometry.py b/lib/utils/geometry.py
index 86205f02..8d29ddb0 100644
--- a/lib/utils/geometry.py
+++ b/lib/utils/geometry.py
@@ -125,6 +125,10 @@ class Point:
self.x = x
self.y = y
+ @classmethod
+ def from_shapely_point(cls, point):
+ return cls(point.x, point.y)
+
def __json__(self):
return vars(self)
@@ -155,12 +159,15 @@ class Point:
else:
raise ValueError("cannot multiply %s by %s" % (type(self), type(other)))
- def __div__(self, other):
+ def __truediv__(self, other):
if isinstance(other, (int, float)):
return self * (1.0 / other)
else:
raise ValueError("cannot divide %s by %s" % (type(self), type(other)))
+ def __eq__(self, other):
+ return self.x == other.x and self.y == other.y
+
def __repr__(self):
return "%s(%s,%s)" % (type(self), self.x, self.y)