diff options
| -rw-r--r-- | lib/elements/stroke.py | 88 | ||||
| -rw-r--r-- | lib/stitches/guided_fill.py | 13 | ||||
| -rw-r--r-- | lib/stitches/ripple_stitch.py | 146 | ||||
| -rw-r--r-- | lib/svg/tags.py | 5 | ||||
| -rw-r--r-- | lib/update.py | 10 |
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 |
