From 2a4f3e8cdf23d34f6fcfe7dd6454824a928512fd Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Fri, 1 Jun 2018 20:34:27 -0400 Subject: add Expand param for fills --- lib/elements/auto_fill.py | 33 ++++++++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 5 deletions(-) (limited to 'lib/elements') diff --git a/lib/elements/auto_fill.py b/lib/elements/auto_fill.py index 08ae67f7..504bae2a 100644 --- a/lib/elements/auto_fill.py +++ b/lib/elements/auto_fill.py @@ -63,20 +63,43 @@ class AutoFill(Fill): return self.get_float_param("fill_underlay_max_stitch_length_mm") or self.max_stitch_length @property - @param('fill_underlay_inset_mm', _('Inset'), unit='mm', group=_('AutoFill Underlay'), type='float', default=0) + @param('fill_underlay_inset_mm', + _('Inset'), + tooltip='Shrink the shape before doing underlay, to prevent underlay from showing around the outside of the fill.', + unit='mm', + group=_('AutoFill Underlay'), + type='float', + default=0) def fill_underlay_inset(self): return self.get_float_param('fill_underlay_inset_mm', 0) @property - def underlay_shape(self): - if self.fill_underlay_inset: - shape = self.shape.buffer(-self.fill_underlay_inset) + @param('expand_mm', + _('Expand'), + tooltip='Expand the shape before fill stitching, to compensate for gaps between shapes.', + unit='mm', + type='float', + default=0) + def expand(self): + return self.get_float_param('expand_mm', 0) + + def shrink_or_grow_shape(self, amount): + if amount: + shape = self.shape.buffer(amount) if not isinstance(shape, shgeo.MultiPolygon): shape = shgeo.MultiPolygon([shape]) return shape else: return self.shape + @property + def underlay_shape(self): + return self.shrink_or_grow_shape(-self.fill_underlay_inset) + + @property + def fill_shape(self): + return self.shrink_or_grow_shape(self.expand) + def to_patches(self, last_patch): stitches = [] @@ -96,7 +119,7 @@ class AutoFill(Fill): starting_point)) starting_point = stitches[-1] - stitches.extend(auto_fill(self.shape, + stitches.extend(auto_fill(self.fill_shape, self.angle, self.row_spacing, self.end_row_spacing, -- cgit v1.2.3 From 692e033e71ed4655cef93be44c762bacf39caaee Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Mon, 4 Jun 2018 20:19:37 -0400 Subject: don't crash on empty subpaths --- lib/elements/element.py | 5 ++++- lib/elements/stroke.py | 8 +++++++- 2 files changed, 11 insertions(+), 2 deletions(-) (limited to 'lib/elements') diff --git a/lib/elements/element.py b/lib/elements/element.py index 300136dd..42f6c470 100644 --- a/lib/elements/element.py +++ b/lib/elements/element.py @@ -29,6 +29,10 @@ class Patch: else: raise TypeError("Patch can only be added to another Patch") + def __len__(self): + # This method allows `len(patch)` and `if patch: + return len(self.stitches) + def add_stitch(self, stitch): self.stitches.append(stitch) @@ -36,7 +40,6 @@ class Patch: return Patch(self.color, self.stitches[::-1]) - class Param(object): def __init__(self, name, description, unit=None, values=[], type=None, group=None, inverse=False, default=None, tooltip=None, sort_index=0): self.name = name diff --git a/lib/elements/stroke.py b/lib/elements/stroke.py index 48662b6d..097b36bc 100644 --- a/lib/elements/stroke.py +++ b/lib/elements/stroke.py @@ -97,6 +97,11 @@ class Stroke(EmbroideryElement): # TODO: use inkstitch.stitches.running_stitch patch = Patch(color=self.color) + + # can't stitch a single point + if len(emb_point_list) < 2: + return patch + p0 = emb_point_list[0] rho = 0.0 side = 1 @@ -156,6 +161,7 @@ class Stroke(EmbroideryElement): else: patch = self.stroke_points(path, self.zigzag_spacing / 2.0, stroke_width=self.stroke_width) - patches.append(patch) + if patch: + patches.append(patch) return patches -- cgit v1.2.3 From 0ff4f2a61bf295449ae0e7b9fdec75ff19c28771 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Mon, 4 Jun 2018 20:43:56 -0400 Subject: convert Stroke to use stitches.running_stitch --- lib/elements/stroke.py | 79 ++++++++++++++++++++++---------------------------- 1 file changed, 34 insertions(+), 45 deletions(-) (limited to 'lib/elements') diff --git a/lib/elements/stroke.py b/lib/elements/stroke.py index 097b36bc..d3054132 100644 --- a/lib/elements/stroke.py +++ b/lib/elements/stroke.py @@ -3,7 +3,7 @@ import sys from .element import param, EmbroideryElement, Patch from ..i18n import _ from ..utils import cache, Point - +from ..stitches import running_stitch warned_about_legacy_running_stitch = False @@ -93,61 +93,50 @@ class Stroke(EmbroideryElement): else: return False - def stroke_points(self, emb_point_list, zigzag_spacing, stroke_width): - # TODO: use inkstitch.stitches.running_stitch + def simple_satin(self, path, zigzag_spacing, stroke_width): + "zig-zag along the path at the specified spacing and wdith" - patch = Patch(color=self.color) + # `self.zigzag_spacing` is the length for a zig and a zag + # together (a V shape). Start with running stitch at half + # that length: + patch = self.running_stitch(path, zigzag_spacing / 2.0) - # can't stitch a single point - if len(emb_point_list) < 2: - return patch + # Now move the points left and right. Consider each pair + # of points in turn, and move perpendicular to them, + # alternating left and right. - p0 = emb_point_list[0] - rho = 0.0 - side = 1 - last_segment_direction = None + offset = stroke_width / 2.0 - for repeat in xrange(self.repeats): - if repeat % 2 == 0: - order = range(1, len(emb_point_list)) - else: - order = range(-2, -len(emb_point_list) - 1, -1) + for i in xrange(len(patch) - 1): + start = patch.stitches[i] + end = patch.stitches[i + 1] + segment_direction = (end - start).unit() + zigzag_direction = segment_direction.rotate_left() - for segi in order: - p1 = emb_point_list[segi] + if i % 2 == 1: + zigzag_direction *= -1 - # how far we have to go along segment - seg_len = (p1 - p0).length() - if (seg_len == 0): - continue + patch.stitches[i] += zigzag_direction * offset - # vector pointing along segment - along = (p1 - p0).unit() + return patch - # vector pointing to edge of stroke width - perp = along.rotate_left() * (stroke_width * 0.5) + def running_stitch(self, path, stitch_length): + repeated_path = [] - if stroke_width == 0.0 and last_segment_direction is not None: - if abs(1.0 - along * last_segment_direction) > 0.5: - # if greater than 45 degree angle, stitch the corner - rho = zigzag_spacing - patch.add_stitch(p0) + # go back and forth along the path as specified by self.repeats + for i in xrange(self.repeats): + if i % 2 == 1: + # reverse every other pass + this_path = path[::-1] + else: + this_path = path[:] - # iteration variable: how far we are along segment - while (rho <= seg_len): - left_pt = p0 + along * rho + perp * side - patch.add_stitch(left_pt) - rho += zigzag_spacing - side = -side + repeated_path.extend(this_path) - p0 = p1 - last_segment_direction = along - rho -= seg_len + stitches = running_stitch(repeated_path, stitch_length) - if (p0 - patch.stitches[-1]).length() > 0.1: - patch.add_stitch(p0) + return Patch(self.color, stitches) - return patch def to_patches(self, last_patch): patches = [] @@ -157,9 +146,9 @@ class Stroke(EmbroideryElement): if self.manual_stitch_mode: patch = Patch(color=self.color, stitches=path, stitch_as_is=True) elif self.is_running_stitch(): - patch = self.stroke_points(path, self.running_stitch_length, stroke_width=0.0) + patch = self.running_stitch(path, self.running_stitch_length) else: - patch = self.stroke_points(path, self.zigzag_spacing / 2.0, stroke_width=self.stroke_width) + patch = self.simple_satin(path, self.zigzag_spacing, self.stroke_width) if patch: patches.append(patch) -- cgit v1.2.3 From d06ff488f0977ab52dbf0169d85cc5d7a413e079 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Sun, 10 Jun 2018 15:13:51 -0400 Subject: fix defaulting of stroke width to 1 --- lib/elements/element.py | 6 +++--- lib/elements/stroke.py | 5 +---- 2 files changed, 4 insertions(+), 7 deletions(-) (limited to 'lib/elements') diff --git a/lib/elements/element.py b/lib/elements/element.py index 42f6c470..39437c9f 100644 --- a/lib/elements/element.py +++ b/lib/elements/element.py @@ -135,10 +135,10 @@ class EmbroideryElement(object): self.node.set("embroider_%s" % name, str(value)) @cache - def get_style(self, style_name): + def get_style(self, style_name, default=None): style = simplestyle.parseStyle(self.node.get("style")) if (style_name not in style): - return None + return default value = style[style_name] if value == 'none': return None @@ -161,7 +161,7 @@ class EmbroideryElement(object): @property @cache def stroke_width(self): - width = self.get_style("stroke-width") + width = self.get_style("stroke-width", "1") if width is None: return 1.0 diff --git a/lib/elements/stroke.py b/lib/elements/stroke.py index d3054132..5239f978 100644 --- a/lib/elements/stroke.py +++ b/lib/elements/stroke.py @@ -57,10 +57,7 @@ class Stroke(EmbroideryElement): def is_running_stitch(self): # using stroke width <= 0.5 pixels to indicate running stitch is deprecated in favor of dashed lines - try: - stroke_width = float(self.get_style("stroke-width")) - except ValueError: - stroke_width = 1 + stroke_width = float(self.get_style("stroke-width", 1)) if self.dashed: return True -- cgit v1.2.3 From f79b3a7a95bba7d927cefd321d52eff819bb9180 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Sun, 10 Jun 2018 15:43:17 -0400 Subject: default fill to black per SVG spec --- lib/elements/fill.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'lib/elements') diff --git a/lib/elements/fill.py b/lib/elements/fill.py index 52a42260..8d1d35f2 100644 --- a/lib/elements/fill.py +++ b/lib/elements/fill.py @@ -27,7 +27,8 @@ class Fill(EmbroideryElement): @property def color(self): - return self.get_style("fill") + # SVG spec says the default fill is black + return self.get_style("fill", "#000000") @property @param('flip', _('Flip fill (start right-to-left)'), type='boolean', default=False) -- cgit v1.2.3 From 83efa9e02fc19a1f4bb0e1524aa601c48c5ca6ef Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Sun, 10 Jun 2018 16:01:37 -0400 Subject: error if satin column set for path with single subpath --- lib/elements/satin_column.py | 2 ++ 1 file changed, 2 insertions(+) (limited to 'lib/elements') diff --git a/lib/elements/satin_column.py b/lib/elements/satin_column.py index 3593db64..1d13c5e0 100644 --- a/lib/elements/satin_column.py +++ b/lib/elements/satin_column.py @@ -97,6 +97,8 @@ class SatinColumn(EmbroideryElement): def flattened_beziers(self): if len(self.csp) == 2: return self.simple_flatten_beziers() + elif len(self.csp) < 2: + self.fatal(_("satin column: %(id)s: at least two subpaths required (%(num)d found)") % dict(num=len(self.csp), id=self.node.get('id'))) else: return self.flatten_beziers_with_rungs() -- cgit v1.2.3 From e29096ee138bd674e96a369a853d75eb7c919823 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Thu, 21 Jun 2018 15:41:06 -0400 Subject: add commands framework --- lib/elements/element.py | 29 ++++++++++------------------- lib/elements/stroke.py | 3 ++- 2 files changed, 12 insertions(+), 20 deletions(-) (limited to 'lib/elements') diff --git a/lib/elements/element.py b/lib/elements/element.py index 39437c9f..465813d4 100644 --- a/lib/elements/element.py +++ b/lib/elements/element.py @@ -4,7 +4,8 @@ from shapely import geometry as shgeo from ..i18n import _ from ..utils import cache -from ..svg import PIXELS_PER_MM, get_viewbox_transform, convert_length, get_doc_size +from ..svg import PIXELS_PER_MM, convert_length, get_doc_size, apply_transforms +from ..commands import find_commands # inkscape-provided utilities import simpletransform @@ -171,10 +172,6 @@ class EmbroideryElement(object): @property def path(self): - return cubicsuperpath.parsePath(self.node.get("d")) - - @cache - def parse_path(self): # A CSP is a "cubic superpath". # # A "path" is a sequence of strung-together bezier curves. @@ -202,22 +199,16 @@ class EmbroideryElement(object): # In a path, each element in the 3-tuple is itself a tuple of (x, y). # Tuples all the way down. Hasn't anyone heard of using classes? - path = self.path - - # start with the identity transform - transform = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]] - - # combine this node's transform with all parent groups' transforms - transform = simpletransform.composeParents(self.node, transform) - - # add in the transform implied by the viewBox - viewbox_transform = get_viewbox_transform(self.node.getroottree().getroot()) - transform = simpletransform.composeTransform(viewbox_transform, transform) + return cubicsuperpath.parsePath(self.node.get("d")) - # apply the combined transform to this node's path - simpletransform.applyTransformToPath(transform, path) + @cache + def parse_path(self): + return apply_transforms(self.path, self.node) - return path + @property + @cache + def commands(self): + return find_commands(self.node) def strip_control_points(self, subpath): return [point for control_before, point, control_after in subpath] diff --git a/lib/elements/stroke.py b/lib/elements/stroke.py index 5239f978..eca9e0ba 100644 --- a/lib/elements/stroke.py +++ b/lib/elements/stroke.py @@ -4,6 +4,7 @@ from .element import param, EmbroideryElement, Patch from ..i18n import _ from ..utils import cache, Point from ..stitches import running_stitch +from ..svg import parse_length_with_units warned_about_legacy_running_stitch = False @@ -57,7 +58,7 @@ class Stroke(EmbroideryElement): def is_running_stitch(self): # using stroke width <= 0.5 pixels to indicate running stitch is deprecated in favor of dashed lines - stroke_width = float(self.get_style("stroke-width", 1)) + stroke_width, units = parse_length_with_units(self.get_style("stroke-width", "1")) if self.dashed: return True -- cgit v1.2.3 From 1f4bc62d960ab99c6911ab0a292d6ea52a309813 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Fri, 22 Jun 2018 22:19:57 -0400 Subject: add quick access methods for commands --- lib/elements/element.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) (limited to 'lib/elements') diff --git a/lib/elements/element.py b/lib/elements/element.py index 465813d4..3c31f1b0 100644 --- a/lib/elements/element.py +++ b/lib/elements/element.py @@ -210,6 +210,22 @@ class EmbroideryElement(object): def commands(self): return find_commands(self.node) + @cache + def get_commands(self, command): + return [c for c in self.commands if c.command == command] + + @cache + def get_command(self, command): + commands = self.get_commands(command) + + if len(commands) == 1: + return commands[0] + elif len(commands) > 1: + raise ValueError(_("%(id)s has more than one command of type '%(command)s' linked to it") % + dict(id=self.node.get(id), command=command)) + else: + return None + def strip_control_points(self, subpath): return [point for control_before, point, control_after in subpath] -- cgit v1.2.3 From 0c527cc51e896f57d15c399c28c8c66c16d1cc59 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Fri, 22 Jun 2018 22:29:23 -0400 Subject: starting point specified by fill_start command --- lib/elements/auto_fill.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) (limited to 'lib/elements') diff --git a/lib/elements/auto_fill.py b/lib/elements/auto_fill.py index 504bae2a..a2c63bd9 100644 --- a/lib/elements/auto_fill.py +++ b/lib/elements/auto_fill.py @@ -100,13 +100,21 @@ class AutoFill(Fill): def fill_shape(self): return self.shrink_or_grow_shape(self.expand) + def get_starting_point(self, last_patch): + # If there is a "fill_start" Command, then use that; otherwise pick + # the point closest to the end of the last patch. + + if self.get_command('fill_start'): + return self.get_command('fill_start').target_point + elif last_patch: + return last_patch.stitches[-1] + else: + return None + def to_patches(self, last_patch): stitches = [] - if last_patch is None: - starting_point = None - else: - starting_point = last_patch.stitches[-1] + starting_point = self.get_starting_point(last_patch) if self.fill_underlay: stitches.extend(auto_fill(self.underlay_shape, -- cgit v1.2.3 From abbda62835bfc99e49d0de1ccdffb6739dd2142e Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Fri, 22 Jun 2018 22:31:42 -0400 Subject: ending point speciifed by fill_end command --- lib/elements/auto_fill.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) (limited to 'lib/elements') diff --git a/lib/elements/auto_fill.py b/lib/elements/auto_fill.py index a2c63bd9..59816878 100644 --- a/lib/elements/auto_fill.py +++ b/lib/elements/auto_fill.py @@ -111,10 +111,17 @@ class AutoFill(Fill): else: return None + def get_ending_point(self): + if self.get_command('fill_end'): + return self.get_command('fill_end').target_point + else: + return None + def to_patches(self, last_patch): stitches = [] starting_point = self.get_starting_point(last_patch) + ending_point = self.get_ending_point() if self.fill_underlay: stitches.extend(auto_fill(self.underlay_shape, @@ -134,6 +141,7 @@ class AutoFill(Fill): self.max_stitch_length, self.running_stitch_length, self.staggers, - starting_point)) + starting_point, + ending_point)) return [Patch(stitches=stitches, color=self.color)] -- cgit v1.2.3 From 61983b615b202bb95c21d7a5021af3373615e839 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Sat, 30 Jun 2018 14:16:56 -0400 Subject: add has_command() --- lib/elements/element.py | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'lib/elements') diff --git a/lib/elements/element.py b/lib/elements/element.py index 3c31f1b0..f0b7ea6f 100644 --- a/lib/elements/element.py +++ b/lib/elements/element.py @@ -214,6 +214,10 @@ class EmbroideryElement(object): def get_commands(self, command): return [c for c in self.commands if c.command == command] + @cache + def has_command(self, command): + return len(self.get_commands(command)) > 0 + @cache def get_command(self, command): commands = self.get_commands(command) -- cgit v1.2.3 From 3893d13b52b2755ea134ec4d3a215ee807dbbc2e Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Sat, 30 Jun 2018 14:18:45 -0400 Subject: add support for trim/stop commands --- lib/elements/element.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'lib/elements') diff --git a/lib/elements/element.py b/lib/elements/element.py index f0b7ea6f..1c67d123 100644 --- a/lib/elements/element.py +++ b/lib/elements/element.py @@ -268,8 +268,8 @@ class EmbroideryElement(object): patches = self.to_patches(last_patch) if patches: - patches[-1].trim_after = self.trim_after - patches[-1].stop_after = self.stop_after + patches[-1].trim_after = self.has_command("trim") or self.trim_after + patches[-1].stop_after = self.has_command("stop") or self.stop_after return patches -- cgit v1.2.3 From aa86dc56ad5cb9166ab1c9cda036d9521855ad29 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Sat, 30 Jun 2018 14:19:28 -0400 Subject: remove 'TRIM after' and 'STOP after' from Params dialog --- lib/elements/element.py | 12 ------------ 1 file changed, 12 deletions(-) (limited to 'lib/elements') diff --git a/lib/elements/element.py b/lib/elements/element.py index 1c67d123..62e9745d 100644 --- a/lib/elements/element.py +++ b/lib/elements/element.py @@ -242,22 +242,10 @@ class EmbroideryElement(object): return [self.strip_control_points(subpath) for subpath in path] @property - @param('trim_after', - _('TRIM after'), - tooltip=_('Trim thread after this object (for supported machines and file formats)'), - type='boolean', - default=False, - sort_index=1000) def trim_after(self): return self.get_boolean_param('trim_after', False) @property - @param('stop_after', - _('STOP after'), - tooltip=_('Add STOP instruction after this object (for supported machines and file formats)'), - type='boolean', - default=False, - sort_index=1000) def stop_after(self): return self.get_boolean_param('stop_after', False) -- cgit v1.2.3 From 62ef2850a2f57d64d0e65fbfc055b85e3c940031 Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Wed, 4 Jul 2018 21:17:20 -0400 Subject: generalize Element.shape and implement in all element types --- lib/elements/element.py | 4 ++++ lib/elements/polyline.py | 7 +++++++ lib/elements/satin_column.py | 11 +++++++++++ lib/elements/stroke.py | 7 +++++++ 4 files changed, 29 insertions(+) (limited to 'lib/elements') diff --git a/lib/elements/element.py b/lib/elements/element.py index 62e9745d..ebca90a4 100644 --- a/lib/elements/element.py +++ b/lib/elements/element.py @@ -205,6 +205,10 @@ class EmbroideryElement(object): def parse_path(self): return apply_transforms(self.path, self.node) + @property + def shape(self): + raise NotImplementedError("INTERNAL ERROR: %s must implement shape()", self.__class__) + @property @cache def commands(self): diff --git a/lib/elements/polyline.py b/lib/elements/polyline.py index 5c474237..b9ffdc0b 100644 --- a/lib/elements/polyline.py +++ b/lib/elements/polyline.py @@ -1,3 +1,5 @@ +from shapely import geometry as shgeo + from .element import param, EmbroideryElement, Patch from ..i18n import _ from ..utils.geometry import Point @@ -27,6 +29,11 @@ class Polyline(EmbroideryElement): return points + @property + @cache + def shape(self): + return shgeo.LineString(self.points) + @property def path(self): # A polyline is a series of connected line segments described by their diff --git a/lib/elements/satin_column.py b/lib/elements/satin_column.py index 1d13c5e0..2ceb38de 100644 --- a/lib/elements/satin_column.py +++ b/lib/elements/satin_column.py @@ -87,6 +87,17 @@ class SatinColumn(EmbroideryElement): # the edges of the satin column. return self.get_float_param("zigzag_underlay_inset_mm") or self.contour_underlay_inset / 2.0 + @property + @cache + def shape(self): + # This isn't used for satins at all, but other parts of the code + # may need to know the general shape of a satin column. + + flattened = self.flatten(self.parse_path()) + line_strings = [shgeo.LineString(path) for path in flattened] + + return shgeo.MultiLineString(line_strings) + @property @cache def csp(self): diff --git a/lib/elements/stroke.py b/lib/elements/stroke.py index eca9e0ba..e8eb4783 100644 --- a/lib/elements/stroke.py +++ b/lib/elements/stroke.py @@ -1,4 +1,5 @@ import sys +import shapely.geometry from .element import param, EmbroideryElement, Patch from ..i18n import _ @@ -50,6 +51,12 @@ class Stroke(EmbroideryElement): else: return self.flatten(path) + @property + @cache + def shape(self): + line_strings = [shapely.geometry.LineString(path) for path in self.paths] + return shapely.geometry.MultiLineString(line_strings) + @property @param('manual_stitch', _('Manual stitch placement'), tooltip=_("Stitch every node in the path. Stitch length and zig-zag spacing are ignored."), type='boolean', default=False) def manual_stitch_mode(self): -- cgit v1.2.3