From 08581d7eae58f08af6bc57fdc9a69e47f1f8768b Mon Sep 17 00:00:00 2001 From: George Steel Date: Thu, 17 Nov 2022 01:51:33 -0500 Subject: [PATCH] Make pull-compensation and insets per-side --- .gitignore | 1 + lib/elements/element.py | 27 +++++++ lib/elements/satin_column.py | 139 ++++++++++++++++------------------ lib/stitches/ripple_stitch.py | 2 +- lib/svg/tags.py | 5 +- 5 files changed, 97 insertions(+), 77 deletions(-) diff --git a/.gitignore b/.gitignore index a64cb5ed8..e6dca05c0 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,4 @@ locales/ /PROFILE /profile_stats /profile_stats.prof +/.vscode diff --git a/lib/elements/element.py b/lib/elements/element.py index 75d225802..8ff821e2d 100644 --- a/lib/elements/element.py +++ b/lib/elements/element.py @@ -5,6 +5,7 @@ import sys from copy import deepcopy +import numpy as np import inkex from inkex import bezier @@ -135,6 +136,32 @@ class EmbroideryElement(object): return value + # returns 2 float values as a numpy array + # if a single number is given in the param, it will apply to both returned values. + # Not cached the cache will crash if the default is a numpy array. + # The ppoperty calling this will need to cache itself and can safely do so since it has no parameters + def get_lr_float_param(self, param, default=(0, 0)): + default = np.array(default) # type coersion in case the default is a tuple + + raw = self.get_param(param, "") + parts = raw.split() + try: + if len(parts) == 0: + return default + elif len(parts) == 1: + a = float(parts[0]) + return np.array([a, a]) + else: + a = float(parts[0]) + b = float(parts[1]) + return np.array([a, b]) + except (TypeError, ValueError): + return default + + # not cached + def get_lr_mm_param_as_px(self, param, default): + return self.get_lr_float_param(param, default) * PIXELS_PER_MM + def set_param(self, name, value): param = INKSTITCH_ATTRIBS[name] self.node.set(param, str(value)) diff --git a/lib/elements/satin_column.py b/lib/elements/satin_column.py index f9c5495d6..400f60d82 100644 --- a/lib/elements/satin_column.py +++ b/lib/elements/satin_column.py @@ -5,6 +5,7 @@ from copy import deepcopy from itertools import chain +import numpy as np from shapely import affinity as shaffinity from shapely import geometry as shgeo @@ -16,7 +17,7 @@ from ..i18n import _ from ..stitch_plan import StitchGroup from ..svg import line_strings_to_csp, point_lists_to_csp from ..utils import Point, cache, collapse_duplicate_point, cut -from .element import EmbroideryElement, param +from .element import EmbroideryElement, param, PIXELS_PER_MM from .validation import ValidationError, ValidationWarning @@ -124,8 +125,8 @@ class SatinColumn(EmbroideryElement): @property @param('zigzag_spacing_mm', _('Zig-zag spacing (peak-to-peak)'), - tooltip=_('Peak-to-peak distance between zig-zags.'), - unit='mm', + tooltip=_('Peak-to-peak distance between zig-zags. This is double the mm/stitch measurement used by most mechanichal machines.'), + unit='mm/cycle', type='float', default=0.4) def zigzag_spacing(self): @@ -136,39 +137,30 @@ class SatinColumn(EmbroideryElement): @param( 'pull_compensation_percent', _('Pull compensation percentage'), - tooltip=_('Additional pull compensation which varries as a percentage of stitch width.'), - unit='%', + tooltip=_('Additional pull compensation which varries as a percentage of stitch width. ' + 'Two values separated by a space may be used for an aysmmetric effect.'), + unit='% (each side)', type='float', default=0) def pull_compensation_percent(self): # pull compensation as a percentage of the width - return self.get_float_param("pull_compensation_percent", 0) + return self.get_lr_float_param("pull_compensation_percent", (0, 0)) @property @param( 'pull_compensation_mm', _('Pull compensation'), - tooltip=_('Satin stitches pull the fabric together, resulting in a column narrower than you draw in Inkscape. ' - 'This setting expands each pair of needle penetrations outward from the center of the satin column by a fixed length.'), - unit='mm', + tooltip=_('Satin stitches pull the fabric together, resulting in a column narrower than you draw in Inkscape. ' + 'This setting expands each pair of needle penetrations outward from the center of the satin column by a fixed length. ' + 'Two values separated by a space may be used for an aysmmetric effect.'), + unit='mm (each side)', type='float', default=0) - def pull_compensation(self): + def pull_compensation_px(self): # In satin stitch, the stitches have a tendency to pull together and # narrow the entire column. We can compensate for this by stitching # wider than we desire the column to end up. - return self.get_float_param("pull_compensation_mm", 0) - - @property - @param( - 'pull_compensation_balance', - _('Pull compensation balance'), - tooltip=_('Percentage of pull compensation which applies to second rail (50% is centered).'), - unit='%', - type='float', - default=50) - def pull_compensation_balance(self): - return min(100, max(0, self.get_float_param("pull_compensation_balance", 50))) + return self.get_lr_mm_param_as_px("pull_compensation_mm", (0, 0)) @property @param('contour_underlay', _('Contour underlay'), type='toggle', group=_('Contour Underlay')) @@ -187,11 +179,11 @@ class SatinColumn(EmbroideryElement): _('Inset distance (fixed)'), tooltip=_('Shrink the outline by a fixed length, to prevent the underlay from showing around the outside of the satin column.'), group=_('Contour Underlay'), - unit='mm', type='float', default=0.4, + unit='mm (each side)', type='float', default=0.4, sort_index=2) - def contour_underlay_inset(self): + def contour_underlay_inset_px(self): # how far inside the edge of the column to stitch the underlay - return self.get_float_param("contour_underlay_inset_mm", 0.4) + return self.get_lr_mm_param_as_px("contour_underlay_inset_mm", (0.4, 0.4)) @property @param('contour_underlay_inset_percent', @@ -199,22 +191,11 @@ class SatinColumn(EmbroideryElement): tooltip=_('Shrink the outline by a proportion of the column width, ' 'to prevent the underlay from showing around the outside of the satin column.'), group=_('Contour Underlay'), - unit='%', type='float', default=0, + unit='% (each side)', type='float', default=0, sort_index=3) def contour_underlay_inset_percent(self): # how far inside the edge of the column to stitch the underlay - return min(100, max(0, self.get_float_param("contour_underlay_inset_percent", 0))) - - @property - @param('contour_underlay_inset_balance', - _('Inset balance'), - tooltip=_('Proportion of the inset which applies to second rail (50% is centered). This can be useful for asymmetric designs.'), - group=_('Contour Underlay'), - unit='%', type='float', default=50, - sort_index=4) - def contour_underlay_inset_balance(self): - # how far inside the edge of the column to stitch the underlay - return min(100, max(0, self.get_float_param("contour_underlay_inset_balance", 50))) + return self.get_lr_float_param("contour_underlay_inset_percent", (0, 0)) @property @param('center_walk_underlay', _('Center-walk underlay'), type='toggle', group=_('Center-Walk Underlay')) @@ -240,14 +221,14 @@ class SatinColumn(EmbroideryElement): return max(self.get_int_param("center_walk_underlay_repeats", 2), 1) @property - @param('center_walk_underlay_balance', - _('Balance'), - tooltip=_('Position of underlay from between the rails (50% is centered), consistent with the Balance parameter for contour underlay.'), + @param('center_walk_underlay_position', + _('Position'), + tooltip=_('Position of underlay from between the rails. 0% is along the first rail, 50% is centered, 100% is along the second rail.'), group=_('Center-Walk Underlay'), type='float', unit='%', default=50, sort_index=3) - def center_walk_underlay_balance(self): - return min(100, max(0, self.get_float_param("center_walk_underlay_balance", 50))) + def center_walk_underlay_position(self): + return min(100, max(0, self.get_float_param("center_walk_underlay_position", 50))) @property @param('zigzag_underlay', _('Zig-zag underlay'), type='toggle', group=_('Zig-zag Underlay')) @@ -267,13 +248,13 @@ class SatinColumn(EmbroideryElement): @property @param('zigzag_underlay_inset_mm', - _('Inset amount'), + _('Inset amount (fixed)'), tooltip=_('default: half of contour underlay inset'), - unit='mm', + unit='mm (each side)', group=_('Zig-zag Underlay'), type='float', default="") - def zigzag_underlay_inset(self): + def zigzag_underlay_inset_px(self): # how far in from the edge of the satin the points in the zigzags # should be @@ -281,7 +262,21 @@ class SatinColumn(EmbroideryElement): # doing both contour underlay and zigzag underlay, make sure the # points of the zigzag fall outside the contour underlay but inside # the edges of the satin column. - return self.get_float_param("zigzag_underlay_inset_mm") or self.contour_underlay_inset / 2.0 + default = self.contour_underlay_inset_px * 0.5 / PIXELS_PER_MM + x = self.get_lr_mm_param_as_px("zigzag_underlay_inset_mm", default) + return x + + @property + @param('zigzag_underlay_inset_percent', + _('Inset amount (proportional)'), + tooltip=_('default: half of contour underlay inset'), + unit='% (each side)', + group=_('Zig-zag Underlay'), + type='float', + default="") + def zigzag_underlay_inset_percent(self): + default = self.contour_underlay_inset_percent * 0.5 + return self.get_lr_float_param("zigzag_underlay_inset_percent", default) @property @param('zigzag_underlay_max_stitch_length_mm', @@ -375,7 +370,7 @@ class SatinColumn(EmbroideryElement): # intersect with the rails even with floating point inaccuracy. start = Point(*start) end = Point(*end) - start, end = self.offset_points(start, end, 0.01) + start, end = self.offset_points(start, end, (0.01, 0.01), (0, 0)) start = list(start) end = list(end) @@ -585,7 +580,7 @@ class SatinColumn(EmbroideryElement): """ # like in do_satin() - points = list(chain.from_iterable(zip(*self.plot_points_on_rails(self.zigzag_spacing, 0)))) + points = list(chain.from_iterable(zip(*self.plot_points_on_rails(self.zigzag_spacing)))) if isinstance(split_point, float): index_of_closest_stitch = int(round(len(points) * split_point)) @@ -677,10 +672,10 @@ class SatinColumn(EmbroideryElement): @cache def center_line(self): # similar technique to do_center_walk() - center_walk, _ = self.plot_points_on_rails(self.zigzag_spacing, -100000) + center_walk, _ = self.plot_points_on_rails(self.zigzag_spacing, (0, 0), (-0.5, -0.5)) return shgeo.LineString(center_walk) - def offset_points(self, pos1, pos2, offset_px, offset_prop=0, offset_balance=0.5): + def offset_points(self, pos1, pos2, offset_px, offset_prop): # Expand or contract two points about their midpoint. This is # useful for pull compensation and insetting underlay. @@ -692,19 +687,18 @@ class SatinColumn(EmbroideryElement): return pos1, pos2 # calculate the offset for each side - offset_total = offset_px + (offset_prop * distance) - inv_offset_balance = 1 - offset_balance - offset1 = offset_total * inv_offset_balance - offset2 = offset_total * offset_balance + offset_a = offset_px[0] + (distance * offset_prop[0]) + offset_b = offset_px[1] + (distance * offset_prop[1]) + offset_total = offset_a + offset_b # don't contract beyond the midpoint, or we'll start expanding - if offset1 < -distance * inv_offset_balance: - offset1 = -distance * inv_offset_balance - if offset2 < -distance * offset_balance: - offset2 = -distance * offset_balance + if offset_total < -distance: + scale = distance / offset_total + offset_a = offset_a * scale + offset_b = offset_b * scale - out1 = pos1 + (pos1 - pos2).unit() * offset1 - out2 = pos2 + (pos2 - pos1).unit() * offset2 + out1 = pos1 + (pos1 - pos2).unit() * offset_a + out2 = pos2 + (pos2 - pos1).unit() * offset_b return out1, out2 @@ -741,13 +735,13 @@ class SatinColumn(EmbroideryElement): distance_remaining -= segment_length pos = segment_end - def plot_points_on_rails(self, spacing, offset_px=0, offset_prop=0, offset_balance=0.5): + def plot_points_on_rails(self, spacing, offset_px=(0, 0), offset_prop=(0, 0)): # Take a section from each rail in turn, and plot out an equal number # of points on both rails. Return the points plotted. The points will # be contracted or expanded by offset using self.offset_points(). def add_pair(pos0, pos1): - pos0, pos1 = self.offset_points(pos0, pos1, offset_px, offset_prop, offset_balance) + pos0, pos1 = self.offset_points(pos0, pos1, offset_px, offset_prop) points[0].append(pos0) points[1].append(pos1) @@ -829,8 +823,7 @@ class SatinColumn(EmbroideryElement): # other. forward, back = self.plot_points_on_rails( self.contour_underlay_stitch_length, - -self.contour_underlay_inset, -self.contour_underlay_inset_percent/100, - self.contour_underlay_inset_balance/100) + -self.contour_underlay_inset_px, -self.contour_underlay_inset_percent/100) stitches = (forward + list(reversed(back))) if self._center_walk_is_odd(): stitches = (list(reversed(back)) + forward) @@ -844,10 +837,11 @@ class SatinColumn(EmbroideryElement): # Center walk underlay is just a running stitch down and back on the # center line between the bezier curves. + inset_prop = -np.array([self.center_walk_underlay_position, 100-self.center_walk_underlay_position]) / 100 # Do it like contour underlay, but inset all the way to the center. forward, back = self.plot_points_on_rails( self.center_walk_underlay_stitch_length, - 0, -1, self.center_walk_underlay_balance/100) + (0, 0), inset_prop) stitches = [] for i in range(self.center_walk_underlay_repeats): @@ -875,7 +869,8 @@ class SatinColumn(EmbroideryElement): patch = StitchGroup(color=self.color) sides = self.plot_points_on_rails(self.zigzag_underlay_spacing / 2.0, - -self.zigzag_underlay_inset) + -self.zigzag_underlay_inset_px, + -self.zigzag_underlay_inset_percent/100) if self._center_walk_is_odd(): sides = [list(reversed(sides[0])), list(reversed(sides[1]))] @@ -914,9 +909,8 @@ class SatinColumn(EmbroideryElement): # pull compensation is automatically converted from mm to pixels by get_float_param sides = self.plot_points_on_rails( self.zigzag_spacing, - self.pull_compensation, - self.pull_compensation_percent/100, - self.pull_compensation_balance/100 + self.pull_compensation_px, + self.pull_compensation_percent/100 ) if self.max_stitch_length: @@ -949,9 +943,8 @@ class SatinColumn(EmbroideryElement): sides = self.plot_points_on_rails( self.zigzag_spacing, - self.pull_compensation, - self.pull_compensation_percent/100, - self.pull_compensation_balance/100 + self.pull_compensation_px, + self.pull_compensation_percent/100 ) # "left" and "right" here are kind of arbitrary designations meaning diff --git a/lib/stitches/ripple_stitch.py b/lib/stitches/ripple_stitch.py index 6a0ef7f0b..67362f127 100644 --- a/lib/stitches/ripple_stitch.py +++ b/lib/stitches/ripple_stitch.py @@ -74,7 +74,7 @@ def _get_satin_ripple_helper_lines(stroke): 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) + rail_points = SatinColumn(stroke.node).plot_points_on_rails(length) steps = _get_steps(stroke.get_line_count(), exponent=stroke.exponent, flip=stroke.flip_exponent) diff --git a/lib/svg/tags.py b/lib/svg/tags.py index 21aee22b5..219a9b56f 100644 --- a/lib/svg/tags.py +++ b/lib/svg/tags.py @@ -106,21 +106,20 @@ inkstitch_attribs = [ 'center_walk_underlay', 'center_walk_underlay_stitch_length_mm', 'center_walk_underlay_repeats', - 'center_walk_underlay_balance', + 'center_walk_underlay_position', 'contour_underlay', 'contour_underlay_stitch_length_mm', 'contour_underlay_inset_mm', 'contour_underlay_inset_percent', - 'contour_underlay_inset_balance', 'zigzag_underlay', 'zigzag_spacing_mm', 'zigzag_underlay_inset_mm', + 'zigzag_underlay_inset_percent', 'zigzag_underlay_spacing_mm', 'zigzag_underlay_max_stitch_length_mm', 'e_stitch', 'pull_compensation_mm', 'pull_compensation_percent', - 'pull_compensation_balance', 'stroke_first', # stitch_plan 'invisible_layers',