diff --git a/embroider.py b/embroider.py index f7e45746e..8c5d135b1 100644 --- a/embroider.py +++ b/embroider.py @@ -14,882 +14,18 @@ import sys import traceback sys.path.append("/usr/share/inkscape/extensions") import os -import subprocess -from copy import deepcopy -import time -from itertools import chain, izip, groupby -from collections import deque -import inkex -import simplepath -import simplestyle -import simpletransform -from bezmisc import bezierlength, beziertatlength, bezierpointatt -from cspsubdiv import cspsubdiv -import cubicsuperpath -import math -import lxml.etree as etree -import shapely.geometry as shgeo -import shapely.affinity as affinity -import shapely.ops -import networkx -from pprint import pformat +import inkex import inkstitch -from inkstitch import _, cache, dbg, param, EmbroideryElement, get_nodes, SVG_POLYLINE_TAG, SVG_GROUP_TAG, PIXELS_PER_MM, get_viewbox_transform -from inkstitch.stitches import running_stitch, auto_fill, legacy_fill +from inkstitch import _, PIXELS_PER_MM +from inkstitch.extensions import InkstitchExtension from inkstitch.stitch_plan import patches_to_stitch_plan from inkstitch.svg import render_stitch_plan -class Fill(EmbroideryElement): - element_name = _("Fill") +class Embroider(InkstitchExtension): def __init__(self, *args, **kwargs): - super(Fill, self).__init__(*args, **kwargs) - - @property - @param('auto_fill', _('Manually routed fill stitching'), type='toggle', inverse=True, default=True) - def auto_fill(self): - return self.get_boolean_param('auto_fill', True) - - @property - @param('angle', _('Angle of lines of stitches'), unit='deg', type='float', default=0) - @cache - def angle(self): - return math.radians(self.get_float_param('angle', 0)) - - @property - def color(self): - return self.get_style("fill") - - @property - @param('flip', _('Flip fill (start right-to-left)'), type='boolean', default=False) - def flip(self): - return self.get_boolean_param("flip", False) - - @property - @param('row_spacing_mm', _('Spacing between rows'), unit='mm', type='float', default=0.25) - def row_spacing(self): - return max(self.get_float_param("row_spacing_mm", 0.25), 0.01) - - @property - def end_row_spacing(self): - return self.get_float_param("end_row_spacing_mm") - - @property - @param('max_stitch_length_mm', _('Maximum fill stitch length'), unit='mm', type='float', default=3.0) - def max_stitch_length(self): - return max(self.get_float_param("max_stitch_length_mm", 3.0), 0.01) - - @property - @param('staggers', _('Stagger rows this many times before repeating'), type='int', default=4) - def staggers(self): - return self.get_int_param("staggers", 4) - - @property - @cache - def paths(self): - return self.flatten(self.parse_path()) - - @property - @cache - def shape(self): - poly_ary = [] - for sub_path in self.paths: - point_ary = [] - last_pt = None - for pt in sub_path: - if (last_pt is not None): - vp = (pt[0] - last_pt[0], pt[1] - last_pt[1]) - dp = math.sqrt(math.pow(vp[0], 2.0) + math.pow(vp[1], 2.0)) - # dbg.write("dp %s\n" % dp) - if (dp > 0.01): - # I think too-close points confuse shapely. - point_ary.append(pt) - last_pt = pt - else: - last_pt = pt - if point_ary: - poly_ary.append(point_ary) - - # shapely's idea of "holes" are to subtract everything in the second set - # from the first. So let's at least make sure the "first" thing is the - # biggest path. - # TODO: actually figure out which things are holes and which are shells - poly_ary.sort(key=lambda point_list: shgeo.Polygon(point_list).area, reverse=True) - - polygon = shgeo.MultiPolygon([(poly_ary[0], poly_ary[1:])]) - # print >> sys.stderr, "polygon valid:", polygon.is_valid - return polygon - - def to_patches(self, last_patch): - stitch_lists = legacy_fill(self.shape, - self.angle, - self.row_spacing, - self.end_row_spacing, - self.max_stitch_length, - self.flip, - self.staggers) - return [Patch(stitches=stitch_list, color=self.color) for stitch_list in stitch_lists] - - rows_of_segments = fill.intersect_region_with_grating(self.shape, self.angle, self.row_spacing, self.end_row_spacing, self.flip) - groups_of_segments = fill.pull_runs(rows_of_segments) - - return [fill.section_to_patch(group) for group in groups_of_segments] - - -class AutoFill(Fill): - element_name = _("Auto-Fill") - - @property - @param('auto_fill', _('Automatically routed fill stitching'), type='toggle', default=True) - def auto_fill(self): - return self.get_boolean_param('auto_fill', True) - - @property - @cache - def outline(self): - return self.shape.boundary[0] - - @property - @cache - def outline_length(self): - return self.outline.length - - @property - def flip(self): - return False - - @property - @param('running_stitch_length_mm', _('Running stitch length (traversal between sections)'), unit='mm', type='float', default=1.5) - def running_stitch_length(self): - return max(self.get_float_param("running_stitch_length_mm", 1.5), 0.01) - - @property - @param('fill_underlay', _('Underlay'), type='toggle', group=_('AutoFill Underlay'), default=False) - def fill_underlay(self): - return self.get_boolean_param("fill_underlay", default=False) - - @property - @param('fill_underlay_angle', _('Fill angle (default: fill angle + 90 deg)'), unit='deg', group=_('AutoFill Underlay'), type='float') - @cache - def fill_underlay_angle(self): - underlay_angle = self.get_float_param("fill_underlay_angle") - - if underlay_angle: - return math.radians(underlay_angle) - else: - return self.angle + math.pi / 2.0 - - @property - @param('fill_underlay_row_spacing_mm', _('Row spacing (default: 3x fill row spacing)'), unit='mm', group=_('AutoFill Underlay'), type='float') - @cache - def fill_underlay_row_spacing(self): - return self.get_float_param("fill_underlay_row_spacing_mm") or self.row_spacing * 3 - - @property - @param('fill_underlay_max_stitch_length_mm', _('Max stitch length'), unit='mm', group=_('AutoFill Underlay'), type='float') - @cache - def fill_underlay_max_stitch_length(self): - 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) - 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) - if not isinstance(shape, shgeo.MultiPolygon): - shape = shgeo.MultiPolygon([shape]) - return shape - else: - return self.shape - - def to_patches(self, last_patch): - stitches = [] - - if last_patch is None: - starting_point = None - else: - starting_point = last_patch.stitches[-1] - - if self.fill_underlay: - stitches.extend(auto_fill(self.underlay_shape, - self.fill_underlay_angle, - self.fill_underlay_row_spacing, - self.fill_underlay_row_spacing, - self.fill_underlay_max_stitch_length, - self.running_stitch_length, - self.staggers, - starting_point)) - starting_point = stitches[-1] - - stitches.extend(auto_fill(self.shape, - self.angle, - self.row_spacing, - self.end_row_spacing, - self.max_stitch_length, - self.running_stitch_length, - self.staggers, - starting_point)) - - return [Patch(stitches=stitches, color=self.color)] - - -class Stroke(EmbroideryElement): - element_name = "Stroke" - - @property - @param('satin_column', _('Satin stitch along paths'), type='toggle', inverse=True) - def satin_column(self): - return self.get_boolean_param("satin_column") - - @property - def color(self): - return self.get_style("stroke") - - @property - @cache - def width(self): - stroke_width = self.get_style("stroke-width") - - if stroke_width.endswith("px"): - stroke_width = stroke_width[:-2] - - return float(stroke_width) - - @property - def dashed(self): - return self.get_style("stroke-dasharray") is not None - - @property - @param('running_stitch_length_mm', _('Running stitch length'), unit='mm', type='float', default=1.5) - def running_stitch_length(self): - 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)'), unit='mm', type='float', default=0.4) - @cache - def zigzag_spacing(self): - return max(self.get_float_param("zigzag_spacing_mm", 0.4), 0.01) - - @property - @param('repeats', _('Repeats'), type='int', default="1") - def repeats(self): - return self.get_int_param("repeats", 1) - - @property - def paths(self): - return self.flatten(self.parse_path()) - - def is_running_stitch(self): - # stroke width <= 0.5 pixels is deprecated in favor of dashed lines - return self.dashed or self.width <= 0.5 - - def stroke_points(self, emb_point_list, zigzag_spacing, stroke_width): - patch = Patch(color=self.color) - p0 = emb_point_list[0] - rho = 0.0 - side = 1 - last_segment_direction = None - - 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 segi in order: - p1 = emb_point_list[segi] - - # how far we have to go along segment - seg_len = (p1 - p0).length() - if (seg_len == 0): - continue - - # vector pointing along segment - along = (p1 - p0).unit() - - # vector pointing to edge of stroke width - perp = along.rotate_left() * (stroke_width * 0.5) - - 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) - - # 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 - - p0 = p1 - last_segment_direction = along - rho -= seg_len - - if (p0 - patch.stitches[-1]).length() > 0.1: - patch.add_stitch(p0) - - return patch - - def to_patches(self, last_patch): - patches = [] - - for path in self.paths: - path = [inkstitch.Point(x, y) for x, y in path] - if self.is_running_stitch(): - patch = self.stroke_points(path, self.running_stitch_length, stroke_width=0.0) - else: - patch = self.stroke_points(path, self.zigzag_spacing / 2.0, stroke_width=self.width) - - patches.append(patch) - - return patches - - -class SatinColumn(EmbroideryElement): - element_name = _("Satin Column") - - def __init__(self, *args, **kwargs): - super(SatinColumn, self).__init__(*args, **kwargs) - - @property - @param('satin_column', _('Custom satin column'), type='toggle') - def satin_column(self): - return self.get_boolean_param("satin_column") - - @property - def color(self): - return self.get_style("stroke") - - @property - @param('zigzag_spacing_mm', _('Zig-zag spacing (peak-to-peak)'), unit='mm', type='float', default=0.4) - def zigzag_spacing(self): - # peak-to-peak distance between zigzags - return max(self.get_float_param("zigzag_spacing_mm", 0.4), 0.01) - - @property - @param('pull_compensation_mm', _('Pull compensation'), unit='mm', type='float') - def pull_compensation(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('contour_underlay', _('Contour underlay'), type='toggle', group=_('Contour Underlay')) - def contour_underlay(self): - # "Contour underlay" is stitching just inside the rectangular shape - # of the satin column; that is, up one side and down the other. - return self.get_boolean_param("contour_underlay") - - @property - @param('contour_underlay_stitch_length_mm', _('Stitch length'), unit='mm', group=_('Contour Underlay'), type='float', default=1.5) - def contour_underlay_stitch_length(self): - return max(self.get_float_param("contour_underlay_stitch_length_mm", 1.5), 0.01) - - @property - @param('contour_underlay_inset_mm', _('Contour underlay inset amount'), unit='mm', group=_('Contour Underlay'), type='float', default=0.4) - def contour_underlay_inset(self): - # how far inside the edge of the column to stitch the underlay - return self.get_float_param("contour_underlay_inset_mm", 0.4) - - @property - @param('center_walk_underlay', _('Center-walk underlay'), type='toggle', group=_('Center-Walk Underlay')) - def center_walk_underlay(self): - # "Center walk underlay" is stitching down and back in the centerline - # between the two sides of the satin column. - return self.get_boolean_param("center_walk_underlay") - - @property - @param('center_walk_underlay_stitch_length_mm', _('Stitch length'), unit='mm', group=_('Center-Walk Underlay'), type='float', default=1.5) - def center_walk_underlay_stitch_length(self): - return max(self.get_float_param("center_walk_underlay_stitch_length_mm", 1.5), 0.01) - - @property - @param('zigzag_underlay', _('Zig-zag underlay'), type='toggle', group=_('Zig-zag Underlay')) - def zigzag_underlay(self): - return self.get_boolean_param("zigzag_underlay") - - @property - @param('zigzag_underlay_spacing_mm', _('Zig-Zag spacing (peak-to-peak)'), unit='mm', group=_('Zig-zag Underlay'), type='float', default=3) - def zigzag_underlay_spacing(self): - return max(self.get_float_param("zigzag_underlay_spacing_mm", 3), 0.01) - - @property - @param('zigzag_underlay_inset_mm', _('Inset amount (default: half of contour underlay inset)'), unit='mm', group=_('Zig-zag Underlay'), type='float') - def zigzag_underlay_inset(self): - # how far in from the edge of the satin the points in the zigzags - # should be - - # Default to half of the contour underlay inset. That is, if we're - # 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 - - @property - @cache - def csp(self): - return self.parse_path() - - @property - @cache - def flattened_beziers(self): - if len(self.csp) == 2: - return self.simple_flatten_beziers() - else: - return self.flatten_beziers_with_rungs() - - - def flatten_beziers_with_rungs(self): - input_paths = [self.flatten([path]) for path in self.csp] - input_paths = [shgeo.LineString(path[0]) for path in input_paths] - - paths = input_paths[:] - paths.sort(key=lambda path: path.length, reverse=True) - - # Imagine a satin column as a curvy ladder. - # The two long paths are the "rails" of the ladder. The remainder are - # the "rungs". - rails = paths[:2] - rungs = shgeo.MultiLineString(paths[2:]) - - # The rails should stay in the order they were in the original CSP. - # (this lets the user control where the satin starts and ends) - rails.sort(key=lambda rail: input_paths.index(rail)) - - result = [] - - for rail in rails: - if not rail.is_simple: - self.fatal(_("One or more rails crosses itself, and this is not allowed. Please split into multiple satin columns.")) - - # handle null intersections here? - linestrings = shapely.ops.split(rail, rungs) - - print >> dbg, "rails and rungs", [str(rail) for rail in rails], [str(rung) for rung in rungs] - if len(linestrings.geoms) < len(rungs.geoms) + 1: - self.fatal(_("satin column: One or more of the rungs doesn't intersect both rails.") + " " + _("Each rail should intersect both rungs once.")) - elif len(linestrings.geoms) > len(rungs.geoms) + 1: - self.fatal(_("satin column: One or more of the rungs intersects the rails more than once.") + " " + _("Each rail should intersect both rungs once.")) - - paths = [[inkstitch.Point(*coord) for coord in ls.coords] for ls in linestrings.geoms] - result.append(paths) - - return zip(*result) - - - def simple_flatten_beziers(self): - # Given a pair of paths made up of bezier segments, flatten - # each individual bezier segment into line segments that approximate - # the curves. Retain the divisions between beziers -- we'll use those - # later. - - paths = [] - - for path in self.csp: - # See the documentation in the parent class for parse_path() for a - # description of the format of the CSP. Each bezier is constructed - # using two neighboring 3-tuples in the list. - - flattened_path = [] - - # iterate over pairs of 3-tuples - for prev, current in zip(path[:-1], path[1:]): - flattened_segment = self.flatten([[prev, current]]) - flattened_segment = [inkstitch.Point(x, y) for x, y in flattened_segment[0]] - flattened_path.append(flattened_segment) - - paths.append(flattened_path) - - return zip(*paths) - - def validate_satin_column(self): - # The node should have exactly two paths with no fill. Each - # path should have the same number of points, meaning that they - # will both be made up of the same number of bezier curves. - - node_id = self.node.get("id") - - if self.get_style("fill") is not None: - self.fatal(_("satin column: object %s has a fill (but should not)") % node_id) - - if len(self.csp) == 2: - if len(self.csp[0]) != len(self.csp[1]): - self.fatal(_("satin column: object %(id)s has two paths with an unequal number of points (%(length1)d and %(length2)d)") % \ - dict(id=node_id, length1=len(self.csp[0]), length2=len(self.csp[1]))) - - def offset_points(self, pos1, pos2, offset_px): - # Expand or contract two points about their midpoint. This is - # useful for pull compensation and insetting underlay. - - distance = (pos1 - pos2).length() - - if distance < 0.0001: - # if they're the same point, we don't know which direction - # to offset in, so we have to just return the points - return pos1, pos2 - - # don't contract beyond the midpoint, or we'll start expanding - if offset_px < -distance / 2.0: - offset_px = -distance / 2.0 - - pos1 = pos1 + (pos1 - pos2).unit() * offset_px - pos2 = pos2 + (pos2 - pos1).unit() * offset_px - - return pos1, pos2 - - def walk(self, path, start_pos, start_index, distance): - # Move pixels along , which is a sequence of line - # segments defined by points. - - # is the index of the line segment in that - # we're currently on. is where along that line - # segment we are. Return a new position and index. - - # print >> dbg, "walk", start_pos, start_index, distance - - pos = start_pos - index = start_index - last_index = len(path) - 1 - distance_remaining = distance - - while True: - if index >= last_index: - return pos, index - - segment_end = path[index + 1] - segment = segment_end - pos - segment_length = segment.length() - - if segment_length > distance_remaining: - # our walk ends partway along this segment - return pos + segment.unit() * distance_remaining, index - else: - # our walk goes past the end of this segment, so advance - # one point - index += 1 - distance_remaining -= segment_length - pos = segment_end - - def walk_paths(self, spacing, offset): - # Take a bezier segment from each path in turn, and plot out an - # equal number of points on each bezier. Return the points plotted. - # The points will be contracted or expanded by offset using - # offset_points(). - - points = [[], []] - - def add_pair(pos1, pos2): - pos1, pos2 = self.offset_points(pos1, pos2, offset) - points[0].append(pos1) - points[1].append(pos2) - - # We may not be able to fit an even number of zigzags in each pair of - # beziers. We'll store the remaining bit of the beziers after handling - # each section. - remainder_path1 = [] - remainder_path2 = [] - - for segment1, segment2 in self.flattened_beziers: - subpath1 = remainder_path1 + segment1 - subpath2 = remainder_path2 + segment2 - - len1 = shgeo.LineString(subpath1).length - len2 = shgeo.LineString(subpath2).length - - # Base the number of stitches in each section on the _longest_ of - # the two beziers. Otherwise, things could get too sparse when one - # side is significantly longer (e.g. when going around a corner). - # The risk here is that we poke a hole in the fabric if we try to - # cram too many stitches on the short bezier. The user will need - # to avoid this through careful construction of paths. - # - # TODO: some commercial machine embroidery software compensates by - # pulling in some of the "inner" stitches toward the center a bit. - - # note, this rounds down using integer-division - num_points = max(len1, len2) / spacing - - spacing1 = len1 / num_points - spacing2 = len2 / num_points - - pos1 = subpath1[0] - index1 = 0 - - pos2 = subpath2[0] - index2 = 0 - - for i in xrange(int(num_points)): - add_pair(pos1, pos2) - - pos1, index1 = self.walk(subpath1, pos1, index1, spacing1) - pos2, index2 = self.walk(subpath2, pos2, index2, spacing2) - - if index1 < len(subpath1) - 1: - remainder_path1 = [pos1] + subpath1[index1 + 1:] - else: - remainder_path1 = [] - - if index2 < len(subpath2) - 1: - remainder_path2 = [pos2] + subpath2[index2 + 1:] - else: - remainder_path2 = [] - - # We're off by one in the algorithm above, so we need one more - # pair of points. We also want to add points at the very end to - # make sure we match the vectors on screen as best as possible. - # Try to avoid doing both if they're going to stack up too - # closely. - - end1 = remainder_path1[-1] - end2 = remainder_path2[-1] - - if (end1 - pos1).length() > 0.3 * spacing: - add_pair(pos1, pos2) - - add_pair(end1, end2) - - return points - - def do_contour_underlay(self): - # "contour walk" underlay: do stitches up one side and down the - # other. - forward, back = self.walk_paths(self.contour_underlay_stitch_length, - -self.contour_underlay_inset) - return Patch(color=self.color, stitches=(forward + list(reversed(back)))) - - def do_center_walk(self): - # Center walk underlay is just a running stitch down and back on the - # center line between the bezier curves. - - # Do it like contour underlay, but inset all the way to the center. - forward, back = self.walk_paths(self.center_walk_underlay_stitch_length, - -100000) - return Patch(color=self.color, stitches=(forward + list(reversed(back)))) - - def do_zigzag_underlay(self): - # zigzag underlay, usually done at a much lower density than the - # satin itself. It looks like this: - # - # \/\/\/\/\/\/\/\/\/\/| - # /\/\/\/\/\/\/\/\/\/\| - # - # In combination with the "contour walk" underlay, this is the - # "German underlay" described here: - # http://www.mrxstitch.com/underlay-what-lies-beneath-machine-embroidery/ - - patch = Patch(color=self.color) - - sides = self.walk_paths(self.zigzag_underlay_spacing / 2.0, - -self.zigzag_underlay_inset) - - # This organizes the points in each side in the order that they'll be - # visited. - sides = [sides[0][::2] + list(reversed(sides[0][1::2])), - sides[1][1::2] + list(reversed(sides[1][::2]))] - - # This fancy bit of iterable magic just repeatedly takes a point - # from each side in turn. - for point in chain.from_iterable(izip(*sides)): - patch.add_stitch(point) - - return patch - - def do_satin(self): - # satin: do a zigzag pattern, alternating between the paths. The - # zigzag looks like this to make the satin stitches look perpendicular - # to the column: - # - # /|/|/|/|/|/|/|/| - - # print >> dbg, "satin", self.zigzag_spacing, self.pull_compensation - - patch = Patch(color=self.color) - - sides = self.walk_paths(self.zigzag_spacing, self.pull_compensation) - - # Like in zigzag_underlay(): take a point from each side in turn. - for point in chain.from_iterable(izip(*sides)): - patch.add_stitch(point) - - return patch - - def to_patches(self, last_patch): - # Stitch a variable-width satin column, zig-zagging between two paths. - - # The algorithm will draw zigzags between each consecutive pair of - # beziers. The boundary points between beziers serve as "checkpoints", - # allowing the user to control how the zigzags flow around corners. - - # First, verify that we have valid paths. - self.validate_satin_column() - - patches = [] - - if self.center_walk_underlay: - patches.append(self.do_center_walk()) - - if self.contour_underlay: - patches.append(self.do_contour_underlay()) - - if self.zigzag_underlay: - # zigzag underlay comes after contour walk underlay, so that the - # zigzags sit on the contour walk underlay like rail ties on rails. - patches.append(self.do_zigzag_underlay()) - - patches.append(self.do_satin()) - - return patches - - -class Polyline(EmbroideryElement): - # Handle a element, which is treated as a set of points to - # stitch exactly. - # - # elements are pretty rare in SVG, from what I can tell. - # Anything you can do with a can also be done with a

