commit 47449d22cb9ef299d6cd9d8661e4d1f5fcb09f2a Author: Stefan Siegl Date: Fri Dec 26 23:15:48 2014 +0100 Initial import of upstream code Embroidery output extension for Inkscape; downloaded from http://www.jonh.net/~jonh/inkscape-embroidery/ on 2014-12-26 19:38 CET Copyright 2010 by Jon Howell, licensed under GPLv3. diff --git a/PyEmb.py b/PyEmb.py new file mode 100644 index 000000000..6f14fdedc --- /dev/null +++ b/PyEmb.py @@ -0,0 +1,241 @@ +#!python +#!/usr/bin/python +# http://www.achatina.de/sewing/main/TECHNICL.HTM + +import math +import sys +dbg = sys.stderr + +def abs(x): + if (x<0): return -x + return x + +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 __repr__(self): + return "Pt(%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 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()) + +class Embroidery: + def __init__(self): + self.coords = [] + + def addStitch(self, coord): + self.coords.append(coord) + + def translate_to_origin(self): + if (len(self.coords)==0): + return + (maxx,maxy) = (self.coords[0].x,self.coords[0].y) + (minx,miny) = (self.coords[0].x,self.coords[0].y) + for p in self.coords: + minx = min(minx,p.x) + miny = min(miny,p.y) + maxx = max(maxx,p.x) + maxy = max(maxy,p.y) + sx = maxx-minx + sy = maxy-miny + 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): + for p in self.coords: + p.x *= sc + p.y *= sc + + def export_ksm(self, dbg): + str = "" + self.pos = Point(0,0) + lastColor = None + for stitch in self.coords: + if (lastColor!=None and stitch.color!=lastColor): + mode_byte = 0x99 + #dbg.write("Color change!\n") + else: + mode_byte = 0x80 + #dbg.write("color still %s\n" % stitch.color) + lastColor = stitch.color + new_int = stitch.as_int() + old_int = self.pos.as_int() + delta = new_int - old_int + assert(abs(delta.x)<=127) + assert(abs(delta.y)<=127) + str+=chr(abs(delta.y)) + str+=chr(abs(delta.x)) + if (delta.y<0): + mode_byte |= 0x20 + if (delta.x<0): + mode_byte |= 0x40 + str+=chr(mode_byte) + self.pos = stitch + return str + + def export_melco(self, dbg): + self.str = "" + self.pos = self.coords[0] + dbg.write("stitch count: %d\n" % len(self.coords)) + lastColor = None + numColors = 0x0 + for stitch in self.coords[1:]: + if (lastColor!=None and stitch.color!=lastColor): + numColors += 1 + # color change + self.str += chr(0x80) + self.str += chr(0x01) +# self.str += chr(numColors) +# self.str += chr(((numColors+0x80)>>8)&0xff) +# self.str += chr(((numColors+0x80)>>0)&0xff) + lastColor = stitch.color + new_int = stitch.as_int() + old_int = self.pos.as_int() + delta = new_int - old_int + + def move(x,y): + if (x<0): x = x + 256 + self.str+=chr(x) + if (y<0): y = y + 256 + self.str+=chr(y) + + while (delta.x!=0 or delta.y!=0): + def clamp(v): + if (v>127): + v = 127 + if (v<-127): + v = -127 + return v + dx = clamp(delta.x) + dy = clamp(delta.y) + move(dx,dy) + delta.x -= dx + delta.y -= dy + + #dbg.write("Stitch: %s delta %s\n" % (stitch, delta)) + self.pos = stitch + return self.str + +class Test: + def __init__(self): + emb = Embroidery() + for x in range(0,301,30): + emb.addStitch(Point(x, 0)); + emb.addStitch(Point(x, 15)); + emb.addStitch(Point(x, 0)); + + for x in range(300,-1,-30): + emb.addStitch(Point(x, -12)); + emb.addStitch(Point(x, -27)); + emb.addStitch(Point(x, -12)); + + fp = open("test.exp", "wb") + fp.write(emb.export_melco()) + fp.close() + +class Turtle: + def __init__(self): + self.emb = Embroidery() + self.pos = Point(0.0,0.0) + self.dir = Point(1.0,0.0) + self.emb.addStitch(self.pos) + + def forward(self, dist): + self.pos = self.pos+self.dir.mul(dist) + self.emb.addStitch(self.pos) + + def turn(self, degreesccw): + radcw = -degreesccw/180.0*3.141592653589 + self.dir = Point( + math.cos(radcw)*self.dir.x-math.sin(radcw)*self.dir.y, + math.sin(radcw)*self.dir.x+math.cos(radcw)*self.dir.y) + + def right(self, degreesccw): + self.turn(degreesccw) + + def left(self, degreesccw): + self.turn(-degreesccw) + +class Koch(Turtle): + def __init__(self, depth): + Turtle.__init__(self) + + edgelen = 750.0 + for i in range(3): + self.edge(depth, edgelen) + self.turn(120.0) + + fp = open("koch%d.exp" % depth, "wb") + fp.write(self.emb.export_melco()) + fp.close() + + def edge(self, depth, dist): + if (depth==0): + self.forward(dist) + else: + self.edge(depth-1, dist/3.0) + self.turn(-60.0) + self.edge(depth-1, dist/3.0) + self.turn(120.0) + self.edge(depth-1, dist/3.0) + self.turn(-60.0) + self.edge(depth-1, dist/3.0) + +class Hilbert(Turtle): + def __init__(self, level): + Turtle.__init__(self) + + self.size = 10.0 + self.hilbert(level, 90.0) + + fp = open("hilbert%d.exp" % level, "wb") + fp.write(self.emb.export_melco()) + fp.close() + + # http://en.wikipedia.org/wiki/Hilbert_curve#Python + def hilbert(self, level, angle): + if (level==0): + return + self.right(angle) + self.hilbert(level-1, -angle) + self.forward(self.size) + self.left(angle) + self.hilbert(level-1, angle) + self.forward(self.size) + self.hilbert(level-1, angle) + self.left(angle) + self.forward(self.size) + self.hilbert(level-1, -angle) + self.right(angle) + +if (__name__=='__main__'): + #Koch(4) + Hilbert(6) diff --git a/embroider.inx b/embroider.inx new file mode 100644 index 000000000..25ea423db --- /dev/null +++ b/embroider.inx @@ -0,0 +1,19 @@ + + + <_name>Embroider + jonh.embroider + embroider.py + inkex.py + 0.40 + 3.0 + false + + all + + + + + + diff --git a/embroider.py b/embroider.py new file mode 100644 index 000000000..c9c30039d --- /dev/null +++ b/embroider.py @@ -0,0 +1,691 @@ +#!/usr/bin/python +# +# documentation: see included index.html +# LICENSE: +# This code is copyright 2010 by Jon Howell, +# licensed under GPLv3. +# +# Important resources: +# lxml interface for walking SVG tree: +# http://codespeak.net/lxml/tutorial.html#elementpath +# Inkscape library for extracting paths from SVG: +# http://wiki.inkscape.org/wiki/index.php/Python_modules_for_extensions#simplepath.py +# Shapely computational geometry library: +# http://gispython.org/shapely/manual.html#multipolygons +# Embroidery file format documentation: +# http://www.achatina.de/sewing/main/TECHNICL.HTM +# + +import sys +sys.path.append("/usr/share/inkscape/extensions") +import os +from copy import deepcopy +import time +import inkex +import simplepath +import simplestyle +import cspsubdiv +import cubicsuperpath +import PyEmb +import math +import random +import operator +import lxml.etree as etree +from lxml.builder import E +import shapely.geometry as shgeo + +dbg = open("embroider-debug.txt", "w") +PyEmb.dbg = dbg +pixels_per_millimeter = 90.0 / 25.4 + +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) + +def area(poly): + return bboxarea(poly) + +def byarea(a,b): + return -cmp(area(a), area(b)) + +def cspToShapelyPolygon(path): + poly_ary = [] + for sub_path in path: + point_ary = [] + last_pt = None + for csp in sub_path: + pt = (csp[1][0],csp[1][1]) + 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) + +def shapelyLineSegmentToPyTuple(shline): + 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 + +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 addStitch(self, stitch): + self.stitches.append(stitch) + + def reverse(self): + return Patch(self.color, self.sortorder, self.stitches[::-1]) + +class DebugHole: + pass + +class PatchList: + def __init__(self, patches): + self.patches = 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.color==lastPatch.color): + 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 emit_melco(self): + emb = PyEmb.Embroidery() + for patch in self.patchList.patches: + for stitch in patch.stitches: + newStitch = PyEmb.Point(stitch.x, -stitch.y) + dbg.write("melco stitch color %s\n" % patch.color) + newStitch.color = patch.color + emb.addStitch(newStitch) + emb.translate_to_origin() + emb.scale(10.0/pixels_per_millimeter) + fp = open("embroider-output.exp", "wb") + #fp = open("output.ksm", "wb") + fp.write(emb.export_melco(dbg)) + fp.close() + + 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*0.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()]) + inkex.etree.SubElement(parent, + inkex.addNS('path', 'svg'), + { 'style':simplestyle.formatStyle( + { 'stroke': patch.color, + 'stroke-width':str(self.row_spacing_px*0.25), + 'fill': 'none' }), + 'd':simplepath.formatPath(new_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, stacking_order, preserve_order): + self.threadcolor = threadcolor + if (preserve_order): + dbg.write("preserve_order is true:\n"); + self.sorttuple = (stacking_order, threadcolor) + else: + dbg.write("preserve_order is false:\n"); + self.sorttuple = (threadcolor, stacking_order) + + 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("-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("-f", "--flatness", + action="store", type="float", + dest="flat", default=0.1, + help="Minimum flatness of the subdivided curves") + self.OptionParser.add_option("-o", "--preserve_order", + action="store", type="choice", + choices=["true","false"], + dest="preserve_order", default="false", + help="Sort by stacking order instead of color") + self.patches = [] + + def get_sort_order(self, threadcolor): + self.stacking_order_counter += 1 + return SortOrder(threadcolor, self.stacking_order_counter, self.options.preserve_order=="true") + + def process_one_path(self, shpath, threadcolor, sortorder): + #self.add_shapely_geo_to_svg(shpath.boundary, color="#c0c000") + + rows_of_segments = self.intersect_region_with_grating(shpath) + segments = self.visit_segments_one_by_one(rows_of_segments) + + def small_stitches(patch, beg, end): + old_dist = None + while (True): + vector = (end-beg) + dist = vector.length() + assert(old_dist==None or dist 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 + + 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)] + 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 != self.svgpath): + 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 = False + desc = node.findtext(inkex.addNS('desc', 'svg')) + if (desc!=None): + israw = desc.find("embroider_raw")>=0 + 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 effect(self): + self.row_spacing_px = self.options.row_spacing_mm * pixels_per_millimeter + self.max_stitch_len_px = self.options.max_stitch_len_mm*pixels_per_millimeter + + self.svgpath = inkex.addNS('path', 'svg') + self.patchList = PatchList([]) + for id, node in self.selected.iteritems(): + self.handle_node(node) + + self.patchList = self.patchList.tsp_by_color() + dbg.write("patch count: %d\n" % len(self.patchList.patches)) + + eo = EmbroideryObject(self.patchList, self.row_spacing_px) + + eo.emit_melco() + + new_group = inkex.etree.SubElement(self.current_layer, + inkex.addNS('g', 'svg'), {}) + eo.emit_inkscape(new_group) + + self.emit_inkscape_bbox(new_group, eo) + + 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 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() + sortorder = self.get_sort_order(threadcolor) + path = simplepath.parsePath(node.get("d")) + + # regularize the points lists. + # (If we're parsing beziers, there will be a list of multi-point + # subarrays.) + + emb_point_list = [] + for (type,points) in path: + dbg.write("path_to_patch_list parses pt %s\n" % points) + pointscopy = list(points) + while (len(pointscopy)>0): + emb_point_list.append(PyEmb.Point(pointscopy[0], pointscopy[1])) + pointscopy = pointscopy[2:] + + 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) + 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.row_spacing_px*0.5, stroke_width, threadcolor, sortorder) + return patch + + def stroke_points(self, emb_point_list, row_spacing_px, stroke_width, threadcolor, sortorder): + patch = Patch(color=threadcolor, sortorder=sortorder) + p0 = emb_point_list[0] + for segi in range(1, len(emb_point_list)): + p1 = emb_point_list[segi] + + # how far we have to go along segment + seg_len = (p1 - p0).length() + if (seg_len < row_spacing_px*0.5): + # hmm. segment so short we can't do much sane with + # it. Ignore the point p1 and move along (but keep p0 + # as the beginning). + 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 + rho = 0.0 + while (rho <= seg_len): + left_pt = p0+along.mul(rho)+perp + patch.addStitch(left_pt) + rho += row_spacing_px + if (rho > seg_len): + break + + right_pt = p0+along.mul(rho)+perp.mul(-1.0) + patch.addStitch(right_pt) + rho += row_spacing_px + + # make sure we turn sharp corners when stroking thin paths. + patch.addStitch(p1) + + p0 = p1 + + return [patch] + + def filled_region_to_patchlist(self, node): + p = cubicsuperpath.parsePath(node.get("d")) + cspsubdiv.cspsubdiv(p, self.options.flat) + shapelyPolygon = cspToShapelyPolygon(p) + threadcolor = simplestyle.parseStyle(node.get("style"))["fill"] + sortorder = self.get_sort_order(threadcolor) + return self.process_one_path( + shapelyPolygon, + threadcolor, + sortorder) + + #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() + +dbg.close() diff --git a/images/draft1.jpg b/images/draft1.jpg new file mode 100644 index 000000000..5127ce351 Binary files /dev/null and b/images/draft1.jpg differ diff --git a/images/draft2.jpg b/images/draft2.jpg new file mode 100644 index 000000000..e053d11f6 Binary files /dev/null and b/images/draft2.jpg differ diff --git a/images/shirt.jpg b/images/shirt.jpg new file mode 100644 index 000000000..da5d635ee Binary files /dev/null and b/images/shirt.jpg differ diff --git a/index.html b/index.html new file mode 100644 index 000000000..c1a2f377c --- /dev/null +++ b/index.html @@ -0,0 +1,157 @@ +Embroidery output extension for Inkscape +

