diff options
| author | Kaalleen <36401965+kaalleen@users.noreply.github.com> | 2022-06-10 16:25:30 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2022-06-10 16:25:30 +0200 |
| commit | 2fde596272b339ebb9b63ceebd66c5e7a0c641f2 (patch) | |
| tree | 8beb5a62880fc66026551fca7fe4676de456029a | |
| parent | 68deec88a8a8c4e469191d8c00641a1077c2508a (diff) | |
Guided ripple stitch (#1675)
Co-authored-by: @lexelby
| -rw-r--r-- | lib/elements/element.py | 4 | ||||
| -rw-r--r-- | lib/elements/stroke.py | 212 | ||||
| -rw-r--r-- | lib/lettering/font.py | 2 | ||||
| -rw-r--r-- | lib/marker.py | 14 | ||||
| -rw-r--r-- | lib/stitches/ripple_stitch.py | 330 | ||||
| -rw-r--r-- | lib/stitches/running_stitch.py | 6 | ||||
| -rw-r--r-- | lib/svg/tags.py | 8 | ||||
| -rw-r--r-- | lib/utils/geometry.py | 9 |
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) |
