summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorKaalleen <36401965+kaalleen@users.noreply.github.com>2023-04-27 20:00:59 +0200
committerGitHub <noreply@github.com>2023-04-27 20:00:59 +0200
commitd458ea563b1adc39000e4c362ca3d2b28f2deefa (patch)
tree9ce440b304705bb297e310ea8c8f5df629bfb524 /lib
parent675898a602e60d69bf3e161d16450338ba0780bf (diff)
Ripple stitch: add density and stagger option (#2206)
Diffstat (limited to 'lib')
-rw-r--r--lib/elements/stroke.py88
-rw-r--r--lib/stitches/guided_fill.py13
-rw-r--r--lib/stitches/ripple_stitch.py146
-rw-r--r--lib/svg/tags.py5
-rw-r--r--lib/update.py10
5 files changed, 175 insertions, 87 deletions
diff --git a/lib/elements/stroke.py b/lib/elements/stroke.py
index 857b6746..b3928135 100644
--- a/lib/elements/stroke.py
+++ b/lib/elements/stroke.py
@@ -20,17 +20,6 @@ from ..utils.param import ParamOption
from .element import EmbroideryElement, param
from .validation import ValidationWarning
-warned_about_legacy_running_stitch = False
-
-
-class IgnoreSkipValues(ValidationWarning):
- name = _("Ignore skip")
- description = _("Skip values are ignored, because there was no line left to embroider.")
- steps_to_solve = [
- _('* Open the params dialog with this object selected'),
- _('* Reduce Skip values or increase number of lines'),
- ]
-
class MultipleGuideLineWarning(ValidationWarning):
name = _("Multiple Guide Lines")
@@ -136,7 +125,7 @@ class Stroke(EmbroideryElement):
unit='mm',
type='float',
select_items=[('stroke_method', 'manual_stitch')],
- sort_index=4)
+ sort_index=5)
def max_stitch_length(self):
max_length = self.get_float_param("max_stitch_length_mm", None)
if not max_length or max_length <= 0:
@@ -151,7 +140,7 @@ class Stroke(EmbroideryElement):
type='float',
default=0.4,
select_items=[('stroke_method', 'zigzag_stitch')],
- sort_index=5)
+ sort_index=6)
@cache
def zigzag_spacing(self):
return max(self.get_float_param("zigzag_spacing_mm", 0.4), 0.01)
@@ -177,15 +166,37 @@ class Stroke(EmbroideryElement):
type='int',
default=10,
select_items=[('stroke_method', 'ripple_stitch')],
- sort_index=5)
+ sort_index=7)
@cache
def line_count(self):
return max(self.get_int_param("line_count", 10), 1)
- def get_line_count(self):
- if self.is_closed or self.join_style == 1:
- return self.line_count + 1
- return self.line_count
+ @property
+ @param('min_line_dist_mm',
+ _('Minimum line distance'),
+ tooltip=_('Overrides the number of lines setting.'),
+ unit='mm',
+ type='float',
+ select_items=[('stroke_method', 'ripple_stitch')],
+ sort_index=8)
+ @cache
+ def min_line_dist(self):
+ min_dist = self.get_float_param("min_line_dist_mm")
+ if min_dist is None:
+ return
+ return max(min_dist, 0.01)
+
+ @property
+ @param('staggers',
+ _('Stagger rows this many times before repeating. For linear ripples only.'),
+ tooltip=_('Length of the cycle by which successive stitch rows are staggered. '
+ 'Fractional values are allowed and can have less visible diagonals than integer values.'),
+ type='int',
+ select_items=[('stroke_method', 'ripple_stitch')],
+ default=1,
+ sort_index=9)
+ def staggers(self):
+ return self.get_float_param("staggers", 1)
@property
@param('skip_start',
@@ -194,7 +205,7 @@ class Stroke(EmbroideryElement):
type='int',
default=0,
select_items=[('stroke_method', 'ripple_stitch')],
- sort_index=6)
+ sort_index=10)
@cache
def skip_start(self):
return abs(self.get_int_param("skip_start", 0))
@@ -206,23 +217,11 @@ class Stroke(EmbroideryElement):
type='int',
default=0,
select_items=[('stroke_method', 'ripple_stitch')],
- sort_index=7)
+ sort_index=11)
@cache
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('exponent',
_('Line distance exponent'),
@@ -230,7 +229,7 @@ class Stroke(EmbroideryElement):
type='float',
default=1,
select_items=[('stroke_method', 'ripple_stitch')],
- sort_index=8)
+ sort_index=12)
@cache
def exponent(self):
return max(self.get_float_param("exponent", 1), 0.1)
@@ -242,7 +241,7 @@ class Stroke(EmbroideryElement):
type='boolean',
default=False,
select_items=[('stroke_method', 'ripple_stitch')],
- sort_index=9)
+ sort_index=13)
@cache
def flip_exponent(self):
return self.get_boolean_param("flip_exponent", False)
@@ -254,23 +253,23 @@ class Stroke(EmbroideryElement):
type='boolean',
default=False,
select_items=[('stroke_method', 'ripple_stitch')],
- sort_index=10)
+ sort_index=14)
@cache
def reverse(self):
return self.get_boolean_param("reverse", False)
@property
- @param('grid_size',
+ @param('grid_size_mm',
_('Grid size'),
tooltip=_('Render as grid. Use with care and watch your stitch density.'),
type='float',
default=0,
unit='mm',
select_items=[('stroke_method', 'ripple_stitch')],
- sort_index=11)
+ sort_index=15)
@cache
def grid_size(self):
- return abs(self.get_float_param("grid_size", 0))
+ return abs(self.get_float_param("grid_size_mm", 0))
@property
@param('scale_axis',
@@ -281,7 +280,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=12)
+ sort_index=16)
def scale_axis(self):
return self.get_int_param('scale_axis', 0)
@@ -293,7 +292,7 @@ class Stroke(EmbroideryElement):
unit='%',
default=100,
select_items=[('stroke_method', 'ripple_stitch')],
- sort_index=13)
+ sort_index=17)
def scale_start(self):
return self.get_float_param('scale_start', 100.0)
@@ -305,7 +304,7 @@ class Stroke(EmbroideryElement):
unit='%',
default=0.0,
select_items=[('stroke_method', 'ripple_stitch')],
- sort_index=14)
+ sort_index=18)
def scale_end(self):
return self.get_float_param('scale_end', 0.0)
@@ -316,7 +315,7 @@ class Stroke(EmbroideryElement):
type='boolean',
default=True,
select_items=[('stroke_method', 'ripple_stitch')],
- sort_index=15)
+ sort_index=19)
@cache
def rotate_ripples(self):
return self.get_boolean_param("rotate_ripples", True)
@@ -329,7 +328,7 @@ class Stroke(EmbroideryElement):
default=0,
options=(_("flat"), _("point")),
select_items=[('stroke_method', 'ripple_stitch')],
- sort_index=16)
+ sort_index=20)
@cache
def join_style(self):
return self.get_int_param('join_style', 0)
@@ -519,9 +518,6 @@ class Stroke(EmbroideryElement):
return coords[int(len(coords)/2)]
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)
diff --git a/lib/stitches/guided_fill.py b/lib/stitches/guided_fill.py
index 4c441b32..b741ac00 100644
--- a/lib/stitches/guided_fill.py
+++ b/lib/stitches/guided_fill.py
@@ -12,10 +12,10 @@ from ..stitch_plan import Stitch
from ..utils.geometry import Point as InkstitchPoint
from ..utils.geometry import (ensure_geometry_collection,
ensure_multi_line_string, reverse_line_string)
+from ..utils.threading import check_stop_flag
from .auto_fill import (auto_fill, build_fill_stitch_graph, build_travel_graph,
collapse_sequential_outline_edges, find_stitch_path,
graph_is_valid, travel)
-from ..utils.threading import check_stop_flag
def guided_fill(shape,
@@ -150,18 +150,23 @@ def take_only_line_strings(thing):
return shgeo.MultiLineString(line_strings)
-def apply_stitches(line, max_stitch_length, num_staggers, row_spacing, row_num):
+def apply_stitches(line, max_stitch_length, num_staggers, row_spacing, row_num, threshold=None):
if num_staggers == 0:
num_staggers = 1 # sanity check to avoid division by zero.
start = ((row_num / num_staggers) % 1) * max_stitch_length
projections = np.arange(start, line.length, max_stitch_length)
points = np.array([line.interpolate(projection).coords[0] for projection in projections])
+
+ if len(points) <= 2:
+ return line
+
stitched_line = shgeo.LineString(points)
# stitched_line may round corners, which will look terrible. This finds the
# corners.
- threshold = row_spacing / 2.0
- simplified_line = line.simplify(row_spacing / 2.0, False)
+ if not threshold:
+ threshold = row_spacing / 2.0
+ simplified_line = line.simplify(threshold, False)
simplified_points = [shgeo.Point(x, y) for x, y in simplified_line.coords]
extra_points = []
diff --git a/lib/stitches/ripple_stitch.py b/lib/stitches/ripple_stitch.py
index f7d2e889..4e1c563e 100644
--- a/lib/stitches/ripple_stitch.py
+++ b/lib/stitches/ripple_stitch.py
@@ -1,15 +1,16 @@
from collections import defaultdict
-from math import atan2
+from math import atan2, ceil
import numpy as np
from shapely.affinity import rotate, scale, translate
from shapely.geometry import LineString, Point
-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
from ..utils.threading import check_stop_flag
+from .guided_fill import apply_stitches
+from .running_stitch import running_stitch
def ripple_stitch(stroke):
@@ -23,34 +24,102 @@ def ripple_stitch(stroke):
'''
is_linear, helper_lines = _get_helper_lines(stroke)
- ripple_points = _do_ripple(stroke, helper_lines, is_linear)
+
+ num_lines = len(helper_lines[0])
+ 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)
+ stitches = _get_stitches(stroke, is_linear, lines, skip_start)
if stroke.reverse:
- ripple_points.reverse()
+ stitches.reverse()
if stroke.grid_size != 0:
- ripple_points.extend(_do_grid(stroke, helper_lines))
-
- stitches = running_stitch(ripple_points, stroke.running_stitch_length, stroke.running_stitch_tolerance)
+ stitches.extend(_do_grid(stroke, helper_lines, skip_start, skip_end))
return _repeat_coords(stitches, stroke.repeats)
-def _do_ripple(stroke, helper_lines, is_linear):
- points = []
+def _get_stitches(stroke, is_linear, lines, skip_start):
+ if is_linear:
+ return _get_staggered_stitches(stroke, lines, skip_start)
+ else:
+ points = [point for line in lines for point in line]
+ return running_stitch(points, stroke.running_stitch_length, stroke.running_stitch_tolerance)
+
+
+def _get_staggered_stitches(stroke, lines, skip_start):
+ stitches = []
+ for i, line in enumerate(lines):
+ stitched_line = []
+ connector = []
+ if i != 0 and stroke.join_style == 0:
+ if i % 2 == 0:
+ last_point = lines[i-1][0]
+ first_point = line[0]
+ else:
+ last_point = lines[i-1][-1]
+ first_point = line[-1]
+ connector = running_stitch([InkstitchPoint(*last_point), InkstitchPoint(*first_point)],
+ stroke.running_stitch_length,
+ stroke.running_stitch_tolerance)
+ points = list(apply_stitches(LineString(line), stroke.running_stitch_length, stroke.staggers, 0.5, i, stroke.running_stitch_tolerance).coords)
+ stitched_line.extend([InkstitchPoint(*point) for point in points])
+ if i % 2 == 1 and stroke.join_style == 0:
+ # reverse every other row in linear ripple
+ stitched_line.reverse()
+ if (stroke.join_style == 1 and ((i % 2 == 1 and skip_start % 2 == 0) or
+ (i % 2 == 0 and skip_start % 2 == 1))):
+ stitched_line.reverse()
+ stitched_line = connector + stitched_line
+ stitches.extend(stitched_line)
+ return stitches
+
+
+def _adjust_skip(stroke, num_lines, skip):
+ if stroke.skip_start + stroke.skip_end >= num_lines:
+ return 0
+ return skip
+
- for point_num in range(stroke.get_skip_start(), len(helper_lines[0]) - stroke.get_skip_end()):
+def _get_ripple_lines(stroke, helper_lines, is_linear, skip_start, skip_end):
+ lines = []
+ for point_num in range(skip_start, len(helper_lines[0]) - skip_end):
row = []
for line_num in range(len(helper_lines)):
row.append(helper_lines[line_num][point_num])
+ lines.append(row)
+ return lines
- if is_linear and point_num % 2 == 1:
- # reverse every other row in linear ripple
- row.reverse()
- points.extend(row)
+def _get_satin_line_count(stroke, pairs):
+ if not stroke.min_line_dist:
+ num_lines = stroke.line_count
+ else:
+ shortest_line_len = 0
+ for point0, point1 in pairs:
+ length = LineString([point0, point1]).length
+ if shortest_line_len == 0 or length < shortest_line_len:
+ shortest_line_len = length
+ num_lines = ceil(shortest_line_len / stroke.min_line_dist)
+ if stroke.join_style == 1:
+ num_lines += 1
+ return num_lines
- return points
+
+def _get_target_line_count(stroke, target, outline):
+ return _get_satin_line_count(stroke, zip(outline, [target]*len(outline)))
+
+
+def _get_guided_line_count(stroke, guide_line):
+ if not stroke.min_line_dist:
+ num_lines = stroke.line_count
+ else:
+ num_lines = ceil(guide_line.length / stroke.min_line_dist)
+ if stroke.is_closed or stroke.join_style == 1:
+ num_lines += 1
+ return num_lines
def _get_helper_lines(stroke):
@@ -76,8 +145,9 @@ def _get_satin_ripple_helper_lines(stroke):
# use satin column points for satin like build ripple stitches
rail_pairs = SatinColumn(stroke.node).plot_points_on_rails(length)
+ count = _get_satin_line_count(stroke, rail_pairs)
- steps = _get_steps(stroke.get_line_count(), exponent=stroke.exponent, flip=stroke.flip_exponent)
+ steps = _get_steps(count, exponent=stroke.exponent, flip=stroke.flip_exponent)
helper_lines = []
for point0, point1 in rail_pairs:
@@ -136,7 +206,8 @@ def _get_linear_ripple_helper_lines(stroke, outline):
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)
+ count = _get_target_line_count(stroke, target, outline.coords)
+ steps = _get_steps(count, exponent=stroke.exponent, flip=stroke.flip_exponent)
for i, point in enumerate(outline.coords):
check_stop_flag()
@@ -148,28 +219,30 @@ def _target_point_helper_lines(stroke, outline):
return helper_lines
-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()
+def _adjust_helper_lines_for_grid(stroke, helper_lines, skip_start, skip_end):
+ 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 skip_end % 2 != 0) or
+ (not stroke.reverse and skip_start % 2 != 0))):
+ count += 1
+ if count % 2 != 0:
+ helper_lines.reverse()
return helper_lines
-def _do_grid(stroke, helper_lines):
- 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
+def _do_grid(stroke, helper_lines, skip_start, skip_end):
+ helper_lines = _adjust_helper_lines_for_grid(stroke, helper_lines, skip_start, skip_end)
for i, helper in enumerate(helper_lines):
end = len(helper) - skip_end
- points = helper[start:end]
+ points = helper[skip_start:end]
if i % 2 == 0:
points.reverse()
+ if stroke.reverse:
+ points.reverse()
yield from points
@@ -192,14 +265,16 @@ def _generate_guided_helper_lines(stroke, outline, max_distance, guide_line):
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)
+ count = _get_guided_line_count(stroke, guide_line)
+
+ 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]))
start_rotation = _get_start_rotation(guide_line)
previous_guide_point = None
- for i in range(stroke.get_line_count()):
+ for i in range(count):
check_stop_flag()
guide_point = InkstitchPoint.from_shapely_point(guide_line.interpolate(outline_steps[i], normalized=True))
@@ -228,7 +303,8 @@ def _get_start_rotation(line):
def _generate_satin_guide_helper_lines(stroke, outline, guide_line):
- spacing = guide_line.center_line.length / (stroke.get_line_count() - 1)
+ 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)
point0 = pairs[0][0]
diff --git a/lib/svg/tags.py b/lib/svg/tags.py
index ba8bf558..f114d159 100644
--- a/lib/svg/tags.py
+++ b/lib/svg/tags.py
@@ -109,6 +109,7 @@ inkstitch_attribs = [
'cutwork_needle',
# ripples
'line_count',
+ 'min_line_dist_mm',
'exponent',
'flip_exponent',
'skip_start',
@@ -117,7 +118,7 @@ inkstitch_attribs = [
'scale_start',
'scale_end',
'rotate_ripples',
- 'grid_size',
+ 'grid_size_mm',
# satin column
'satin_column',
'satin_method',
@@ -157,6 +158,8 @@ inkstitch_attribs = [
'stop_after',
'random_seed',
'manual_stitch',
+ # legacy
+ 'grid_size'
]
for attrib in inkstitch_attribs:
INKSTITCH_ATTRIBS[attrib] = inkex.addNS(attrib, 'inkstitch')
diff --git a/lib/update.py b/lib/update.py
index 5f458d23..f8e6740c 100644
--- a/lib/update.py
+++ b/lib/update.py
@@ -1,8 +1,9 @@
from inkex import errormsg
-from .i18n import _
from .elements import EmbroideryElement
+from .i18n import _
from .metadata import InkStitchMetadata
+from .svg import PIXELS_PER_MM
from .svg.tags import INKSTITCH_ATTRIBS
INKSTITCH_SVG_VERSION = 1
@@ -115,6 +116,13 @@ def _update_to_one(element): # noqa: C901
element.get_param('satin_column', False) is False and
not element.node.style('stroke-dasharray')):
element.set_param('stroke_method', 'zigzag_stitch')
+ # grid_size was supposed to be mm, but it was in pixels
+ grid_size = element.get_float_param('grid_size', None)
+ if grid_size:
+ size = grid_size / PIXELS_PER_MM
+ size = "{:.2f}".format(size)
+ element.set_param('grid_size_mm', size)
+ element.remove_param('grid_size')
if element.get_boolean_param('satin_column', False):
# reverse_rails defaults to Automatic, but we should never reverse an