* Simulate now works regardless of the output format you chose when you ran Embroider.
* Simulate (and the preview in Params) now respects TRIMs.
* Inkscape restart required (embroider.inx changed).

This one kind of grew in the telling. #37 was a theoretically simple bug, but in reality, the code necessary to fix it was the straw that broke the camel's back, and I had to do a fair bit of (much needed) code reorganization. Mostly the reorganization was just under the hood, but there was one user-facing change around the Embroider extension's settings window.

Way back in the day, the only way to control things like the stitch length or satin density was through global options specified in the extension settings. We've long since moved to per-object params, but for backward compatibility, ink/stitch defaulted to the command-line arguments.

That means that it was possible to get different stitch results from the same SVG file if you changed the extension's settings. For that reason, I never touched mine. I didn't intend for my users to use those extension-level settings at all, and I've planned to remove those settings for awhile now.

At this point, the extension settings just getting in the way of implementing more features, so I'm getting rid of them and moving the defaults into the parameters system. I've still left things like the output format and the collapse length (although I'm considering moving that one too).
pull/44/head
Lex Neva 2018-01-28 16:10:37 -05:00 zatwierdzone przez GitHub
rodzic ede0b2d0e6
commit fabe5bcd32
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
6 zmienionych plików z 618 dodań i 680 usunięć

160
PyEmb.py
Wyświetl plik

@ -1,160 +0,0 @@
#!/usr/bin/env python
# http://www.achatina.de/sewing/main/TECHNICL.HTM
import math
import libembroidery
PIXELS_PER_MM = 96 / 25.4
try:
from functools import lru_cache
except ImportError:
from backports.functools_lru_cache import lru_cache
# simplify use of lru_cache decorator
def cache(*args, **kwargs):
return lru_cache(maxsize=None)(*args, **kwargs)
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 __mul__(self, other):
if isinstance(other, Point):
# dot product
return self.x * other.x + self.y * other.y
elif isinstance(other, (int, float)):
return Point(self.x * other, self.y * other)
else:
raise ValueError("cannot multiply Point by %s" % type(other))
def __rmul__(self, other):
if isinstance(other, (int, float)):
return self.__mul__(other)
else:
raise ValueError("cannot multiply Point by %s" % type(other))
def __repr__(self):
return "Point(%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 rotate(self, angle):
return Point(self.x * math.cos(angle) - self.y * math.sin(angle), self.y * math.cos(angle) + self.x * math.sin(angle))
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())
def __getitem__(self, item):
return self.as_tuple()[item]
def __len__(self):
return 2
class Stitch(Point):
def __init__(self, x, y, color=None, jump=False, stop=False, trim=False):
self.x = x
self.y = y
self.color = color
self.jump = jump
self.trim = trim
self.stop = stop
def __repr__(self):
return "Stitch(%s, %s, %s, %s, %s, %s)" % (self.x, self.y, self.color, "JUMP" if self.jump else "", "TRIM" if self.trim else "", "STOP" if self.stop else "")
def make_thread(color):
# strip off the leading "#"
if color.startswith("#"):
color = color[1:]
thread = libembroidery.EmbThread()
thread.color = libembroidery.embColor_fromHexStr(color)
thread.description = color
thread.catalogNumber = ""
return thread
def add_thread(pattern, thread):
"""Add a thread to a pattern and return the thread's index"""
libembroidery.embPattern_addThread(pattern, thread)
return libembroidery.embThreadList_count(pattern.threadList) - 1
def get_flags(stitch):
flags = 0
if stitch.jump:
flags |= libembroidery.JUMP
if stitch.trim:
flags |= libembroidery.TRIM
if stitch.stop:
flags |= libembroidery.STOP
return flags
def write_embroidery_file(file_path, stitches):
# Embroidery machines don't care about our canvas size, so we relocate the
# design to the origin. It might make sense to center it about the origin
# instead.
min_x = min(stitch.x for stitch in stitches)
min_y = min(stitch.y for stitch in stitches)
pattern = libembroidery.embPattern_create()
threads = {}
last_color = None
for stitch in stitches:
if stitch.color != last_color:
if stitch.color not in threads:
thread = make_thread(stitch.color)
thread_index = add_thread(pattern, thread)
threads[stitch.color] = thread_index
else:
thread_index = threads[stitch.color]
libembroidery.embPattern_changeColor(pattern, thread_index)
last_color = stitch.color
flags = get_flags(stitch)
libembroidery.embPattern_addStitchAbs(pattern, stitch.x - min_x, stitch.y - min_y, flags, 0)
libembroidery.embPattern_addStitchAbs(pattern, stitch.x - min_x, stitch.y - min_y, libembroidery.END, 0)
# convert from pixels to millimeters
libembroidery.embPattern_scale(pattern, 1/PIXELS_PER_MM)
# SVG and embroidery disagree on the direction of the Y axis
libembroidery.embPattern_flipVertical(pattern)
libembroidery.embPattern_write(pattern, file_path)

Wyświetl plik

@ -4,10 +4,6 @@
<id>jonh.embroider</id>
<dependency type="executable" location="extensions">embroider.py</dependency>
<dependency type="executable" location="extensions">inkex.py</dependency>
<param name="zigzag_spacing_mm" type="float" min="0.01" max="5.00" precision="2" _gui-text="Zigzag 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.25</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)">1.5</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="hide_layers" type="boolean" _gui-text="Hide other layers" description="Hide all other top-level layers when the embroidery layer is generated, in order to make stitching discernable.">true</param>
<param name="output_format" type="optiongroup" _gui-text="Output file format" appearance="minimal">

Wyświetl plik

