diff options
Diffstat (limited to 'lib/elements')
| -rw-r--r-- | lib/elements/auto_fill.py | 59 | ||||
| -rw-r--r-- | lib/elements/element.py | 74 | ||||
| -rw-r--r-- | lib/elements/fill.py | 3 | ||||
| -rw-r--r-- | lib/elements/polyline.py | 7 | ||||
| -rw-r--r-- | lib/elements/satin_column.py | 13 | ||||
| -rw-r--r-- | lib/elements/stroke.py | 94 |
6 files changed, 158 insertions, 92 deletions
diff --git a/lib/elements/auto_fill.py b/lib/elements/auto_fill.py index 08ae67f7..59816878 100644 --- a/lib/elements/auto_fill.py +++ b/lib/elements/auto_fill.py @@ -63,27 +63,65 @@ 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 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 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 = [] - if last_patch is None: - starting_point = None - else: - starting_point = last_patch.stitches[-1] + 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, @@ -96,13 +134,14 @@ 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, self.max_stitch_length, self.running_stitch_length, self.staggers, - starting_point)) + starting_point, + ending_point)) return [Patch(stitches=stitches, color=self.color)] diff --git a/lib/elements/element.py b/lib/elements/element.py index 300136dd..ebca90a4 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 @@ -29,6 +30,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 +41,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 @@ -132,10 +136,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 @@ -158,7 +162,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 @@ -168,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. @@ -199,22 +199,40 @@ 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 + return cubicsuperpath.parsePath(self.node.get("d")) + + @cache + def parse_path(self): + return apply_transforms(self.path, self.node) - # start with the identity transform - transform = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]] + @property + def shape(self): + raise NotImplementedError("INTERNAL ERROR: %s must implement shape()", self.__class__) - # combine this node's transform with all parent groups' transforms - transform = simpletransform.composeParents(self.node, transform) + @property + @cache + def commands(self): + return find_commands(self.node) - # add in the transform implied by the viewBox - viewbox_transform = get_viewbox_transform(self.node.getroottree().getroot()) - transform = simpletransform.composeTransform(viewbox_transform, transform) + @cache + def get_commands(self, command): + return [c for c in self.commands if c.command == command] - # apply the combined transform to this node's path - simpletransform.applyTransformToPath(transform, path) + @cache + def has_command(self, command): + return len(self.get_commands(command)) > 0 - return path + @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] @@ -228,22 +246,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) @@ -254,8 +260,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 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) 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 @@ -28,6 +30,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 # points. In order to make use of the existing logic for incorporating diff --git a/lib/elements/satin_column.py b/lib/elements/satin_column.py index 3593db64..2ceb38de 100644 --- a/lib/elements/satin_column.py +++ b/lib/elements/satin_column.py @@ -89,6 +89,17 @@ class SatinColumn(EmbroideryElement): @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): return self.parse_path() @@ -97,6 +108,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() diff --git a/lib/elements/stroke.py b/lib/elements/stroke.py index 48662b6d..e8eb4783 100644 --- a/lib/elements/stroke.py +++ b/lib/elements/stroke.py @@ -1,9 +1,11 @@ import sys +import shapely.geometry 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 @@ -50,6 +52,12 @@ class Stroke(EmbroideryElement): 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): return self.get_boolean_param('manual_stitch') @@ -57,10 +65,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, units = parse_length_with_units(self.get_style("stroke-width", "1")) if self.dashed: return True @@ -93,56 +98,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) - p0 = emb_point_list[0] - rho = 0.0 - side = 1 - last_segment_direction = None + # `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) - 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) + # Now move the points left and right. Consider each pair + # of points in turn, and move perpendicular to them, + # alternating left and right. - for segi in order: - p1 = emb_point_list[segi] + offset = stroke_width / 2.0 - # how far we have to go along segment - seg_len = (p1 - p0).length() - if (seg_len == 0): - continue + 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() - # vector pointing along segment - along = (p1 - p0).unit() + if i % 2 == 1: + zigzag_direction *= -1 - # vector pointing to edge of stroke width - perp = along.rotate_left() * (stroke_width * 0.5) + patch.stitches[i] += zigzag_direction * offset - 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) + return patch - # 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 + def running_stitch(self, path, stitch_length): + repeated_path = [] - p0 = p1 - last_segment_direction = along - rho -= seg_len + # 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[:] - if (p0 - patch.stitches[-1]).length() > 0.1: - patch.add_stitch(p0) + repeated_path.extend(this_path) + + stitches = running_stitch(repeated_path, stitch_length) + + return Patch(self.color, stitches) - return patch def to_patches(self, last_patch): patches = [] @@ -152,10 +151,11 @@ 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) - patches.append(patch) + if patch: + patches.append(patch) return patches |