Embroidery output extension for Inkscape

+ +Inkscape is a natural tool for designing embroidery patterns; +the only challenge is converting the Inkscape design to a stitch file. +Here's a rough cut as such a tool that got me through my first project; +it may work for you, or maybe you can fix a bug or two and make it +more robust for your application. + +
+
+
My very first outputs. Scale is wrong, stitch spacing is wrong. +
+
A better version. Mostly correct, but still shows poor spacing. +Jump stitches all over the place because +the first TSP implementation was very broken. +
+
And now it's working well enough to embroider this shirt! +

+The most difficult part was carefully lining up the sequential panels +to make the design appear continuous. One tip: baste the working piece down +to a big piece of stabilizer, so that they stay together as the hoop is +repositioned. +

+ +

Installation.

+ +
Download the distribution from here. +
Install shapely, Python bindings to the GEOS library. +
apt-get install python-shapely
+
Place or link embroider.{inx,py} into ${HOME}/.config/inkscape/extensions. + +

Usage.

+ +Create a drawing in Inkscape made of filled regions. +Select the regions you want to export as a stitch file. +Ungroup repeatedly until there are no groups left, +and convert objects to paths. +(Embroider doesn't know how to handle text or rectangle objects; +they must be converted down to paths before it can work with them. +I don't know how to call "back into" Inkscape to do this automatically.) +Select the Embroider filter. +