@ -34,319 +34,8 @@ import shapely.ops
import networkx
from pprint import pformat
import PyEmb
from PyEmb import cache
dbg = open("/tmp/embroider-debug.txt", "w")
PyEmb.dbg = dbg
SVG_PATH_TAG = inkex.addNS('path', 'svg')
SVG_POLYLINE_TAG = inkex.addNS('polyline', 'svg')
SVG_DEFS_TAG = inkex.addNS('defs', 'svg')
SVG_GROUP_TAG = inkex.addNS('g', 'svg')
EMBROIDERABLE_TAGS = (SVG_PATH_TAG, SVG_POLYLINE_TAG)
# modern versions of Inkscape use 96 pixels per inch as per the CSS standard
PIXELS_PER_MM = 96 / 25.4
class Param(object):
def __init__(self, name, description, unit=None, values=[], type=None, group=None, inverse=False, default=None, tooltip=None, sort_index=0):
self.name = name
self.description = description
self.unit = unit
self.values = values or [""]
self.type = type
self.group = group
self.inverse = inverse
self.default = default
self.tooltip = tooltip
self.sort_index = sort_index
def __repr__(self):
return "Param(%s)" % vars(self)
# Decorate a member function or property with information about
# the embroidery parameter it corresponds to
def param(*args, **kwargs):
p = Param(*args, **kwargs)
def decorator(func):
func.param = p
return func
return decorator
# cribbed from inkscape-silhouette
def parse_length_with_units( str ):
'''
Parse an SVG value which may or may not have units attached
This version is greatly simplified in that it only allows: no units,
units of px, mm, and %. Everything else, it returns None for.
There is a more general routine to consider in scour.py if more
generality is ever needed.
'''
u = 'px'
s = str.strip()
if s[-2:] == 'px':
s = s[:-2]
elif s[-2:] == 'mm':
u = 'mm'
s = s[:-2]
elif s[-2:] == 'pt':
u = 'pt'
s = s[:-2]
elif s[-2:] == 'pc':
u = 'pc'
s = s[:-2]
elif s[-2:] == 'cm':
u = 'cm'
s = s[:-2]
elif s[-2:] == 'in':
u = 'in'
s = s[:-2]
elif s[-1:] == '%':
u = '%'
s = s[:-1]
try:
v = float( s )
except:
raise ValueError("parseLengthWithUnits: unknown unit %s" % s)
return v, u
def convert_length(length):
value, units = parse_length_with_units(length)
if not units or units == "px":
return value
if units == 'cm':
value *= 10
units == 'mm'
if units == 'mm':
value = value / 25.4
units = 'in'
if units == 'in':
# modern versions of Inkscape use CSS's 96 pixels per inch. When you
# open an old document, inkscape will add a viewbox for you.
return value * 96
raise ValueError("Unknown unit: %s" % units)
@cache
def get_viewbox_transform(node):
# somewhat cribbed from inkscape-silhouette
doc_width = convert_length(node.get('width'))
doc_height = convert_length(node.get('height'))
viewbox = node.get('viewBox').strip().replace(',', ' ').split()
dx = -float(viewbox[0])
dy = -float(viewbox[1])
transform = simpletransform.parseTransform("translate(%f, %f)" % (dx, dy))
try:
sx = doc_width / float(viewbox[2])
sy = doc_height / float(viewbox[3])
scale_transform = simpletransform.parseTransform("scale(%f, %f)" % (sx, sy))
transform = simpletransform.composeTransform(transform, scale_transform)
except ZeroDivisionError:
pass
return transform
class EmbroideryElement(object):
def __init__(self, node, options=None):
self.node = node
self.options = options
@property
def id(self):
return self.node.get('id')
@classmethod
def get_params(cls):
params = []
for attr in dir(cls):
prop = getattr(cls, attr)
if isinstance(prop, property):
# The 'param' attribute is set by the 'param' decorator defined above.
if hasattr(prop.fget, 'param'):
params.append(prop.fget.param)
return params
@cache
def get_param(self, param, default):
value = self.node.get("embroider_" + param, "").strip()
if not value:
value = getattr(self.options, param, default)
return value
@cache
def get_boolean_param(self, param, default=None):
value = self.get_param(param, default)
if isinstance(value, bool):
return value
else:
return value and (value.lower() in ('yes', 'y', 'true', 't', '1'))
@cache
def get_float_param(self, param, default=None):
try:
value = float(self.get_param(param, default))
except (TypeError, ValueError):
return default
if param.endswith('_mm'):
value = value * PIXELS_PER_MM
return value
@cache
def get_int_param(self, param, default=None):
try:
value = int(self.get_param(param, default))
except (TypeError, ValueError):
return default
if param.endswith('_mm'):
value = int(value * PIXELS_PER_MM)
return value
def set_param(self, name, value):
self.node.set("embroider_%s" % name, str(value))
@cache
def get_style(self, style_name):
style = simplestyle.parseStyle(self.node.get("style"))
if (style_name not in style):
return None
value = style[style_name]
if value == 'none':
return None
return value
@cache
def has_style(self, style_name):
style = simplestyle.parseStyle(self.node.get("style"))
return style_name in style
@property
def path(self):
return cubicsuperpath.parsePath(self.node.get("d"))
@cache
def parse_path(self):
# A CSP is a "cubic superpath".
#
# A "path" is a sequence of strung-together bezier curves.
#
# A "superpath" is a collection of paths that are all in one object.
#
# The "cubic" bit in "cubic superpath" is because the bezier curves
# inkscape uses involve cubic polynomials.
#
# Each path is a collection of tuples, each of the form:
#
# (control_before, point, control_after)
#
# A bezier curve segment is defined by an endpoint, a control point,
# a second control point, and a final endpoint. A path is a bunch of
# bezier curves strung together. One could represent a path as a set
# of four-tuples, but there would be redundancy because the ending
# point of one bezier is the starting point of the next. Instead, a
# path is a set of 3-tuples as shown above, and one must construct
# each bezier curve by taking the appropriate endpoints and control
# points. Bleh. It should be noted that a straight segment is
# represented by having the control point on each end equal to that
# end's point.
#
# In a path, each element in the 3-tuple is itself a tuple of (x, y).
# Tuples all the way down. Hasn't anyone heard of using classes?
path = self.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(self.node, transform)
# add in the transform implied by the viewBox
viewbox_transform = get_viewbox_transform(self.node.getroottree().getroot())
transform = simpletransform.composeTransform(viewbox_transform, transform)
# apply the combined transform to this node's path
simpletransform.applyTransformToPath(transform, path)
return path
def flatten(self, path):
"""approximate a path containing beziers with a series of points"""
path = deepcopy(path)
cspsubdiv(path, getattr(self.options, "flat", 0.1))
flattened = []
for comp in path:
vertices = []
for ctl in comp:
vertices.append((ctl[1][0], ctl[1][1]))
flattened.append(vertices)
return flattened
@property
@param('trim_after',
'TRIM after',
tooltip='Trim thread after this object (for supported machines and file formats)',
type='boolean',
default=False,
sort_index=1000)
def trim_after(self):
return self.get_boolean_param('trim_after', False)
@property
@param('stop_after',
'STOP after',
tooltip='Add STOP instruction after this object (for supported machines and file formats)',
type='boolean',
default=False,
sort_index=1000)
def stop_after(self):
return self.get_boolean_param('stop_after', False)
def to_patches(self, last_patch):
raise NotImplementedError("%s must implement to_patches()" % self.__class__.__name__)
def embroider(self, last_patch):
patches = self.to_patches(last_patch)
if patches:
patches[-1].trim_after = self.trim_after
patches[-1].stop_after = self.stop_after
return patches
def fatal(self, message):
print >> sys.stderr, "error:", message
sys.exit(1)
import inkstitch
from inkstitch import cache, dbg, param, EmbroideryElement, get_nodes, SVG_POLYLINE_TAG, SVG_GROUP_TAG, PIXELS_PER_MM, get_viewbox_transform
class Fill(EmbroideryElement):
@ -359,7 +48,7 @@ class Fill(EmbroideryElement):
return self.get_boolean_param('auto_fill', True)
@property
@param('angle', 'Angle of lines of stitches', unit='deg', type='float')
@param('angle', 'Angle of lines of stitches', unit='deg', type='float', default=0)
@cache
def angle(self):
return math.radians(self.get_float_param('angle', 0))
@ -369,26 +58,26 @@ class Fill(EmbroideryElement):
return self.get_style("fill")
@property
@param('flip', 'Flip fill (start right-to-left)', type='boolean')
@param('flip', 'Flip fill (start right-to-left)', type='boolean', default=False)
def flip(self):
return self.get_boolean_param("flip", False)
@property
@param('row_spacing_mm', 'Spacing between rows', unit='mm', type='float')
@param('row_spacing_mm', 'Spacing between rows', unit='mm', type='float', default=0.25)
def row_spacing(self):
return self.get_float_param("row_spacing_mm")
return self.get_float_param("row_spacing_mm", 0.25)
@property
def end_row_spacing(self):
return self.get_float_param("end_row_spacing_mm")
@property
@param('max_stitch_length_mm', 'Maximum fill stitch length', unit='mm', type='float')
@param('max_stitch_length_mm', 'Maximum fill stitch length', unit='mm', type='float', default=3.0)
def max_stitch_length(self):
return self.get_float_param("max_stitch_length_mm")
return self.get_float_param("max_stitch_length_mm", 3.0)
@property
@param('staggers', 'Stagger rows this many times before repeating', type='int')
@param('staggers', 'Stagger rows this many times before repeating', type='int', default=4)
def staggers(self):
return self.get_int_param("staggers", 4)
@ -431,7 +120,7 @@ class Fill(EmbroideryElement):
@cache
def east(self, angle):
# "east" is the name of the direction that is to the right along a row
return PyEmb.Point(1, 0).rotate(-angle)
return inkstitch.Point(1, 0).rotate(-angle)
@cache
def north(self, angle):
@ -460,15 +149,15 @@ class Fill(EmbroideryElement):
# the max line length I'll need to intersect the whole shape is the diagonal
(minx, miny, maxx, maxy) = self.shape.bounds
upper_left = PyEmb.Point(minx, miny)
lower_right = PyEmb.Point(maxx, maxy)
upper_left = inkstitch.Point(minx, miny)
lower_right = inkstitch.Point(maxx, maxy)
length = (upper_left - lower_right).length()
half_length = length / 2.0
# Now get a unit vector rotated to the requested angle. I use -angle
# because shapely rotates clockwise, but my geometry textbooks taught
# me to consider angles as counter-clockwise from the X axis.
direction = PyEmb.Point(1, 0).rotate(-angle)
direction = inkstitch.Point(1, 0).rotate(-angle)
# and get a normal vector
normal = direction.rotate(math.pi / 2)
@ -476,7 +165,7 @@ class Fill(EmbroideryElement):
# I'll start from the center, move in the normal direction some amount,
# and then walk left and right half_length in each direction to create
# a line segment in the grating.
center = PyEmb.Point((minx + maxx) / 2.0, (miny + maxy) / 2.0)
center = inkstitch.Point((minx + maxx) / 2.0, (miny + maxy) / 2.0)
# I need to figure out how far I need to go along the normal to get to
# the edge of the shape. To do that, I'll rotate the bounding box
@ -520,7 +209,7 @@ class Fill(EmbroideryElement):
runs = [res.coords]
if runs:
runs.sort(key=lambda seg: (PyEmb.Point(*seg[0]) - upper_left).length())
runs.sort(key=lambda seg: (inkstitch.Point(*seg[0]) - upper_left).length())
if self.flip:
runs.reverse()
@ -614,8 +303,8 @@ class Fill(EmbroideryElement):
# abutting fill regions from pull_runs().
beg = PyEmb.Point(*beg)
end = PyEmb.Point(*end)
beg = inkstitch.Point(*beg)
end = inkstitch.Point(*end)
row_direction = (end - beg).unit()
segment_length = (end - beg).length()
@ -701,14 +390,14 @@ class AutoFill(Fill):
return False
@property
@param('running_stitch_length_mm', 'Running stitch length (traversal between sections)', unit='mm', type='float')
@param('running_stitch_length_mm', 'Running stitch length (traversal between sections)', unit='mm', type='float', default=1.5)
def running_stitch_length(self):
return self.get_float_param("running_stitch_length_mm")
return self.get_float_param("running_stitch_length_mm", 1.5)
@property
@param('fill_underlay', 'Underlay', type='toggle', group='AutoFill Underlay')
@param('fill_underlay', 'Underlay', type='toggle', group='AutoFill Underlay', default=False)
def fill_underlay(self):
return self.get_boolean_param("fill_underlay")
return self.get_boolean_param("fill_underlay", default=False)
@property
@param('fill_underlay_angle', 'Fill angle (default: fill angle + 90 deg)', unit='deg', group='AutoFill Underlay', type='float')
@ -816,8 +505,8 @@ class AutoFill(Fill):
# If the start and endpoints are in the same row, I can't tell which row
# I should treat it as being in.
for i in xrange(len(nodes)):
row0 = self.row_num(PyEmb.Point(*nodes[0]), angle, row_spacing)
row1 = self.row_num(PyEmb.Point(*nodes[1]), angle, row_spacing)
row0 = self.row_num(inkstitch.Point(*nodes[0]), angle, row_spacing)
row1 = self.row_num(inkstitch.Point(*nodes[1]), angle, row_spacing)
if row0 == row1:
nodes = nodes[1:] + [nodes[0]]
@ -832,7 +521,7 @@ class AutoFill(Fill):
else:
edge_set = 1
#print >> sys.stderr, outline_index, "es", edge_set, "rn", row_num, PyEmb.Point(*nodes[0]) * self.north(angle), PyEmb.Point(*nodes[1]) * self.north(angle)
#print >> sys.stderr, outline_index, "es", edge_set, "rn", row_num, inkstitch.Point(*nodes[0]) * self.north(angle), inkstitch.Point(*nodes[1]) * self.north(angle)
# add an edge between each successive node
for i, (node1, node2) in enumerate(zip(nodes, nodes[1:] + [nodes[0]])):
@ -840,8 +529,8 @@ class AutoFill(Fill):
# duplicate edges contained in every other row (exactly half
# will be duplicated)
row_num = min(self.row_num(PyEmb.Point(*node1), angle, row_spacing),
self.row_num(PyEmb.Point(*node2), angle, row_spacing))
row_num = min(self.row_num(inkstitch.Point(*node1), angle, row_spacing),
self.row_num(inkstitch.Point(*node2), angle, row_spacing))
# duplicate every other edge around this outline
if i % 2 == edge_set:
@ -1114,14 +803,14 @@ class AutoFill(Fill):
print >> dbg, "connect_points:", outline_index, start, end, distance, stitches, direction
dbg.flush()
patch.add_stitch(PyEmb.Point(*start))
patch.add_stitch(inkstitch.Point(*start))
for i in xrange(stitches):
pos = (pos + one_stitch) % self.outline_length
patch.add_stitch(PyEmb.Point(*outline.interpolate(pos).coords[0]))
patch.add_stitch(inkstitch.Point(*outline.interpolate(pos).coords[0]))
end = PyEmb.Point(*end)
end = inkstitch.Point(*end)
if (end - patch.stitches[-1]).length() > 0.1 * PIXELS_PER_MM:
patch.add_stitch(end)
@ -1132,10 +821,10 @@ class AutoFill(Fill):
path = self.collapse_sequential_outline_edges(graph, path)
patch = Patch(color=self.color)
#patch.add_stitch(PyEmb.Point(*path[0][0]))
#patch.add_stitch(inkstitch.Point(*path[0][0]))
#for edge in path:
# patch.add_stitch(PyEmb.Point(*edge[1]))
# patch.add_stitch(inkstitch.Point(*edge[1]))
for edge in path:
if graph.has_edge(*edge, key="segment"):
@ -1177,7 +866,7 @@ class AutoFill(Fill):
starting_point = None
else:
nearest_point = self.outline.interpolate(self.outline.project(shgeo.Point(last_patch.stitches[-1])))
starting_point = PyEmb.Point(*nearest_point.coords[0])
starting_point = inkstitch.Point(*nearest_point.coords[0])
if self.fill_underlay:
patches.extend(self.do_auto_fill(self.fill_underlay_angle, self.fill_underlay_row_spacing, self.fill_underlay_max_stitch_length, starting_point))
@ -1216,15 +905,15 @@ class Stroke(EmbroideryElement):
return self.get_style("stroke-dasharray") is not None
@property
@param('running_stitch_length_mm', 'Running stitch length', unit='mm', type='float')
@param('running_stitch_length_mm', 'Running stitch length', unit='mm', type='float', default=1.5)
def running_stitch_length(self):
return self.get_float_param("running_stitch_length_mm")
return self.get_float_param("running_stitch_length_mm", 1.5)
@property
@param('zigzag_spacing_mm', 'Zig-zag spacing (peak-to-peak)', unit='mm', type='float')
@param('zigzag_spacing_mm', 'Zig-zag spacing (peak-to-peak)', unit='mm', type='float', default=0.4)
@cache
def zigzag_spacing(self):
return self.get_float_param("zigzag_spacing_mm")
return self.get_float_param("zigzag_spacing_mm", 0.4)
@property
@param('repeats', 'Repeats', type='int', default="1")
@ -1292,7 +981,7 @@ class Stroke(EmbroideryElement):
patches = []
for path in self.paths:
path = [PyEmb.Point(x, y) for x, y in path]
path = [inkstitch.Point(x, y) for x, y in path]
if self.is_running_stitch():
patch = self.stroke_points(path, self.running_stitch_length, stroke_width=0.0)
else:
@ -1317,10 +1006,10 @@ class SatinColumn(EmbroideryElement):
return self.get_style("stroke")
@property
@param('zigzag_spacing_mm', 'Zig-zag spacing (peak-to-peak)', unit='mm', type='float')
@param('zigzag_spacing_mm', 'Zig-zag spacing (peak-to-peak)', unit='mm', type='float', default=0.4)
def zigzag_spacing(self):
# peak-to-peak distance between zigzags
return self.get_float_param("zigzag_spacing_mm")
return self.get_float_param("zigzag_spacing_mm", 0.4)
@property
@param('pull_compensation_mm', 'Pull compensation', unit='mm', type='float')
@ -1338,13 +1027,12 @@ class SatinColumn(EmbroideryElement):
return self.get_boolean_param("contour_underlay")
@property
@param('contour_underlay_stitch_length_mm', 'Stitch length', unit='mm', group='Contour Underlay', type='float')
@param('contour_underlay_stitch_length_mm', 'Stitch length', unit='mm', group='Contour Underlay', type='float', default=1.5)
def contour_underlay_stitch_length(self):
# use "contour_underlay_stitch_length", or, if not set, default to "stitch_length"
return self.get_float_param("contour_underlay_stitch_length_mm") or self.get_float_param("running_stitch_length_mm")
return self.get_float_param("contour_underlay_stitch_length_mm", 1.5)
@property
@param('contour_underlay_inset_mm', 'Contour underlay inset amount', unit='mm', group='Contour Underlay', type='float')
@param('contour_underlay_inset_mm', 'Contour underlay inset amount', unit='mm', group='Contour Underlay', type='float', default=0.4)
def contour_underlay_inset(self):
# how far inside the edge of the column to stitch the underlay
return self.get_float_param("contour_underlay_inset_mm", 0.4)
@ -1357,10 +1045,9 @@ class SatinColumn(EmbroideryElement):
return self.get_boolean_param("center_walk_underlay")
@property
@param('center_walk_underlay_stitch_length_mm', 'Stitch length', unit='mm', group='Center-Walk Underlay', type='float')
@param('center_walk_underlay_stitch_length_mm', 'Stitch length', unit='mm', group='Center-Walk Underlay', type='float', default=1.5)
def center_walk_underlay_stitch_length(self):
# use "center_walk_underlay_stitch_length", or, if not set, default to "stitch_length"
return self.get_float_param("center_walk_underlay_stitch_length_mm") or self.get_float_param("running_stitch_length_mm")
return self.get_float_param("center_walk_underlay_stitch_length_mm", 1.5)
@property
@param('zigzag_underlay', 'Zig-zag underlay', type='toggle', group='Zig-zag Underlay')
@ -1368,10 +1055,9 @@ class SatinColumn(EmbroideryElement):
return self.get_boolean_param("zigzag_underlay")
@property
@param('zigzag_underlay_spacing_mm', 'Zig-Zag spacing (peak-to-peak)', unit='mm', group='Zig-zag Underlay', type='float')
@param('zigzag_underlay_spacing_mm', 'Zig-Zag spacing (peak-to-peak)', unit='mm', group='Zig-zag Underlay', type='float', default=3)
def zigzag_underlay_spacing(self):
# peak-to-peak distance between zigzags in zigzag underlay
return self.get_float_param("zigzag_underlay_spacing_mm", 1)
return self.get_float_param("zigzag_underlay_spacing_mm", 3)
@property
@param('zigzag_underlay_inset_mm', 'Inset amount (default: half of contour underlay inset)', unit='mm', group='Zig-zag Underlay', type='float')
@ -1429,7 +1115,7 @@ class SatinColumn(EmbroideryElement):
print >> dbg, [str(rail) for rail in rails], [str(rung) for rung in rungs]
self.fatal("Expected %d linestrings, got %d" % (len(rungs.geoms) + 1, len(linestrings.geoms)))
paths = [[PyEmb.Point(*coord) for coord in ls.coords] for ls in linestrings.geoms]
paths = [[inkstitch.Point(*coord) for coord in ls.coords] for ls in linestrings.geoms]
result.append(paths)
return zip(*result)
@ -1453,7 +1139,7 @@ class SatinColumn(EmbroideryElement):
# iterate over pairs of 3-tuples
for prev, current in zip(path[:-1], path[1:]):
flattened_segment = self.flatten([[prev, current]])
flattened_segment = [PyEmb.Point(x, y) for x, y in flattened_segment[0]]
flattened_segment = [inkstitch.Point(x, y) for x, y in flattened_segment[0]]
flattened_path.append(flattened_segment)
paths.append(flattened_path)
@ -1762,7 +1448,7 @@ class Polyline(EmbroideryElement):
patch = Patch(color=self.color)
for stitch in self.stitches:
patch.add_stitch(PyEmb.Point(*stitch))
patch.add_stitch(inkstitch.Point(*stitch))
return [patch]
@ -1792,25 +1478,6 @@ def detect_classes(node):
return classes
def descendants(node):
nodes = []
element = EmbroideryElement(node)
if element.has_style('display') and element.get_style('display') is None:
return []
if node.tag == SVG_DEFS_TAG:
return []
for child in node:
nodes.extend(descendants(child))
if node.tag in EMBROIDERABLE_TAGS:
nodes.append(node)
return nodes
class Patch:
def __init__(self, color=None, stitches=None, trim_after=False, stop_after=False):
self.color = color
@ -1872,7 +1539,7 @@ def process_trim(stitches, next_stitch):
for i in xrange(3):
pos += delta
stitches.append(PyEmb.Stitch(pos.x, pos.y, stitches[-1].color, jump=True))
stitches.append(inkstitch.Stitch(pos.x, pos.y, stitches[-1].color, jump=True))
# first one should be TRIM instead of JUMP
stitches[-3].jump = False
@ -1906,16 +1573,16 @@ def patches_to_stitches(patch_list, collapse_len_px=0):
if stitches and last_color and last_color != patch.color:
# add a color change
stitches.append(PyEmb.Stitch(last_stitch.x, last_stitch.y, last_color, stop=True))
stitches.append(inkstitch.Stitch(last_stitch.x, last_stitch.y, last_color, stop=True))
if need_trim:
process_trim(stitches, stitch)
need_trim = False
if jump_stitch:
stitches.append(PyEmb.Stitch(stitch.x, stitch.y, patch.color, jump=True))
stitches.append(inkstitch.Stitch(stitch.x, stitch.y, patch.color, jump=True))
stitches.append(PyEmb.Stitch(stitch.x, stitch.y, patch.color, jump=False))
stitches.append(inkstitch.Stitch(stitch.x, stitch.y, patch.color, jump=False))
jump_stitch = False
last_stitch = stitch
@ -1971,35 +1638,37 @@ def emit_inkscape(parent, stitches):
'transform': transform
})
def get_elements(effect):
elements = []
nodes = get_nodes(effect)
for node in nodes:
classes = detect_classes(node)
elements.extend(cls(node) for cls in classes)
return elements
def elements_to_patches(elements):
patches = []
for element in elements:
if patches:
last_patch = patches[-1]
else:
last_patch = None
patches.extend(element.embroider(last_patch))
return patches
class Embroider(inkex.Effect):
def __init__(self, *args, **kwargs):
inkex.Effect.__init__(self)
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_length_mm", default=3.0,
help="max stitch length (mm)")
self.OptionParser.add_option("--running_stitch_len_mm",
action="store", type="float",
dest="running_stitch_length_mm", default=3.0,
help="running stitch length (mm)")
self.OptionParser.add_option("-c", "--collapse_len_mm",
action="store", type="float",
dest="collapse_length_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("--hide_layers",
action="store", type="choice",
choices=["true", "false"],
@ -2021,15 +1690,7 @@ class Embroider(inkex.Effect):
action="store", type="int",
dest="max_backups", default=5,
help="Max number of backups of output files to keep.")
self.patches = []
def handle_node(self, node):
print >> dbg, "handling node", node.get('id'), node.tag
nodes = descendants(node)
for node in nodes:
classes = detect_classes(node)
print >> dbg, "classes:", classes
self.elements.extend(cls(node, self.options) for cls in classes)
self.OptionParser.usage += "\n\nSeeing a 'no such option' message? Please restart Inkscape to fix."
def get_output_path(self):
if self.options.output_file:
@ -2073,22 +1734,7 @@ class Embroider(inkex.Effect):
self.patch_list = []
print >> dbg, "starting nodes: %s\n" % time.time()
dbg.flush()
self.elements = []
if self.selected:
# be sure to visit selected nodes in the order they're stacked in
# the document
for node in self.document.getroot().iter():
if node.get("id") in self.selected:
self.handle_node(node)
else:
self.handle_node(self.document.getroot())
print >> dbg, "finished nodes: %s" % time.time()
dbg.flush()
self.elements = get_elements(self)
if not self.elements:
if self.selected:
@ -2101,17 +1747,9 @@ class Embroider(inkex.Effect):
if self.options.hide_layers:
self.hide_layers()
patches = []
for element in self.elements:
if patches:
last_patch = patches[-1]
else:
last_patch = None
patches.extend(element.embroider(last_patch))
patches = elements_to_patches(self.elements)
stitches = patches_to_stitches(patches, self.options.collapse_length_mm * PIXELS_PER_MM)
PyEmb.write_embroidery_file(self.get_output_path(), stitches)
inkstitch.write_embroidery_file(self.get_output_path(), stitches)
new_layer = inkex.etree.SubElement(self.document.getroot(), SVG_GROUP_TAG, {})
new_layer.set('id', self.uniqueId("embroidery"))

