summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/elements/fill_stitch.py50
-rw-r--r--lib/elements/stroke.py17
-rw-r--r--lib/stitch_plan/stitch.py35
-rw-r--r--lib/stitches/auto_fill.py12
-rw-r--r--lib/stitches/fill.py6
-rw-r--r--lib/stitches/guided_fill.py112
-rw-r--r--lib/stitches/ripple_stitch.py50
-rw-r--r--lib/svg/tags.py21
-rw-r--r--lib/utils/geometry.py4
9 files changed, 214 insertions, 93 deletions
diff --git a/lib/elements/fill_stitch.py b/lib/elements/fill_stitch.py
index 157169ab..a0ab0d33 100644
--- a/lib/elements/fill_stitch.py
+++ b/lib/elements/fill_stitch.py
@@ -266,6 +266,10 @@ class FillStitch(EmbroideryElement):
valid_shape = make_valid(shape)
logger.setLevel(level)
+
+ if isinstance(valid_shape, shgeo.Polygon):
+ return shgeo.MultiPolygon([valid_shape])
+
polygons = []
for polygon in valid_shape.geoms:
if isinstance(polygon, shgeo.Polygon):
@@ -499,15 +503,18 @@ class FillStitch(EmbroideryElement):
return self.get_boolean_param('underlay_underpath', True)
def shrink_or_grow_shape(self, shape, amount, validate=False):
+ new_shape = shape
if amount:
- shape = shape.buffer(amount)
+ new_shape = shape.buffer(amount)
# changing the size can empty the shape
# in this case we want to use the original shape rather than returning an error
- if shape.is_empty and not validate:
- return shape
- if not isinstance(shape, shgeo.MultiPolygon):
- shape = shgeo.MultiPolygon([shape])
- return shape
+ if (new_shape.is_empty and not validate):
+ new_shape = shape
+
+ if not isinstance(new_shape, shgeo.MultiPolygon):
+ new_shape = shgeo.MultiPolygon([new_shape])
+
+ return new_shape
def underlay_shape(self, shape):
return self.shrink_or_grow_shape(shape, -self.fill_underlay_inset)
@@ -532,26 +539,31 @@ class FillStitch(EmbroideryElement):
else:
return None
- def to_stitch_groups(self, last_patch):
+ def to_stitch_groups(self, last_patch): # noqa: C901
# backwards compatibility: legacy_fill used to be inkstitch:auto_fill == False
if not self.auto_fill or self.fill_method == 3:
return self.do_legacy_fill()
else:
stitch_groups = []
- start = self.get_starting_point(last_patch)
end = self.get_ending_point()
for shape in self.shape.geoms:
+ start = self.get_starting_point(last_patch)
try:
if self.fill_underlay:
- underlay_stitch_groups, start = self.do_underlay(shape, start)
- stitch_groups.extend(underlay_stitch_groups)
- if self.fill_method == 0:
- stitch_groups.extend(self.do_auto_fill(shape, last_patch, start, end))
- if self.fill_method == 1:
- stitch_groups.extend(self.do_contour_fill(self.fill_shape(shape), last_patch, start))
- elif self.fill_method == 2:
- stitch_groups.extend(self.do_guided_fill(shape, last_patch, start, end))
+ underlay_shapes = self.underlay_shape(shape)
+ for underlay_shape in underlay_shapes.geoms:
+ underlay_stitch_groups, start = self.do_underlay(underlay_shape, start)
+ stitch_groups.extend(underlay_stitch_groups)
+
+ fill_shapes = self.fill_shape(shape)
+ for fill_shape in fill_shapes.geoms:
+ if self.fill_method == 0:
+ stitch_groups.extend(self.do_auto_fill(fill_shape, last_patch, start, end))
+ if self.fill_method == 1:
+ stitch_groups.extend(self.do_contour_fill(fill_shape, last_patch, start))
+ elif self.fill_method == 2:
+ stitch_groups.extend(self.do_guided_fill(fill_shape, last_patch, start, end))
except Exception:
self.fatal_fill_error()
last_patch = stitch_groups[-1]
@@ -576,7 +588,7 @@ class FillStitch(EmbroideryElement):
color=self.color,
tags=("auto_fill", "auto_fill_underlay"),
stitches=auto_fill(
- self.underlay_shape(shape),
+ shape,
self.fill_underlay_angle[i],
self.fill_underlay_row_spacing,
self.fill_underlay_row_spacing,
@@ -597,7 +609,7 @@ class FillStitch(EmbroideryElement):
color=self.color,
tags=("auto_fill", "auto_fill_top"),
stitches=auto_fill(
- self.fill_shape(shape),
+ shape,
self.angle,
self.row_spacing,
self.end_row_spacing,
@@ -663,7 +675,7 @@ class FillStitch(EmbroideryElement):
color=self.color,
tags=("guided_fill", "auto_fill_top"),
stitches=guided_fill(
- self.fill_shape(shape),
+ shape,
guide_line.geoms[0],
self.angle,
self.row_spacing,
diff --git a/lib/elements/stroke.py b/lib/elements/stroke.py
index bf5e1d35..ce973c0e 100644
--- a/lib/elements/stroke.py
+++ b/lib/elements/stroke.py
@@ -150,7 +150,7 @@ class Stroke(EmbroideryElement):
return max(self.get_int_param("line_count", 10), 1)
def get_line_count(self):
- if self.is_closed:
+ if self.is_closed or self.join_style == 1:
return self.line_count + 1
return self.line_count
@@ -246,7 +246,7 @@ class Stroke(EmbroideryElement):
type='dropdown',
default=0,
# 0: xy, 1: x, 2: y, 3: none
- options=[_("X Y"), _("X"), _("Y"), _("None")],
+ options=["X Y", "X", "Y", _("None")],
select_items=[('stroke_method', 1)],
sort_index=12)
def scale_axis(self):
@@ -287,6 +287,19 @@ class Stroke(EmbroideryElement):
return self.get_boolean_param("rotate_ripples", True)
@property
+ @param('join_style',
+ _('Join style'),
+ tooltip=_('Join style for non circular ripples.'),
+ type='dropdown',
+ default=0,
+ options=(_("flat"), _("point")),
+ select_items=[('stroke_method', 1)],
+ sort_index=16)
+ @cache
+ def join_style(self):
+ return self.get_int_param('join_style', 0)
+
+ @property
@cache
def is_closed(self):
# returns true if the outline of a single line stroke is a closed shape
diff --git a/lib/stitch_plan/stitch.py b/lib/stitch_plan/stitch.py
index a4c50b60..0d46b85d 100644
--- a/lib/stitch_plan/stitch.py
+++ b/lib/stitch_plan/stitch.py
@@ -4,7 +4,6 @@
# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details.
from ..utils.geometry import Point
-from copy import deepcopy
class Stitch(Point):
@@ -12,10 +11,14 @@ class Stitch(Point):
def __init__(self, x, y=None, color=None, jump=False, stop=False, trim=False, color_change=False,
tie_modus=0, force_lock_stitches=False, no_ties=False, tags=None):
+
+ base_stitch = None
if isinstance(x, Stitch):
# Allow creating a Stitch from another Stitch. Attributes passed as
# arguments will override any existing attributes.
- vars(self).update(deepcopy(vars(x)))
+ base_stitch = x
+ self.x = base_stitch.x
+ self.y = base_stitch.y
elif isinstance(x, Point):
# Allow creating a Stitch from a Point
point = x
@@ -24,17 +27,19 @@ class Stitch(Point):
else:
Point.__init__(self, x, y)
- self.color = color
- self.jump = jump
- self.trim = trim
- self.stop = stop
- self.color_change = color_change
- self.force_lock_stitches = force_lock_stitches
- self.tie_modus = tie_modus
- self.no_ties = no_ties
- self.tags = set()
+ self._set('color', color, base_stitch)
+ self._set('jump', jump, base_stitch)
+ self._set('trim', trim, base_stitch)
+ self._set('stop', stop, base_stitch)
+ self._set('color_change', color_change, base_stitch)
+ self._set('force_lock_stitches', force_lock_stitches, base_stitch)
+ self._set('tie_modus', tie_modus, base_stitch)
+ self._set('no_ties', no_ties, base_stitch)
+ self.tags = set()
self.add_tags(tags or [])
+ if base_stitch is not None:
+ self.add_tags(base_stitch.tags)
def __repr__(self):
return "Stitch(%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)" % (self.x,
@@ -48,6 +53,14 @@ class Stitch(Point):
"NO TIES" if self.no_ties else " ",
"COLOR CHANGE" if self.color_change else " ")
+ def _set(self, attribute, value, base_stitch):
+ # Set an attribute. If the caller passed a Stitch object, use its value, unless
+ # they overrode it with arguments.
+ if base_stitch is not None:
+ setattr(self, attribute, getattr(base_stitch, attribute))
+ if value or base_stitch is None:
+ setattr(self, attribute, value)
+
def add_tags(self, tags):
for tag in tags:
self.add_tag(tag)
diff --git a/lib/stitches/auto_fill.py b/lib/stitches/auto_fill.py
index ac1e477b..3ff5a24f 100644
--- a/lib/stitches/auto_fill.py
+++ b/lib/stitches/auto_fill.py
@@ -59,14 +59,14 @@ def auto_fill(shape,
starting_point,
ending_point=None,
underpath=True):
- try:
- rows = intersect_region_with_grating(shape, angle, row_spacing, end_row_spacing)
- segments = [segment for row in rows for segment in row]
- fill_stitch_graph = build_fill_stitch_graph(shape, segments, starting_point, ending_point)
- except ValueError:
- # Small shapes will cause the graph to fail - min() arg is an empty sequence through insert node
+ rows = intersect_region_with_grating(shape, angle, row_spacing, end_row_spacing)
+ if not rows:
+ # Small shapes may not intersect with the grating at all.
return fallback(shape, running_stitch_length, running_stitch_tolerance)
+ segments = [segment for row in rows for segment in row]
+ fill_stitch_graph = build_fill_stitch_graph(shape, segments, starting_point, ending_point)
+
if not graph_is_valid(fill_stitch_graph, shape, max_stitch_length):
return fallback(shape, running_stitch_length, running_stitch_tolerance)
diff --git a/lib/stitches/fill.py b/lib/stitches/fill.py
index 46352d4f..7c07b5c2 100644
--- a/lib/stitches/fill.py
+++ b/lib/stitches/fill.py
@@ -132,7 +132,7 @@ def intersect_region_with_grating(shape, angle, row_spacing, end_row_spacing=Non
start -= (start + normal * center) % row_spacing
current_row_y = start
-
+ rows = []
while current_row_y < end:
p0 = center + normal * current_row_y + direction * half_length
p1 = center + normal * current_row_y - direction * half_length
@@ -157,13 +157,15 @@ def intersect_region_with_grating(shape, angle, row_spacing, end_row_spacing=Non
runs.reverse()
runs = [tuple(reversed(run)) for run in runs]
- yield runs
+ rows.append(runs)
if end_row_spacing:
current_row_y += row_spacing + (end_row_spacing - row_spacing) * ((current_row_y - start) / height)
else:
current_row_y += row_spacing
+ return rows
+
def section_to_stitches(group_of_segments, angle, row_spacing, max_stitch_length, staggers, skip_last):
stitches = []
diff --git a/lib/stitches/guided_fill.py b/lib/stitches/guided_fill.py
index 7eb49e86..05de14cd 100644
--- a/lib/stitches/guided_fill.py
+++ b/lib/stitches/guided_fill.py
@@ -1,15 +1,20 @@
+from math import atan2, copysign
+from random import random
+
import numpy as np
+import shapely.prepared
from shapely import geometry as shgeo
from shapely.affinity import translate
-from shapely.ops import linemerge, unary_union
+from shapely.ops import linemerge, nearest_points, unary_union
-from .auto_fill import (build_fill_stitch_graph,
- build_travel_graph, collapse_sequential_outline_edges, fallback,
- find_stitch_path, graph_is_valid, travel)
from ..debug import debug
-from ..i18n import _
from ..stitch_plan import Stitch
-from ..utils.geometry import Point as InkstitchPoint, ensure_geometry_collection, ensure_multi_line_string, reverse_line_string
+from ..utils.geometry import Point as InkstitchPoint
+from ..utils.geometry import (ensure_geometry_collection,
+ ensure_multi_line_string, reverse_line_string)
+from .auto_fill import (auto_fill, build_fill_stitch_graph, build_travel_graph,
+ collapse_sequential_outline_edges, find_stitch_path,
+ graph_is_valid, travel)
def guided_fill(shape,
@@ -27,10 +32,15 @@ def guided_fill(shape,
strategy
):
segments = intersect_region_with_grating_guideline(shape, guideline, row_spacing, num_staggers, max_stitch_length, strategy)
+ if not segments:
+ return fallback(shape, guideline, row_spacing, max_stitch_length, running_stitch_length, running_stitch_tolerance,
+ num_staggers, skip_last, starting_point, ending_point, underpath)
+
fill_stitch_graph = build_fill_stitch_graph(shape, segments, starting_point, ending_point)
if not graph_is_valid(fill_stitch_graph, shape, max_stitch_length):
- return fallback(shape, running_stitch_length, running_stitch_tolerance)
+ return fallback(shape, guideline, row_spacing, max_stitch_length, running_stitch_length, running_stitch_tolerance,
+ num_staggers, skip_last, starting_point, ending_point, underpath)
travel_graph = build_travel_graph(fill_stitch_graph, shape, angle, underpath)
path = find_stitch_path(fill_stitch_graph, travel_graph, starting_point, ending_point)
@@ -39,6 +49,15 @@ def guided_fill(shape,
return result
+def fallback(shape, guideline, row_spacing, max_stitch_length, running_stitch_length, running_stitch_tolerance,
+ num_staggers, skip_last, starting_point, ending_point, underpath):
+ # fall back to normal auto-fill with an angle that matches the guideline (sorta)
+ guide_start, guide_end = [guideline.coords[0], guideline.coords[-1]]
+ angle = atan2(guide_end[1] - guide_start[1], guide_end[0] - guide_start[0]) * -1
+ return auto_fill(shape, angle, row_spacing, None, max_stitch_length, running_stitch_length, running_stitch_tolerance,
+ num_staggers, skip_last, starting_point, ending_point, underpath)
+
+
def path_to_stitches(path, travel_graph, fill_stitch_graph, stitch_length, running_stitch_length, running_stitch_tolerance, skip_last):
path = collapse_sequential_outline_edges(path)
@@ -75,22 +94,23 @@ def path_to_stitches(path, travel_graph, fill_stitch_graph, stitch_length, runni
def extend_line(line, shape):
(minx, miny, maxx, maxy) = shape.bounds
- line = line.simplify(0.01, False)
-
upper_left = InkstitchPoint(minx, miny)
lower_right = InkstitchPoint(maxx, maxy)
length = (upper_left - lower_right).length()
- point1 = InkstitchPoint(*line.coords[0])
- point2 = InkstitchPoint(*line.coords[1])
- new_starting_point = point1 - (point2 - point1).unit() * length
+ start_point = InkstitchPoint.from_tuple(line.coords[0])
+ end_point = InkstitchPoint.from_tuple(line.coords[-1])
+ direction = (end_point - start_point).unit()
- point3 = InkstitchPoint(*line.coords[-2])
- point4 = InkstitchPoint(*line.coords[-1])
- new_ending_point = point4 + (point4 - point3).unit() * length
+ new_start_point = start_point - direction * length
+ new_end_point = end_point + direction * length
- return shgeo.LineString([new_starting_point.as_tuple()] +
- line.coords[1:-1] + [new_ending_point.as_tuple()])
+ # without this, we seem especially likely to run into this libgeos bug:
+ # https://github.com/shapely/shapely/issues/820
+ new_start_point += InkstitchPoint(random() * 0.01, random() * 0.01)
+ new_end_point += InkstitchPoint(random() * 0.01, random() * 0.01)
+
+ return shgeo.LineString((new_start_point, *line.coords, new_end_point))
def repair_multiple_parallel_offset_curves(multi_line):
@@ -114,8 +134,8 @@ def repair_non_simple_line(line):
repaired = unary_union(linemerge(line_segments))
counter += 1
if repaired.geom_type != 'LineString':
- raise ValueError(
- _("Guide line (or offset copy) is self crossing!"))
+ # They gave us a line with complicated self-intersections. Use a fallback.
+ return shgeo.LineString((line.coords[0], line.coords[-1]))
else:
return repaired
@@ -158,7 +178,12 @@ def prepare_guide_line(line, shape):
if line.geom_type != 'LineString' or not line.is_simple:
line = repair_non_simple_line(line)
- # extend the line towards the ends to increase probability that all offsetted curves cross the shape
+ if line.is_ring:
+ # If they pass us a ring, break it to avoid dividing by zero when
+ # calculating a unit vector from start to end.
+ line = shgeo.LineString(line.coords[:-2])
+
+ # extend the end points away from each other
line = extend_line(line, shape)
return line
@@ -176,24 +201,41 @@ def clean_offset_line(offset_line):
return offset_line
+def _get_start_row(line, shape, row_spacing, line_direction):
+ if line.intersects(shape):
+ return 0
+
+ point1, point2 = nearest_points(line, shape.centroid)
+ distance = point1.distance(point2)
+ row = int(distance / row_spacing)
+
+ # This flips the sign of the starting row if the shape is on the other side
+ # of the guide line
+ shape_direction = InkstitchPoint.from_shapely_point(point2) - InkstitchPoint.from_shapely_point(point1)
+ return copysign(row, shape_direction * line_direction)
+
+
def intersect_region_with_grating_guideline(shape, line, row_spacing, num_staggers, max_stitch_length, strategy):
+ line = prepare_guide_line(line, shape)
+
debug.log_line_string(shape.exterior, "guided fill shape")
- if strategy == 0:
- translate_direction = InkstitchPoint(*line.coords[-1]) - InkstitchPoint(*line.coords[0])
- translate_direction = translate_direction.unit().rotate_left()
+ translate_direction = InkstitchPoint(*line.coords[-1]) - InkstitchPoint(*line.coords[0])
+ translate_direction = translate_direction.unit().rotate_left()
- line = prepare_guide_line(line, shape)
+ shape_envelope = shapely.prepared.prep(shape.convex_hull)
- row = 0
+ start_row = _get_start_row(line, shape, row_spacing, translate_direction)
+ row = start_row
direction = 1
offset_line = None
+ rows = []
while True:
if strategy == 0:
- translate_amount = translate_direction * row * direction * row_spacing
+ translate_amount = translate_direction * row * row_spacing
offset_line = translate(line, xoff=translate_amount.x, yoff=translate_amount.y)
elif strategy == 1:
- offset_line = line.parallel_offset(row * row_spacing * direction, 'left', join_style=shgeo.JOIN_STYLE.bevel)
+ offset_line = line.parallel_offset(row * row_spacing, 'left', join_style=shgeo.JOIN_STYLE.round)
offset_line = clean_offset_line(offset_line)
@@ -201,18 +243,20 @@ def intersect_region_with_grating_guideline(shape, line, row_spacing, num_stagge
# negative parallel offsets are reversed, so we need to compensate
offset_line = reverse_line_string(offset_line)
- debug.log_line_string(offset_line, f"offset {row * direction}")
+ debug.log_line_string(offset_line, f"offset {row}")
stitched_line = apply_stitches(offset_line, max_stitch_length, num_staggers, row_spacing, row * direction)
intersection = shape.intersection(stitched_line)
- if intersection.is_empty:
+ if shape_envelope.intersects(stitched_line):
+ for segment in take_only_line_strings(intersection).geoms:
+ rows.append(segment.coords[:])
+ row += direction
+ else:
if direction == 1:
direction = -1
- row = 1
+ row = start_row - 1
else:
break
- else:
- for segment in take_only_line_strings(intersection).geoms:
- yield segment.coords[:]
- row += 1
+
+ return rows
diff --git a/lib/stitches/ripple_stitch.py b/lib/stitches/ripple_stitch.py
index 489666b0..6a0ef7f0 100644
--- a/lib/stitches/ripple_stitch.py
+++ b/lib/stitches/ripple_stitch.py
@@ -63,6 +63,8 @@ def _get_helper_lines(stroke):
if stroke.is_closed:
return False, _get_circular_ripple_helper_lines(stroke, outline)
+ elif stroke.join_style == 1:
+ return True, _get_point_style_linear_helper_lines(stroke, outline)
else:
return True, _get_linear_ripple_helper_lines(stroke, outline)
@@ -74,7 +76,7 @@ def _get_satin_ripple_helper_lines(stroke):
# 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)
+ steps = _get_steps(stroke.get_line_count(), exponent=stroke.exponent, flip=stroke.flip_exponent)
helper_lines = []
for point0, point1 in zip(*rail_points):
@@ -83,24 +85,39 @@ 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)))
- return helper_lines
+ if stroke.join_style == 1:
+ helper_lines = _converge_helper_line_points(helper_lines, 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.
+def _converge_helper_line_points(helper_lines, point_edge=False):
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])
+ 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])
helper_lines[i] = points
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.
+ 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)
+
+
def _get_linear_ripple_helper_lines(stroke, outline):
guide_line = stroke.get_guide_line()
max_dist = stroke.grid_size or stroke.running_stitch_length
@@ -124,10 +141,25 @@ 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()
+
+ 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
for i, helper in enumerate(helper_lines):
- start = stroke.get_skip_start()
- end = len(helper) - stroke.get_skip_end()
+ end = len(helper) - skip_end
points = helper[start:end]
if i % 2 == 0:
points.reverse()
@@ -146,7 +178,7 @@ 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 alog the guide line
+ # helper lines are generated by making copies of the outline along the guide line
line_point_dict = defaultdict(list)
outline = LineString(running_stitch(line_string_to_point_list(outline), max_distance, stroke.running_stitch_tolerance))
diff --git a/lib/svg/tags.py b/lib/svg/tags.py
index 63c815fc..d113bb6d 100644
--- a/lib/svg/tags.py
+++ b/lib/svg/tags.py
@@ -64,17 +64,7 @@ inkstitch_attribs = [
'join_style',
'avoid_self_crossing',
'clockwise',
- 'line_count',
- 'skip_start',
- 'skip_end',
- 'grid_size',
'reverse',
- 'exponent',
- 'flip_exponent',
- 'scale_axis',
- 'scale_start',
- 'scale_end',
- 'rotate_ripples',
'expand_mm',
'fill_underlay',
'fill_underlay_angle',
@@ -97,6 +87,17 @@ inkstitch_attribs = [
'repeats',
'running_stitch_length_mm',
'running_stitch_tolerance_mm',
+ # ripples
+ 'line_count',
+ 'exponent',
+ 'flip_exponent',
+ 'skip_start',
+ 'skip_end',
+ 'scale_axis',
+ 'scale_start',
+ 'scale_end',
+ 'rotate_ripples',
+ 'grid_size',
# satin column
'satin_column',
'short_stitch_distance_mm',
diff --git a/lib/utils/geometry.py b/lib/utils/geometry.py
index f5ba4ad8..30457749 100644
--- a/lib/utils/geometry.py
+++ b/lib/utils/geometry.py
@@ -129,6 +129,10 @@ class Point:
def from_shapely_point(cls, point):
return cls(point.x, point.y)
+ @classmethod
+ def from_tuple(cls, point):
+ return cls(point[0], point[1])
+
def __json__(self):
return vars(self)