From 48d957860e6109d66f7cf749b73cc12f44bb00aa Mon Sep 17 00:00:00 2001 From: Lex Neva Date: Mon, 18 Jan 2016 18:31:10 -0500 Subject: convert tabs to spaces --- embroider.py | 1534 +++++++++++++++++++++++++++++----------------------------- 1 file changed, 767 insertions(+), 767 deletions(-) (limited to 'embroider.py') diff --git a/embroider.py b/embroider.py index 320c214a..1b6f2c0f 100644 --- a/embroider.py +++ b/embroider.py @@ -117,802 +117,802 @@ def flatten(path, flatness): return flattened def bboxarea(poly): - x0=None - x1=None - y0=None - y1=None - for pt in poly: - if (x0==None or pt[0]x1): x1 = pt[0] - if (y0==None or pt[1]y1): y1 = pt[1] - return (x1-x0)*(y1-y0) + x0=None + x1=None + y0=None + y1=None + for pt in poly: + if (x0==None or pt[0]x1): x1 = pt[0] + if (y0==None or pt[1]y1): y1 = pt[1] + return (x1-x0)*(y1-y0) def area(poly): - return bboxarea(poly) + return bboxarea(poly) def byarea(a,b): - return -cmp(area(a), area(b)) + return -cmp(area(a), area(b)) def cspToShapelyPolygon(path): - poly_ary = [] - for sub_path in path: - point_ary = [] - last_pt = None - for pt in sub_path: - if (last_pt!=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 - 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. - poly_ary.sort(byarea) - - polygon = shgeo.MultiPolygon([(poly_ary[0], poly_ary[1:])]) - return polygon + poly_ary = [] + for sub_path in path: + point_ary = [] + last_pt = None + for pt in sub_path: + if (last_pt!=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 + 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. + poly_ary.sort(byarea) + + polygon = shgeo.MultiPolygon([(poly_ary[0], poly_ary[1:])]) + return polygon def shapelyCoordsToSvgD(geo): - coords = list(geo.coords) - new_path = [] - new_path.append(['M', coords[0]]) - for c in coords[1:]: - new_path.append(['L', c]) - return simplepath.formatPath(new_path) + coords = list(geo.coords) + new_path = [] + new_path.append(['M', coords[0]]) + for c in coords[1:]: + new_path.append(['L', c]) + return simplepath.formatPath(new_path) def shapelyLineSegmentToPyTuple(shline): - tuple = ((shline.coords[0][0],shline.coords[0][1]), - (shline.coords[1][0],shline.coords[1][1])) - return tuple + tuple = ((shline.coords[0][0],shline.coords[0][1]), + (shline.coords[1][0],shline.coords[1][1])) + return tuple def dupNodeAttrs(node): - n2 = E.node() - for k in node.attrib.keys(): - n2.attrib[k] = node.attrib[k] - del n2.attrib["id"] - del n2.attrib["d"] - return n2 + n2 = E.node() + for k in node.attrib.keys(): + n2.attrib[k] = node.attrib[k] + del n2.attrib["id"] + del n2.attrib["d"] + return n2 class Patch: - def __init__(self, color, sortorder, stitches=None): - self.color = color - self.sortorder = sortorder - if (stitches!=None): - self.stitches = stitches - else: - self.stitches = [] + def __init__(self, color, sortorder, stitches=None): + self.color = color + self.sortorder = sortorder + if (stitches!=None): + self.stitches = stitches + else: + self.stitches = [] - def addStitch(self, stitch): - self.stitches.append(stitch) + def addStitch(self, stitch): + self.stitches.append(stitch) - def reverse(self): - return Patch(self.color, self.sortorder, self.stitches[::-1]) + def reverse(self): + return Patch(self.color, self.sortorder, self.stitches[::-1]) class DebugHole: - pass + pass class PatchList: - def __init__(self, patches): - self.patches = patches - - def __len__(self): - return len(self.patches) - - def sort_by_sortorder(self): - def by_sort_order(a,b): - return cmp(a.sortorder, b.sortorder) - self.patches.sort(by_sort_order) - - def partition_by_color(self): - self.sort_by_sortorder() - #dbg.write("Sorted by sortorder:\n"); - #dbg.write(" %s\n" % ("\n".join(map(lambda p: str(p.sortorder), self.patches)))) - out = [] - lastPatch = None - for patch in self.patches: - if (lastPatch!=None and patch.sortorder==lastPatch.sortorder): - out[-1].patches.append(patch) - else: - out.append(PatchList([patch])) - lastPatch = patch - #dbg.write("Emitted %s partitions\n" % len(out)) - return out - - def tsp_by_color(self): - list_of_patchLists = self.partition_by_color() - for patchList in list_of_patchLists: - patchList.traveling_salesman() - return PatchList(reduce(operator.add, - map(lambda pl: pl.patches, list_of_patchLists))) - -# # TODO apparently dead code; replaced by partition_by_color above -# def clump_like_colors_together(self): -# out = PatchList([]) -# lastPatch = None -# for patch in self.patches: -# if (lastPatch!=None and patch.color==lastPatch.color): -# out.patches[-1] = Patch( -# out.patches[-1].color, -# out.patches[-1].sortorder, -# out.patches[-1].stitches+patch.stitches) -# else: -# out.patches.append(patch) -# lastPatch = patch -# return out - - def get(self, i): - if (i<0 or i>=len(self.patches)): - return None - return self.patches[i] - - def cost(self, a, b): - if (a==None or b==None): - rc = 0.0 - else: - rc = (a.stitches[-1] - b.stitches[0]).length() - #dbg.write("cost(%s, %s) = %5.1f\n" % (a, b, rc)) - return rc - - def try_swap(self, i, j): - # i,j are indices; - #dbg.write("swap(%d, %d)\n" % (i,j)) - oldCost = ( - self.cost(self.get(i-1), self.get(i)) - +self.cost(self.get(i), self.get(i+1)) - +self.cost(self.get(j-1), self.get(j)) - +self.cost(self.get(j), self.get(j+1))) - npi = self.get(j) - npj = self.get(i) - rpi = npi.reverse() - rpj = npj.reverse() - options = [ - (npi,npj), - (rpi,npj), - (npi,rpj), - (rpi,rpj), - ] - def costOf(np): - (npi,npj) = np - return ( - self.cost(self.get(i-1), npi) - +self.cost(npi, self.get(i+1)) - +self.cost(self.get(j-1), npj) - +self.cost(npj, self.get(j+1))) - costs = map(lambda o: (costOf(o), o), options) - costs.sort() - (cost,option) = costs[0] - savings = oldCost - cost - if (savings > 0): - self.patches[i] = option[0] - self.patches[j] = option[1] - success = "!" - else: - success = "." - - #dbg.write("old %5.1f new %5.1f savings: %5.1f\n" % (oldCost, cost, savings)) - return success - - def try_reverse(self, i): - #dbg.write("reverse(%d)\n" % i) - oldCost = (self.cost(self.get(i-1), self.get(i)) - +self.cost(self.get(i), self.get(i+1))) - reversed = self.get(i).reverse() - newCost = (self.cost(self.get(i-1), reversed) - +self.cost(reversed, self.get(i+1))) - savings = oldCost - newCost - if (savings > 0.0): - self.patches[i] = reversed - success = "#" - else: - success = "_" - return success - - def traveling_salesman(self): - # shockingly, this is non-optimal and pretty much non-efficient. Sorry. - self.centroid = PyEmb.Point(0.0,0.0) - self.pointList = [] - for patch in self.patches: - def visit(idx): - ep = deepcopy(patch.stitches[idx]) - ep.patch = patch - self.centroid+=ep - self.pointList.append(ep) - - visit(0) - visit(-1) - - self.centroid = self.centroid.mul(1.0/float(len(self.pointList))) - - def linear_min(list, func): - min_item = None - min_value = None - for item in list: - value = func(item) - #dbg.write('linear_min %s: value %s => %s (%s)\n' % (func, item, value, value0): - #dbg.write('pass %s\n' % len(self.pointList)); - last_point = sortedPatchList.patches[-1].stitches[-1] - #dbg.write('last_point now %s\n' % last_point) - def distance_from_last_point(p): - return (p-last_point).length() - nearestPoint = linear_min(self.pointList, distance_from_last_point) - takePatchStartingAtPoint(nearestPoint) - - # install the initial result - self.patches = sortedPatchList.patches - - if (1): - # Then hill-climb. - #dbg.write("len(self.patches) = %d\n" % len(self.patches)) - count = 0 - successStr = "" - while (count < 100): - i = random.randint(0, len(self.patches)-1) - j = random.randint(0, len(self.patches)-1) - successStr += self.try_swap(i,j) - - count += 1 - # tidy up at end as best we can - for i in range(len(self.patches)): - successStr += self.try_reverse(i) - - #dbg.write("success: %s\n" % successStr) + def __init__(self, patches): + self.patches = patches + + def __len__(self): + return len(self.patches) + + def sort_by_sortorder(self): + def by_sort_order(a,b): + return cmp(a.sortorder, b.sortorder) + self.patches.sort(by_sort_order) + + def partition_by_color(self): + self.sort_by_sortorder() + #dbg.write("Sorted by sortorder:\n"); + #dbg.write(" %s\n" % ("\n".join(map(lambda p: str(p.sortorder), self.patches)))) + out = [] + lastPatch = None + for patch in self.patches: + if (lastPatch!=None and patch.sortorder==lastPatch.sortorder): + out[-1].patches.append(patch) + else: + out.append(PatchList([patch])) + lastPatch = patch + #dbg.write("Emitted %s partitions\n" % len(out)) + return out + + def tsp_by_color(self): + list_of_patchLists = self.partition_by_color() + for patchList in list_of_patchLists: + patchList.traveling_salesman() + return PatchList(reduce(operator.add, + map(lambda pl: pl.patches, list_of_patchLists))) + +# # TODO apparently dead code; replaced by partition_by_color above +# def clump_like_colors_together(self): +# out = PatchList([]) +# lastPatch = None +# for patch in self.patches: +# if (lastPatch!=None and patch.color==lastPatch.color): +# out.patches[-1] = Patch( +# out.patches[-1].color, +# out.patches[-1].sortorder, +# out.patches[-1].stitches+patch.stitches) +# else: +# out.patches.append(patch) +# lastPatch = patch +# return out + + def get(self, i): + if (i<0 or i>=len(self.patches)): + return None + return self.patches[i] + + def cost(self, a, b): + if (a==None or b==None): + rc = 0.0 + else: + rc = (a.stitches[-1] - b.stitches[0]).length() + #dbg.write("cost(%s, %s) = %5.1f\n" % (a, b, rc)) + return rc + + def try_swap(self, i, j): + # i,j are indices; + #dbg.write("swap(%d, %d)\n" % (i,j)) + oldCost = ( + self.cost(self.get(i-1), self.get(i)) + +self.cost(self.get(i), self.get(i+1)) + +self.cost(self.get(j-1), self.get(j)) + +self.cost(self.get(j), self.get(j+1))) + npi = self.get(j) + npj = self.get(i) + rpi = npi.reverse() + rpj = npj.reverse() + options = [ + (npi,npj), + (rpi,npj), + (npi,rpj), + (rpi,rpj), + ] + def costOf(np): + (npi,npj) = np + return ( + self.cost(self.get(i-1), npi) + +self.cost(npi, self.get(i+1)) + +self.cost(self.get(j-1), npj) + +self.cost(npj, self.get(j+1))) + costs = map(lambda o: (costOf(o), o), options) + costs.sort() + (cost,option) = costs[0] + savings = oldCost - cost + if (savings > 0): + self.patches[i] = option[0] + self.patches[j] = option[1] + success = "!" + else: + success = "." + + #dbg.write("old %5.1f new %5.1f savings: %5.1f\n" % (oldCost, cost, savings)) + return success + + def try_reverse(self, i): + #dbg.write("reverse(%d)\n" % i) + oldCost = (self.cost(self.get(i-1), self.get(i)) + +self.cost(self.get(i), self.get(i+1))) + reversed = self.get(i).reverse() + newCost = (self.cost(self.get(i-1), reversed) + +self.cost(reversed, self.get(i+1))) + savings = oldCost - newCost + if (savings > 0.0): + self.patches[i] = reversed + success = "#" + else: + success = "_" + return success + + def traveling_salesman(self): + # shockingly, this is non-optimal and pretty much non-efficient. Sorry. + self.centroid = PyEmb.Point(0.0,0.0) + self.pointList = [] + for patch in self.patches: + def visit(idx): + ep = deepcopy(patch.stitches[idx]) + ep.patch = patch + self.centroid+=ep + self.pointList.append(ep) + + visit(0) + visit(-1) + + self.centroid = self.centroid.mul(1.0/float(len(self.pointList))) + + def linear_min(list, func): + min_item = None + min_value = None + for item in list: + value = func(item) + #dbg.write('linear_min %s: value %s => %s (%s)\n' % (func, item, value, value0): + #dbg.write('pass %s\n' % len(self.pointList)); + last_point = sortedPatchList.patches[-1].stitches[-1] + #dbg.write('last_point now %s\n' % last_point) + def distance_from_last_point(p): + return (p-last_point).length() + nearestPoint = linear_min(self.pointList, distance_from_last_point) + takePatchStartingAtPoint(nearestPoint) + + # install the initial result + self.patches = sortedPatchList.patches + + if (1): + # Then hill-climb. + #dbg.write("len(self.patches) = %d\n" % len(self.patches)) + count = 0 + successStr = "" + while (count < 100): + i = random.randint(0, len(self.patches)-1) + j = random.randint(0, len(self.patches)-1) + successStr += self.try_swap(i,j) + + count += 1 + # tidy up at end as best we can + for i in range(len(self.patches)): + successStr += self.try_reverse(i) + + #dbg.write("success: %s\n" % successStr) class EmbroideryObject: - def __init__(self, patchList, row_spacing_px): - self.patchList = patchList - self.row_spacing_px = row_spacing_px - - - def make_preamble_stitch(self, lastp, nextp): - def fromPolar(r, phi): - x = r * math.cos(phi) - y = r * math.sin(phi) - return (x, y) - - def toPolar(x, y): - r = math.sqrt(x ** 2 + y ** 2) - if r == 0: - phi = 0 - elif y == 0: - phi = 0 if x > 0 else math.pi - else: - phi = cmp(y, 0) * math.acos(x / r) - return (r, phi) - - v = nextp - lastp - (r, phi) = toPolar(v.x, v.y) - - PREAMBLE_MAX_DIST = 0.5 * pixels_per_millimeter # 1/2mm - if r < PREAMBLE_MAX_DIST: - # nextp is close enough to lastp, so we don't generate - # extra points in between, but just use nextp - return nextp - r = PREAMBLE_MAX_DIST - (x, y) = fromPolar(r, phi) - return PyEmb.Point(x, y) + lastp - - def emit_file(self, filename, output_format, collapse_len_px, add_preamble): - emb = PyEmb.Embroidery() - lastStitch = None - lastColor = None - for patch in self.patchList.patches: - jumpStitch = True - for stitch in patch.stitches: - if lastStitch and lastColor == patch.color: - c = math.sqrt((stitch.x - lastStitch.x) ** 2 + (stitch.y - lastStitch.y) ** 2) - #dbg.write("stitch length: %f (%d/%d -> %d/%d)\n" % (c, lastStitch.x, lastStitch.y, stitch.x, stitch.y)) - - if c == 0: - # filter out duplicate successive stitches - jumpStitch = False - continue - - if jumpStitch: - # consider collapsing jump stich, if it is pretty short - if c < collapse_len_px: - #dbg.write("... collapsed\n") - jumpStitch = False - - #dbg.write("stitch color %s\n" % patch.color) - - newStitch = PyEmb.Point(stitch.x, -stitch.y) - newStitch.color = patch.color - newStitch.jumpStitch = jumpStitch - emb.addStitch(newStitch) - - if jumpStitch and add_preamble != "0": - locs = [ newStitch ] - i = 0 - nextp = PyEmb.Point(patch.stitches[i].x, -patch.stitches[i].y) - - try: - for j in xrange(1, 4): - if locs[-1] == nextp: - i += 1 - nextp = PyEmb.Point(patch.stitches[i].x, -patch.stitches[i].y) - locs.append(self.make_preamble_stitch(locs[-1], nextp)) - except IndexError: - # happens when the patch is very short and we increment i beyond the number of stitches - pass - #dbg.write("preamble locations: %s\n" % locs) - - for j in add_preamble[1:]: - try: - stitch = deepcopy(locs[int(j)]) - stitch.color = patch.color - stitch.jumpStitch = False - emb.addStitch(stitch) - except IndexError: - pass - - jumpStitch = False - lastStitch = newStitch - lastColor = patch.color - - dx, dy = emb.translate_to_origin() - emb.scale(1.0/pixels_per_millimeter) - - fp = open(filename, "wb") - - if output_format == "melco": - fp.write(emb.export_melco(dbg)) - elif output_format == "csv": - fp.write(emb.export_csv(dbg)) - elif output_format == "gcode": - fp.write(emb.export_gcode(dbg)) - fp.close() - emb.scale(pixels_per_millimeter) - emb.translate(dx, dy) - return emb - - def emit_inkscape(self, parent, emb): - emb.scale((1, -1)); - for color, path in emb.export_paths(dbg): - dbg.write('path: %s %s\n' % (color, repr(path))) - inkex.etree.SubElement(parent, - inkex.addNS('path', 'svg'), - { 'style':simplestyle.formatStyle( - { 'stroke': color if color is not None else '#000000', - 'stroke-width':"1", - 'fill': 'none' }), - 'd':simplepath.formatPath(path), - }) - - def bbox(self): - x = [] - y = [] - for patch in self.patchList.patches: - for stitch in patch.stitches: - x.append(stitch.x) - y.append(stitch.y) - return (min(x), min(y), max(x), max(y)) + def __init__(self, patchList, row_spacing_px): + self.patchList = patchList + self.row_spacing_px = row_spacing_px + + + def make_preamble_stitch(self, lastp, nextp): + def fromPolar(r, phi): + x = r * math.cos(phi) + y = r * math.sin(phi) + return (x, y) + + def toPolar(x, y): + r = math.sqrt(x ** 2 + y ** 2) + if r == 0: + phi = 0 + elif y == 0: + phi = 0 if x > 0 else math.pi + else: + phi = cmp(y, 0) * math.acos(x / r) + return (r, phi) + + v = nextp - lastp + (r, phi) = toPolar(v.x, v.y) + + PREAMBLE_MAX_DIST = 0.5 * pixels_per_millimeter # 1/2mm + if r < PREAMBLE_MAX_DIST: + # nextp is close enough to lastp, so we don't generate + # extra points in between, but just use nextp + return nextp + r = PREAMBLE_MAX_DIST + (x, y) = fromPolar(r, phi) + return PyEmb.Point(x, y) + lastp + + def emit_file(self, filename, output_format, collapse_len_px, add_preamble): + emb = PyEmb.Embroidery() + lastStitch = None + lastColor = None + for patch in self.patchList.patches: + jumpStitch = True + for stitch in patch.stitches: + if lastStitch and lastColor == patch.color: + c = math.sqrt((stitch.x - lastStitch.x) ** 2 + (stitch.y - lastStitch.y) ** 2) + #dbg.write("stitch length: %f (%d/%d -> %d/%d)\n" % (c, lastStitch.x, lastStitch.y, stitch.x, stitch.y)) + + if c == 0: + # filter out duplicate successive stitches + jumpStitch = False + continue + + if jumpStitch: + # consider collapsing jump stich, if it is pretty short + if c < collapse_len_px: + #dbg.write("... collapsed\n") + jumpStitch = False + + #dbg.write("stitch color %s\n" % patch.color) + + newStitch = PyEmb.Point(stitch.x, -stitch.y) + newStitch.color = patch.color + newStitch.jumpStitch = jumpStitch + emb.addStitch(newStitch) + + if jumpStitch and add_preamble != "0": + locs = [ newStitch ] + i = 0 + nextp = PyEmb.Point(patch.stitches[i].x, -patch.stitches[i].y) + + try: + for j in xrange(1, 4): + if locs[-1] == nextp: + i += 1 + nextp = PyEmb.Point(patch.stitches[i].x, -patch.stitches[i].y) + locs.append(self.make_preamble_stitch(locs[-1], nextp)) + except IndexError: + # happens when the patch is very short and we increment i beyond the number of stitches + pass + #dbg.write("preamble locations: %s\n" % locs) + + for j in add_preamble[1:]: + try: + stitch = deepcopy(locs[int(j)]) + stitch.color = patch.color + stitch.jumpStitch = False + emb.addStitch(stitch) + except IndexError: + pass + + jumpStitch = False + lastStitch = newStitch + lastColor = patch.color + + dx, dy = emb.translate_to_origin() + emb.scale(1.0/pixels_per_millimeter) + + fp = open(filename, "wb") + + if output_format == "melco": + fp.write(emb.export_melco(dbg)) + elif output_format == "csv": + fp.write(emb.export_csv(dbg)) + elif output_format == "gcode": + fp.write(emb.export_gcode(dbg)) + fp.close() + emb.scale(pixels_per_millimeter) + emb.translate(dx, dy) + return emb + + def emit_inkscape(self, parent, emb): + emb.scale((1, -1)); + for color, path in emb.export_paths(dbg): + dbg.write('path: %s %s\n' % (color, repr(path))) + inkex.etree.SubElement(parent, + inkex.addNS('path', 'svg'), + { 'style':simplestyle.formatStyle( + { 'stroke': color if color is not None else '#000000', + 'stroke-width':"1", + 'fill': 'none' }), + 'd':simplepath.formatPath(path), + }) + + def bbox(self): + x = [] + y = [] + for patch in self.patchList.patches: + for stitch in patch.stitches: + x.append(stitch.x) + y.append(stitch.y) + return (min(x), min(y), max(x), max(y)) class SortOrder: - def __init__(self, threadcolor, layer, preserve_layers): - self.threadcolor = threadcolor - if (preserve_layers): - #dbg.write("preserve_layers is true: %s %s\n" % (layer, threadcolor)); - self.sorttuple = (layer, threadcolor) - else: - #dbg.write("preserve_layers is false:\n"); - self.sorttuple = (threadcolor,) - - def __cmp__(self, other): - return cmp(self.sorttuple, other.sorttuple) - - def __repr__(self): - return "sort %s color %s" % (self.sorttuple, self.threadcolor) + def __init__(self, threadcolor, layer, preserve_layers): + self.threadcolor = threadcolor + if (preserve_layers): + #dbg.write("preserve_layers is true: %s %s\n" % (layer, threadcolor)); + self.sorttuple = (layer, threadcolor) + else: + #dbg.write("preserve_layers is false:\n"); + self.sorttuple = (threadcolor,) + + def __cmp__(self, other): + return cmp(self.sorttuple, other.sorttuple) + + def __repr__(self): + return "sort %s color %s" % (self.sorttuple, self.threadcolor) class Embroider(inkex.Effect): - def __init__(self, *args, **kwargs): - #dbg.write("args: %s\n" % repr(sys.argv)) - inkex.Effect.__init__(self) - self.stacking_order_counter = 0 - self.OptionParser.add_option("-r", "--row_spacing_mm", - action="store", type="float", - dest="row_spacing_mm", default=0.4, - help="row spacing (mm)") - self.OptionParser.add_option("-z", "--zigzag_spacing_mm", - action="store", type="float", - dest="zigzag_spacing_mm", default=1.0, - help="zigzag spacing (mm)") - self.OptionParser.add_option("-l", "--max_stitch_len_mm", - action="store", type="float", - dest="max_stitch_len_mm", default=3.0, - help="max stitch length (mm)") - self.OptionParser.add_option("--running_stitch_len_mm", - action="store", type="float", - dest="running_stitch_len_mm", default=3.0, - help="running stitch length (mm)") - self.OptionParser.add_option("-c", "--collapse_len_mm", - action="store", type="float", - dest="collapse_len_mm", default=0.0, - help="max collapse length (mm)") - self.OptionParser.add_option("-f", "--flatness", - action="store", type="float", - dest="flat", default=0.1, - help="Minimum flatness of the subdivided curves") - self.OptionParser.add_option("-o", "--preserve_layers", - action="store", type="choice", - choices=["true","false"], - dest="preserve_layers", default="false", - help="Sort by stacking order instead of color") - self.OptionParser.add_option("-H", "--hatch_filled_paths", - action="store", type="choice", - choices=["true","false"], - dest="hatch_filled_paths", default="false", - help="Use hatching lines instead of equally-spaced lines to fill paths") - self.OptionParser.add_option("--hide_layers", - action="store", type="choice", - choices=["true","false"], - dest="hide_layers", default="true", - help="Hide all other layers when the embroidery layer is generated") - self.OptionParser.add_option("-p", "--add_preamble", - action="store", type="choice", - choices=["0","010","01010","01210","012101210"], - dest="add_preamble", default="0", - help="Add preamble") - self.OptionParser.add_option("-O", "--output_format", - action="store", type="choice", - choices=["melco", "csv", "gcode"], - dest="output_format", default="melco", - help="File output format") - self.OptionParser.add_option("-F", "--filename", - action="store", type="string", - dest="filename", default="embroider-output.exp", - help="Name (and possibly path) of output file") - self.patches = [] - self.layer_cache = {} - - def get_sort_order(self, threadcolor, node): - #print >> sys.stderr, "node", node.get("id"), self.layer_cache.get(node.get("id")) - return SortOrder(threadcolor, self.layer_cache.get(node.get("id")), self.options.preserve_layers=="true") - - def process_one_path(self, node, shpath, threadcolor, sortorder, angle): - #self.add_shapely_geo_to_svg(shpath.boundary, color="#c0c000") - - hatching = get_boolean_param(node, "hatching", self.hatching) - row_spacing_px = get_float_param(node, "row_spacing", self.row_spacing_px) - max_stitch_len_px = get_float_param(node, "max_stitch_length", self.max_stitch_len_px) - - rows_of_segments = self.intersect_region_with_grating(shpath, row_spacing_px, angle) - segments = self.visit_segments_one_by_one(rows_of_segments) - - def small_stitches(patch, beg, end): - vector = (end-beg) - patch.addStitch(beg) - old_dist = vector.length() - if (old_dist < max_stitch_len_px): - patch.addStitch(end) - return - one_stitch = vector.mul(1.0 / old_dist * max_stitch_len_px * random.random()) - beg = beg + one_stitch - while (True): - vector = (end-beg) - dist = vector.length() - assert(old_dist==None or dist 0: - rows.append([(rows[-1][0][1], runs[0][0])]) - rows.append(runs) - except Exception, ex: - dbg.write("--------------\n") - dbg.write("%s\n" % ex) - dbg.write("%s\n" % shline) - dbg.write("%s\n" % shpath) - dbg.write("==============\n") - continue - return rows - - def visit_segments_one_by_one(self, rows): - def pull_runs(rows): - new_rows = [] - run = [] - for r in rows: - (first,rest) = (r[0], r[1:]) - run.append(first) - if (len(rest)>0): - new_rows.append(rest) - return (run, new_rows) - - linearized_runs = [] - count = 0 - while (len(rows) > 0): - (one_run,rows) = pull_runs(rows) - linearized_runs.extend(one_run) - - rows = rows[::-1] - count += 1 - if (count>100): raise "kablooey" - return linearized_runs - - def handle_node(self, node): - if (node.tag == inkex.addNS('g', 'svg')): - #dbg.write("%s\n"%str((id, etree.tostring(node, pretty_print=True)))) - #dbg.write("not a path; recursing:\n") - for child in node.iter(self.svgpath): - self.handle_node(child) - return - - #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)) - else: - if (self.get_style(node, "fill")!=None): - self.patchList.patches.extend(self.filled_region_to_patchlist(node)) - if (self.get_style(node, "stroke")!=None): - self.patchList.patches.extend(self.path_to_patch_list(node)) - - def get_style(self, node, style_name): - style = simplestyle.parseStyle(node.get("style")) - if (style_name not in style): - return None - value = style[style_name] - if (value==None or value=="none"): - return None - return value - - def cache_layers(self): - self.layer_cache = {} - - layer_tag = inkex.addNS("g", "svg") - group_attr = inkex.addNS('groupmode', 'inkscape') - - - def is_layer(node): - return node.tag == layer_tag and node.get(group_attr) == "layer" - - def process(node, layer=0): - if is_layer(node): - layer += 1 - else: - self.layer_cache[node.get("id")] = layer - - for child in node: - layer = process(child, layer) - - return layer - - process(self.document.getroot()) - - def effect(self): - if self.options.preserve_layers == "true": - self.cache_layers() - #print >> sys.stderr, "cached stacking order:", self.stacking_order - - self.row_spacing_px = self.options.row_spacing_mm * pixels_per_millimeter - self.zigzag_spacing_px = self.options.zigzag_spacing_mm * pixels_per_millimeter - self.max_stitch_len_px = self.options.max_stitch_len_mm*pixels_per_millimeter - self.running_stitch_len_px = self.options.running_stitch_len_mm*pixels_per_millimeter - self.collapse_len_px = self.options.collapse_len_mm*pixels_per_millimeter - self.hatching = self.options.hatch_filled_paths == "true" - - self.svgpath = inkex.addNS('path', 'svg') - self.patchList = PatchList([]) - for node in self.selected.itervalues(): - self.handle_node(node) - - if not self.patchList: - inkex.errormsg("No paths selected.") + def __init__(self, *args, **kwargs): + #dbg.write("args: %s\n" % repr(sys.argv)) + inkex.Effect.__init__(self) + self.stacking_order_counter = 0 + self.OptionParser.add_option("-r", "--row_spacing_mm", + action="store", type="float", + dest="row_spacing_mm", default=0.4, + help="row spacing (mm)") + self.OptionParser.add_option("-z", "--zigzag_spacing_mm", + action="store", type="float", + dest="zigzag_spacing_mm", default=1.0, + help="zigzag spacing (mm)") + self.OptionParser.add_option("-l", "--max_stitch_len_mm", + action="store", type="float", + dest="max_stitch_len_mm", default=3.0, + help="max stitch length (mm)") + self.OptionParser.add_option("--running_stitch_len_mm", + action="store", type="float", + dest="running_stitch_len_mm", default=3.0, + help="running stitch length (mm)") + self.OptionParser.add_option("-c", "--collapse_len_mm", + action="store", type="float", + dest="collapse_len_mm", default=0.0, + help="max collapse length (mm)") + self.OptionParser.add_option("-f", "--flatness", + action="store", type="float", + dest="flat", default=0.1, + help="Minimum flatness of the subdivided curves") + self.OptionParser.add_option("-o", "--preserve_layers", + action="store", type="choice", + choices=["true","false"], + dest="preserve_layers", default="false", + help="Sort by stacking order instead of color") + self.OptionParser.add_option("-H", "--hatch_filled_paths", + action="store", type="choice", + choices=["true","false"], + dest="hatch_filled_paths", default="false", + help="Use hatching lines instead of equally-spaced lines to fill paths") + self.OptionParser.add_option("--hide_layers", + action="store", type="choice", + choices=["true","false"], + dest="hide_layers", default="true", + help="Hide all other layers when the embroidery layer is generated") + self.OptionParser.add_option("-p", "--add_preamble", + action="store", type="choice", + choices=["0","010","01010","01210","012101210"], + dest="add_preamble", default="0", + help="Add preamble") + self.OptionParser.add_option("-O", "--output_format", + action="store", type="choice", + choices=["melco", "csv", "gcode"], + dest="output_format", default="melco", + help="File output format") + self.OptionParser.add_option("-F", "--filename", + action="store", type="string", + dest="filename", default="embroider-output.exp", + help="Name (and possibly path) of output file") + self.patches = [] + self.layer_cache = {} + + def get_sort_order(self, threadcolor, node): + #print >> sys.stderr, "node", node.get("id"), self.layer_cache.get(node.get("id")) + return SortOrder(threadcolor, self.layer_cache.get(node.get("id")), self.options.preserve_layers=="true") + + def process_one_path(self, node, shpath, threadcolor, sortorder, angle): + #self.add_shapely_geo_to_svg(shpath.boundary, color="#c0c000") + + hatching = get_boolean_param(node, "hatching", self.hatching) + row_spacing_px = get_float_param(node, "row_spacing", self.row_spacing_px) + max_stitch_len_px = get_float_param(node, "max_stitch_length", self.max_stitch_len_px) + + rows_of_segments = self.intersect_region_with_grating(shpath, row_spacing_px, angle) + segments = self.visit_segments_one_by_one(rows_of_segments) + + def small_stitches(patch, beg, end): + vector = (end-beg) + patch.addStitch(beg) + old_dist = vector.length() + if (old_dist < max_stitch_len_px): + patch.addStitch(end) + return + one_stitch = vector.mul(1.0 / old_dist * max_stitch_len_px * random.random()) + beg = beg + one_stitch + while (True): + vector = (end-beg) + dist = vector.length() + assert(old_dist==None or dist\n" % repr(stroke_width)) - #dbg.flush() - - running_stitch_len_px = get_float_param(node, "stitch_length", self.running_stitch_len_px) - zigzag_spacing_px = get_float_param(node, "zigzag_spacing", self.zigzag_spacing_px) - repeats = get_int_param(node, "repeats", 1) - - sortorder = self.get_sort_order(threadcolor, node) - paths = flatten(parse_path(node), self.options.flat) - - # regularize the points lists. - # (If we're parsing beziers, there will be a list of multi-point - # subarrays.) - - patches = [] - - for path in paths: - path = [PyEmb.Point(x, y) for x, y in path] - if (stroke_width <= STROKE_MIN): - #dbg.write("self.max_stitch_len_px = %s\n" % self.max_stitch_len_px) - patch = self.stroke_points(path, running_stitch_len_px, 0.0, repeats, threadcolor, sortorder) - else: - patch = self.stroke_points(path, zigzag_spacing_px*0.5, stroke_width, repeats, threadcolor, sortorder) - patches.extend(patch) - - return patches - - def stroke_points(self, emb_point_list, zigzag_spacing_px, stroke_width, repeats, threadcolor, sortorder): - patch = Patch(color=threadcolor, sortorder=sortorder) - p0 = emb_point_list[0] - rho = 0.0 - fact = 1 - - for repeat in xrange(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().mul(stroke_width*0.5) - - # iteration variable: how far we are along segment - while (rho <= seg_len): - left_pt = p0+along.mul(rho)+perp.mul(fact) - patch.addStitch(left_pt) - rho += zigzag_spacing_px - fact = -fact - - p0 = p1 - rho -= seg_len - - return [patch] - - def filled_region_to_patchlist(self, node): - angle = math.radians(float(get_float_param(node,'angle',0))) - paths = flatten(parse_path(node), self.options.flat) - shapelyPolygon = cspToShapelyPolygon(paths) - threadcolor = simplestyle.parseStyle(node.get("style"))["fill"] - sortorder = self.get_sort_order(threadcolor, node) - return self.process_one_path( - node, - shapelyPolygon, - threadcolor, - sortorder, - angle) - - #TODO def make_stroked_patch(self, node): + one_stitch = vector.mul(1.0/dist*max_stitch_len_px) + beg = beg + one_stitch + + swap = False + patch = Patch(color=threadcolor,sortorder=sortorder) + for (beg,end) in segments: + if (swap): + (beg,end)=(end,beg) + if not hatching: + swap = not swap + small_stitches(patch, PyEmb.Point(*beg),PyEmb.Point(*end)) + return [patch] + + def intersect_region_with_grating(self, shpath, row_spacing_px, angle): + #dbg.write("bounds = %s\n" % str(shpath.bounds)) + rotated_shpath = affinity.rotate(shpath, angle, use_radians = True) + bbox = rotated_shpath.bounds + delta = row_spacing_px * 50 # *2 should be enough but isn't. TODO: find out why, and if this always works. + bbox = affinity.rotate(shgeo.LinearRing(((bbox[0] - delta, bbox[1] - delta), (bbox[2] + delta, bbox[1] - delta), (bbox[2] + delta, bbox[3] + delta), (bbox[0] - delta, bbox[3] + delta))), -angle, use_radians = True).coords + + p0 = PyEmb.Point(bbox[0][0], bbox[0][1]) + p1 = PyEmb.Point(bbox[1][0], bbox[1][1]) + p2 = PyEmb.Point(bbox[3][0], bbox[3][1]) + count = (p2 - p0).length() / row_spacing_px + p_inc = (p2 - p0).mul(1 / count) + count += 2 + + rows = [] + steps = 0 + while (steps < count): + try: + steps += 1 + p0 += p_inc + p1 += p_inc + endpoints = [p0.as_tuple(), p1.as_tuple()] + shline = shgeo.LineString(endpoints) + res = shline.intersection(shpath) + if (isinstance(res, shgeo.MultiLineString)): + runs = map(shapelyLineSegmentToPyTuple, res.geoms) + else: + runs = [shapelyLineSegmentToPyTuple(res)] + if self.hatching and len(rows) > 0: + rows.append([(rows[-1][0][1], runs[0][0])]) + rows.append(runs) + except Exception, ex: + dbg.write("--------------\n") + dbg.write("%s\n" % ex) + dbg.write("%s\n" % shline) + dbg.write("%s\n" % shpath) + dbg.write("==============\n") + continue + return rows + + def visit_segments_one_by_one(self, rows): + def pull_runs(rows): + new_rows = [] + run = [] + for r in rows: + (first,rest) = (r[0], r[1:]) + run.append(first) + if (len(rest)>0): + new_rows.append(rest) + return (run, new_rows) + + linearized_runs = [] + count = 0 + while (len(rows) > 0): + (one_run,rows) = pull_runs(rows) + linearized_runs.extend(one_run) + + rows = rows[::-1] + count += 1 + if (count>100): raise "kablooey" + return linearized_runs + + def handle_node(self, node): + if (node.tag == inkex.addNS('g', 'svg')): + #dbg.write("%s\n"%str((id, etree.tostring(node, pretty_print=True)))) + #dbg.write("not a path; recursing:\n") + for child in node.iter(self.svgpath): + self.handle_node(child) + return + + #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)) + else: + if (self.get_style(node, "fill")!=None): + self.patchList.patches.extend(self.filled_region_to_patchlist(node)) + if (self.get_style(node, "stroke")!=None): + self.patchList.patches.extend(self.path_to_patch_list(node)) + + def get_style(self, node, style_name): + style = simplestyle.parseStyle(node.get("style")) + if (style_name not in style): + return None + value = style[style_name] + if (value==None or value=="none"): + return None + return value + + def cache_layers(self): + self.layer_cache = {} + + layer_tag = inkex.addNS("g", "svg") + group_attr = inkex.addNS('groupmode', 'inkscape') + + + def is_layer(node): + return node.tag == layer_tag and node.get(group_attr) == "layer" + + def process(node, layer=0): + if is_layer(node): + layer += 1 + else: + self.layer_cache[node.get("id")] = layer + + for child in node: + layer = process(child, layer) + + return layer + + process(self.document.getroot()) + + def effect(self): + if self.options.preserve_layers == "true": + self.cache_layers() + #print >> sys.stderr, "cached stacking order:", self.stacking_order + + self.row_spacing_px = self.options.row_spacing_mm * pixels_per_millimeter + self.zigzag_spacing_px = self.options.zigzag_spacing_mm * pixels_per_millimeter + self.max_stitch_len_px = self.options.max_stitch_len_mm*pixels_per_millimeter + self.running_stitch_len_px = self.options.running_stitch_len_mm*pixels_per_millimeter + self.collapse_len_px = self.options.collapse_len_mm*pixels_per_millimeter + self.hatching = self.options.hatch_filled_paths == "true" + + self.svgpath = inkex.addNS('path', 'svg') + self.patchList = PatchList([]) + for node in self.selected.itervalues(): + self.handle_node(node) + + if not self.patchList: + inkex.errormsg("No paths selected.") + return + + self.patchList = self.patchList.tsp_by_color() + #dbg.write("patch count: %d\n" % len(self.patchList.patches)) + + if self.options.hide_layers: + self.hide_layers() + + eo = EmbroideryObject(self.patchList, self.row_spacing_px) + emb = eo.emit_file(self.options.filename, self.options.output_format, + self.collapse_len_px, self.options.add_preamble) + + new_layer = inkex.etree.SubElement(self.document.getroot(), + inkex.addNS('g', 'svg'), {}) + new_layer.set('id', self.uniqueId("embroidery")) + new_layer.set(inkex.addNS('label', 'inkscape'), 'Embroidery') + new_layer.set(inkex.addNS('groupmode', 'inkscape'), 'layer') + eo.emit_inkscape(new_layer, emb) + + def emit_inkscape_bbox(self, parent, eo): + (x0, y0, x1, y1) = eo.bbox() + new_path = [] + new_path.append(['M', (x0,y0)]) + new_path.append(['L', (x1,y0)]) + new_path.append(['L', (x1,y1)]) + new_path.append(['L', (x0,y1)]) + new_path.append(['L', (x0,y0)]) + inkex.etree.SubElement(parent, + inkex.addNS('path', 'svg'), + { 'style':simplestyle.formatStyle( + { 'stroke': '#ff00ff', + 'stroke-width':str(1), + 'fill': 'none' }), + 'd':simplepath.formatPath(new_path), + }) + + def hide_layers(self): + for g in self.document.getroot().findall(inkex.addNS("g","svg")): + if g.get(inkex.addNS("groupmode", "inkscape")) == "layer": + g.set("style", "display:none") + + def path_to_patch_list(self, node): + threadcolor = simplestyle.parseStyle(node.get("style"))["stroke"] + stroke_width_str = simplestyle.parseStyle(node.get("style"))["stroke-width"] + if (stroke_width_str.endswith("px")): + # don't really know how we should be doing unit conversions. + # but let's hope px are kind of like pts? + stroke_width_str = stroke_width_str[:-2] + stroke_width = float(stroke_width_str) + #dbg.write("stroke_width is <%s>\n" % repr(stroke_width)) + #dbg.flush() + + running_stitch_len_px = get_float_param(node, "stitch_length", self.running_stitch_len_px) + zigzag_spacing_px = get_float_param(node, "zigzag_spacing", self.zigzag_spacing_px) + repeats = get_int_param(node, "repeats", 1) + + sortorder = self.get_sort_order(threadcolor, node) + paths = flatten(parse_path(node), self.options.flat) + + # regularize the points lists. + # (If we're parsing beziers, there will be a list of multi-point + # subarrays.) + + patches = [] + + for path in paths: + path = [PyEmb.Point(x, y) for x, y in path] + if (stroke_width <= STROKE_MIN): + #dbg.write("self.max_stitch_len_px = %s\n" % self.max_stitch_len_px) + patch = self.stroke_points(path, running_stitch_len_px, 0.0, repeats, threadcolor, sortorder) + else: + patch = self.stroke_points(path, zigzag_spacing_px*0.5, stroke_width, repeats, threadcolor, sortorder) + patches.extend(patch) + + return patches + + def stroke_points(self, emb_point_list, zigzag_spacing_px, stroke_width, repeats, threadcolor, sortorder): + patch = Patch(color=threadcolor, sortorder=sortorder) + p0 = emb_point_list[0] + rho = 0.0 + fact = 1 + + for repeat in xrange(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().mul(stroke_width*0.5) + + # iteration variable: how far we are along segment + while (rho <= seg_len): + left_pt = p0+along.mul(rho)+perp.mul(fact) + patch.addStitch(left_pt) + rho += zigzag_spacing_px + fact = -fact + + p0 = p1 + rho -= seg_len + + return [patch] + + def filled_region_to_patchlist(self, node): + angle = math.radians(float(get_float_param(node,'angle',0))) + paths = flatten(parse_path(node), self.options.flat) + shapelyPolygon = cspToShapelyPolygon(paths) + threadcolor = simplestyle.parseStyle(node.get("style"))["fill"] + sortorder = self.get_sort_order(threadcolor, node) + return self.process_one_path( + node, + shapelyPolygon, + threadcolor, + sortorder, + angle) + + #TODO def make_stroked_patch(self, node): if __name__ == '__main__': - sys.setrecursionlimit(100000); - e = Embroider() - e.affect() - #dbg.write("aaaand, I'm done. seeeya!\n") - dbg.flush() + sys.setrecursionlimit(100000); + e = Embroider() + e.affect() + #dbg.write("aaaand, I'm done. seeeya!\n") + dbg.flush() dbg.close() -- cgit v1.2.3