diff --git a/PyEmb.py b/PyEmb.py index 1b8ba707c..46d7bf77f 100644 --- a/PyEmb.py +++ b/PyEmb.py @@ -4,11 +4,6 @@ import math import sys -dbg = sys.stderr - -def abs(x): - if (x<0): return -x - return x class Point: def __init__(self, x, y): @@ -50,7 +45,8 @@ class Embroidery: self.coords = [] def addStitch(self, coord): - self.coords.append(coord) + if len(self.coords) == 0 or self.coords[-1] != coord: + self.coords.append(coord) def translate_to_origin(self): if (len(self.coords)==0): @@ -67,12 +63,13 @@ class Embroidery: for p in self.coords: p.x -= minx p.y -= miny - dbg.write("Field size %s x %s\n" % (sx,sy)) def scale(self, sc): + if not isinstance(sc, (tuple, list)): + sc = (sc, sc) for p in self.coords: - p.x *= sc - p.y *= sc + p.x *= sc[0] + p.y *= sc[1] def export_ksm(self, dbg): str = "" @@ -104,7 +101,7 @@ class Embroidery: def export_melco(self, dbg): self.str = "" self.pos = self.coords[0] - dbg.write("stitch count: %d\n" % len(self.coords)) + #dbg.write("stitch count: %d\n" % len(self.coords)) lastColor = None numColors = 0x0 for stitch in self.coords[1:]: @@ -144,29 +141,59 @@ class Embroidery: self.pos = stitch return self.str - def export_csv(self, dbg): - self.str = "" - self.str += '"#","[THREAD_NUMBER]","[RED]","[GREEN]","[BLUE]","[DESCRIPTION]","[CATALOG_NUMBER]"\n' - self.str += '"#","[STITCH_TYPE]","[X]","[Y]"\n' + def export_csv(self, dbg): + self.str = "" + self.str += '"#","[THREAD_NUMBER]","[RED]","[GREEN]","[BLUE]","[DESCRIPTION]","[CATALOG_NUMBER]"\n' + self.str += '"#","[STITCH_TYPE]","[X]","[Y]"\n' - lastColor = None - colorIndex = 0 - for stitch in self.coords: - if lastColor == None or stitch.color != lastColor: - colorIndex += 1 - self.str += '"$","%d","%d","%d","%d","(null)","(null)"\n' % ( - colorIndex, - int(stitch.color[1:3], 16), - int(stitch.color[3:5], 16), - int(stitch.color[5:7], 16)) - if stitch.jumpStitch: - self.str += '"*","JUMP","%f","%f"\n' % (stitch.x/10, stitch.y/10) - if lastColor != None and stitch.color != lastColor: - # not first color choice, add color change record - self.str += '"*","COLOR","%f","%f"\n' % (stitch.x/10, stitch.y/10) - self.str += '"*","STITCH","%f","%f"\n' % (stitch.x/10, stitch.y/10) - lastColor = stitch.color - return self.str + lastColor = None + colorIndex = 0 + for stitch in self.coords: + if lastColor == None or stitch.color != lastColor: + colorIndex += 1 + self.str += '"$","%d","%d","%d","%d","(null)","(null)"\n' % ( + colorIndex, + int(stitch.color[1:3], 16), + int(stitch.color[3:5], 16), + int(stitch.color[5:7], 16)) + if stitch.jumpStitch: + self.str += '"*","JUMP","%f","%f"\n' % (stitch.x/10, stitch.y/10) + if lastColor != None and stitch.color != lastColor: + # not first color choice, add color change record + self.str += '"*","COLOR","%f","%f"\n' % (stitch.x/10, stitch.y/10) + self.str += '"*","STITCH","%f","%f"\n' % (stitch.x/10, stitch.y/10) + lastColor = stitch.color + return self.str + + def export_gcode(self, dbg): + ret = [] + lastColor = None + for stitch in self.coords: + if stitch.color != lastColor: + ret.append('M0 ;MSG, Color change; prepare for %s\n' % stitch.color) + lastColor = stitch.color + ret.append('G1 X%f Y%f\n' % stitch.as_tuple()) + ret.append('M0 ;MSG, EMBROIDER stitch\n') + return ''.join(ret) + + def export_paths(self, dbg): + paths = [] + lastColor = None + lastStitch = None + for stitch in self.coords: + if stitch.jumpStitch: + if lastColor == stitch.color: + paths.append([None, []]) + if lastStitch is not None: + paths[-1][1].append(['M', lastStitch.as_tuple()]) + paths[-1][1].append(['L', stitch.as_tuple()]) + lastColor = None + if stitch.color != lastColor: + paths.append([stitch.color, []]) + paths[-1][1].append(['L' if len(paths[-1][1]) > 0 else 'M', stitch.as_tuple()]) + lastColor = stitch.color + lastStitch = stitch + return paths class Test: def __init__(self): diff --git a/embroider.inx b/embroider.inx index 8fc221316..cb12591ab 100644 --- a/embroider.inx +++ b/embroider.inx @@ -6,7 +6,7 @@ inkex.py 1.00 0.40 - 3.0 + 3.0 0.0 false false @@ -20,6 +20,7 @@ <_option value="melco">Melco <_option value="csv">Embroidermodder 2 CSV + <_option value="gcode">Franklin G-Code embroider-output.exp diff --git a/embroider.py b/embroider.py index f582487d2..ca239d3db 100644 --- a/embroider.py +++ b/embroider.py @@ -34,7 +34,7 @@ import lxml.etree as etree from lxml.builder import E import shapely.geometry as shgeo -dbg = open("embroider-debug.txt", "w") +dbg = open("/tmp/embroider-debug.txt", "w") PyEmb.dbg = dbg pixels_per_millimeter = 90.0 / 25.4 @@ -132,8 +132,8 @@ class PatchList: 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)))) + #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: @@ -142,7 +142,7 @@ class PatchList: else: out.append(PatchList([patch])) lastPatch = patch - dbg.write("Emitted %s partitions\n" % len(out)) + #dbg.write("Emitted %s partitions\n" % len(out)) return out def tsp_by_color(self): @@ -182,7 +182,7 @@ class PatchList: def try_swap(self, i, j): # i,j are indices; - dbg.write("swap(%d, %d)\n" % (i,j)) + #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)) @@ -216,11 +216,11 @@ class PatchList: else: success = "." - dbg.write("old %5.1f new %5.1f savings: %5.1f\n" % (oldCost, cost, savings)) + #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) + #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() @@ -265,27 +265,19 @@ class PatchList: sortedPatchList = PatchList([]) def takePatchStartingAtPoint(point): patch = point.patch - dbg.write("takePatchStartingAtPoint angling for patch %s--%s\n" % ( - patch.stitches[0], - patch.stitches[-1])) + #dbg.write("takePatchStartingAtPoint angling for patch %s--%s\n" % (patch.stitches[0],patch.stitches[-1])) self.pointList = filter(lambda pt: pt.patch!=patch, self.pointList) reversed = "" if (point!=patch.stitches[0]): reversed = " (reversed)" - dbg.write('patch.stitches[0] %s point %s match %s\n' % ( - patch.stitches[0], - point, - point==patch.stitches[0])) + #dbg.write('patch.stitches[0] %s point %s match %s\n' % (patch.stitches[0], point, point==patch.stitches[0])) patch = patch.reverse() sortedPatchList.patches.append(patch) - dbg.write('took patch %s--%s %s\n' % ( - patch.stitches[0], - patch.stitches[-1], - reversed)) + #dbg.write('took patch %s--%s %s\n' % (patch.stitches[0], patch.stitches[-1], reversed)) # Take the patch farthest from the centroid first # O(n) - dbg.write('centroid: %s\n' % self.centroid) + #dbg.write('centroid: %s\n' % self.centroid) def neg_distance_from_centroid(p): return -(p-self.centroid).length() farthestPoint = linear_min(self.pointList, neg_distance_from_centroid) @@ -295,9 +287,9 @@ class PatchList: # Then greedily take closer-and-closer patches # O(n^2) while (len(self.pointList)>0): - dbg.write('pass %s\n' % len(self.pointList)); + #dbg.write('pass %s\n' % len(self.pointList)); last_point = sortedPatchList.patches[-1].stitches[-1] - dbg.write('last_point now %s\n' % last_point) + #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) @@ -308,7 +300,7 @@ class PatchList: if (1): # Then hill-climb. - dbg.write("len(self.patches) = %d\n" % len(self.patches)) + #dbg.write("len(self.patches) = %d\n" % len(self.patches)) count = 0 successStr = "" while (count < 100): @@ -321,7 +313,7 @@ class PatchList: for i in range(len(self.patches)): successStr += self.try_reverse(i) - dbg.write("success: %s\n" % successStr) + #dbg.write("success: %s\n" % successStr) class EmbroideryObject: def __init__(self, patchList, row_spacing_px): @@ -365,8 +357,8 @@ class EmbroideryObject: 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)) + 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 @@ -376,10 +368,10 @@ class EmbroideryObject: if jumpStitch: # consider collapsing jump stich, if it is pretty short if c < collapse_len_px: - dbg.write("... collapsed\n") + #dbg.write("... collapsed\n") jumpStitch = False - dbg.write("stitch color %s\n" % patch.color) + #dbg.write("stitch color %s\n" % patch.color) newStitch = PyEmb.Point(stitch.x, -stitch.y) newStitch.color = patch.color @@ -400,7 +392,7 @@ class EmbroideryObject: 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) + #dbg.write("preamble locations: %s\n" % locs) for j in add_preamble[1:]: try: @@ -415,8 +407,8 @@ class EmbroideryObject: lastStitch = newStitch lastColor = patch.color - emb.translate_to_origin() - emb.scale(10.0/pixels_per_millimeter) + #emb.translate_to_origin() + emb.scale(1.0/pixels_per_millimeter) fp = open(filename, "wb") @@ -424,38 +416,23 @@ class EmbroideryObject: 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) + return emb - def emit_inkscape(self, parent): - lastPatch = None - for patch in self.patchList.patches: - if (lastPatch!=None): - # draw jump stitch - inkex.etree.SubElement(parent, - inkex.addNS('path', 'svg'), - { 'style':simplestyle.formatStyle( - { 'stroke': lastPatch.color, - 'stroke-width':str(self.row_spacing_px*.25), - 'stroke-dasharray':'0.99, 1.98', - 'fill': 'none' }), - 'd':simplepath.formatPath([ - ['M', (lastPatch.stitches[-1].as_tuple())], - ['L', (patch.stitches[0].as_tuple())] - ]), - }) - lastPatch = patch - - new_path = [] - new_path.append(['M', patch.stitches[0].as_tuple()]) - for stitch in patch.stitches[1:]: - new_path.append(['L', stitch.as_tuple()]) + 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': patch.color, - 'stroke-width':str(self.row_spacing_px*0.25), + { 'stroke': color if color is not None else '#000000', + 'stroke-width':str(self.row_spacing_px*0.5), 'fill': 'none' }), - 'd':simplepath.formatPath(new_path), + 'd':simplepath.formatPath(path), }) def bbox(self): @@ -471,10 +448,10 @@ class SortOrder: def __init__(self, threadcolor, stacking_order, preserve_order): self.threadcolor = threadcolor if (preserve_order): - dbg.write("preserve_order is true:\n"); + #dbg.write("preserve_order is true:\n"); self.sorttuple = (stacking_order, threadcolor) else: - dbg.write("preserve_order is false:\n"); + #dbg.write("preserve_order is false:\n"); self.sorttuple = (threadcolor, stacking_order) def __cmp__(self, other): @@ -485,7 +462,7 @@ class SortOrder: class Embroider(inkex.Effect): def __init__(self, *args, **kwargs): - dbg.write("args: %s\n" % repr(sys.argv)) + #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", @@ -525,7 +502,7 @@ class Embroider(inkex.Effect): help="Add preamble") self.OptionParser.add_option("-O", "--output_format", action="store", type="choice", - choices=["melco", "csv"], + choices=["melco", "csv", "gcode"], dest="output_format", default="melco", help="File output format") self.OptionParser.add_option("-F", "--filename", @@ -560,53 +537,34 @@ class Embroider(inkex.Effect): beg = beg + one_stitch swap = False - patches = [] + patch = Patch(color=threadcolor,sortorder=sortorder) for (beg,end) in segments: - patch = Patch(color=threadcolor,sortorder=sortorder) if (swap): (beg,end)=(end,beg) - swap = not swap + if not self.hatching: + swap = not swap small_stitches(patch, PyEmb.Point(*beg),PyEmb.Point(*end)) - patches.append(patch) - return patches + return [patch] def intersect_region_with_grating(self, shpath): - dbg.write("bounds = %s\n" % str(shpath.bounds)) + #dbg.write("bounds = %s\n" % str(shpath.bounds)) bbox = shpath.bounds - hatching = self.options.hatch_filled_paths == "true" - dbg.write("hatching is %s\n" % hatching) + #dbg.write("hatching is %s\n" % hatching) delta = self.row_spacing_px/2.0 bbox_sz = (bbox[2]-bbox[0],bbox[3]-bbox[1]) - if (bbox_sz[0] > bbox_sz[1]): - # wide box, use vertical stripes - p0 = PyEmb.Point(bbox[0]-delta,bbox[1]) - p1 = PyEmb.Point(bbox[0]-delta,bbox[3]) - p_inc = PyEmb.Point(self.row_spacing_px, 0) - count = (bbox[2]-bbox[0])/self.row_spacing_px + 2 - else: - # narrow box, use horizontal stripes - p0 = PyEmb.Point(bbox[0], bbox[1]-delta) - p1 = PyEmb.Point(bbox[2], bbox[1]-delta) - p_inc = PyEmb.Point(0, self.row_spacing_px) - count = (bbox[3]-bbox[1])/self.row_spacing_px + 2 - - if hatching: - count *= 2 + p0 = PyEmb.Point(bbox[0]-delta,bbox[1]) + p1 = PyEmb.Point(bbox[0]-delta,bbox[3]) + p_inc = PyEmb.Point(self.row_spacing_px, 0) + count = (bbox[2]-bbox[0])/self.row_spacing_px + 2 rows = [] steps = 0 while (steps < count): try: steps += 1 - if hatching: - if steps % 2 == 1: - p1 += p_inc - else: - p0 += p_inc - else: - p0 += p_inc - p1 += p_inc + p0 += p_inc + p1 += p_inc endpoints = [p0.as_tuple(), p1.as_tuple()] shline = shgeo.LineString(endpoints) res = shline.intersection(shpath) @@ -614,6 +572,8 @@ class Embroider(inkex.Effect): 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") @@ -649,13 +609,13 @@ class Embroider(inkex.Effect): def handle_node(self, node): if (node.tag != self.svgpath): - dbg.write("%s\n"%str((id, etree.tostring(node, pretty_print=True)))) - dbg.write("not a path; recursing:\n") + #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)))) + #dbg.write("Node: %s\n"%str((id, etree.tostring(node, pretty_print=True)))) israw = False desc = node.findtext(inkex.addNS('desc', 'svg')) @@ -683,6 +643,7 @@ class Embroider(inkex.Effect): 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.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([]) @@ -690,15 +651,15 @@ class Embroider(inkex.Effect): self.handle_node(node) self.patchList = self.patchList.tsp_by_color() - dbg.write("patch count: %d\n" % len(self.patchList.patches)) + #dbg.write("patch count: %d\n" % len(self.patchList.patches)) eo = EmbroideryObject(self.patchList, self.row_spacing_px) - eo.emit_file(self.options.filename, self.options.output_format, + emb = eo.emit_file(self.options.filename, self.options.output_format, self.collapse_len_px, self.options.add_preamble) new_group = inkex.etree.SubElement(self.current_layer, inkex.addNS('g', 'svg'), {}) - eo.emit_inkscape(new_group) + eo.emit_inkscape(new_group, emb) self.emit_inkscape_bbox(new_group, eo) @@ -727,8 +688,8 @@ class Embroider(inkex.Effect): # 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() + #dbg.write("stroke_width is <%s>\n" % repr(stroke_width)) + #dbg.flush() sortorder = self.get_sort_order(threadcolor) path = simplepath.parsePath(node.get("d")) @@ -742,7 +703,7 @@ class Embroider(inkex.Effect): def flush_point_list(): STROKE_MIN = 0.5 # a 0.5pt stroke becomes a straight line. if (stroke_width <= STROKE_MIN): - dbg.write("self.max_stitch_len_px = %s\n" % self.max_stitch_len_px) + #dbg.write("self.max_stitch_len_px = %s\n" % self.max_stitch_len_px) patch = self.stroke_points(emb_point_list, self.max_stitch_len_px, 0.0, threadcolor, sortorder) else: patch = self.stroke_points(emb_point_list, self.zigzag_spacing_px*0.5, stroke_width, threadcolor, sortorder) @@ -750,13 +711,13 @@ class Embroider(inkex.Effect): close_point = None for (type,points) in path: - dbg.write("path_to_patch_list parses pt %s with type=%s\n" % (points, type)) + #dbg.write("path_to_patch_list parses pt %s with type=%s\n" % (points, type)) if type == 'M' and len(emb_point_list): flush_point_list() emb_point_list = [] if type == 'Z': - dbg.write("... closing patch to %s\n" % close_point) + #dbg.write("... closing patch to %s\n" % close_point) emb_point_list.append(close_point) else: pointscopy = list(points) @@ -764,7 +725,7 @@ class Embroider(inkex.Effect): emb_point_list.append(PyEmb.Point(pointscopy[0], pointscopy[1])) pointscopy = pointscopy[2:] if type == 'M': - dbg.write("latching close_point %s\n" % emb_point_list[-1]) + #dbg.write("latching close_point %s\n" % emb_point_list[-1]) close_point = emb_point_list[-1] flush_point_list() @@ -818,7 +779,7 @@ if __name__ == '__main__': sys.setrecursionlimit(100000); e = Embroider() e.affect() - dbg.write("aaaand, I'm done. seeeya!\n") + #dbg.write("aaaand, I'm done. seeeya!\n") dbg.flush() dbg.close()