+If it works (and it very well might!), you'll get a new grouped object +showing the proposed stitching path. It may be easy to miss, since the +new strokes appear in the same color as the underlying fill. (If you +forgot the "ungroup" step, it may also appear at a random place on +your canvas; see BUGS below.) +

+As a side effect, Embroider also creates a file in Inkscape's current +directory called embroider-output.exp. +If you like the stitch pattern you see, then open that output file +in a converter program and save it to the appropriate format for +your machine. +(I use Wilcom's TrueSizer, available as free-as-in-beerware, +inside WINE to convert my output to Brother .PES format.) + + + +

Theory of operation.

+ +For each input path, +if the path is closed & filled, +we fill it with rows of stitches. +That's done by finding the path's bounding box, +deciding whether to use horizontal or vertical rows based on the long +axis of the region, +drawing a bunch of equally-spaced line segments across the bounding box, +and finding the intersection of the row lines with the path region. +(We import shapely to do the intersection computation.) +

+Each path generates a "patch" of stitching. +We sort all the patches by color, to minimize thread changes. +Then we use a Traveling Salesman Problem implementation +(a cheesy, greedy one, plus a little hill-climbing at the end) +to sort the patches to minimize the length of the jump stitches +(the unintended stitches between patches). + +

updates

+ +2012.10.19 Implemented stroke stitches. Strokes <= 0.5pt are rendered +as straight lines, following the Inkscape path, obeying the max_stitch_len +parameter. +Strokes wider than 0.5pt are drawn with a zig-zag stitch. It's a bit +ugly around corners and sharp curves, leaving gaps at the outside edge, +but come on, I wrote it in like 45 minutes. [An ideal algorithm would +compute the boundary of the stroke correctly, and then come up with a nice +way to fill it with the zig-zag. This one isn't ideal.] + +

