* properly process transform parameters (ungrouping no longer necessary!)
  * handle satin on beziers properly
    * previously beziers were stroked as straight line segments that included control points
  * allow overriding parameters on individual paths by adding extra svg params
    * embroider_angle, embroider_stitch_length, embroider_zigzag_spacing, embroider_row_spacing, etc
    * set using "Edit XML"
  * default to 10 pixels per millimeter
  * properly write CSV files in millimeters (was dividing by 10)
  * always translate pattern to origin to fit in hoop
  * add "running stitch length" for < 0.5 stroke width)
  * don't traceback if no paths were selected
  * add "repeats" option for stroke (satin/running stitch) to go back and forth over the line
    * good for a double line of center-line underlay below satin
pull/1/head
Lex Neva 2016-01-08 22:56:10 -05:00
rodzic 142f0a5681
commit 3e3d540089
3 zmienionych plików z 166 dodań i 95 usunięć

Wyświetl plik

@ -63,9 +63,14 @@ class Embroidery:
maxy = max(maxy,p.y) maxy = max(maxy,p.y)
sx = maxx-minx sx = maxx-minx
sy = maxy-miny sy = maxy-miny
self.translate(-minx, -miny)
return (minx, miny)
def translate(self, dx, dy):
for p in self.coords: for p in self.coords:
p.x -= minx p.x += dx
p.y -= miny p.y += dy
def scale(self, sc): def scale(self, sc):
if not isinstance(sc, (tuple, list)): if not isinstance(sc, (tuple, list)):
@ -160,11 +165,11 @@ class Embroidery:
int(stitch.color[3:5], 16), int(stitch.color[3:5], 16),
int(stitch.color[5:7], 16)) int(stitch.color[5:7], 16))
if stitch.jumpStitch: if stitch.jumpStitch:
self.str += '"*","JUMP","%f","%f"\n' % (stitch.x/10, stitch.y/10) self.str += '"*","JUMP","%f","%f"\n' % (stitch.x, stitch.y)
if lastColor != None and stitch.color != lastColor: if lastColor != None and stitch.color != lastColor:
# not first color choice, add color change record # not first color choice, add color change record
self.str += '"*","COLOR","%f","%f"\n' % (stitch.x/10, stitch.y/10) self.str += '"*","COLOR","%f","%f"\n' % (stitch.x, stitch.y)
self.str += '"*","STITCH","%f","%f"\n' % (stitch.x/10, stitch.y/10) self.str += '"*","STITCH","%f","%f"\n' % (stitch.x, stitch.y)
lastColor = stitch.color lastColor = stitch.color
return self.str return self.str

Wyświetl plik

@ -7,6 +7,7 @@
<param name="zigzag_spacing_mm" type="float" min="0.01" max="5.00" precision="2" _gui-text="Zigzag spacing (mm)">1.00</param> <param name="zigzag_spacing_mm" type="float" min="0.01" max="5.00" precision="2" _gui-text="Zigzag spacing (mm)">1.00</param>
<param name="row_spacing_mm" type="float" min="0.01" max="5.00" precision="2" _gui-text="Row spacing (mm)">0.40</param> <param name="row_spacing_mm" type="float" min="0.01" max="5.00" precision="2" _gui-text="Row spacing (mm)">0.40</param>
<param name="max_stitch_len_mm" type="float" min="0.1" max="100.0" _gui-text="Maximum stitch length (mm)">3.0</param> <param name="max_stitch_len_mm" type="float" min="0.1" max="100.0" _gui-text="Maximum stitch length (mm)">3.0</param>
<param name="running_stitch_len_mm" type="float" min="0.1" max="100.0" _gui-text="Running stitch length (mm)">3.0</param>
<param name="collapse_len_mm" type="float" min="0.0" max="10.0" _gui-text="Maximum collapse length (mm)">0.0</param> <param name="collapse_len_mm" type="float" min="0.0" max="10.0" _gui-text="Maximum collapse length (mm)">0.0</param>
<param name="preserve_order" type="boolean" _gui-text="Preserve stacking order" description="if false, sorts by color, which saves thread changes. True preserves stacking order, important if you're laying colors over each other.">false</param> <param name="preserve_order" type="boolean" _gui-text="Preserve stacking order" description="if false, sorts by color, which saves thread changes. True preserves stacking order, important if you're laying colors over each other.">false</param>
<param name="hatch_filled_paths" type="boolean" _gui-text="Hatch filled paths" description="If false, filled paths are filled using equally-spaced lines. If true, filled paths are filled using hatching lines.">false</param> <param name="hatch_filled_paths" type="boolean" _gui-text="Hatch filled paths" description="If false, filled paths are filled using equally-spaced lines. If true, filled paths are filled using hatching lines.">false</param>