Wyświetl plik

@ -13,7 +13,8 @@ import wx
from wx.lib.scrolledpanel import ScrolledPanel
from collections import defaultdict
import inkex
from embroider import Param, EmbroideryElement, Fill, AutoFill, Stroke, SatinColumn, descendants
from inkstitch import Param, EmbroideryElement, get_nodes
from embroider import Fill, AutoFill, Stroke, SatinColumn
from functools import partial
from itertools import groupby
from embroider_simulate import EmbroiderySimulator
@ -307,7 +308,7 @@ class ParamsTab(ScrolledPanel):
input.Bind(wx.EVT_TEXT, self.changed)
else:
value = param.values[0] if param.values else ""
input = wx.TextCtrl(self, wx.ID_ANY, value=value)
input = wx.TextCtrl(self, wx.ID_ANY, value=str(value))
input.Bind(wx.EVT_TEXT, self.changed)
self.param_inputs[param.name] = input
@ -328,7 +329,8 @@ class SettingsFrame(wx.Frame):
self.tabs_factory = kwargs.pop('tabs_factory', [])
self.cancel_hook = kwargs.pop('on_cancel', None)
wx.Frame.__init__(self, None, wx.ID_ANY,
"Embroidery Params"
"Embroidery Params",
pos=wx.Point(0,0)
)
self.notebook = wx.Notebook(self, wx.ID_ANY)
self.tabs = self.tabs_factory(self.notebook)
@ -623,17 +625,6 @@ class EmbroiderParams(inkex.Effect):
self.cancelled = False
inkex.Effect.__init__(self, *args, **kwargs)
def get_nodes(self):
if self.selected:
nodes = []
for node in self.document.getroot().iter():
if node.get("id") in self.selected:
nodes.extend(descendants(node))
else:
nodes = descendants(self.document.getroot())
return nodes
def embroidery_classes(self, node):
element = EmbroideryElement(node)
classes = []
@ -651,10 +642,10 @@ class EmbroiderParams(inkex.Effect):
return classes
def get_nodes_by_class(self):
nodes = self.get_nodes()
nodes = get_nodes(self)
nodes_by_class = defaultdict(list)
for z, node in enumerate(self.get_nodes()):
for z, node in enumerate(nodes):
for cls in self.embroidery_classes(node):
element = cls(node)
element.order = z