You can use strokes to do applique embroidery. Draw a (not-too-complicated) +closed curve. Generate it both as a 0.5pt line and again with a wider stroke +width, like 3mm. Stack two fabrics in the hoop, and embroider the thin path. +Remove the hoop from the machine (but leave the fabrics in the hoop). +Carefully trim away the top fabric at the stitched boundary. Then replace +the hoop and embroider the wide path. The wider path will cover the first +stitch line and secure the applique'd piece. + +

+Tips on strokes: use Extensions -> Modify Path -> Flatten Beziers +to change curves down to linear approximations. (The Embroider +extension's supposed to do this, but it's not so good at it.) + +

+ +
+More tips on using Inkscape to get from a raster example to an embroidery file. +
+ + +

TODOs.

+

+TODO: when a single patch is split into multiple sections (because +of concavities), two problems occur: +

+First, the sections are treated +as one big patch with an implicit jump. It would be better to make +them separate patches so that TSP can do a better job planning to +minimize jumps. +

+Second, the algorithm "assumes" that all the stitches +in the left "column" are part of the same patch, so it will also incur +horizontal implied jump stitches because it doesn't realize that the +rows are from disjoint parts of the underlying region. A smarter algorithm +would break each time the number-of-segments changes, and start a new +patch each time, again relying on TSP to put them back together in a +sane order. +