, and - # much more. - # - # Notably, EmbroiderModder2 uses elements when converting from - # common machine embroidery file formats to SVG. Handling those here lets - # users use File -> Import to pull in existing designs they may have - # obtained, for example purchased fonts. - - @property - def points(self): - # example: "1,2 0,0 1.5,3 4,2" - - points = self.node.get('points') - points = points.split(" ") - points = [[float(coord) for coord in point.split(",")] for point in points] - - return 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 - # svg transforms that is in our superclass, we'll convert the polyline - # to a degenerate cubic superpath in which the bezier handles are on - # the segment endpoints. - - path = [[[point[:], point[:], point[:]] for point in self.points]] - - return path - - @property - @cache - def csp(self): - csp = self.parse_path() - - return csp - - @property - def color(self): - # EmbroiderModder2 likes to use the `stroke` property directly instead - # of CSS. - return self.get_style("stroke") or self.node.get("stroke") - - @property - def stitches(self): - # For a , we'll stitch the points exactly as they exist in - # the SVG, with no stitch spacing interpolation, flattening, etc. - - # See the comments in the parent class's parse_path method for a - # description of the CSP data structure. - - stitches = [point for handle_before, point, handle_after in self.csp[0]] - - return stitches - - def to_patches(self, last_patch): - patch = Patch(color=self.color) - - for stitch in self.stitches: - patch.add_stitch(inkstitch.Point(*stitch)) - - return [patch] - -def detect_classes(node): - if node.tag == SVG_POLYLINE_TAG: - return [Polyline] - else: - element = EmbroideryElement(node) - - if element.get_boolean_param("satin_column"): - return [SatinColumn] - else: - classes = [] - - if element.get_style("fill"): - if element.get_boolean_param("auto_fill", True): - classes.append(AutoFill) - else: - classes.append(Fill) - - if element.get_style("stroke"): - classes.append(Stroke) - - if element.get_boolean_param("stroke_first", False): - classes.reverse() - - return classes - - -class Patch: - # TODO: merge this with inkstitch.stitch_plan.ColorBlock - def __init__(self, color=None, stitches=None, trim_after=False, stop_after=False): - self.color = color - self.stitches = stitches or [] - self.trim_after = trim_after - self.stop_after = stop_after - - def __add__(self, other): - if isinstance(other, Patch): - return Patch(self.color, self.stitches + other.stitches) - else: - raise TypeError("Patch can only be added to another Patch") - - def add_stitch(self, stitch): - self.stitches.append(stitch) - - def reverse(self): - return Patch(self.color, self.stitches[::-1]) - -def get_elements(effect): - elements = [] - nodes = get_nodes(effect) - - for node in nodes: - classes = detect_classes(node) - elements.extend(cls(node) for cls in classes) - - return elements - - -def elements_to_patches(elements): - patches = [] - for element in elements: - if patches: - last_patch = patches[-1] - else: - last_patch = None - - patches.extend(element.embroider(last_patch)) - - return patches - -class Embroider(inkex.Effect): - - def __init__(self, *args, **kwargs): - inkex.Effect.__init__(self) + InkstitchExtension.__init__(self) self.OptionParser.add_option("-c", "--collapse_len_mm", action="store", type="float", dest="collapse_length_mm", default=3.0, @@ -951,37 +87,18 @@ class Embroider(inkex.Effect): return output_path - def hide_layers(self): - for g in self.document.getroot().findall(SVG_GROUP_TAG): - if g.get(inkex.addNS("groupmode", "inkscape")) == "layer": - g.set("style", "display:none") - def effect(self): - # Printing anything other than a valid SVG on stdout blows inkscape up. - old_stdout = sys.stdout - sys.stdout = sys.stderr - - self.patch_list = [] - - self.elements = get_elements(self) - - if not self.elements: - if self.selected: - inkex.errormsg(_("No embroiderable paths selected.")) - else: - inkex.errormsg(_("No embroiderable paths found in document.")) - inkex.errormsg(_("Tip: use Path -> Object to Path to convert non-paths before embroidering.")) + if not self.get_elements(): return if self.options.hide_layers: - self.hide_layers() + self.hide_all_layers() - patches = elements_to_patches(self.elements) + patches = self.elements_to_patches(self.elements) stitch_plan = patches_to_stitch_plan(patches, self.options.collapse_length_mm * PIXELS_PER_MM) inkstitch.write_embroidery_file(self.get_output_path(), stitch_plan, self.document.getroot()) render_stitch_plan(self.document.getroot(), stitch_plan) - sys.stdout = old_stdout if __name__ == '__main__': sys.setrecursionlimit(100000) @@ -990,8 +107,6 @@ if __name__ == '__main__': try: e.affect() except KeyboardInterrupt: - print >> dbg, "interrupted!" - - print >> dbg, traceback.format_exc() - - dbg.flush() + # for use at the command prompt for debugging + print >> sys.stderr, "interrupted!" + print >> sys.stderr, traceback.format_exc() diff --git a/embroider_params.py b/embroider_params.py index 73c6f479c..5b4c4622e 100644 --- a/embroider_params.py +++ b/embroider_params.py @@ -12,12 +12,13 @@ from cStringIO import StringIO import wx from wx.lib.scrolledpanel import ScrolledPanel from collections import defaultdict -import inkex import inkstitch -from inkstitch import _, Param, EmbroideryElement, get_nodes -from embroider import Fill, AutoFill, Stroke, SatinColumn from functools import partial from itertools import groupby +from inkstitch import _ +from inkstitch.extensions import InkstitchExtension +from inkstitch.stitch_plan import patches_to_stitch_plan +from inkstitch.elements import EmbroideryElement, Fill, AutoFill, Stroke, SatinColumn from embroider_simulate import EmbroiderySimulator @@ -412,9 +413,10 @@ class SettingsFrame(wx.Frame): wx.CallAfter(self.refresh_simulator, patches) def refresh_simulator(self, patches): + stitch_plan = patches_to_stitch_plan(patches) if self.simulate_window: self.simulate_window.stop() - self.simulate_window.load(patches=patches) + self.simulate_window.load(stitch_plan=stitch_plan) else: my_rect = self.GetRect() simulator_pos = my_rect.GetTopRight() @@ -428,7 +430,7 @@ class SettingsFrame(wx.Frame): self.simulate_window = EmbroiderySimulator(None, -1, _("Preview"), simulator_pos, size=(300, 300), - patches=patches, + stitch_plan=stitch_plan, on_close=self.simulate_window_closed, target_duration=5, max_width=max_width, @@ -631,10 +633,10 @@ class SettingsFrame(wx.Frame): self.Layout() # end wxGlade -class EmbroiderParams(inkex.Effect): +class EmbroiderParams(InkstitchExtension): def __init__(self, *args, **kwargs): self.cancelled = False - inkex.Effect.__init__(self, *args, **kwargs) + InkstitchExtension.__init__(self, *args, **kwargs) def embroidery_classes(self, node): element = EmbroideryElement(node) @@ -653,7 +655,7 @@ class EmbroiderParams(inkex.Effect): return classes def get_nodes_by_class(self): - nodes = get_nodes(self) + nodes = self.get_nodes() nodes_by_class = defaultdict(list) for z, node in enumerate(nodes): diff --git a/embroider_simulate.py b/embroider_simulate.py index 862d4b3da..8a8070dc2 100644 --- a/embroider_simulate.py +++ b/embroider_simulate.py @@ -8,15 +8,15 @@ import colorsys from itertools import izip import inkstitch +from inkstitch.extensions import InkstitchExtension from inkstitch import PIXELS_PER_MM -from embroider import _, patches_to_stitch_plan, get_elements, elements_to_patches +from inkstitch.stitch_plan import patches_to_stitch_plan from inkstitch.svg import color_block_to_point_lists class EmbroiderySimulator(wx.Frame): def __init__(self, *args, **kwargs): - stitch_file = kwargs.pop('stitch_file', None) - patches = kwargs.pop('patches', None) + stitch_plan = kwargs.pop('stitch_plan', None) self.on_close_hook = kwargs.pop('on_close', None) self.frame_period = kwargs.pop('frame_period', 80) self.stitches_per_frame = kwargs.pop('stitches_per_frame', 1) @@ -34,7 +34,7 @@ class EmbroiderySimulator(wx.Frame): self.panel = wx.Panel(self, wx.ID_ANY) self.panel.SetFocus() - self.load(stitch_file, patches) + self.load(stitch_plan) if self.target_duration: self.adjust_speed(self.target_duration) @@ -56,13 +56,10 @@ class EmbroiderySimulator(wx.Frame): self.Bind(wx.EVT_CLOSE, self.on_close) - def load(self, stitch_file=None, patches=None): - if stitch_file: - self.mirror = True - self.segments = self._parse_stitch_file(stitch_file) - elif patches: + def load(self, stitch_plan=None): + if stitch_plan: self.mirror = False - self.segments = self._patches_to_segments(patches) + self.segments = self._stitch_plan_to_segments(stitch_plan) else: return @@ -135,19 +132,13 @@ class EmbroiderySimulator(wx.Frame): return wx.Pen(color) - def _patches_to_segments(self, patches): - stitch_plan = patches_to_stitch_plan(patches) - + def _stitch_plan_to_segments(self, stitch_plan): segments = [] for color_block in stitch_plan: pen = self.color_to_pen(color_block.color) for point_list in color_block_to_point_lists(color_block): - # The polyline is made up of a set of points denoting the - # vertices along the path. We need to break this up into - # individual line segments to be drawn on the screen. - # if there's only one point, there's nothing to do, so skip if len(point_list) < 2: continue @@ -283,18 +274,22 @@ class EmbroiderySimulator(wx.Frame): except IndexError: self.timer.Stop() -class SimulateEffect(inkex.Effect): +class SimulateEffect(InkstitchExtension): def __init__(self): - inkex.Effect.__init__(self) + InkstitchExtension.__init__(self) self.OptionParser.add_option("-P", "--path", action="store", type="string", dest="path", default=".", help="Directory in which to store output file") def effect(self): - patches = elements_to_patches(get_elements(self)) + if not self.get_elements(): + return + + patches = self.elements_to_patches(self.elements) + stitch_plan = patches_to_stitch_plan(patches) app = wx.App() - frame = EmbroiderySimulator(None, -1, _("Embroidery Simulation"), wx.DefaultPosition, size=(1000, 1000), patches=patches) + frame = EmbroiderySimulator(None, -1, _("Embroidery Simulation"), wx.DefaultPosition, size=(1000, 1000), stitch_plan=stitch_plan) app.SetTopWindow(frame) frame.Show() wx.CallAfter(frame.go) diff --git a/inkstitch/__init__.py b/inkstitch/__init__.py index 149858ef0..a09d31e51 100644 --- a/inkstitch/__init__.py +++ b/inkstitch/__init__.py @@ -7,6 +7,9 @@ import gettext from copy import deepcopy import math import libembroidery +from inkstitch.utils import cache +from inkstitch.utils.geometry import Point + import inkex import simplepath import simplestyle @@ -17,11 +20,6 @@ import cubicsuperpath from shapely import geometry as shgeo -try: - from functools import lru_cache -except ImportError: - from backports.functools_lru_cache import lru_cache - # modern versions of Inkscape use 96 pixels per inch as per the CSS standard PIXELS_PER_MM = 96 / 25.4 @@ -38,9 +36,6 @@ dbg = open(os.devnull, "w") _ = lambda message: message -# simplify use of lru_cache decorator -def cache(*args, **kwargs): - return lru_cache(maxsize=None)(*args, **kwargs) def localize(): if getattr(sys, 'frozen', False): @@ -157,277 +152,6 @@ def get_viewbox_transform(node): return transform -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 - self.description = description - self.unit = unit - self.values = values or [""] - self.type = type - self.group = group - self.inverse = inverse - self.default = default - self.tooltip = tooltip - self.sort_index = sort_index - - def __repr__(self): - return "Param(%s)" % vars(self) - - -# Decorate a member function or property with information about -# the embroidery parameter it corresponds to -def param(*args, **kwargs): - p = Param(*args, **kwargs) - - def decorator(func): - func.param = p - return func - - return decorator - - -class EmbroideryElement(object): - def __init__(self, node): - self.node = node - - @property - def id(self): - return self.node.get('id') - - @classmethod - def get_params(cls): - params = [] - for attr in dir(cls): - prop = getattr(cls, attr) - if isinstance(prop, property): - # The 'param' attribute is set by the 'param' decorator defined above. - if hasattr(prop.fget, 'param'): - params.append(prop.fget.param) - - return params - - @cache - def get_param(self, param, default): - value = self.node.get("embroider_" + param, "").strip() - - return value or default - - @cache - def get_boolean_param(self, param, default=None): - value = self.get_param(param, default) - - if isinstance(value, bool): - return value - else: - return value and (value.lower() in ('yes', 'y', 'true', 't', '1')) - - @cache - def get_float_param(self, param, default=None): - try: - value = float(self.get_param(param, default)) - except (TypeError, ValueError): - return default - - if param.endswith('_mm'): - value = value * PIXELS_PER_MM - - return value - - @cache - def get_int_param(self, param, default=None): - try: - value = int(self.get_param(param, default)) - except (TypeError, ValueError): - return default - - if param.endswith('_mm'): - value = int(value * PIXELS_PER_MM) - - return value - - def set_param(self, name, value): - self.node.set("embroider_%s" % name, str(value)) - - @cache - def get_style(self, style_name): - style = simplestyle.parseStyle(self.node.get("style")) - if (style_name not in style): - return None - value = style[style_name] - if value == 'none': - return None - return value - - @cache - def has_style(self, style_name): - style = simplestyle.parseStyle(self.node.get("style")) - return style_name in style - - @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. - # - # A "superpath" is a collection of paths that are all in one object. - # - # The "cubic" bit in "cubic superpath" is because the bezier curves - # inkscape uses involve cubic polynomials. - # - # Each path is a collection of tuples, each of the form: - # - # (control_before, point, control_after) - # - # A bezier curve segment is defined by an endpoint, a control point, - # a second control point, and a final endpoint. A path is a bunch of - # bezier curves strung together. One could represent a path as a set - # of four-tuples, but there would be redundancy because the ending - # point of one bezier is the starting point of the next. Instead, a - # path is a set of 3-tuples as shown above, and one must construct - # each bezier curve by taking the appropriate endpoints and control - # points. Bleh. It should be noted that a straight segment is - # represented by having the control point on each end equal to that - # end's point. - # - # 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) - - # apply the combined transform to this node's path - simpletransform.applyTransformToPath(transform, path) - - - return path - - def flatten(self, path): - """approximate a path containing beziers with a series of points""" - - path = deepcopy(path) - - cspsubdiv(path, 0.1) - - flattened = [] - - for comp in path: - vertices = [] - for ctl in comp: - vertices.append((ctl[1][0], ctl[1][1])) - flattened.append(vertices) - - return flattened - - @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) - - def to_patches(self, last_patch): - raise NotImplementedError("%s must implement to_patches()" % self.__class__.__name__) - - def embroider(self, last_patch): - patches = self.to_patches(last_patch) - - if patches: - patches[-1].trim_after = self.trim_after - patches[-1].stop_after = self.stop_after - - return patches - - def fatal(self, message): - print >> sys.stderr, "error:", message - sys.exit(1) - - -class Point: - def __init__(self, x, y): - self.x = x - self.y = y - - def __add__(self, other): - return Point(self.x + other.x, self.y + other.y) - - def __sub__(self, other): - return Point(self.x - other.x, self.y - other.y) - - def mul(self, scalar): - return Point(self.x * scalar, self.y * scalar) - - def __mul__(self, other): - if isinstance(other, Point): - # dot product - return self.x * other.x + self.y * other.y - elif isinstance(other, (int, float)): - return Point(self.x * other, self.y * other) - else: - raise ValueError("cannot multiply Point by %s" % type(other)) - - def __rmul__(self, other): - if isinstance(other, (int, float)): - return self.__mul__(other) - else: - raise ValueError("cannot multiply Point by %s" % type(other)) - - def __repr__(self): - return "Point(%s,%s)" % (self.x, self.y) - - def length(self): - return math.sqrt(math.pow(self.x, 2.0) + math.pow(self.y, 2.0)) - - def unit(self): - return self.mul(1.0 / self.length()) - - def rotate_left(self): - return Point(-self.y, self.x) - - def rotate(self, angle): - return Point(self.x * math.cos(angle) - self.y * math.sin(angle), self.y * math.cos(angle) + self.x * math.sin(angle)) - - def as_int(self): - return Point(int(round(self.x)), int(round(self.y))) - - def as_tuple(self): - return (self.x, self.y) - - def __cmp__(self, other): - return cmp(self.as_tuple(), other.as_tuple()) - - def __getitem__(self, item): - return self.as_tuple()[item] - - def __len__(self): - return 2 - class Stitch(Point): def __init__(self, x, y, color=None, jump=False, stop=False, trim=False): @@ -442,42 +166,6 @@ class Stitch(Point): return "Stitch(%s, %s, %s, %s, %s, %s)" % (self.x, self.y, self.color, "JUMP" if self.jump else "", "TRIM" if self.trim else "", "STOP" if self.stop else "") -def descendants(node): - nodes = [] - element = EmbroideryElement(node) - - if element.has_style('display') and element.get_style('display') is None: - return [] - - if node.tag == SVG_DEFS_TAG: - return [] - - for child in node: - nodes.extend(descendants(child)) - - if node.tag in EMBROIDERABLE_TAGS: - nodes.append(node) - - return nodes - - -def get_nodes(effect): - """Get all XML nodes, or just those selected - - effect is an instance of a subclass of inkex.Effect. - """ - - if effect.selected: - nodes = [] - for node in effect.document.getroot().iter(): - if node.get("id") in effect.selected: - nodes.extend(descendants(node)) - else: - nodes = descendants(effect.document.getroot()) - - return nodes - - def make_thread(color): # strip off the leading "#" if color.startswith("#"): diff --git a/inkstitch/elements/__init__.py b/inkstitch/elements/__init__.py new file mode 100644 index 000000000..6f949479a --- /dev/null +++ b/inkstitch/elements/__init__.py @@ -0,0 +1,5 @@ +from auto_fill import AutoFill +from fill import Fill +from stroke import Stroke +from satin_column import SatinColumn +from element import EmbroideryElement diff --git a/inkstitch/elements/auto_fill.py b/inkstitch/elements/auto_fill.py new file mode 100644 index 000000000..83fa8c0b0 --- /dev/null +++ b/inkstitch/elements/auto_fill.py @@ -0,0 +1,107 @@ +from .. import _ +from .element import param, Patch +from ..utils import cache +from .fill import Fill +from shapely import geometry as shgeo +from ..stitches import auto_fill + + +class AutoFill(Fill): + element_name = _("Auto-Fill") + + @property + @param('auto_fill', _('Automatically routed fill stitching'), type='toggle', default=True) + def auto_fill(self): + return self.get_boolean_param('auto_fill', True) + + @property + @cache + def outline(self): + return self.shape.boundary[0] + + @property + @cache + def outline_length(self): + return self.outline.length + + @property + def flip(self): + return False + + @property + @param('running_stitch_length_mm', _('Running stitch length (traversal between sections)'), unit='mm', type='float', default=1.5) + def running_stitch_length(self): + return max(self.get_float_param("running_stitch_length_mm", 1.5), 0.01) + + @property + @param('fill_underlay', _('Underlay'), type='toggle', group=_('AutoFill Underlay'), default=False) + def fill_underlay(self): + return self.get_boolean_param("fill_underlay", default=False) + + @property + @param('fill_underlay_angle', _('Fill angle (default: fill angle + 90 deg)'), unit='deg', group=_('AutoFill Underlay'), type='float') + @cache + def fill_underlay_angle(self): + underlay_angle = self.get_float_param("fill_underlay_angle") + + if underlay_angle: + return math.radians(underlay_angle) + else: + return self.angle + math.pi / 2.0 + + @property + @param('fill_underlay_row_spacing_mm', _('Row spacing (default: 3x fill row spacing)'), unit='mm', group=_('AutoFill Underlay'), type='float') + @cache + def fill_underlay_row_spacing(self): + return self.get_float_param("fill_underlay_row_spacing_mm") or self.row_spacing * 3 + + @property + @param('fill_underlay_max_stitch_length_mm', _('Max stitch length'), unit='mm', group=_('AutoFill Underlay'), type='float') + @cache + def fill_underlay_max_stitch_length(self): + 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) + 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) + if not isinstance(shape, shgeo.MultiPolygon): + shape = shgeo.MultiPolygon([shape]) + return shape + else: + return self.shape + + def to_patches(self, last_patch): + stitches = [] + + if last_patch is None: + starting_point = None + else: + starting_point = last_patch.stitches[-1] + + if self.fill_underlay: + stitches.extend(auto_fill(self.underlay_shape, + self.fill_underlay_angle, + self.fill_underlay_row_spacing, + self.fill_underlay_row_spacing, + self.fill_underlay_max_stitch_length, + self.running_stitch_length, + self.staggers, + starting_point)) + starting_point = stitches[-1] + + stitches.extend(auto_fill(self.shape, + self.angle, + self.row_spacing, + self.end_row_spacing, + self.max_stitch_length, + self.running_stitch_length, + self.staggers, + starting_point)) + + return [Patch(stitches=stitches, color=self.color)] diff --git a/inkstitch/elements/element.py b/inkstitch/elements/element.py new file mode 100644 index 000000000..533734705 --- /dev/null +++ b/inkstitch/elements/element.py @@ -0,0 +1,245 @@ +import sys +from copy import deepcopy + +from ..utils import cache +from shapely import geometry as shgeo +from .. import _, PIXELS_PER_MM, get_viewbox_transform + +# inkscape-provided utilities +import simpletransform +import simplestyle +import cubicsuperpath +from cspsubdiv import cspsubdiv + +class Patch: + # TODO: merge this with inkstitch.stitch_plan.ColorBlock + def __init__(self, color=None, stitches=None, trim_after=False, stop_after=False): + self.color = color + self.stitches = stitches or [] + self.trim_after = trim_after + self.stop_after = stop_after + + def __add__(self, other): + if isinstance(other, Patch): + return Patch(self.color, self.stitches + other.stitches) + else: + raise TypeError("Patch can only be added to another Patch") + + def add_stitch(self, stitch): + self.stitches.append(stitch) + + def reverse(self): + 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 + self.description = description + self.unit = unit + self.values = values or [""] + self.type = type + self.group = group + self.inverse = inverse + self.default = default + self.tooltip = tooltip + self.sort_index = sort_index + + def __repr__(self): + return "Param(%s)" % vars(self) + + +# Decorate a member function or property with information about +# the embroidery parameter it corresponds to +def param(*args, **kwargs): + p = Param(*args, **kwargs) + + def decorator(func): + func.param = p + return func + + return decorator + + +class EmbroideryElement(object): + def __init__(self, node): + self.node = node + + @property + def id(self): + return self.node.get('id') + + @classmethod + def get_params(cls): + params = [] + for attr in dir(cls): + prop = getattr(cls, attr) + if isinstance(prop, property): + # The 'param' attribute is set by the 'param' decorator defined above. + if hasattr(prop.fget, 'param'): + params.append(prop.fget.param) + + return params + + @cache + def get_param(self, param, default): + value = self.node.get("embroider_" + param, "").strip() + + return value or default + + @cache + def get_boolean_param(self, param, default=None): + value = self.get_param(param, default) + + if isinstance(value, bool): + return value + else: + return value and (value.lower() in ('yes', 'y', 'true', 't', '1')) + + @cache + def get_float_param(self, param, default=None): + try: + value = float(self.get_param(param, default)) + except (TypeError, ValueError): + return default + + if param.endswith('_mm'): + value = value * PIXELS_PER_MM + + return value + + @cache + def get_int_param(self, param, default=None): + try: + value = int(self.get_param(param, default)) + except (TypeError, ValueError): + return default + + if param.endswith('_mm'): + value = int(value * PIXELS_PER_MM) + + return value + + def set_param(self, name, value): + self.node.set("embroider_%s" % name, str(value)) + + @cache + def get_style(self, style_name): + style = simplestyle.parseStyle(self.node.get("style")) + if (style_name not in style): + return None + value = style[style_name] + if value == 'none': + return None + return value + + @cache + def has_style(self, style_name): + style = simplestyle.parseStyle(self.node.get("style")) + return style_name in style + + @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. + # + # A "superpath" is a collection of paths that are all in one object. + # + # The "cubic" bit in "cubic superpath" is because the bezier curves + # inkscape uses involve cubic polynomials. + # + # Each path is a collection of tuples, each of the form: + # + # (control_before, point, control_after) + # + # A bezier curve segment is defined by an endpoint, a control point, + # a second control point, and a final endpoint. A path is a bunch of + # bezier curves strung together. One could represent a path as a set + # of four-tuples, but there would be redundancy because the ending + # point of one bezier is the starting point of the next. Instead, a + # path is a set of 3-tuples as shown above, and one must construct + # each bezier curve by taking the appropriate endpoints and control + # points. Bleh. It should be noted that a straight segment is + # represented by having the control point on each end equal to that + # end's point. + # + # 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) + + # apply the combined transform to this node's path + simpletransform.applyTransformToPath(transform, path) + + + return path + + def flatten(self, path): + """approximate a path containing beziers with a series of points""" + + path = deepcopy(path) + + cspsubdiv(path, 0.1) + + flattened = [] + + for comp in path: + vertices = [] + for ctl in comp: + vertices.append((ctl[1][0], ctl[1][1])) + flattened.append(vertices) + + return flattened + + @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) + + def to_patches(self, last_patch): + raise NotImplementedError("%s must implement to_patches()" % self.__class__.__name__) + + def embroider(self, last_patch): + patches = self.to_patches(last_patch) + + if patches: + patches[-1].trim_after = self.trim_after + patches[-1].stop_after = self.stop_after + + return patches + + def fatal(self, message): + print >> sys.stderr, "error:", message + sys.exit(1) diff --git a/inkstitch/elements/fill.py b/inkstitch/elements/fill.py new file mode 100644 index 000000000..9591d3c65 --- /dev/null +++ b/inkstitch/elements/fill.py @@ -0,0 +1,97 @@ +from .. import _ +from .element import param, EmbroideryElement, Patch +from ..utils import cache +from shapely import geometry as shgeo +import math +from ..stitches import running_stitch, auto_fill, legacy_fill + +class Fill(EmbroideryElement): + element_name = _("Fill") + + def __init__(self, *args, **kwargs): + super(Fill, self).__init__(*args, **kwargs) + + @property + @param('auto_fill', _('Manually routed fill stitching'), type='toggle', inverse=True, default=True) + def auto_fill(self): + return self.get_boolean_param('auto_fill', True) + + @property + @param('angle', _('Angle of lines of stitches'), unit='deg', type='float', default=0) + @cache + def angle(self): + return math.radians(self.get_float_param('angle', 0)) + + @property + def color(self): + return self.get_style("fill") + + @property + @param('flip', _('Flip fill (start right-to-left)'), type='boolean', default=False) + def flip(self): + return self.get_boolean_param("flip", False) + + @property + @param('row_spacing_mm', _('Spacing between rows'), unit='mm', type='float', default=0.25) + def row_spacing(self): + return max(self.get_float_param("row_spacing_mm", 0.25), 0.01) + + @property + def end_row_spacing(self): + return self.get_float_param("end_row_spacing_mm") + + @property + @param('max_stitch_length_mm', _('Maximum fill stitch length'), unit='mm', type='float', default=3.0) + def max_stitch_length(self): + return max(self.get_float_param("max_stitch_length_mm", 3.0), 0.01) + + @property + @param('staggers', _('Stagger rows this many times before repeating'), type='int', default=4) + def staggers(self): + return self.get_int_param("staggers", 4) + + @property + @cache + def paths(self): + return self.flatten(self.parse_path()) + + @property + @cache + def shape(self): + poly_ary = [] + for sub_path in self.paths: + point_ary = [] + last_pt = None + for pt in sub_path: + if (last_pt is not None): + vp = (pt[0] - last_pt[0], pt[1] - last_pt[1]) + dp = math.sqrt(math.pow(vp[0], 2.0) + math.pow(vp[1], 2.0)) + # dbg.write("dp %s\n" % dp) + if (dp > 0.01): + # I think too-close points confuse shapely. + point_ary.append(pt) + last_pt = pt + else: + last_pt = pt + if point_ary: + poly_ary.append(point_ary) + + # shapely's idea of "holes" are to subtract everything in the second set + # from the first. So let's at least make sure the "first" thing is the + # biggest path. + # TODO: actually figure out which things are holes and which are shells + poly_ary.sort(key=lambda point_list: shgeo.Polygon(point_list).area, reverse=True) + + polygon = shgeo.MultiPolygon([(poly_ary[0], poly_ary[1:])]) + # print >> sys.stderr, "polygon valid:", polygon.is_valid + return polygon + + def to_patches(self, last_patch): + stitch_lists = legacy_fill(self.shape, + self.angle, + self.row_spacing, + self.end_row_spacing, + self.max_stitch_length, + self.flip, + self.staggers) + return [Patch(stitches=stitch_list, color=self.color) for stitch_list in stitch_lists] diff --git a/inkstitch/elements/polyline.py b/inkstitch/elements/polyline.py new file mode 100644 index 000000000..6ded9fd14 --- /dev/null +++ b/inkstitch/elements/polyline.py @@ -0,0 +1,72 @@ +from .. import _, Point +from .element import param, EmbroideryElement, Patch +from ..utils import cache + + +class Polyline(EmbroideryElement): + # Handle a element, which is treated as a set of points to + # stitch exactly. + # + # elements are pretty rare in SVG, from what I can tell. + # Anything you can do with a can also be done with a