Wyświetl plik

@ -6,7 +6,8 @@ import inkex
import simplestyle
import colorsys
from embroider import patches_to_stitches, stitches_to_polylines, PIXELS_PER_MM
from inkstitch import PIXELS_PER_MM
from embroider import patches_to_stitches, get_elements, elements_to_patches
class EmbroiderySimulator(wx.Frame):
def __init__(self, *args, **kwargs):
@ -134,12 +135,25 @@ class EmbroiderySimulator(wx.Frame):
last_pos = None
last_color = None
pen = None
trimming = False
for stitch in stitches:
if stitch.trim:
trimming = True
last_pos = None
continue
if trimming:
if stitch.jump:
continue
else:
trimming = False
pos = (stitch.x, stitch.y)
if stitch.color == last_color:
segments.append(((last_pos, pos), pen))
if last_pos:
segments.append(((last_pos, pos), pen))
else:
pen = self.color_to_pen(simplestyle.parseColor(stitch.color))
@ -148,55 +162,6 @@ class EmbroiderySimulator(wx.Frame):
return segments
def _parse_stitch_file(self, stitch_file_path):
# "#", "comment"
# "$","1","229","229","229","(null)","(null)"
# "*","JUMP","1.595898","48.731899"
# "*","STITCH","1.595898","48.731899"
segments = []
pos = (0, 0)
pen = wx.Pen('black')
cut = True
with open(stitch_file_path) as stitch_file:
for line in stitch_file:
line = line.strip()
if not line:
continue
fields = line.strip().split(",")
fields = [self._strip_quotes(field) for field in fields]
symbol, command = fields[:2]
if symbol == "$":
red, green, blue = fields[2:5]
pen = self.color_to_pen((int(red), int(green), int(blue)))
elif symbol == "*":
if command == "COLOR":
# change color
# The next command should be a JUMP, and we'll need to skip stitching.
cut = True
elif command == "JUMP" or command == "STITCH":
# JUMP just means a long stitch, really.
x, y = fields[2:]
new_pos = (float(x) * PIXELS_PER_MM, float(y) * PIXELS_PER_MM)
if not segments and new_pos == (0.0, 0.0):
# libembroidery likes to throw an extra JUMP in at the start
continue
if not cut:
segments.append(((pos, new_pos), pen))
cut = False
pos = new_pos
return segments
def all_coordinates(self):
for segment in self.segments:
start, end = segment[0]
@ -330,20 +295,14 @@ class SimulateEffect(inkex.Effect):
help="Directory in which to store output file")
def effect(self):
patches = elements_to_patches(get_elements(self))
app = wx.App()
frame = EmbroiderySimulator(None, -1, "Embroidery Simulation", wx.DefaultPosition, size=(1000, 1000), stitch_file=self.get_stitch_file())
frame = EmbroiderySimulator(None, -1, "Embroidery Simulation", wx.DefaultPosition, size=(1000, 1000), patches=patches)
app.SetTopWindow(frame)
frame.Show()
wx.CallAfter(frame.go)
app.MainLoop()
def get_stitch_file(self):
svg_filename = self.document.getroot().get(inkex.addNS('docname', 'sodipodi'))
csv_filename = svg_filename.replace('.svg', '.csv')
stitch_file = os.path.join(self.options.path, csv_filename)
return stitch_file
if __name__ == "__main__":
effect = SimulateEffect()

