From 8c40bd95e0b0ab97cb3744c0627c138e8b259dbf Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Wed, 20 Jan 2016 03:04:43 -0500 Subject: [PATCH] add variable satin feature Variable satin does zigzag stitches between two arbitrary paths. This lets you do satin stitching of variable width and angles, which is useful for filling smaller regions where tatami filling doesn't make sense. It's also good for doing satin around corners, because it doesn't leave the outer part of the corner sparsely-stitched. To use variable satin, create an object containing two paths in inkscape. Each path should have the same number of vertices. Zigzag satin will be drawn between the two paths such that a stitch always aligns with each corresponding pair of vertices. This lets you control the way the stitches are angled by subdividing the paths appropriately. Tip: you can add a vertex to a path by double-clicking the place on the path where you want the new vertex. Be careful when going around corners, especially sharp ones. The number of stitches is based on the zigzag spacing parameter measured around the _longer_ of the two beziers in each segment. Around corners, even if the outside angle is sharp, you may want to make the inside angle slightly rounded to avoid having a bunch of stitches all land at exactly the same point, which could poke a hole in your fabric. --- embroider.py | 166 +++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 160 insertions(+), 6 deletions(-) diff --git a/embroider.py b/embroider.py index 95807209f..4602ba4dd 100644 --- a/embroider.py +++ b/embroider.py @@ -28,6 +28,7 @@ import inkex import simplepath import simplestyle import simpletransform +from bezmisc import bezierlength, beziertatlength, bezierpointatt from cspsubdiv import cspsubdiv import cubicsuperpath import PyEmb @@ -54,7 +55,7 @@ def parse_boolean(s): if isinstance(s, bool): return s else: - return s and s.lower in ('yes', 'y', 'true', 't', '1') + return s and (s.lower() in ('yes', 'y', 'true', 't', '1')) def get_param(node, param, default): value = node.get("embroider_" + param) @@ -544,7 +545,7 @@ class EmbroideryObject: inkex.addNS('path', 'svg'), { 'style':simplestyle.formatStyle( { 'stroke': color if color is not None else '#000000', - 'stroke-width':"1", + 'stroke-width':"0.25", 'fill': 'none' }), 'd':simplepath.formatPath(path), }) @@ -799,9 +800,8 @@ class Embroider(inkex.Effect): #dbg.write("Node: %s\n"%str((id, etree.tostring(node, pretty_print=True)))) - israw = parse_boolean(node.get('embroider_raw')) - if (israw): - self.patchList.patches.extend(self.path_to_patch_list(node)) + if get_boolean_param(node, "satin_column"): + self.patchList.patches.extend(self.satin_column(node)) else: if (self.get_style(node, "fill")!=None): self.patchList.patches.extend(self.filled_region_to_patchlist(node)) @@ -985,7 +985,161 @@ class Embroider(inkex.Effect): sortorder, angle) - #TODO def make_stroked_patch(self, node): + def fatal(self, message): + print >> sys.stderr, "error:", message + sys.exit(1) + + def validate_satin_column(self, node, csp): + node_id = node.get("id") + + if len(csp) != 2: + self.fatal("satin column: object %s invalid: expected exactly two sub-paths" % node_id) + + if self.get_style(node, "fill")!=None: + self.fatal("satin column: object %s has a fill (but should not)" % node_id) + + if len(csp[0]) != len(csp[1]): + self.fatal("satin column: object %s has two paths with an unequal number of points (should be equal)" % node_id) + + def satin_column(self, node): + # Stitch a variable-width satin column, zig-zagging between two paths. + + # The node should have exactly two paths with no fill. Each + # path should have the same number of points. The two paths will be + # split into segments, and each segment will have a number of zigzags + # defined by the length of the longer of the two segments, divided + # by the zigzag spacing parameter. + + id = node.get("id") + + # First, verify that we have a valid node. + csp = cubicsuperpath.parsePath(node.get("d")) + self.validate_satin_column(node, csp) + + # fetch parameters + zigzag_spacing_px = get_float_param(node, "zigzag_spacing", self.zigzag_spacing_px) + + # A 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. + # + # 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. + # + # 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? + + path1 = csp[0] + path2 = csp[1] + + threadcolor = simplestyle.parseStyle(node.get("style"))["stroke"] + sortorder = self.get_sort_order(threadcolor, node) + patch = Patch(color=threadcolor, sortorder=sortorder) + + # Take each bezier segment in turn, drawing zigzags between the two + # paths in that segment. + + for segment in xrange(1, len(path1)): + # construct the current bezier segments + bezier1 = (path1[segment - 1][1], # point from previous 3-tuple + path1[segment - 1][2], # "after" control point from previous 3-tuple + path1[segment][0], # "before" control point from this 3-tuple + path1[segment][1], # point from this 3-tuple + ) + + bezier2 = (path2[segment - 1][1], + path2[segment - 1][2], + path2[segment][0], + path2[segment][1], + ) + + # Find their lengths. The math is actually fairly involved. + len1 = bezierlength(bezier1) + len2 = bezierlength(bezier2) + + # 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. + num_zigzags = int(max(len1, len2) / zigzag_spacing_px) + + stitch_len1 = len1 / num_zigzags + stitch_len2 = len2 / num_zigzags + + # Now do the stitches. Each "zigzag" has a "zig" and a "zag", that + # is, go from path1 to path2 and then back to path1. + + # Here's what I want to be able to do. However, beziertatlength is so incredibly slow that it's unusable. + #for stitch in xrange(num_zigzags): + # patch.addStitch(bezierpointatt(bezier1, beziertatlength(bezier1, stitch_len1 * stitch))) + # patch.addStitch(bezierpointatt(bezier2, beziertatlength(bezier2, stitch_len2 * (stitch + 0.5)))) + + # Instead, flatten the beziers down to a set of line segments. + subpath1 = flatten([[path1[segment - 1], path1[segment]]], self.options.flat) + subpath2 = flatten([[path2[segment - 1], path2[segment]]], self.options.flat) + + subpath1 = [PyEmb.Point(*p) for p in subpath1[0]] + subpath2 = [PyEmb.Point(*p) for p in subpath2[0]] + + def walk(path, start_pos, start_index, distance): + # Move pixels along 's line segments. + # 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. + + pos = start_pos + index = start_index + + if index >= len(path) - 1: + # it's possible we'll go too far due to inaccuracy in the + # bezier length calculation + return start_pos, start_index + + while True: + segment_end = path[index + 1] + segment_remaining = (next_pos - pos) + distance_remaining = remaining.length() + + if distance_remaining > distance: + return pos + segment_remaining.unit().mul(distance), index + else: + index += 1 + + if index >= len(path) - 1: + return segment_end, index + + distance -= distance_remaining + pos = segment_end + + pos1 = subpath1[0] + i1 = 0 + + pos2, i2 = walk(subpath2, subpath2[0], 0, stitch_len2 * 0.5) + i2 = 0 + + for stitch in xrange(num_zigzags): + # In each iteration, do a "zig" and a "zag". + patch.addStitch(pos1) + pos1, i1 = walk(subpath1, pos1, i1, stitch_len1) + + patch.addStitch(pos2) + pos2, i2 = walk(subpath2, pos2, i2, stitch_len2) + + return [patch] if __name__ == '__main__': sys.setrecursionlimit(100000);