, and + # much more. + # + # Notably, EmbroiderModder2 uses elements when converting from + # common machine embroidery file formats to SVG. Handling those here lets + # users use File -> Import to pull in existing designs they may have + # obtained, for example purchased fonts. + + @property + def points(self): + # example: "1,2 0,0 1.5,3 4,2" + + points = self.node.get('points') + points = points.split(" ") + points = [[float(coord) for coord in point.split(",")] for point in points] + + return 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 + # svg transforms that is in our superclass, we'll convert the polyline + # to a degenerate cubic superpath in which the bezier handles are on + # the segment endpoints. + + path = [[[point[:], point[:], point[:]] for point in self.points]] + + return path + + @property + @cache + def csp(self): + csp = self.parse_path() + + return csp + + @property + def color(self): + # EmbroiderModder2 likes to use the `stroke` property directly instead + # of CSS. + return self.get_style("stroke") or self.node.get("stroke") + + @property + def stitches(self): + # For a , we'll stitch the points exactly as they exist in + # the SVG, with no stitch spacing interpolation, flattening, etc. + + # See the comments in the parent class's parse_path method for a + # description of the CSP data structure. + + stitches = [point for handle_before, point, handle_after in self.csp[0]] + + return stitches + + def to_patches(self, last_patch): + patch = Patch(color=self.color) + + for stitch in self.stitches: + patch.add_stitch(Point(*stitch)) + + return [patch] diff --git a/inkstitch/elements/satin_column.py b/inkstitch/elements/satin_column.py new file mode 100644 index 000000000..d22f5145a --- /dev/null +++ b/inkstitch/elements/satin_column.py @@ -0,0 +1,403 @@ +from itertools import chain, izip + +from .. import _, Point +from .element import param, EmbroideryElement, Patch +from ..utils import cache +from shapely import geometry as shgeo, ops as shops + + +class SatinColumn(EmbroideryElement): + element_name = _("Satin Column") + + def __init__(self, *args, **kwargs): + super(SatinColumn, self).__init__(*args, **kwargs) + + @property + @param('satin_column', _('Custom satin column'), type='toggle') + def satin_column(self): + return self.get_boolean_param("satin_column") + + @property + def color(self): + return self.get_style("stroke") + + @property + @param('zigzag_spacing_mm', _('Zig-zag spacing (peak-to-peak)'), unit='mm', type='float', default=0.4) + def zigzag_spacing(self): + # peak-to-peak distance between zigzags + return max(self.get_float_param("zigzag_spacing_mm", 0.4), 0.01) + + @property + @param('pull_compensation_mm', _('Pull compensation'), unit='mm', type='float') + def pull_compensation(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('contour_underlay', _('Contour underlay'), type='toggle', group=_('Contour Underlay')) + def contour_underlay(self): + # "Contour underlay" is stitching just inside the rectangular shape + # of the satin column; that is, up one side and down the other. + return self.get_boolean_param("contour_underlay") + + @property + @param('contour_underlay_stitch_length_mm', _('Stitch length'), unit='mm', group=_('Contour Underlay'), type='float', default=1.5) + def contour_underlay_stitch_length(self): + return max(self.get_float_param("contour_underlay_stitch_length_mm", 1.5), 0.01) + + @property + @param('contour_underlay_inset_mm', _('Contour underlay inset amount'), unit='mm', group=_('Contour Underlay'), type='float', default=0.4) + def contour_underlay_inset(self): + # how far inside the edge of the column to stitch the underlay + return self.get_float_param("contour_underlay_inset_mm", 0.4) + + @property + @param('center_walk_underlay', _('Center-walk underlay'), type='toggle', group=_('Center-Walk Underlay')) + def center_walk_underlay(self): + # "Center walk underlay" is stitching down and back in the centerline + # between the two sides of the satin column. + return self.get_boolean_param("center_walk_underlay") + + @property + @param('center_walk_underlay_stitch_length_mm', _('Stitch length'), unit='mm', group=_('Center-Walk Underlay'), type='float', default=1.5) + def center_walk_underlay_stitch_length(self): + return max(self.get_float_param("center_walk_underlay_stitch_length_mm", 1.5), 0.01) + + @property + @param('zigzag_underlay', _('Zig-zag underlay'), type='toggle', group=_('Zig-zag Underlay')) + def zigzag_underlay(self): + return self.get_boolean_param("zigzag_underlay") + + @property + @param('zigzag_underlay_spacing_mm', _('Zig-Zag spacing (peak-to-peak)'), unit='mm', group=_('Zig-zag Underlay'), type='float', default=3) + def zigzag_underlay_spacing(self): + return max(self.get_float_param("zigzag_underlay_spacing_mm", 3), 0.01) + + @property + @param('zigzag_underlay_inset_mm', _('Inset amount (default: half of contour underlay inset)'), unit='mm', group=_('Zig-zag Underlay'), type='float') + def zigzag_underlay_inset(self): + # how far in from the edge of the satin the points in the zigzags + # should be + + # Default to half of the contour underlay inset. That is, if we're + # 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 + + @property + @cache + def csp(self): + return self.parse_path() + + @property + @cache + def flattened_beziers(self): + if len(self.csp) == 2: + return self.simple_flatten_beziers() + else: + return self.flatten_beziers_with_rungs() + + + def flatten_beziers_with_rungs(self): + input_paths = [self.flatten([path]) for path in self.csp] + input_paths = [shgeo.LineString(path[0]) for path in input_paths] + + paths = input_paths[:] + paths.sort(key=lambda path: path.length, reverse=True) + + # Imagine a satin column as a curvy ladder. + # The two long paths are the "rails" of the ladder. The remainder are + # the "rungs". + rails = paths[:2] + rungs = shgeo.MultiLineString(paths[2:]) + + # The rails should stay in the order they were in the original CSP. + # (this lets the user control where the satin starts and ends) + rails.sort(key=lambda rail: input_paths.index(rail)) + + result = [] + + for rail in rails: + if not rail.is_simple: + self.fatal(_("One or more rails crosses itself, and this is not allowed. Please split into multiple satin columns.")) + + # handle null intersections here? + linestrings = shops.split(rail, rungs) + + #print >> dbg, "rails and rungs", [str(rail) for rail in rails], [str(rung) for rung in rungs] + if len(linestrings.geoms) < len(rungs.geoms) + 1: + self.fatal(_("satin column: One or more of the rungs doesn't intersect both rails.") + " " + _("Each rail should intersect both rungs once.")) + elif len(linestrings.geoms) > len(rungs.geoms) + 1: + self.fatal(_("satin column: One or more of the rungs intersects the rails more than once.") + " " + _("Each rail should intersect both rungs once.")) + + paths = [[Point(*coord) for coord in ls.coords] for ls in linestrings.geoms] + result.append(paths) + + return zip(*result) + + + def simple_flatten_beziers(self): + # Given a pair of paths made up of bezier segments, flatten + # each individual bezier segment into line segments that approximate + # the curves. Retain the divisions between beziers -- we'll use those + # later. + + paths = [] + + for path in self.csp: + # See the documentation in the parent class for parse_path() for a + # description of the format of the CSP. Each bezier is constructed + # using two neighboring 3-tuples in the list. + + flattened_path = [] + + # iterate over pairs of 3-tuples + for prev, current in zip(path[:-1], path[1:]): + flattened_segment = self.flatten([[prev, current]]) + flattened_segment = [Point(x, y) for x, y in flattened_segment[0]] + flattened_path.append(flattened_segment) + + paths.append(flattened_path) + + return zip(*paths) + + def validate_satin_column(self): + # The node should have exactly two paths with no fill. Each + # path should have the same number of points, meaning that they + # will both be made up of the same number of bezier curves. + + node_id = self.node.get("id") + + if self.get_style("fill") is not None: + self.fatal(_("satin column: object %s has a fill (but should not)") % node_id) + + if len(self.csp) == 2: + if len(self.csp[0]) != len(self.csp[1]): + self.fatal(_("satin column: object %(id)s has two paths with an unequal number of points (%(length1)d and %(length2)d)") % \ + dict(id=node_id, length1=len(self.csp[0]), length2=len(self.csp[1]))) + + def offset_points(self, pos1, pos2, offset_px): + # Expand or contract two points about their midpoint. This is + # useful for pull compensation and insetting underlay. + + distance = (pos1 - pos2).length() + + if distance < 0.0001: + # if they're the same point, we don't know which direction + # to offset in, so we have to just return the points + return pos1, pos2 + + # don't contract beyond the midpoint, or we'll start expanding + if offset_px < -distance / 2.0: + offset_px = -distance / 2.0 + + pos1 = pos1 + (pos1 - pos2).unit() * offset_px + pos2 = pos2 + (pos2 - pos1).unit() * offset_px + + return pos1, pos2 + + def walk(self, path, start_pos, start_index, distance): + # Move pixels along , which is a sequence of line + # segments defined by points. + + # is the index of the line segment in that + # we're currently on. is where along that line + # segment we are. Return a new position and index. + + # print >> dbg, "walk", start_pos, start_index, distance + + pos = start_pos + index = start_index + last_index = len(path) - 1 + distance_remaining = distance + + while True: + if index >= last_index: + return pos, index + + segment_end = path[index + 1] + segment = segment_end - pos + segment_length = segment.length() + + if segment_length > distance_remaining: + # our walk ends partway along this segment + return pos + segment.unit() * distance_remaining, index + else: + # our walk goes past the end of this segment, so advance + # one point + index += 1 + distance_remaining -= segment_length + pos = segment_end + + def walk_paths(self, spacing, offset): + # Take a bezier segment from each path in turn, and plot out an + # equal number of points on each bezier. Return the points plotted. + # The points will be contracted or expanded by offset using + # offset_points(). + + points = [[], []] + + def add_pair(pos1, pos2): + pos1, pos2 = self.offset_points(pos1, pos2, offset) + points[0].append(pos1) + points[1].append(pos2) + + # We may not be able to fit an even number of zigzags in each pair of + # beziers. We'll store the remaining bit of the beziers after handling + # each section. + remainder_path1 = [] + remainder_path2 = [] + + for segment1, segment2 in self.flattened_beziers: + subpath1 = remainder_path1 + segment1 + subpath2 = remainder_path2 + segment2 + + len1 = shgeo.LineString(subpath1).length + len2 = shgeo.LineString(subpath2).length + + # Base the number of stitches in each section on the _longest_ of + # the two beziers. Otherwise, things could get too sparse when one + # side is significantly longer (e.g. when going around a corner). + # The risk here is that we poke a hole in the fabric if we try to + # cram too many stitches on the short bezier. The user will need + # to avoid this through careful construction of paths. + # + # TODO: some commercial machine embroidery software compensates by + # pulling in some of the "inner" stitches toward the center a bit. + + # note, this rounds down using integer-division + num_points = max(len1, len2) / spacing + + spacing1 = len1 / num_points + spacing2 = len2 / num_points + + pos1 = subpath1[0] + index1 = 0 + + pos2 = subpath2[0] + index2 = 0 + + for i in xrange(int(num_points)): + add_pair(pos1, pos2) + + pos1, index1 = self.walk(subpath1, pos1, index1, spacing1) + pos2, index2 = self.walk(subpath2, pos2, index2, spacing2) + + if index1 < len(subpath1) - 1: + remainder_path1 = [pos1] + subpath1[index1 + 1:] + else: + remainder_path1 = [] + + if index2 < len(subpath2) - 1: + remainder_path2 = [pos2] + subpath2[index2 + 1:] + else: + remainder_path2 = [] + + # We're off by one in the algorithm above, so we need one more + # pair of points. We also want to add points at the very end to + # make sure we match the vectors on screen as best as possible. + # Try to avoid doing both if they're going to stack up too + # closely. + + end1 = remainder_path1[-1] + end2 = remainder_path2[-1] + + if (end1 - pos1).length() > 0.3 * spacing: + add_pair(pos1, pos2) + + add_pair(end1, end2) + + return points + + def do_contour_underlay(self): + # "contour walk" underlay: do stitches up one side and down the + # other. + forward, back = self.walk_paths(self.contour_underlay_stitch_length, + -self.contour_underlay_inset) + return Patch(color=self.color, stitches=(forward + list(reversed(back)))) + + def do_center_walk(self): + # Center walk underlay is just a running stitch down and back on the + # center line between the bezier curves. + + # Do it like contour underlay, but inset all the way to the center. + forward, back = self.walk_paths(self.center_walk_underlay_stitch_length, + -100000) + return Patch(color=self.color, stitches=(forward + list(reversed(back)))) + + def do_zigzag_underlay(self): + # zigzag underlay, usually done at a much lower density than the + # satin itself. It looks like this: + # + # \/\/\/\/\/\/\/\/\/\/| + # /\/\/\/\/\/\/\/\/\/\| + # + # In combination with the "contour walk" underlay, this is the + # "German underlay" described here: + # http://www.mrxstitch.com/underlay-what-lies-beneath-machine-embroidery/ + + patch = Patch(color=self.color) + + sides = self.walk_paths(self.zigzag_underlay_spacing / 2.0, + -self.zigzag_underlay_inset) + + # This organizes the points in each side in the order that they'll be + # visited. + sides = [sides[0][::2] + list(reversed(sides[0][1::2])), + sides[1][1::2] + list(reversed(sides[1][::2]))] + + # This fancy bit of iterable magic just repeatedly takes a point + # from each side in turn. + for point in chain.from_iterable(izip(*sides)): + patch.add_stitch(point) + + return patch + + def do_satin(self): + # satin: do a zigzag pattern, alternating between the paths. The + # zigzag looks like this to make the satin stitches look perpendicular + # to the column: + # + # /|/|/|/|/|/|/|/| + + # print >> dbg, "satin", self.zigzag_spacing, self.pull_compensation + + patch = Patch(color=self.color) + + sides = self.walk_paths(self.zigzag_spacing, self.pull_compensation) + + # Like in zigzag_underlay(): take a point from each side in turn. + for point in chain.from_iterable(izip(*sides)): + patch.add_stitch(point) + + return patch + + def to_patches(self, last_patch): + # Stitch a variable-width satin column, zig-zagging between two paths. + + # The algorithm will draw zigzags between each consecutive pair of + # beziers. The boundary points between beziers serve as "checkpoints", + # allowing the user to control how the zigzags flow around corners. + + # First, verify that we have valid paths. + self.validate_satin_column() + + patches = [] + + if self.center_walk_underlay: + patches.append(self.do_center_walk()) + + if self.contour_underlay: + patches.append(self.do_contour_underlay()) + + if self.zigzag_underlay: + # zigzag underlay comes after contour walk underlay, so that the + # zigzags sit on the contour walk underlay like rail ties on rails. + patches.append(self.do_zigzag_underlay()) + + patches.append(self.do_satin()) + + return patches diff --git a/inkstitch/elements/stroke.py b/inkstitch/elements/stroke.py new file mode 100644 index 000000000..8ad9a9842 --- /dev/null +++ b/inkstitch/elements/stroke.py @@ -0,0 +1,119 @@ +from .. import _, Point +from .element import param, EmbroideryElement, Patch +from ..utils import cache + + +class Stroke(EmbroideryElement): + element_name = "Stroke" + + @property + @param('satin_column', _('Satin stitch along paths'), type='toggle', inverse=True) + def satin_column(self): + return self.get_boolean_param("satin_column") + + @property + def color(self): + return self.get_style("stroke") + + @property + @cache + def width(self): + stroke_width = self.get_style("stroke-width") + + if stroke_width.endswith("px"): + stroke_width = stroke_width[:-2] + + return float(stroke_width) + + @property + def dashed(self): + return self.get_style("stroke-dasharray") is not None + + @property + @param('running_stitch_length_mm', _('Running stitch length'), unit='mm', type='float', default=1.5) + def running_stitch_length(self): + 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)'), unit='mm', type='float', default=0.4) + @cache + def zigzag_spacing(self): + return max(self.get_float_param("zigzag_spacing_mm", 0.4), 0.01) + + @property + @param('repeats', _('Repeats'), type='int', default="1") + def repeats(self): + return self.get_int_param("repeats", 1) + + @property + def paths(self): + return self.flatten(self.parse_path()) + + def is_running_stitch(self): + # stroke width <= 0.5 pixels is deprecated in favor of dashed lines + return self.dashed or self.width <= 0.5 + + def stroke_points(self, emb_point_list, zigzag_spacing, stroke_width): + # TODO: use inkstitch.stitches.running_stitch + + patch = Patch(color=self.color) + p0 = emb_point_list[0] + rho = 0.0 + side = 1 + last_segment_direction = None + + 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 segi in order: + p1 = emb_point_list[segi] + + # how far we have to go along segment + seg_len = (p1 - p0).length() + if (seg_len == 0): + continue + + # vector pointing along segment + along = (p1 - p0).unit() + + # vector pointing to edge of stroke width + perp = along.rotate_left() * (stroke_width * 0.5) + + 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) + + # 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 + + p0 = p1 + last_segment_direction = along + rho -= seg_len + + if (p0 - patch.stitches[-1]).length() > 0.1: + patch.add_stitch(p0) + + return patch + + def to_patches(self, last_patch): + patches = [] + + for path in self.paths: + path = [Point(x, y) for x, y in path] + if self.is_running_stitch(): + patch = self.stroke_points(path, self.running_stitch_length, stroke_width=0.0) + else: + patch = self.stroke_points(path, self.zigzag_spacing / 2.0, stroke_width=self.width) + + patches.append(patch) + + return patches diff --git a/inkstitch/extensions.py b/inkstitch/extensions.py new file mode 100644 index 000000000..0258a4909 --- /dev/null +++ b/inkstitch/extensions.py @@ -0,0 +1,103 @@ +import inkex +from .elements import AutoFill, Fill, Stroke, SatinColumn, EmbroideryElement +from . import SVG_POLYLINE_TAG, SVG_GROUP_TAG, SVG_DEFS_TAG, INKSCAPE_GROUPMODE, EMBROIDERABLE_TAGS, PIXELS_PER_MM + + +class InkstitchExtension(inkex.Effect): + """Base class for Inkstitch extensions. Not intended for direct use.""" + + def hide_all_layers(self): + for g in self.document.getroot().findall(SVG_GROUP_TAG): + if g.get(INKSCAPE_GROUPMODE) == "layer": + g.set("style", "display:none") + + def no_elements_error(self): + if self.selected: + inkex.errormsg(_("No embroiderable paths selected.")) + else: + inkex.errormsg(_("No embroiderable paths found in document.")) + inkex.errormsg(_("Tip: use Path -> Object to Path to convert non-paths before embroidering.")) + + def descendants(self, node): + nodes = [] + element = EmbroideryElement(node) + + if element.has_style('display') and element.get_style('display') is None: + return [] + + if node.tag == SVG_DEFS_TAG: + return [] + + for child in node: + nodes.extend(self.descendants(child)) + + if node.tag in EMBROIDERABLE_TAGS: + nodes.append(node) + + return nodes + + def get_nodes(self): + """Get all XML nodes, or just those selected + + effect is an instance of a subclass of inkex.Effect. + """ + + if self.selected: + nodes = [] + for node in self.document.getroot().iter(): + if node.get("id") in self.selected: + nodes.extend(self.descendants(node)) + else: + nodes = self.descendants(self.document.getroot()) + + return nodes + + def detect_classes(self, node): + if node.tag == SVG_POLYLINE_TAG: + return [Polyline] + else: + element = EmbroideryElement(node) + + if element.get_boolean_param("satin_column"): + return [SatinColumn] + else: + classes = [] + + if element.get_style("fill"): + if element.get_boolean_param("auto_fill", True): + classes.append(AutoFill) + else: + classes.append(Fill) + + if element.get_style("stroke"): + classes.append(Stroke) + + if element.get_boolean_param("stroke_first", False): + classes.reverse() + + return classes + + + def get_elements(self): + self.elements = [] + for node in self.get_nodes(): + classes = self.detect_classes(node) + self.elements.extend(cls(node) for cls in classes) + + if self.elements: + return True + else: + self.no_elements_error() + return False + + def elements_to_patches(self, elements): + patches = [] + for element in elements: + if patches: + last_patch = patches[-1] + else: + last_patch = None + + patches.extend(element.embroider(last_patch)) + + return patches diff --git a/inkstitch/stitch_plan/stitch_plan.py b/inkstitch/stitch_plan/stitch_plan.py index 6bb62b1c9..c7f21d59e 100644 --- a/inkstitch/stitch_plan/stitch_plan.py +++ b/inkstitch/stitch_plan/stitch_plan.py @@ -1,4 +1,5 @@ -from .. import Stitch, Point, PIXELS_PER_MM +from .. import Stitch, PIXELS_PER_MM +from ..utils.geometry import Point from .stop import process_stop from .trim import process_trim from .ties import add_ties diff --git a/inkstitch/stitch_plan/ties.py b/inkstitch/stitch_plan/ties.py index c103ae497..9c688e6ba 100644 --- a/inkstitch/stitch_plan/ties.py +++ b/inkstitch/stitch_plan/ties.py @@ -40,4 +40,7 @@ def add_ties(stitch_plan): else: new_stitches.append(stitch) + if not need_tie_in: + add_tie_off(new_stitches) + color_block.replace_stitches(new_stitches) diff --git a/inkstitch/stitches/auto_fill.py b/inkstitch/stitches/auto_fill.py index cf765bc20..7f2659095 100644 --- a/inkstitch/stitches/auto_fill.py +++ b/inkstitch/stitches/auto_fill.py @@ -416,7 +416,7 @@ def connect_points(shape, start, end, running_stitch_length): #print >> dbg, "connect_points:", outline_index, start, end, distance, stitches, direction #dbg.flush() - stitches = [InkstitchPoint(*start)] + stitches = [InkstitchPoint(*outline.interpolate(pos).coords[0])] for i in xrange(num_stitches): pos = (pos + one_stitch) % outline.length diff --git a/inkstitch/stitches/fill.py b/inkstitch/stitches/fill.py index 52c951724..1b7377b0c 100644 --- a/inkstitch/stitches/fill.py +++ b/inkstitch/stitches/fill.py @@ -1,4 +1,5 @@ -from .. import Point as InkstitchPoint, cache, PIXELS_PER_MM +from .. import PIXELS_PER_MM +from ..utils import cache, Point as InkstitchPoint import shapely import math import sys diff --git a/inkstitch/utils/__init__.py b/inkstitch/utils/__init__.py index f0d5783bf..8e95287f3 100644 --- a/inkstitch/utils/__init__.py +++ b/inkstitch/utils/__init__.py @@ -1 +1,2 @@ from geometry import * +from cache import cache diff --git a/inkstitch/utils/cache.py b/inkstitch/utils/cache.py new file mode 100644 index 000000000..38fe8f2c5 --- /dev/null +++ b/inkstitch/utils/cache.py @@ -0,0 +1,8 @@ +try: + from functools import lru_cache +except ImportError: + from backports.functools_lru_cache import lru_cache + +# simplify use of lru_cache decorator +def cache(*args, **kwargs): + return lru_cache(maxsize=None)(*args, **kwargs) diff --git a/inkstitch/utils/geometry.py b/inkstitch/utils/geometry.py index 052bc2a2e..61b98bcbb 100644 --- a/inkstitch/utils/geometry.py +++ b/inkstitch/utils/geometry.py @@ -1,5 +1,6 @@ -from .. import Point as InkstitchPoint from shapely.geometry import LineString, Point as ShapelyPoint +import math + def cut(line, distance): """ Cuts a LineString in two at a distance from its starting point. @@ -38,5 +39,64 @@ def cut_path(points, length): path = LineString(points) subpath, rest = cut(path, length) - return [InkstitchPoint(*point) for point in subpath.coords] + return [Point(*point) for point in subpath.coords] + +class Point: + def __init__(self, x, y): + self.x = x + self.y = y + + def __add__(self, other): + return Point(self.x + other.x, self.y + other.y) + + def __sub__(self, other): + return Point(self.x - other.x, self.y - other.y) + + def mul(self, scalar): + return Point(self.x * scalar, self.y * scalar) + + def __mul__(self, other): + if isinstance(other, Point): + # dot product + return self.x * other.x + self.y * other.y + elif isinstance(other, (int, float)): + return Point(self.x * other, self.y * other) + else: + raise ValueError("cannot multiply Point by %s" % type(other)) + + def __rmul__(self, other): + if isinstance(other, (int, float)): + return self.__mul__(other) + else: + raise ValueError("cannot multiply Point by %s" % type(other)) + + def __repr__(self): + return "Point(%s,%s)" % (self.x, self.y) + + def length(self): + return math.sqrt(math.pow(self.x, 2.0) + math.pow(self.y, 2.0)) + + def unit(self): + return self.mul(1.0 / self.length()) + + def rotate_left(self): + return Point(-self.y, self.x) + + def rotate(self, angle): + return Point(self.x * math.cos(angle) - self.y * math.sin(angle), self.y * math.cos(angle) + self.x * math.sin(angle)) + + def as_int(self): + return Point(int(round(self.x)), int(round(self.y))) + + def as_tuple(self): + return (self.x, self.y) + + def __cmp__(self, other): + return cmp(self.as_tuple(), other.as_tuple()) + + def __getitem__(self, item): + return self.as_tuple()[item] + + def __len__(self): + return 2 diff --git a/messages.po b/messages.po index b4dc1c2e1..a6b154cc7 100644 --- a/messages.po +++ b/messages.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-03-10 22:11-0500\n" +"POT-Creation-Date: 2018-03-11 21:59-0400\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -17,146 +17,12 @@ msgstr "" "Content-Type: text/plain; charset=CHARSET\n" "Content-Transfer-Encoding: 8bit\n" -msgid "Fill" -msgstr "" - -msgid "Manually routed fill stitching" -msgstr "" - -msgid "Angle of lines of stitches" -msgstr "" - -msgid "Flip fill (start right-to-left)" -msgstr "" - -msgid "Spacing between rows" -msgstr "" - -msgid "Maximum fill stitch length" -msgstr "" - -msgid "Stagger rows this many times before repeating" -msgstr "" - -msgid "Auto-Fill" -msgstr "" - -msgid "Automatically routed fill stitching" -msgstr "" - -msgid "Running stitch length (traversal between sections)" -msgstr "" - -msgid "Underlay" -msgstr "" - -msgid "AutoFill Underlay" -msgstr "" - -msgid "Fill angle (default: fill angle + 90 deg)" -msgstr "" - -msgid "Row spacing (default: 3x fill row spacing)" -msgstr "" - -msgid "Max stitch length" -msgstr "" - -msgid "Inset" -msgstr "" - -msgid "Satin stitch along paths" -msgstr "" - -msgid "Running stitch length" -msgstr "" - -msgid "Zig-zag spacing (peak-to-peak)" -msgstr "" - -msgid "Repeats" -msgstr "" - -msgid "Satin Column" -msgstr "" - -msgid "Custom satin column" -msgstr "" - -msgid "Pull compensation" -msgstr "" - -msgid "Contour underlay" -msgstr "" - -msgid "Contour Underlay" -msgstr "" - -msgid "Stitch length" -msgstr "" - -msgid "Contour underlay inset amount" -msgstr "" - -msgid "Center-walk underlay" -msgstr "" - -msgid "Center-Walk Underlay" -msgstr "" - -msgid "Zig-zag underlay" -msgstr "" - -msgid "Zig-zag Underlay" -msgstr "" - -msgid "Zig-Zag spacing (peak-to-peak)" -msgstr "" - -msgid "Inset amount (default: half of contour underlay inset)" -msgstr "" - -msgid "" -"One or more rails crosses itself, and this is not allowed. Please split " -"into multiple satin columns." -msgstr "" - -msgid "satin column: One or more of the rungs doesn't intersect both rails." -msgstr "" - -msgid "Each rail should intersect both rungs once." -msgstr "" - -msgid "" -"satin column: One or more of the rungs intersects the rails more than once." -msgstr "" - -#, python-format -msgid "satin column: object %s has a fill (but should not)" -msgstr "" - -#, python-format -msgid "" -"satin column: object %(id)s has two paths with an unequal number of points " -"(%(length1)d and %(length2)d)" -msgstr "" - msgid "" "\n" "\n" "Seeing a 'no such option' message? Please restart Inkscape to fix." msgstr "" -msgid "No embroiderable paths selected." -msgstr "" - -msgid "No embroiderable paths found in document." -msgstr "" - -msgid "" -"Tip: use Path -> Object to Path to convert non-paths before embroidering." -msgstr "" - msgid "These settings will be applied to 1 object." msgstr "" @@ -241,17 +107,3 @@ msgstr "" #, python-format msgid "Unknown unit: %s" msgstr "" - -msgid "TRIM after" -msgstr "" - -msgid "Trim thread after this object (for supported machines and file formats)" -msgstr "" - -msgid "STOP after" -msgstr "" - -msgid "" -"Add STOP instruction after this object (for supported machines and file " -"formats)" -msgstr ""