+TODO: when a row is longer than the max stitch length, use a global-phase +("tajima") stitch, rather than phase relative to where the row starts, +to avoid troughs in the middle of the filled region. +

+TODO: remove small stitches. TrueSizer uses a 0.5mm threshhold. +

+TODO: implement melco jump-stitch, so jumps don't put holes in the fabric. +

+done: sort compound paths biggest-area first, to at least get holes right. +

+BUGS: shapely thinks all compound paths are holes; it doesn't understand +the even-odd rule. +

+BUGS: Can't handle the "transform=" property that inkscape loves to +glue onto <g>roups. To work around this, ungroup all the way down to +separate <path>s, # which applies all the transforms down to the path +point level, then regroup as desired. +

+TODO: Call into Inkscape to do this behind the scenes. +

+TODO: Call into Inkscape to convert objects to paths automatically. + +

LICENSE

+This code is copyright 2010 by Jon Howell, +licensed under GPLv3. + +

AUTHOR

+Written by Jon Howell, jonh@jonh.net. +If you email me, expect an initial bounce with instructions to pass the +spam filter. diff --git a/makefile b/makefile new file mode 100644 index 000000000..4c306e116 --- /dev/null +++ b/makefile @@ -0,0 +1,3 @@ +embroider.tgz: makefile index.html embroider.py embroider.inx images/draft1.jpg images/draft2.jpg images/shirt.jpg PyEmb.py + ln -fs embroider . + tar czf $@ $^