514
inkstitch.py 100644
Wyświetl plik

@ -0,0 +1,514 @@
#!/usr/bin/env python
# http://www.achatina.de/sewing/main/TECHNICL.HTM
import sys
from copy import deepcopy
import math
import libembroidery
import inkex
import simplepath
import simplestyle
import simpletransform
from bezmisc import bezierlength, beziertatlength, bezierpointatt
from cspsubdiv import cspsubdiv
import cubicsuperpath
try:
from functools import lru_cache
except ImportError:
from backports.functools_lru_cache import lru_cache
# modern versions of Inkscape use 96 pixels per inch as per the CSS standard
PIXELS_PER_MM = 96 / 25.4
SVG_PATH_TAG = inkex.addNS('path', 'svg')
SVG_POLYLINE_TAG = inkex.addNS('polyline', 'svg')
SVG_DEFS_TAG = inkex.addNS('defs', 'svg')
SVG_GROUP_TAG = inkex.addNS('g', 'svg')
EMBROIDERABLE_TAGS = (SVG_PATH_TAG, SVG_POLYLINE_TAG)
dbg = open("/tmp/embroider-debug.txt", "w")
# simplify use of lru_cache decorator
def cache(*args, **kwargs):
return lru_cache(maxsize=None)(*args, **kwargs)
# cribbed from inkscape-silhouette
def parse_length_with_units( str ):
'''
Parse an SVG value which may or may not have units attached
This version is greatly simplified in that it only allows: no units,
units of px, mm, and %. Everything else, it returns None for.
There is a more general routine to consider in scour.py if more
generality is ever needed.
'''
u = 'px'
s = str.strip()
if s[-2:] == 'px':
s = s[:-2]
elif s[-2:] == 'mm':
u = 'mm'
s = s[:-2]
elif s[-2:] == 'pt':
u = 'pt'
s = s[:-2]
elif s[-2:] == 'pc':
u = 'pc'
s = s[:-2]
elif s[-2:] == 'cm':
u = 'cm'
s = s[:-2]
elif s[-2:] == 'in':
u = 'in'
s = s[:-2]
elif s[-1:] == '%':
u = '%'
s = s[:-1]
try:
v = float( s )
except:
raise ValueError("parseLengthWithUnits: unknown unit %s" % s)
return v, u
def convert_length(length):
value, units = parse_length_with_units(length)
if not units or units == "px":
return value
if units == 'cm':
value *= 10
units == 'mm'
if units == 'mm':
value = value / 25.4
units = 'in'
if units == 'in':
# modern versions of Inkscape use CSS's 96 pixels per inch. When you
# open an old document, inkscape will add a viewbox for you.
return value * 96
raise ValueError("Unknown unit: %s" % units)
@cache
def get_viewbox_transform(node):
# somewhat cribbed from inkscape-silhouette
doc_width = convert_length(node.get('width'))
doc_height = convert_length(node.get('height'))
viewbox = node.get('viewBox').strip().replace(',', ' ').split()
dx = -float(viewbox[0])
dy = -float(viewbox[1])
transform = simpletransform.parseTransform("translate(%f, %f)" % (dx, dy))
try:
sx = doc_width / float(viewbox[2])
sy = doc_height / float(viewbox[3])
scale_transform = simpletransform.parseTransform("scale(%f, %f)" % (sx, sy))
transform = simpletransform.composeTransform(transform, scale_transform)
except ZeroDivisionError:
pass
return transform
class Param(object):
def __init__(self, name, description, unit=None, values=[], type=None, group=None, inverse=False, default=None, tooltip=None, sort_index=0):
self.name = name
self.description = description
self.unit = unit
self.values = values or [""]
self.type = type
self.group = group
self.inverse = inverse
self.default = default
self.tooltip = tooltip
self.sort_index = sort_index
def __repr__(self):
return "Param(%s)" % vars(self)
# Decorate a member function or property with information about
# the embroidery parameter it corresponds to
def param(*args, **kwargs):
p = Param(*args, **kwargs)
def decorator(func):
func.param = p
return func
return decorator
class EmbroideryElement(object):
def __init__(self, node):
self.node = node
@property
def id(self):
return self.node.get('id')
@classmethod
def get_params(cls):
params = []
for attr in dir(cls):
prop = getattr(cls, attr)
if isinstance(prop, property):
# The 'param' attribute is set by the 'param' decorator defined above.
if hasattr(prop.fget, 'param'):
params.append(prop.fget.param)
return params
@cache
def get_param(self, param, default):
value = self.node.get("embroider_" + param, "").strip()
return value or default
@cache
def get_boolean_param(self, param, default=None):
value = self.get_param(param, default)
if isinstance(value, bool):
return value
else:
return value and (value.lower() in ('yes', 'y', 'true', 't', '1'))
@cache
def get_float_param(self, param, default=None):
try:
value = float(self.get_param(param, default))
except (TypeError, ValueError):
return default
if param.endswith('_mm'):
value = value * PIXELS_PER_MM
return value
@cache
def get_int_param(self, param, default=None):
try:
value = int(self.get_param(param, default))
except (TypeError, ValueError):
return default
if param.endswith('_mm'):
value = int(value * PIXELS_PER_MM)
return value
def set_param(self, name, value):
self.node.set("embroider_%s" % name, str(value))
@cache
def get_style(self, style_name):
style = simplestyle.parseStyle(self.node.get("style"))
if (style_name not in style):
return None
value = style[style_name]
if value == 'none':
return None
return value
@cache
def has_style(self, style_name):
style = simplestyle.parseStyle(self.node.get("style"))
return style_name in style
@property
def path(self):
return cubicsuperpath.parsePath(self.node.get("d"))
@cache
def parse_path(self):
# A CSP is a "cubic superpath".
#
# A "path" is a sequence of strung-together bezier curves.
#
# A "superpath" is a collection of paths that are all in one object.
#
# The "cubic" bit in "cubic superpath" is because the bezier curves
# inkscape uses involve cubic polynomials.
#
# Each path is a collection of tuples, each of the form:
#
# (control_before, point, control_after)
#
# A bezier curve segment is defined by an endpoint, a control point,
# a second control point, and a final endpoint. A path is a bunch of
# bezier curves strung together. One could represent a path as a set
# of four-tuples, but there would be redundancy because the ending
# point of one bezier is the starting point of the next. Instead, a
# path is a set of 3-tuples as shown above, and one must construct
# each bezier curve by taking the appropriate endpoints and control
# points. Bleh. It should be noted that a straight segment is
# represented by having the control point on each end equal to that
# end's point.
#
# In a path, each element in the 3-tuple is itself a tuple of (x, y).
# Tuples all the way down. Hasn't anyone heard of using classes?
path = self.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(self.node, transform)
# add in the transform implied by the viewBox
viewbox_transform = get_viewbox_transform(self.node.getroottree().getroot())
transform = simpletransform.composeTransform(viewbox_transform, transform)
# apply the combined transform to this node's path
simpletransform.applyTransformToPath(transform, path)
return path
def flatten(self, path):
"""approximate a path containing beziers with a series of points"""
path = deepcopy(path)
cspsubdiv(path, 0.1)
flattened = []
for comp in path:
vertices = []
for ctl in comp:
vertices.append((ctl[1][0], ctl[1][1]))
flattened.append(vertices)
return flattened
@property
@param('trim_after',
'TRIM after',
tooltip='Trim thread after this object (for supported machines and file formats)',
type='boolean',
default=False,
sort_index=1000)
def trim_after(self):
return self.get_boolean_param('trim_after', False)
@property
@param('stop_after',
'STOP after',
tooltip='Add STOP instruction after this object (for supported machines and file formats)',
type='boolean',
default=False,
sort_index=1000)
def stop_after(self):
return self.get_boolean_param('stop_after', False)
def to_patches(self, last_patch):
raise NotImplementedError("%s must implement to_patches()" % self.__class__.__name__)
def embroider(self, last_patch):
patches = self.to_patches(last_patch)
if patches:
patches[-1].trim_after = self.trim_after
patches[-1].stop_after = self.stop_after
return patches
def fatal(self, message):
print >> sys.stderr, "error:", message
sys.exit(1)
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 __mul__(self, other):
if isinstance(other, Point):
# dot product
return self.x * other.x + self.y * other.y
elif isinstance(other, (int, float)):
return Point(self.x * other, self.y * other)
else:
raise ValueError("cannot multiply Point by %s" % type(other))
def __rmul__(self, other):
if isinstance(other, (int, float)):
return self.__mul__(other)
else:
raise ValueError("cannot multiply Point by %s" % type(other))
def __repr__(self):
return "Point(%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 rotate(self, angle):
return Point(self.x * math.cos(angle) - self.y * math.sin(angle), self.y * math.cos(angle) + self.x * math.sin(angle))
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())
def __getitem__(self, item):
return self.as_tuple()[item]
def __len__(self):
return 2
class Stitch(Point):
def __init__(self, x, y, color=None, jump=False, stop=False, trim=False):
self.x = x
self.y = y
self.color = color
self.jump = jump
self.trim = trim
self.stop = stop
def __repr__(self):
return "Stitch(%s, %s, %s, %s, %s, %s)" % (self.x, self.y, self.color, "JUMP" if self.jump else "", "TRIM" if self.trim else "", "STOP" if self.stop else "")
def descendants(node):
nodes = []
element = EmbroideryElement(node)
if element.has_style('display') and element.get_style('display') is None:
return []
if node.tag == SVG_DEFS_TAG:
return []
for child in node:
nodes.extend(descendants(child))
if node.tag in EMBROIDERABLE_TAGS:
nodes.append(node)
return nodes
def get_nodes(effect):
"""Get all XML nodes, or just those selected
effect is an instance of a subclass of inkex.Effect.
"""
if effect.selected:
nodes = []
for node in effect.document.getroot().iter():
if node.get("id") in effect.selected:
nodes.extend(descendants(node))
else:
nodes = descendants(effect.document.getroot())
return nodes
def make_thread(color):
# strip off the leading "#"
if color.startswith("#"):
color = color[1:]
thread = libembroidery.EmbThread()
thread.color = libembroidery.embColor_fromHexStr(color)
thread.description = color
thread.catalogNumber = ""
return thread
def add_thread(pattern, thread):
"""Add a thread to a pattern and return the thread's index"""
libembroidery.embPattern_addThread(pattern, thread)
return libembroidery.embThreadList_count(pattern.threadList) - 1
def get_flags(stitch):
flags = 0
if stitch.jump:
flags |= libembroidery.JUMP
if stitch.trim:
flags |= libembroidery.TRIM
if stitch.stop:
flags |= libembroidery.STOP
return flags
def write_embroidery_file(file_path, stitches):
# Embroidery machines don't care about our canvas size, so we relocate the
# design to the origin. It might make sense to center it about the origin
# instead.
min_x = min(stitch.x for stitch in stitches)
min_y = min(stitch.y for stitch in stitches)
pattern = libembroidery.embPattern_create()
threads = {}
last_color = None
for stitch in stitches:
if stitch.color != last_color:
if stitch.color not in threads:
thread = make_thread(stitch.color)
thread_index = add_thread(pattern, thread)
threads[stitch.color] = thread_index
else:
thread_index = threads[stitch.color]
libembroidery.embPattern_changeColor(pattern, thread_index)
last_color = stitch.color
flags = get_flags(stitch)
libembroidery.embPattern_addStitchAbs(pattern, stitch.x - min_x, stitch.y - min_y, flags, 0)
libembroidery.embPattern_addStitchAbs(pattern, stitch.x - min_x, stitch.y - min_y, libembroidery.END, 0)
# convert from pixels to millimeters
libembroidery.embPattern_scale(pattern, 1/PIXELS_PER_MM)
# SVG and embroidery disagree on the direction of the Y axis
libembroidery.embPattern_flipVertical(pattern)
libembroidery.embPattern_write(pattern, file_path)