Wyświetl plik

@ -27,7 +27,8 @@ import time
import inkex import inkex
import simplepath import simplepath
import simplestyle import simplestyle
import cspsubdiv import simpletransform
from cspsubdiv import cspsubdiv
import cubicsuperpath import cubicsuperpath
import PyEmb import PyEmb
import math import math
@ -37,13 +38,83 @@ import lxml.etree as etree
from lxml.builder import E from lxml.builder import E
import shapely.geometry as shgeo import shapely.geometry as shgeo
import shapely.affinity as affinity import shapely.affinity as affinity
from pprint import pformat
dbg = open("/tmp/embroider-debug.txt", "w") dbg = open("/tmp/embroider-debug.txt", "w")
PyEmb.dbg = dbg PyEmb.dbg = dbg
#pixels_per_millimeter = 90.0 / 25.4 #pixels_per_millimeter = 90.0 / 25.4
#this actually makes each pixel worth one tenth of a millimeter #this actually makes each pixel worth one tenth of a millimeter
pixels_per_millimeter = 1 pixels_per_millimeter = 10
# a 0.5pt stroke becomes a straight line.
STROKE_MIN = 0.5
def parse_boolean(s):
if isinstance(s, bool):
return s
else:
return s and s.lower in ('yes', 'y', 'true', 't', '1')
def get_param(node, param, default):
value = node.get("embroider_" + param)
if value is None or not value.strip():
return default
return value.strip()
def get_boolean_param(node, param, default=False):
value = get_param(node, param, default)
return parse_boolean(value)
def get_float_param(node, param, default=None):
value = get_param(node, param, default)
try:
return float(value)
except ValueError:
return default
def get_int_param(node, param, default=None):
value = get_param(node, param, default)
try:
return int(value)
except ValueError:
return default
def parse_path(node):
path = cubicsuperpath.parsePath(node.get("d"))
# print >> sys.stderr, pformat(path)
# start with the identity transform
transform = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]
# combine this node's transform with all parent groups' transforms
transform = simpletransform.composeParents(node, transform)
# apply the combined transform to this node's path
simpletransform.applyTransformToPath(transform, path)
return path
def flatten(path, flatness):
"""approximate a path containing beziers with a series of points"""
cspsubdiv(path, flatness)
flattened = []
for comp in path:
vertices = []
for ctl in comp:
vertices.append((ctl[1][0], ctl[1][1]))
flattened.append(vertices)
return flattened
def bboxarea(poly): def bboxarea(poly):
x0=None x0=None
@ -68,8 +139,7 @@ def cspToShapelyPolygon(path):
for sub_path in path: for sub_path in path:
point_ary = [] point_ary = []
last_pt = None last_pt = None
for csp in sub_path: for pt in sub_path:
pt = (csp[1][0],csp[1][1])
if (last_pt!=None): if (last_pt!=None):
vp = (pt[0]-last_pt[0],pt[1]-last_pt[1]) 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)) dp = math.sqrt(math.pow(vp[0],2.0)+math.pow(vp[1],2.0))
@ -132,6 +202,9 @@ class PatchList:
def __init__(self, patches): def __init__(self, patches):
self.patches = patches self.patches = patches
def __len__(self):
return len(self.patches)
def sort_by_sortorder(self): def sort_by_sortorder(self):
def by_sort_order(a,b): def by_sort_order(a,b):
return cmp(a.sortorder, b.sortorder) return cmp(a.sortorder, b.sortorder)
@ -414,7 +487,7 @@ class EmbroideryObject:
lastStitch = newStitch lastStitch = newStitch
lastColor = patch.color lastColor = patch.color
emb.translate_to_origin() dx, dy = emb.translate_to_origin()
emb.scale(1.0/pixels_per_millimeter) emb.scale(1.0/pixels_per_millimeter)
fp = open(filename, "wb") fp = open(filename, "wb")
@ -427,6 +500,7 @@ class EmbroideryObject:
fp.write(emb.export_gcode(dbg)) fp.write(emb.export_gcode(dbg))
fp.close() fp.close()
emb.scale(pixels_per_millimeter) emb.scale(pixels_per_millimeter)
emb.translate(dx, dy)
return emb return emb
def emit_inkscape(self, parent, emb): def emit_inkscape(self, parent, emb):
@ -484,6 +558,10 @@ class Embroider(inkex.Effect):
action="store", type="float", action="store", type="float",
dest="max_stitch_len_mm", default=3.0, dest="max_stitch_len_mm", default=3.0,
help="max stitch length (mm)") 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", self.OptionParser.add_option("-c", "--collapse_len_mm",
action="store", type="float", action="store", type="float",
dest="collapse_len_mm", default=0.0, dest="collapse_len_mm", default=0.0,
@ -519,23 +597,27 @@ class Embroider(inkex.Effect):
self.patches = [] self.patches = []
self.stacking_order = {} self.stacking_order = {}
def get_sort_order(self, threadcolor, id): def get_sort_order(self, threadcolor, node):
return SortOrder(threadcolor, self.stacking_order.get(id), self.options.preserve_order=="true") return SortOrder(threadcolor, self.stacking_order.get(node.get("id")), self.options.preserve_order=="true")
def process_one_path(self, shpath, threadcolor, sortorder, angle): def process_one_path(self, node, shpath, threadcolor, sortorder, angle):
#self.add_shapely_geo_to_svg(shpath.boundary, color="#c0c000") #self.add_shapely_geo_to_svg(shpath.boundary, color="#c0c000")
rows_of_segments = self.intersect_region_with_grating(shpath, angle) 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) segments = self.visit_segments_one_by_one(rows_of_segments)
def small_stitches(patch, beg, end): def small_stitches(patch, beg, end):
vector = (end-beg) vector = (end-beg)
patch.addStitch(beg) patch.addStitch(beg)
old_dist = vector.length() old_dist = vector.length()
if (old_dist < self.max_stitch_len_px): if (old_dist < max_stitch_len_px):
patch.addStitch(end) patch.addStitch(end)
return return
one_stitch = vector.mul(1.0 / old_dist * self.max_stitch_len_px * random.random()) one_stitch = vector.mul(1.0 / old_dist * max_stitch_len_px * random.random())
beg = beg + one_stitch beg = beg + one_stitch
while (True): while (True):
vector = (end-beg) vector = (end-beg)
@ -543,11 +625,11 @@ class Embroider(inkex.Effect):
assert(old_dist==None or dist<old_dist) assert(old_dist==None or dist<old_dist)
old_dist = dist old_dist = dist
patch.addStitch(beg) patch.addStitch(beg)
if (dist < self.max_stitch_len_px): if (dist < max_stitch_len_px):
patch.addStitch(end) patch.addStitch(end)
return return
one_stitch = vector.mul(1.0/dist*self.max_stitch_len_px) one_stitch = vector.mul(1.0/dist*max_stitch_len_px)
beg = beg + one_stitch beg = beg + one_stitch
swap = False swap = False
@ -555,22 +637,22 @@ class Embroider(inkex.Effect):
for (beg,end) in segments: for (beg,end) in segments:
if (swap): if (swap):
(beg,end)=(end,beg) (beg,end)=(end,beg)
if not self.hatching: if not hatching:
swap = not swap swap = not swap
small_stitches(patch, PyEmb.Point(*beg),PyEmb.Point(*end)) small_stitches(patch, PyEmb.Point(*beg),PyEmb.Point(*end))
return [patch] return [patch]
def intersect_region_with_grating(self, shpath, angle): def intersect_region_with_grating(self, shpath, row_spacing_px, angle):
#dbg.write("bounds = %s\n" % str(shpath.bounds)) #dbg.write("bounds = %s\n" % str(shpath.bounds))
rotated_shpath = affinity.rotate(shpath, angle, use_radians = True) rotated_shpath = affinity.rotate(shpath, angle, use_radians = True)
bbox = rotated_shpath.bounds bbox = rotated_shpath.bounds
delta = self.row_spacing_px * 50 # *2 should be enough but isn't. TODO: find out why, and if this always works. 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 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]) p0 = PyEmb.Point(bbox[0][0], bbox[0][1])
p1 = PyEmb.Point(bbox[1][0], bbox[1][1]) p1 = PyEmb.Point(bbox[1][0], bbox[1][1])
p2 = PyEmb.Point(bbox[3][0], bbox[3][1]) p2 = PyEmb.Point(bbox[3][0], bbox[3][1])
count = (p2 - p0).length() / self.row_spacing_px count = (p2 - p0).length() / row_spacing_px
p_inc = (p2 - p0).mul(1 / count) p_inc = (p2 - p0).mul(1 / count)
count += 2 count += 2
@ -622,9 +704,8 @@ class Embroider(inkex.Effect):
if (count>100): raise "kablooey" if (count>100): raise "kablooey"
return linearized_runs return linearized_runs
def handle_node(self, node, id): def handle_node(self, node):
if (node.tag == inkex.addNS('g', 'svg')):
if (node.tag != self.svgpath):
#dbg.write("%s\n"%str((id, etree.tostring(node, pretty_print=True)))) #dbg.write("%s\n"%str((id, etree.tostring(node, pretty_print=True))))
#dbg.write("not a path; recursing:\n") #dbg.write("not a path; recursing:\n")
for child in node.iter(self.svgpath): for child in node.iter(self.svgpath):
@ -633,26 +714,14 @@ class Embroider(inkex.Effect):
#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 israw = parse_boolean(node.get('embroider_raw'))
desc = node.findtext(inkex.addNS('desc', 'svg'))
if desc is None:
desc = ''
descparts = {}
for part in desc.split(';'):
if '=' in part:
k, v = part.split('=', 1)
else:
k, v = part, ''
descparts[k] = v
israw = 'embroider_raw' in descparts
if (israw): if (israw):
self.patchList.patches.extend(self.path_to_patch_list(node)) self.patchList.patches.extend(self.path_to_patch_list(node))
else: else:
if (self.get_style(node, "fill")!=None): if (self.get_style(node, "fill")!=None):
angle = math.radians(float(descparts.get('embroider_angle', 0))) self.patchList.patches.extend(self.filled_region_to_patchlist(node))
self.patchList.patches.extend(self.filled_region_to_patchlist(node, id, angle))
if (self.get_style(node, "stroke")!=None): if (self.get_style(node, "stroke")!=None):
self.patchList.patches.extend(self.path_to_patch_list(node, id)) self.patchList.patches.extend(self.path_to_patch_list(node))
def get_style(self, node, style_name): def get_style(self, node, style_name):
style = simplestyle.parseStyle(node.get("style")) style = simplestyle.parseStyle(node.get("style"))
@ -676,13 +745,18 @@ class Embroider(inkex.Effect):
self.row_spacing_px = self.options.row_spacing_mm * pixels_per_millimeter 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.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.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.collapse_len_px = self.options.collapse_len_mm*pixels_per_millimeter
self.hatching = self.options.hatch_filled_paths == "true" self.hatching = self.options.hatch_filled_paths == "true"
self.svgpath = inkex.addNS('path', 'svg') self.svgpath = inkex.addNS('path', 'svg')
self.patchList = PatchList([]) self.patchList = PatchList([])
for id, node in self.selected.iteritems(): for node in self.selected.itervalues():
self.handle_node(node, id) self.handle_node(node)
if not self.patchList:
inkex.errormsg("No paths selected.")
return
self.patchList = self.patchList.tsp_by_color() 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))
@ -714,7 +788,7 @@ class Embroider(inkex.Effect):
'd':simplepath.formatPath(new_path), 'd':simplepath.formatPath(new_path),
}) })
def path_to_patch_list(self, node, id): def path_to_patch_list(self, node):
threadcolor = simplestyle.parseStyle(node.get("style"))["stroke"] threadcolor = simplestyle.parseStyle(node.get("style"))["stroke"]
stroke_width_str = simplestyle.parseStyle(node.get("style"))["stroke-width"] stroke_width_str = simplestyle.parseStyle(node.get("style"))["stroke-width"]
if (stroke_width_str.endswith("px")): if (stroke_width_str.endswith("px")):
@ -724,85 +798,76 @@ class Embroider(inkex.Effect):
stroke_width = float(stroke_width_str) stroke_width = float(stroke_width_str)
#dbg.write("stroke_width is <%s>\n" % repr(stroke_width)) #dbg.write("stroke_width is <%s>\n" % repr(stroke_width))
#dbg.flush() #dbg.flush()
sortorder = self.get_sort_order(threadcolor, id)
path = simplepath.parsePath(node.get("d")) 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. # regularize the points lists.
# (If we're parsing beziers, there will be a list of multi-point # (If we're parsing beziers, there will be a list of multi-point
# subarrays.) # subarrays.)
patches = [] patches = []
emb_point_list = []
for path in paths:
def flush_point_list(): path = [PyEmb.Point(x, y) for x, y in path]
STROKE_MIN = 0.5 # a 0.5pt stroke becomes a straight line.
if (stroke_width <= STROKE_MIN): 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) patch = self.stroke_points(path, running_stitch_len_px, 0.0, repeats, threadcolor, sortorder)
else: else:
patch = self.stroke_points(emb_point_list, self.zigzag_spacing_px*0.5, stroke_width, threadcolor, sortorder) patch = self.stroke_points(path, zigzag_spacing_px*0.5, stroke_width, repeats, threadcolor, sortorder)
patches.extend(patch) patches.extend(patch)
close_point = None
for (type,points) in path:
#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)
emb_point_list.append(close_point)
else:
pointscopy = list(points)
while (len(pointscopy)>0):
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])
close_point = emb_point_list[-1]
flush_point_list()
return patches return patches
def stroke_points(self, emb_point_list, zigzag_spacing_px, stroke_width, threadcolor, sortorder): def stroke_points(self, emb_point_list, zigzag_spacing_px, stroke_width, repeats, threadcolor, sortorder):
patch = Patch(color=threadcolor, sortorder=sortorder) patch = Patch(color=threadcolor, sortorder=sortorder)
p0 = emb_point_list[0] p0 = emb_point_list[0]
rho = 0.0 rho = 0.0
fact = 1 fact = 1
for segi in range(1, len(emb_point_list)): for repeat in xrange(repeats):
p1 = emb_point_list[segi] if repeat % 2 == 0:
order = range(1, len(emb_point_list))
else:
order = range(-2, -len(emb_point_list) - 1, -1)
# how far we have to go along segment for segi in order:
seg_len = (p1 - p0).length() p1 = emb_point_list[segi]
if (seg_len == 0):
continue
# vector pointing along segment # how far we have to go along segment
along = (p1 - p0).unit() seg_len = (p1 - p0).length()
# vector pointing to edge of stroke width if (seg_len == 0):
perp = along.rotate_left().mul(stroke_width*0.5) continue
# iteration variable: how far we are along segment # vector pointing along segment
while (rho <= seg_len): along = (p1 - p0).unit()
left_pt = p0+along.mul(rho)+perp.mul(fact) # vector pointing to edge of stroke width
patch.addStitch(left_pt) perp = along.rotate_left().mul(stroke_width*0.5)
rho += zigzag_spacing_px
fact = -fact
p0 = p1 # iteration variable: how far we are along segment
rho -= seg_len 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] return [patch]
def filled_region_to_patchlist(self, node, id, angle): def filled_region_to_patchlist(self, node):
p = cubicsuperpath.parsePath(node.get("d")) angle = math.radians(float(get_float_param(node,'angle',0)))
cspsubdiv.cspsubdiv(p, self.options.flat) paths = flatten(parse_path(node), self.options.flat)
shapelyPolygon = cspToShapelyPolygon(p) shapelyPolygon = cspToShapelyPolygon(paths)
threadcolor = simplestyle.parseStyle(node.get("style"))["fill"] threadcolor = simplestyle.parseStyle(node.get("style"))["fill"]
sortorder = self.get_sort_order(threadcolor, id) sortorder = self.get_sort_order(threadcolor, node)
return self.process_one_path( return self.process_one_path(
node,
shapelyPolygon, shapelyPolygon,
threadcolor, threadcolor,
sortorder, sortorder,