summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/commands.py86
-rw-r--r--lib/elements/auto_fill.py59
-rw-r--r--lib/elements/element.py74
-rw-r--r--lib/elements/fill.py3
-rw-r--r--lib/elements/polyline.py7
-rw-r--r--lib/elements/satin_column.py13
-rw-r--r--lib/elements/stroke.py94
-rw-r--r--lib/extensions/__init__.py6
-rw-r--r--lib/extensions/base.py31
-rw-r--r--lib/extensions/commands.py161
-rw-r--r--lib/extensions/embroider.py3
-rw-r--r--lib/extensions/flip.py40
-rw-r--r--lib/extensions/input.py40
-rw-r--r--lib/extensions/install.py (renamed from lib/extensions/palettes.py)63
-rw-r--r--lib/extensions/output.py48
-rw-r--r--lib/extensions/params.py39
-rw-r--r--lib/extensions/print_pdf.py85
-rw-r--r--lib/extensions/zip.py74
-rw-r--r--lib/output.py76
-rw-r--r--lib/stitch_plan/stitch.py14
-rw-r--r--lib/stitch_plan/stitch_plan.py126
-rw-r--r--lib/stitch_plan/stop.py43
-rw-r--r--lib/stitch_plan/ties.py18
-rw-r--r--lib/stitch_plan/trim.py23
-rw-r--r--lib/stitches/auto_fill.py197
-rw-r--r--lib/svg/__init__.py1
-rw-r--r--lib/svg/path.py25
-rw-r--r--lib/svg/realistic_rendering.py129
-rw-r--r--lib/svg/svg.py44
-rw-r--r--lib/svg/tags.py6
-rw-r--r--lib/svg/units.py19
-rw-r--r--lib/threads/color.py32
-rw-r--r--lib/utils/__init__.py1
-rw-r--r--lib/utils/geometry.py9
-rw-r--r--lib/utils/io.py21
-rw-r--r--lib/utils/paths.py10
36 files changed, 1247 insertions, 473 deletions
diff --git a/lib/commands.py b/lib/commands.py
new file mode 100644
index 00000000..02c13b25
--- /dev/null
+++ b/lib/commands.py
@@ -0,0 +1,86 @@
+import inkex
+import cubicsuperpath
+
+from .svg import apply_transforms
+from .svg.tags import SVG_USE_TAG, SVG_SYMBOL_TAG, CONNECTION_START, CONNECTION_END, XLINK_HREF
+
+
+class Command(object):
+ def __init__(self, connector):
+ self.connector = connector
+ self.svg = self.connector.getroottree().getroot()
+
+ self.parse_command()
+
+ def get_node_by_url(self, url):
+ # url will be #path12345. Find the object at the other end.
+
+ if url is None:
+ raise ValueError("url is None")
+
+ if not url.startswith('#'):
+ raise ValueError("invalid connection url: %s" % url)
+
+ id = url[1:]
+
+ try:
+ return self.svg.xpath(".//*[@id='%s']" % id)[0]
+ except (IndexError, AttributeError):
+ raise ValueError("could not find node by url %s" % id)
+
+ def parse_connector_path(self):
+ path = cubicsuperpath.parsePath(self.connector.get('d'))
+ return apply_transforms(path, self.connector)
+
+ def parse_command(self):
+ path = self.parse_connector_path()
+
+ neighbors = [
+ (self.get_node_by_url(self.connector.get(CONNECTION_START)), path[0][0][1]),
+ (self.get_node_by_url(self.connector.get(CONNECTION_END)), path[0][-1][1])
+ ]
+
+ if neighbors[0][0].tag != SVG_USE_TAG:
+ neighbors.reverse()
+
+ if neighbors[0][0].tag != SVG_USE_TAG:
+ raise ValueError("connector does not point to a use tag")
+
+ self.symbol = self.get_node_by_url(neighbors[0][0].get(XLINK_HREF))
+
+ if self.symbol.tag != SVG_SYMBOL_TAG:
+ raise ValueError("use points to non-symbol")
+
+ self.command = self.symbol.get('id')
+
+ if self.command.startswith('inkstitch_'):
+ self.command = self.command[10:]
+ else:
+ raise ValueError("symbol is not an Ink/Stitch command")
+
+ self.target = neighbors[1][0]
+ self.target_point = neighbors[1][1]
+
+ def __repr__(self):
+ return "Command('%s', %s)" % (self.command, self.target_point)
+
+def find_commands(node):
+ """Find the symbols this node is connected to and return them as Commands"""
+
+ # find all paths that have this object as a connection
+ xpath = ".//*[@inkscape:connection-start='#%(id)s' or @inkscape:connection-end='#%(id)s']" % dict(id=node.get('id'))
+ connectors = node.getroottree().getroot().xpath(xpath, namespaces=inkex.NSS)
+
+ # try to turn them into commands
+ commands = []
+ for connector in connectors:
+ try:
+ commands.append(Command(connector))
+ except ValueError:
+ # Parsing the connector failed, meaning it's not actually an Ink/Stitch command.
+ pass
+
+ return commands
+
+def is_command(node):
+ return CONNECTION_START in node.attrib or CONNECTION_END in node.attrib
diff --git a/lib/elements/auto_fill.py b/lib/elements/auto_fill.py
index 08ae67f7..59816878 100644
--- a/lib/elements/auto_fill.py
+++ b/lib/elements/auto_fill.py
@@ -63,27 +63,65 @@ class AutoFill(Fill):
return self.get_float_param("fill_underlay_max_stitch_length_mm") or self.max_stitch_length
@property
- @param('fill_underlay_inset_mm', _('Inset'), unit='mm', group=_('AutoFill Underlay'), type='float', default=0)
+ @param('fill_underlay_inset_mm',
+ _('Inset'),
+ tooltip='Shrink the shape before doing underlay, to prevent underlay from showing around the outside of the fill.',
+ unit='mm',
+ group=_('AutoFill Underlay'),
+ type='float',
+ default=0)
def fill_underlay_inset(self):
return self.get_float_param('fill_underlay_inset_mm', 0)
@property
- def underlay_shape(self):
- if self.fill_underlay_inset:
- shape = self.shape.buffer(-self.fill_underlay_inset)
+ @param('expand_mm',
+ _('Expand'),
+ tooltip='Expand the shape before fill stitching, to compensate for gaps between shapes.',
+ unit='mm',
+ type='float',
+ default=0)
+ def expand(self):
+ return self.get_float_param('expand_mm', 0)
+
+ def shrink_or_grow_shape(self, amount):
+ if amount:
+ shape = self.shape.buffer(amount)
if not isinstance(shape, shgeo.MultiPolygon):
shape = shgeo.MultiPolygon([shape])
return shape
else:
return self.shape
+ @property
+ def underlay_shape(self):
+ return self.shrink_or_grow_shape(-self.fill_underlay_inset)
+
+ @property
+ def fill_shape(self):
+ return self.shrink_or_grow_shape(self.expand)
+
+ def get_starting_point(self, last_patch):
+ # If there is a "fill_start" Command, then use that; otherwise pick
+ # the point closest to the end of the last patch.
+
+ if self.get_command('fill_start'):
+ return self.get_command('fill_start').target_point
+ elif last_patch:
+ return last_patch.stitches[-1]
+ else:
+ return None
+
+ def get_ending_point(self):
+ if self.get_command('fill_end'):
+ return self.get_command('fill_end').target_point
+ else:
+ return None
+
def to_patches(self, last_patch):
stitches = []
- if last_patch is None:
- starting_point = None
- else:
- starting_point = last_patch.stitches[-1]
+ starting_point = self.get_starting_point(last_patch)
+ ending_point = self.get_ending_point()
if self.fill_underlay:
stitches.extend(auto_fill(self.underlay_shape,
@@ -96,13 +134,14 @@ class AutoFill(Fill):
starting_point))
starting_point = stitches[-1]
- stitches.extend(auto_fill(self.shape,
+ stitches.extend(auto_fill(self.fill_shape,
self.angle,
self.row_spacing,
self.end_row_spacing,
self.max_stitch_length,
self.running_stitch_length,
self.staggers,
- starting_point))
+ starting_point,
+ ending_point))
return [Patch(stitches=stitches, color=self.color)]
diff --git a/lib/elements/element.py b/lib/elements/element.py
index 300136dd..ebca90a4 100644
--- a/lib/elements/element.py
+++ b/lib/elements/element.py
@@ -4,7 +4,8 @@ from shapely import geometry as shgeo
from ..i18n import _
from ..utils import cache
-from ..svg import PIXELS_PER_MM, get_viewbox_transform, convert_length, get_doc_size
+from ..svg import PIXELS_PER_MM, convert_length, get_doc_size, apply_transforms
+from ..commands import find_commands
# inkscape-provided utilities
import simpletransform
@@ -29,6 +30,10 @@ class Patch:
else:
raise TypeError("Patch can only be added to another Patch")
+ def __len__(self):
+ # This method allows `len(patch)` and `if patch:
+ return len(self.stitches)
+
def add_stitch(self, stitch):
self.stitches.append(stitch)
@@ -36,7 +41,6 @@ class Patch:
return Patch(self.color, self.stitches[::-1])
-
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
@@ -132,10 +136,10 @@ class EmbroideryElement(object):
self.node.set("embroider_%s" % name, str(value))
@cache
- def get_style(self, style_name):
+ def get_style(self, style_name, default=None):
style = simplestyle.parseStyle(self.node.get("style"))
if (style_name not in style):
- return None
+ return default
value = style[style_name]
if value == 'none':
return None
@@ -158,7 +162,7 @@ class EmbroideryElement(object):
@property
@cache
def stroke_width(self):
- width = self.get_style("stroke-width")
+ width = self.get_style("stroke-width", "1")
if width is None:
return 1.0
@@ -168,10 +172,6 @@ class EmbroideryElement(object):
@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.
@@ -199,22 +199,40 @@ class EmbroideryElement(object):
# 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
+ return cubicsuperpath.parsePath(self.node.get("d"))
+
+ @cache
+ def parse_path(self):
+ return apply_transforms(self.path, self.node)
- # start with the identity transform
- transform = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]
+ @property
+ def shape(self):
+ raise NotImplementedError("INTERNAL ERROR: %s must implement shape()", self.__class__)
- # combine this node's transform with all parent groups' transforms
- transform = simpletransform.composeParents(self.node, transform)
+ @property
+ @cache
+ def commands(self):
+ return find_commands(self.node)
- # add in the transform implied by the viewBox
- viewbox_transform = get_viewbox_transform(self.node.getroottree().getroot())
- transform = simpletransform.composeTransform(viewbox_transform, transform)
+ @cache
+ def get_commands(self, command):
+ return [c for c in self.commands if c.command == command]
- # apply the combined transform to this node's path
- simpletransform.applyTransformToPath(transform, path)
+ @cache
+ def has_command(self, command):
+ return len(self.get_commands(command)) > 0
- return path
+ @cache
+ def get_command(self, command):
+ commands = self.get_commands(command)
+
+ if len(commands) == 1:
+ return commands[0]
+ elif len(commands) > 1:
+ raise ValueError(_("%(id)s has more than one command of type '%(command)s' linked to it") %
+ dict(id=self.node.get(id), command=command))
+ else:
+ return None
def strip_control_points(self, subpath):
return [point for control_before, point, control_after in subpath]
@@ -228,22 +246,10 @@ class EmbroideryElement(object):
return [self.strip_control_points(subpath) for subpath in path]
@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)
@@ -254,8 +260,8 @@ class EmbroideryElement(object):
patches = self.to_patches(last_patch)
if patches:
- patches[-1].trim_after = self.trim_after
- patches[-1].stop_after = self.stop_after
+ patches[-1].trim_after = self.has_command("trim") or self.trim_after
+ patches[-1].stop_after = self.has_command("stop") or self.stop_after
return patches
diff --git a/lib/elements/fill.py b/lib/elements/fill.py
index 52a42260..8d1d35f2 100644
--- a/lib/elements/fill.py
+++ b/lib/elements/fill.py
@@ -27,7 +27,8 @@ class Fill(EmbroideryElement):
@property
def color(self):
- return self.get_style("fill")
+ # SVG spec says the default fill is black
+ return self.get_style("fill", "#000000")
@property
@param('flip', _('Flip fill (start right-to-left)'), type='boolean', default=False)
diff --git a/lib/elements/polyline.py b/lib/elements/polyline.py
index 5c474237..b9ffdc0b 100644
--- a/lib/elements/polyline.py
+++ b/lib/elements/polyline.py
@@ -1,3 +1,5 @@
+from shapely import geometry as shgeo
+
from .element import param, EmbroideryElement, Patch
from ..i18n import _
from ..utils.geometry import Point
@@ -28,6 +30,11 @@ class Polyline(EmbroideryElement):
return points
@property
+ @cache
+ def shape(self):
+ return shgeo.LineString(self.points)
+
+ @property
def path(self):
# A polyline is a series of connected line segments described by their
# points. In order to make use of the existing logic for incorporating
diff --git a/lib/elements/satin_column.py b/lib/elements/satin_column.py
index 3593db64..2ceb38de 100644
--- a/lib/elements/satin_column.py
+++ b/lib/elements/satin_column.py
@@ -89,6 +89,17 @@ class SatinColumn(EmbroideryElement):
@property
@cache
+ def shape(self):
+ # This isn't used for satins at all, but other parts of the code
+ # may need to know the general shape of a satin column.
+
+ flattened = self.flatten(self.parse_path())
+ line_strings = [shgeo.LineString(path) for path in flattened]
+
+ return shgeo.MultiLineString(line_strings)
+
+ @property
+ @cache
def csp(self):
return self.parse_path()
@@ -97,6 +108,8 @@ class SatinColumn(EmbroideryElement):
def flattened_beziers(self):
if len(self.csp) == 2:
return self.simple_flatten_beziers()
+ elif len(self.csp) < 2:
+ self.fatal(_("satin column: %(id)s: at least two subpaths required (%(num)d found)") % dict(num=len(self.csp), id=self.node.get('id')))
else:
return self.flatten_beziers_with_rungs()
diff --git a/lib/elements/stroke.py b/lib/elements/stroke.py
index 48662b6d..e8eb4783 100644
--- a/lib/elements/stroke.py
+++ b/lib/elements/stroke.py
@@ -1,9 +1,11 @@
import sys
+import shapely.geometry
from .element import param, EmbroideryElement, Patch
from ..i18n import _
from ..utils import cache, Point
-
+from ..stitches import running_stitch
+from ..svg import parse_length_with_units
warned_about_legacy_running_stitch = False
@@ -50,6 +52,12 @@ class Stroke(EmbroideryElement):
return self.flatten(path)
@property
+ @cache
+ def shape(self):
+ line_strings = [shapely.geometry.LineString(path) for path in self.paths]
+ return shapely.geometry.MultiLineString(line_strings)
+
+ @property
@param('manual_stitch', _('Manual stitch placement'), tooltip=_("Stitch every node in the path. Stitch length and zig-zag spacing are ignored."), type='boolean', default=False)
def manual_stitch_mode(self):
return self.get_boolean_param('manual_stitch')
@@ -57,10 +65,7 @@ class Stroke(EmbroideryElement):
def is_running_stitch(self):
# using stroke width <= 0.5 pixels to indicate running stitch is deprecated in favor of dashed lines
- try:
- stroke_width = float(self.get_style("stroke-width"))
- except ValueError:
- stroke_width = 1
+ stroke_width, units = parse_length_with_units(self.get_style("stroke-width", "1"))
if self.dashed:
return True
@@ -93,56 +98,50 @@ class Stroke(EmbroideryElement):
else:
return False
- def stroke_points(self, emb_point_list, zigzag_spacing, stroke_width):
- # TODO: use inkstitch.stitches.running_stitch
+ def simple_satin(self, path, zigzag_spacing, stroke_width):
+ "zig-zag along the path at the specified spacing and wdith"
- patch = Patch(color=self.color)
- p0 = emb_point_list[0]
- rho = 0.0
- side = 1
- last_segment_direction = None
+ # `self.zigzag_spacing` is the length for a zig and a zag
+ # together (a V shape). Start with running stitch at half
+ # that length:
+ patch = self.running_stitch(path, zigzag_spacing / 2.0)
- for repeat in xrange(self.repeats):
- if repeat % 2 == 0:
- order = range(1, len(emb_point_list))
- else:
- order = range(-2, -len(emb_point_list) - 1, -1)
+ # Now move the points left and right. Consider each pair
+ # of points in turn, and move perpendicular to them,
+ # alternating left and right.
- for segi in order:
- p1 = emb_point_list[segi]
+ offset = stroke_width / 2.0
- # how far we have to go along segment
- seg_len = (p1 - p0).length()
- if (seg_len == 0):
- continue
+ for i in xrange(len(patch) - 1):
+ start = patch.stitches[i]
+ end = patch.stitches[i + 1]
+ segment_direction = (end - start).unit()
+ zigzag_direction = segment_direction.rotate_left()
- # vector pointing along segment
- along = (p1 - p0).unit()
+ if i % 2 == 1:
+ zigzag_direction *= -1
- # vector pointing to edge of stroke width
- perp = along.rotate_left() * (stroke_width * 0.5)
+ patch.stitches[i] += zigzag_direction * offset
- if stroke_width == 0.0 and last_segment_direction is not None:
- if abs(1.0 - along * last_segment_direction) > 0.5:
- # if greater than 45 degree angle, stitch the corner
- rho = zigzag_spacing
- patch.add_stitch(p0)
+ return patch
- # iteration variable: how far we are along segment
- while (rho <= seg_len):
- left_pt = p0 + along * rho + perp * side
- patch.add_stitch(left_pt)
- rho += zigzag_spacing
- side = -side
+ def running_stitch(self, path, stitch_length):
+ repeated_path = []
- p0 = p1
- last_segment_direction = along
- rho -= seg_len
+ # go back and forth along the path as specified by self.repeats
+ for i in xrange(self.repeats):
+ if i % 2 == 1:
+ # reverse every other pass
+ this_path = path[::-1]
+ else:
+ this_path = path[:]
- if (p0 - patch.stitches[-1]).length() > 0.1:
- patch.add_stitch(p0)
+ repeated_path.extend(this_path)
+
+ stitches = running_stitch(repeated_path, stitch_length)
+
+ return Patch(self.color, stitches)
- return patch
def to_patches(self, last_patch):
patches = []
@@ -152,10 +151,11 @@ class Stroke(EmbroideryElement):
if self.manual_stitch_mode:
patch = Patch(color=self.color, stitches=path, stitch_as_is=True)
elif self.is_running_stitch():
- patch = self.stroke_points(path, self.running_stitch_length, stroke_width=0.0)
+ patch = self.running_stitch(path, self.running_stitch_length)
else:
- patch = self.stroke_points(path, self.zigzag_spacing / 2.0, stroke_width=self.stroke_width)
+ patch = self.simple_satin(path, self.zigzag_spacing, self.stroke_width)
- patches.append(patch)
+ if patch:
+ patches.append(patch)
return patches
diff --git a/lib/extensions/__init__.py b/lib/extensions/__init__.py
index ebdd2fc9..8b243176 100644
--- a/lib/extensions/__init__.py
+++ b/lib/extensions/__init__.py
@@ -1,6 +1,10 @@
from embroider import Embroider
-from palettes import Palettes
+from install import Install
from params import Params
from print_pdf import Print
from simulate import Simulate
from input import Input
+from output import Output
+from zip import Zip
+from flip import Flip
+from commands import Commands
diff --git a/lib/extensions/base.py b/lib/extensions/base.py
index ff587ca5..d230f1b0 100644
--- a/lib/extensions/base.py
+++ b/lib/extensions/base.py
@@ -7,6 +7,7 @@ from collections import MutableMapping
from ..svg.tags import *
from ..elements import AutoFill, Fill, Stroke, SatinColumn, Polyline, EmbroideryElement
from ..utils import cache
+from ..commands import is_command
SVG_METADATA_TAG = inkex.addNS("metadata", "svg")
@@ -57,16 +58,12 @@ class InkStitchMetadata(MutableMapping):
def __setitem__(self, name, value):
item = self._find_item(name)
+ item.text = json.dumps(value)
- if value:
- item.text = json.dumps(value)
- else:
- item.getparent().remove(item)
-
- def _find_item(self, name):
+ def _find_item(self, name, create=True):
tag = inkex.addNS(name, "inkstitch")
item = self.metadata.find(tag)
- if item is None:
+ if item is None and create:
item = inkex.etree.SubElement(self.metadata, tag)
return item
@@ -80,9 +77,9 @@ class InkStitchMetadata(MutableMapping):
return None
def __delitem__(self, name):
- item = self._find_item(name)
+ item = self._find_item(name, create=False)
- if item:
+ if item is not None:
self.metadata.remove(item)
def __iter__(self):
@@ -111,7 +108,7 @@ class InkstitchExtension(inkex.Effect):
inkex.errormsg(_("No embroiderable paths selected."))
else:
inkex.errormsg(_("No embroiderable paths found in document."))
- inkex.errormsg(_("Tip: use Path -> Object to Path to convert non-paths before embroidering."))
+ inkex.errormsg(_("Tip: use Path -> Object to Path to convert non-paths."))
def descendants(self, node):
nodes = []
@@ -158,14 +155,15 @@ class InkstitchExtension(inkex.Effect):
else:
classes = []
- if element.get_style("fill"):
+ if element.get_style("fill", "black"):
if element.get_boolean_param("auto_fill", True):
classes.append(AutoFill)
else:
classes.append(Fill)
if element.get_style("stroke"):
- classes.append(Stroke)
+ if not is_command(element.node):
+ classes.append(Stroke)
if element.get_boolean_param("stroke_first", False):
classes.reverse()
@@ -200,6 +198,15 @@ class InkstitchExtension(inkex.Effect):
def get_inkstitch_metadata(self):
return InkStitchMetadata(self.document)
+ def get_base_file_name(self):
+ svg_filename = self.document.getroot().get(inkex.addNS('docname', 'sodipodi'), "embroidery.svg")
+
+ if svg_filename.endswith('.svg'):
+ svg_filename = svg_filename[:-4]
+
+ return svg_filename
+
+
def parse(self):
"""Override inkex.Effect to add Ink/Stitch xml namespace"""
diff --git a/lib/extensions/commands.py b/lib/extensions/commands.py
new file mode 100644
index 00000000..2f3006ff
--- /dev/null
+++ b/lib/extensions/commands.py
@@ -0,0 +1,161 @@
+import os
+import sys
+import inkex
+import simpletransform
+import cubicsuperpath
+from copy import deepcopy
+from random import random
+from shapely import geometry as shgeo
+
+from .base import InkstitchExtension
+from ..i18n import _
+from ..elements import SatinColumn
+from ..utils import get_bundled_dir, cache
+from ..svg.tags import SVG_DEFS_TAG, SVG_GROUP_TAG, SVG_USE_TAG, SVG_PATH_TAG, INKSCAPE_GROUPMODE, XLINK_HREF, CONNECTION_START, CONNECTION_END, CONNECTOR_TYPE
+from ..svg import get_node_transform
+
+
+class Commands(InkstitchExtension):
+ COMMANDS = ["fill_start", "fill_end", "stop", "trim"]
+
+ def __init__(self, *args, **kwargs):
+ InkstitchExtension.__init__(self, *args, **kwargs)
+ for command in self.COMMANDS:
+ self.OptionParser.add_option("--%s" % command, type="inkbool")
+
+ @property
+ def symbols_path(self):
+ return os.path.join(get_bundled_dir("symbols"), "inkstitch.svg")
+
+ @property
+ @cache
+ def symbols_svg(self):
+ with open(self.symbols_path) as symbols_file:
+ return inkex.etree.parse(symbols_file)
+
+ @property
+ @cache
+ def symbol_defs(self):
+ return self.symbols_svg.find(SVG_DEFS_TAG)
+
+ @property
+ @cache
+ def defs(self):
+ return self.document.find(SVG_DEFS_TAG)
+
+ def ensure_symbol(self, command):
+ path = "./*[@id='inkstitch_%s']" % command
+ if self.defs.find(path) is None:
+ self.defs.append(deepcopy(self.symbol_defs.find(path)))
+
+ def get_correction_transform(self, node):
+ # if we want to place our new nodes in the same group as this node,
+ # then we'll need to factor in the effects of any transforms set on
+ # the parents of this node.
+
+ # we can ignore the transform on the node itself since it won't apply
+ # to the objects we add
+ transform = get_node_transform(node.getparent())
+
+ # now invert it, so that we can position our objects in absolute
+ # coordinates
+ transform = simpletransform.invertTransform(transform)
+
+ return simpletransform.formatTransform(transform)
+
+ def add_connector(self, symbol, element):
+ # I'd like it if I could position the connector endpoint nicely but inkscape just
+ # moves it to the element's center immediately after the extension runs.
+ start_pos = (symbol.get('x'), symbol.get('y'))
+ end_pos = element.shape.centroid
+
+ path = inkex.etree.Element(SVG_PATH_TAG,
+ {
+ "id": self.uniqueId("connector"),
+ "d": "M %s,%s %s,%s" % (start_pos[0], start_pos[1], end_pos.x, end_pos.y),
+ "style": "stroke:#000000;stroke-width:1px;stroke-opacity:0.5;fill:none;",
+ "transform": self.get_correction_transform(symbol),
+ CONNECTION_START: "#%s" % symbol.get('id'),
+ CONNECTION_END: "#%s" % element.node.get('id'),
+ CONNECTOR_TYPE: "polyline",
+ }
+ )
+
+ symbol.getparent().insert(symbol.getparent().index(symbol), path)
+
+ def get_command_pos(self, element, index, total):
+ # Put command symbols 30 pixels out from the shape, spaced evenly around it.
+
+ # get a line running 30 pixels out from the shape
+ outline = element.shape.buffer(30).exterior
+
+ # pick this item's spot arond the outline and perturb it a bit to avoid
+ # stacking up commands if they run the extension multiple times
+ position = index / float(total)
+ position += random() * 0.1
+
+ return outline.interpolate(position, normalized=True)
+
+ def remove_legacy_param(self, element, command):
+ if command == "trim" or command == "stop":
+ # If they had the old "TRIM after" or "STOP after" attributes set,
+ # automatically delete them. THe new commands will do the same
+ # thing.
+ #
+ # If we didn't delete these here, then things would get confusing.
+ # If the user were to delete a "trim" symbol added by this extension
+ # but the "embroider_trim_after" attribute is still set, then the
+ # trim would keep happening.
+
+ attribute = "embroider_%s_after" % command
+
+ if attribute in element.node.attrib:
+ del element.node.attrib[attribute]
+
+ def add_command(self, element, commands):
+ for i, command in enumerate(commands):
+ self.remove_legacy_param(element, command)
+
+ pos = self.get_command_pos(element, i, len(commands))
+
+ symbol = inkex.etree.SubElement(element.node.getparent(), SVG_USE_TAG,
+ {
+ "id": self.uniqueId("use"),
+ XLINK_HREF: "#inkstitch_%s" % command,
+ "height": "100%",
+ "width": "100%",
+ "x": str(pos.x),
+ "y": str(pos.y),
+ "transform": self.get_correction_transform(element.node)
+ }
+ )
+
+ self.add_connector(symbol, element)
+
+ def effect(self):
+ if not self.get_elements():
+ return
+
+ if not self.selected:
+ inkex.errormsg(_("Please select one or more objects to which to attach commands."))
+ return
+
+ self.svg = self.document.getroot()
+
+ commands = [command for command in self.COMMANDS if getattr(self.options, command)]
+
+ if not commands:
+ inkex.errormsg(_("Please choose one or more commands to attach."))
+ return
+
+ for command in commands:
+ self.ensure_symbol(command)
+
+ # Each object (node) in the SVG may correspond to multiple Elements of different
+ # types (e.g. stroke + fill). We only want to process each one once.
+ seen_nodes = set()
+
+ for element in self.elements:
+ if element.node not in seen_nodes:
+ self.add_command(element, commands)
+ seen_nodes.add(element.node)
diff --git a/lib/extensions/embroider.py b/lib/extensions/embroider.py
index a213be64..1e994e27 100644
--- a/lib/extensions/embroider.py
+++ b/lib/extensions/embroider.py
@@ -44,8 +44,7 @@ class Embroider(InkstitchExtension):
if self.options.output_file:
output_path = os.path.join(self.options.path, self.options.output_file)
else:
- svg_filename = self.document.getroot().get(inkex.addNS('docname', 'sodipodi'), "embroidery.svg")
- csv_filename = svg_filename.replace('.svg', '.%s' % self.options.output_format)
+ csv_filename = '%s.%s' % (self.get_base_file_name(), self.options.output_format)
output_path = os.path.join(self.options.path, csv_filename)
def add_suffix(path, suffix):
diff --git a/lib/extensions/flip.py b/lib/extensions/flip.py
new file mode 100644
index 00000000..d8d78cb5
--- /dev/null
+++ b/lib/extensions/flip.py
@@ -0,0 +1,40 @@
+import sys
+import inkex
+import cubicsuperpath
+from shapely import geometry as shgeo
+
+from .base import InkstitchExtension
+from ..i18n import _
+from ..elements import SatinColumn
+
+class Flip(InkstitchExtension):
+ def subpath_to_linestring(self, subpath):
+ return shgeo.LineString()
+
+ def flip(self, satin):
+ csp = satin.path
+
+ if len(csp) > 1:
+ flattened = satin.flatten(csp)
+
+ # find the rails (the two longest paths) and swap them
+ indices = range(len(csp))
+ indices.sort(key=lambda i: shgeo.LineString(flattened[i]).length, reverse=True)
+
+ first = indices[0]
+ second = indices[1]
+ csp[first], csp[second] = csp[second], csp[first]
+
+ satin.node.set("d", cubicsuperpath.formatPath(csp))
+
+ def effect(self):
+ if not self.get_elements():
+ return
+
+ if not self.selected:
+ inkex.errormsg(_("Please select one or more satin columns to flip."))
+ return
+
+ for element in self.elements:
+ if isinstance(element, SatinColumn):
+ self.flip(element)
diff --git a/lib/extensions/input.py b/lib/extensions/input.py
index 251859c5..cb5ac452 100644
--- a/lib/extensions/input.py
+++ b/lib/extensions/input.py
@@ -3,49 +3,29 @@ from os.path import realpath, dirname, join as path_join
import sys
from inkex import etree
import inkex
-
-# help python find libembroidery when running in a local repo clone
-if getattr(sys, 'frozen', None) is None:
- sys.path.append(realpath(path_join(dirname(__file__), '..', '..')))
-
-from libembroidery import *
+import pyembroidery
from ..svg import PIXELS_PER_MM, render_stitch_plan
from ..svg.tags import INKSCAPE_LABEL
from ..i18n import _
-from ..stitch_plan import StitchPlan
+from ..stitch_plan import StitchPlan, ColorBlock
+from ..utils.io import save_stdout
class Input(object):
- def pattern_stitches(self, pattern):
- stitch_pointer = pattern.stitchList
- while stitch_pointer:
- yield stitch_pointer.stitch
- stitch_pointer = stitch_pointer.next
-
-
def affect(self, args):
embroidery_file = args[0]
- pattern = embPattern_create()
- embPattern_read(pattern, embroidery_file)
- embPattern_flipVertical(pattern)
+ pattern = pyembroidery.read(embroidery_file)
stitch_plan = StitchPlan()
color_block = None
- current_color = None
-
- for stitch in self.pattern_stitches(pattern):
- if stitch.color != current_color:
- thread = embThreadList_getAt(pattern.threadList, stitch.color)
- color = thread.color
- color_block = stitch_plan.new_color_block((color.r, color.g, color.b))
- current_color = stitch.color
- if not stitch.flags & END:
- color_block.add_stitch(stitch.xx * PIXELS_PER_MM, stitch.yy * PIXELS_PER_MM,
- jump=stitch.flags & JUMP,
- color_change=stitch.flags & STOP,
- trim=stitch.flags & TRIM)
+ for raw_stitches, thread in pattern.get_as_colorblocks():
+ color_block = stitch_plan.new_color_block(thread)
+ for x, y, command in raw_stitches:
+ color_block.add_stitch(x * PIXELS_PER_MM / 10.0, y * PIXELS_PER_MM / 10.0,
+ jump=(command == pyembroidery.JUMP),
+ trim=(command == pyembroidery.TRIM))
extents = stitch_plan.extents
svg = etree.Element("svg", nsmap=inkex.NSS, attrib=
diff --git a/lib/extensions/palettes.py b/lib/extensions/install.py
index f7a6c7a5..42a92113 100644
--- a/lib/extensions/palettes.py
+++ b/lib/extensions/install.py
@@ -1,3 +1,5 @@
+# -*- coding: UTF-8 -*-
+
import sys
import traceback
import os
@@ -11,29 +13,29 @@ import logging
import wx
import inkex
-from ..utils import guess_inkscape_config_path
+from ..utils import guess_inkscape_config_path, get_bundled_dir
-class InstallPalettesFrame(wx.Frame):
+class InstallerFrame(wx.Frame):
def __init__(self, *args, **kwargs):
wx.Frame.__init__(self, *args, **kwargs)
- default_path = os.path.join(guess_inkscape_config_path(), "palettes")
+ self.path = guess_inkscape_config_path()
panel = wx.Panel(self)
sizer = wx.BoxSizer(wx.VERTICAL)
- text = wx.StaticText(panel, label=_("Directory in which to install palettes:"))
- font = wx.Font(12, wx.DEFAULT, wx.NORMAL, wx.NORMAL)
- text.SetFont(font)
- sizer.Add(text, proportion=0, flag=wx.ALL|wx.EXPAND, border=10)
+ text_sizer = wx.BoxSizer(wx.HORIZONTAL)
- path_sizer = wx.BoxSizer(wx.HORIZONTAL)
- self.path_input = wx.TextCtrl(panel, wx.ID_ANY, value=default_path)
- path_sizer.Add(self.path_input, proportion=3, flag=wx.RIGHT|wx.EXPAND, border=20)
- chooser_button = wx.Button(panel, wx.ID_OPEN, _('Choose another directory...'))
- path_sizer.Add(chooser_button, proportion=1, flag=wx.EXPAND)
- sizer.Add(path_sizer, proportion=0, flag=wx.ALL|wx.EXPAND, border=10)
+ text = _('Ink/Stitch can install files ("add-ons") that make it easier to use Inkscape to create machine embroidery designs. These add-ons will be installed:') + \
+ "\n\n • " + _("thread manufacturer color palettes") + \
+ "\n • " + _("Ink/Stitch visual commands (Object -> Symbols...)")
+
+ static_text = wx.StaticText(panel, label=text)
+ font = wx.Font(12, wx.DEFAULT, wx.NORMAL, wx.NORMAL)
+ static_text.SetFont(font)
+ text_sizer.Add(static_text, proportion=0, flag=wx.ALL|wx.EXPAND, border=10)
+ sizer.Add(text_sizer, proportion=3, flag=wx.ALL|wx.EXPAND, border=0)
buttons_sizer = wx.BoxSizer(wx.HORIZONTAL)
install_button = wx.Button(panel, wx.ID_ANY, _("Install"))
@@ -41,15 +43,11 @@ class InstallPalettesFrame(wx.Frame):
buttons_sizer.Add(install_button, proportion=0, flag=wx.ALIGN_RIGHT|wx.ALL, border=5)
cancel_button = wx.Button(panel, wx.ID_CANCEL, _("Cancel"))
buttons_sizer.Add(cancel_button, proportion=0, flag=wx.ALIGN_RIGHT|wx.ALL, border=5)
- sizer.Add(buttons_sizer, proportion=0, flag=wx.ALIGN_RIGHT)
+ sizer.Add(buttons_sizer, proportion=1, flag=wx.ALIGN_RIGHT|wx.ALIGN_BOTTOM)
- outer_sizer = wx.BoxSizer(wx.HORIZONTAL)
- outer_sizer.Add(sizer, proportion=0, flag=wx.ALIGN_CENTER_VERTICAL)
-
- panel.SetSizer(outer_sizer)
+ panel.SetSizer(sizer)
panel.Layout()
- chooser_button.Bind(wx.EVT_BUTTON, self.chooser_button_clicked)
cancel_button.Bind(wx.EVT_BUTTON, self.cancel_button_clicked)
install_button.Bind(wx.EVT_BUTTON, self.install_button_clicked)
@@ -57,36 +55,31 @@ class InstallPalettesFrame(wx.Frame):
self.Destroy()
def chooser_button_clicked(self, event):
- dialog = wx.DirDialog(self, _("Choose Inkscape palettes directory"))
+ dialog = wx.DirDialog(self, _("Choose Inkscape directory"))
if dialog.ShowModal() != wx.ID_CANCEL:
self.path_input.SetValue(dialog.GetPath())
def install_button_clicked(self, event):
try:
- self.install_palettes()
+ self.install_addons('palettes')
+ self.install_addons('symbols')
except Exception, e:
wx.MessageDialog(self,
- _('Thread palette installation failed') + ': \n' + traceback.format_exc(),
+ _('Inkscape add-on installation failed') + ': \n' + traceback.format_exc(),
_('Installation Failed'),
wx.OK).ShowModal()
else:
wx.MessageDialog(self,
- _('Thread palette files have been installed. Please restart Inkscape to load the new palettes.'),
+ _('Inkscape add-on files have been installed. Please restart Inkscape to load the new add-ons.'),
_('Installation Completed'),
wx.OK).ShowModal()
self.Destroy()
- def install_palettes(self):
- path = self.path_input.GetValue()
- palettes_dir = self.get_bundled_palettes_dir()
- self.copy_files(glob(os.path.join(palettes_dir, "*")), path)
-
- def get_bundled_palettes_dir(self):
- if getattr(sys, 'frozen', None) is not None:
- return realpath(os.path.join(sys._MEIPASS, '..', 'palettes'))
- else:
- return os.path.join(dirname(realpath(__file__)), 'palettes')
+ def install_addons(self, type):
+ path = os.path.join(self.path, type)
+ src_dir = get_bundled_dir(type)
+ self.copy_files(glob(os.path.join(src_dir, "*")), path)
if (sys.platform == "win32"):
# If we try to just use shutil.copy it says the operation requires elevation.
@@ -104,9 +97,9 @@ class InstallPalettesFrame(wx.Frame):
for palette_file in files:
shutil.copy(palette_file, dest)
-class Palettes(inkex.Effect):
+class Install(inkex.Effect):
def effect(self):
app = wx.App()
- installer_frame = InstallPalettesFrame(None, title=_("Ink/Stitch Thread Palette Installer"), size=(450, 200))
+ installer_frame = InstallerFrame(None, title=_("Ink/Stitch Add-ons Installer"), size=(550, 250))
installer_frame.Show()
app.MainLoop()
diff --git a/lib/extensions/output.py b/lib/extensions/output.py
new file mode 100644
index 00000000..1dc8d19d
--- /dev/null
+++ b/lib/extensions/output.py
@@ -0,0 +1,48 @@
+import sys
+import traceback
+import os
+import inkex
+import tempfile
+
+from .base import InkstitchExtension
+from ..i18n import _
+from ..output import write_embroidery_file
+from ..stitch_plan import patches_to_stitch_plan
+from ..svg import render_stitch_plan, PIXELS_PER_MM
+from ..utils.io import save_stdout
+
+class Output(InkstitchExtension):
+ def __init__(self, *args, **kwargs):
+ InkstitchExtension.__init__(self)
+ self.OptionParser.add_option("-c", "--collapse_len_mm",
+ action="store", type="float",
+ dest="collapse_length_mm", default=3.0,
+ help="max collapse length (mm)")
+ self.OptionParser.add_option("-f", "--format",
+ dest="file_extension",
+ help="file extension to output (example: DST)")
+
+ def effect(self):
+ if not self.get_elements():
+ return
+
+ patches = self.elements_to_patches(self.elements)
+ stitch_plan = patches_to_stitch_plan(patches, self.options.collapse_length_mm * PIXELS_PER_MM)
+
+ temp_file = tempfile.NamedTemporaryFile(suffix=".%s" % self.options.file_extension, delete=False)
+
+ # in windows, failure to close here will keep the file locked
+ temp_file.close()
+
+ write_embroidery_file(temp_file.name, stitch_plan, self.document.getroot())
+
+ # inkscape will read the file contents from stdout and copy
+ # to the destination file that the user chose
+ with open(temp_file.name) as output_file:
+ sys.stdout.write(output_file.read())
+
+ # clean up the temp file
+ os.remove(temp_file.name)
+
+ # don't let inkex output the SVG!
+ sys.exit(0)
diff --git a/lib/extensions/params.py b/lib/extensions/params.py
index 03a6f3cc..58fedd6b 100644
--- a/lib/extensions/params.py
+++ b/lib/extensions/params.py
@@ -19,6 +19,7 @@ from ..stitch_plan import patches_to_stitch_plan
from ..elements import EmbroideryElement, Fill, AutoFill, Stroke, SatinColumn
from ..utils import save_stderr, restore_stderr
from ..simulator import EmbroiderySimulator
+from ..commands import is_command
def presets_path():
@@ -354,6 +355,9 @@ class SettingsFrame(wx.Frame):
self.simulate_thread = None
self.simulate_refresh_needed = Event()
+ # used when closing to avoid having the window reopen at the last second
+ self.disable_simulate_window = False
+
wx.CallLater(1000, self.update_simulator)
self.presets_box = wx.StaticBox(self, wx.ID_ANY, label=_("Presets"))
@@ -392,6 +396,9 @@ class SettingsFrame(wx.Frame):
self.simulate_window.stop()
self.simulate_window.clear()
+ if self.disable_simulate_window:
+ return
+
if not self.simulate_thread or not self.simulate_thread.is_alive():
self.simulate_thread = Thread(target=self.simulate_worker)
self.simulate_thread.daemon = True
@@ -586,6 +593,7 @@ class SettingsFrame(wx.Frame):
self.close()
def use_last(self, event):
+ self.disable_simulate_window = True
self._load_preset("__LAST__")
self.apply(event)
@@ -632,6 +640,9 @@ class SettingsFrame(wx.Frame):
self.Layout()
# end wxGlade
+class NoValidObjects(Exception):
+ pass
+
class Params(InkstitchExtension):
def __init__(self, *args, **kwargs):
self.cancelled = False
@@ -645,7 +656,7 @@ class Params(InkstitchExtension):
classes.append(AutoFill)
classes.append(Fill)
- if element.get_style("stroke"):
+ if element.get_style("stroke") and not is_command(node):
classes.append(Stroke)
if element.get_style("stroke-dasharray") is None:
@@ -689,6 +700,11 @@ class Params(InkstitchExtension):
def create_tabs(self, parent):
tabs = []
+ nodes_by_class = self.get_nodes_by_class()
+
+ if not nodes_by_class:
+ raise NoValidObjects()
+
for cls, nodes in self.get_nodes_by_class():
params = cls.get_params()
@@ -745,12 +761,15 @@ class Params(InkstitchExtension):
self.cancelled = True
def effect(self):
- app = wx.App()
- frame = SettingsFrame(tabs_factory=self.create_tabs, on_cancel=self.cancel)
- frame.Show()
- app.MainLoop()
-
- if self.cancelled:
- # This prevents the superclass from outputting the SVG, because we
- # may have modified the DOM.
- sys.exit(0)
+ try:
+ app = wx.App()
+ frame = SettingsFrame(tabs_factory=self.create_tabs, on_cancel=self.cancel)
+ frame.Show()
+ app.MainLoop()
+
+ if self.cancelled:
+ # This prevents the superclass from outputting the SVG, because we
+ # may have modified the DOM.
+ sys.exit(0)
+ except NoValidObjects:
+ self.no_elements_error()
diff --git a/lib/extensions/print_pdf.py b/lib/extensions/print_pdf.py
index baeb7eba..6e2eff58 100644
--- a/lib/extensions/print_pdf.py
+++ b/lib/extensions/print_pdf.py
@@ -21,7 +21,7 @@ import requests
from .base import InkstitchExtension
from ..i18n import _, translation as inkstitch_translation
from ..svg import PIXELS_PER_MM, render_stitch_plan
-from ..svg.tags import SVG_GROUP_TAG
+from ..svg.tags import SVG_GROUP_TAG, INKSCAPE_GROUPMODE
from ..stitch_plan import patches_to_stitch_plan
from ..threads import ThreadCatalog
@@ -94,6 +94,8 @@ class PrintPreviewServer(Thread):
self.html = kwargs.pop('html')
self.metadata = kwargs.pop('metadata')
self.stitch_plan = kwargs.pop('stitch_plan')
+ self.realistic_overview_svg = kwargs.pop('realistic_overview_svg')
+ self.realistic_color_block_svgs = kwargs.pop('realistic_color_block_svgs')
Thread.__init__(self, *args, **kwargs)
self.daemon = True
self.last_request_time = None
@@ -202,6 +204,14 @@ class PrintPreviewServer(Thread):
return jsonify(threads)
+ @self.app.route('/realistic/block<int:index>', methods=['GET'])
+ def get_realistic_block(index):
+ return Response(self.realistic_color_block_svgs[index], mimetype='image/svg+xml')
+
+ @self.app.route('/realistic/overview', methods=['GET'])
+ def get_realistic_overview():
+ return Response(self.realistic_overview_svg, mimetype='image/svg+xml')
+
def stop(self):
# for whatever reason, shutting down only seems possible in
# the context of a flask request, so we'll just make one
@@ -295,38 +305,24 @@ class Print(InkstitchExtension):
return env
- def strip_namespaces(self):
+ def strip_namespaces(self, svg):
# namespace prefixes seem to trip up HTML, so get rid of them
- for element in self.document.iter():
+ for element in svg.iter():
if element.tag[0]=='{':
element.tag = element.tag[element.tag.index('}',1) + 1:]
- def effect(self):
- # It doesn't really make sense to print just a couple of selected
- # objects. It's almost certain they meant to print the whole design.
- # If they really wanted to print just a few objects, they could set
- # the rest invisible temporarily.
- self.selected = {}
+ def render_svgs(self, stitch_plan, realistic=False):
+ svg = deepcopy(self.document).getroot()
+ render_stitch_plan(svg, stitch_plan, realistic)
- if not self.get_elements():
- return
-
- self.hide_all_layers()
-
- patches = self.elements_to_patches(self.elements)
- stitch_plan = patches_to_stitch_plan(patches)
- palette = ThreadCatalog().match_and_apply_palette(stitch_plan, self.get_inkstitch_metadata()['thread-palette'])
- render_stitch_plan(self.document.getroot(), stitch_plan)
-
- self.strip_namespaces()
+ self.strip_namespaces(svg)
# Now the stitch plan layer will contain a set of groups, each
# corresponding to a color block. We'll create a set of SVG files
# corresponding to each individual color block and a final one
# for all color blocks together.
- svg = self.document.getroot()
- layers = svg.findall("./g[@{http://www.inkscape.org/namespaces/inkscape}groupmode='layer']")
+ layers = svg.findall("./g[@%s='layer']" % INKSCAPE_GROUPMODE)
stitch_plan_layer = svg.find(".//*[@id='__inkstitch_stitch_plan__']")
# First, delete all of the other layers. We don't need them and they'll
@@ -335,9 +331,9 @@ class Print(InkstitchExtension):
if layer is not stitch_plan_layer:
svg.remove(layer)
- overview_svg = inkex.etree.tostring(self.document)
-
+ overview_svg = inkex.etree.tostring(svg)
color_block_groups = stitch_plan_layer.getchildren()
+ color_block_svgs = []
for i, group in enumerate(color_block_groups):
# clear the stitch plan layer
@@ -347,12 +343,15 @@ class Print(InkstitchExtension):
stitch_plan_layer.append(group)
# save an SVG preview
- stitch_plan.color_blocks[i].svg_preview = inkex.etree.tostring(self.document)
+ color_block_svgs.append(inkex.etree.tostring(svg))
+ return overview_svg, color_block_svgs
+
+ def render_html(self, stitch_plan, overview_svg, selected_palette):
env = self.build_environment()
template = env.get_template('index.html')
- html = template.render(
+ return template.render(
view = {'client_overview': False, 'client_detailedview': False, 'operator_overview': True, 'operator_detailedview': True},
logo = {'src' : '', 'title' : 'LOGO'},
date = date.today(),
@@ -371,14 +370,38 @@ class Print(InkstitchExtension):
svg_overview = overview_svg,
color_blocks = stitch_plan.color_blocks,
palettes = ThreadCatalog().palette_names(),
- selected_palette = palette,
+ selected_palette = selected_palette,
)
- # We've totally mucked with the SVG. Restore it so that we can save
- # metadata into it.
- self.document = deepcopy(self.original_document)
+ def effect(self):
+ # It doesn't really make sense to print just a couple of selected
+ # objects. It's almost certain they meant to print the whole design.
+ # If they really wanted to print just a few objects, they could set
+ # the rest invisible temporarily.
+ self.selected = {}
+
+ if not self.get_elements():
+ return
+
+ patches = self.elements_to_patches(self.elements)
+ stitch_plan = patches_to_stitch_plan(patches)
+ palette = ThreadCatalog().match_and_apply_palette(stitch_plan, self.get_inkstitch_metadata()['thread-palette'])
+
+ overview_svg, color_block_svgs = self.render_svgs(stitch_plan, realistic=False)
+ realistic_overview_svg, realistic_color_block_svgs = self.render_svgs(stitch_plan, realistic=True)
+
+ for i, svg in enumerate(color_block_svgs):
+ stitch_plan.color_blocks[i].svg_preview = svg
+
+ html = self.render_html(stitch_plan, overview_svg, palette)
- print_server = PrintPreviewServer(html=html, metadata=self.get_inkstitch_metadata(), stitch_plan=stitch_plan)
+ print_server = PrintPreviewServer(
+ html=html,
+ metadata=self.get_inkstitch_metadata(),
+ stitch_plan=stitch_plan,
+ realistic_overview_svg=realistic_overview_svg,
+ realistic_color_block_svgs=realistic_color_block_svgs
+ )
print_server.start()
time.sleep(1)
diff --git a/lib/extensions/zip.py b/lib/extensions/zip.py
new file mode 100644
index 00000000..02f29e8a
--- /dev/null
+++ b/lib/extensions/zip.py
@@ -0,0 +1,74 @@
+import sys
+import traceback
+import os
+import inkex
+import tempfile
+from zipfile import ZipFile
+import pyembroidery
+
+from .base import InkstitchExtension
+from ..i18n import _
+from ..output import write_embroidery_file
+from ..stitch_plan import patches_to_stitch_plan
+from ..svg import render_stitch_plan, PIXELS_PER_MM
+from ..utils.io import save_stdout
+
+
+class Zip(InkstitchExtension):
+ def __init__(self, *args, **kwargs):
+ InkstitchExtension.__init__(self)
+ self.OptionParser.add_option("-c", "--collapse_len_mm",
+ action="store", type="float",
+ dest="collapse_length_mm", default=3.0,
+ help="max collapse length (mm)")
+
+ # it's kind of obnoxious that I have to do this...
+ self.formats = []
+ for format in pyembroidery.supported_formats():
+ if 'writer' in format and format['category'] == 'embroidery':
+ extension = format['extension']
+ self.OptionParser.add_option('--format-%s' % extension, type="inkbool", dest=extension)
+ self.formats.append(extension)
+
+ def effect(self):
+ if not self.get_elements():
+ return
+
+ patches = self.elements_to_patches(self.elements)
+ stitch_plan = patches_to_stitch_plan(patches, self.options.collapse_length_mm * PIXELS_PER_MM)
+
+ base_file_name = self.get_base_file_name()
+ path = tempfile.mkdtemp()
+
+ files = []
+
+ for format in self.formats:
+ if getattr(self.options, format):
+ output_file = os.path.join(path, "%s.%s" % (base_file_name, format))
+ write_embroidery_file(output_file, stitch_plan, self.document.getroot())
+ files.append(output_file)
+
+ if not files:
+ self.errormsg(_("No embroidery file formats selected."))
+
+ temp_file = tempfile.NamedTemporaryFile(suffix=".zip", delete=False)
+
+ # in windows, failure to close here will keep the file locked
+ temp_file.close()
+
+ with ZipFile(temp_file.name, "w") as zip_file:
+ for file in files:
+ zip_file.write(file, os.path.basename(file))
+
+ # inkscape will read the file contents from stdout and copy
+ # to the destination file that the user chose
+ with open(temp_file.name) as output_file:
+ sys.stdout.write(output_file.read())
+
+ os.remove(temp_file.name)
+ for file in files:
+ os.remove(file)
+ os.rmdir(path)
+
+ # don't let inkex output the SVG!
+ sys.exit(0)
diff --git a/lib/output.py b/lib/output.py
index 84128a25..0d7f9918 100644
--- a/lib/output.py
+++ b/lib/output.py
@@ -1,4 +1,4 @@
-import libembroidery
+import pyembroidery
import inkex
import simpletransform
import shapely.geometry as shgeo
@@ -7,36 +7,17 @@ from .utils import Point
from .svg import PIXELS_PER_MM, get_doc_size, get_viewbox_transform
-def make_thread(color):
- thread = libembroidery.EmbThread()
- thread.color = libembroidery.embColor_make(*color.rgb)
-
- thread.description = color.name
- 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
-
+def get_command(stitch):
if stitch.jump:
- flags |= libembroidery.JUMP
-
- if stitch.trim:
- flags |= libembroidery.TRIM
-
- if stitch.color_change:
- flags |= libembroidery.STOP
-
- return flags
-
+ return pyembroidery.JUMP
+ elif stitch.trim:
+ return pyembroidery.TRIM
+ elif stitch.color_change:
+ return pyembroidery.COLOR_CHANGE
+ elif stitch.stop:
+ return pyembroidery.STOP
+ else:
+ return pyembroidery.NEEDLE_AT
def _string_to_floats(string):
floats = string.split(',')
@@ -102,27 +83,32 @@ def get_origin(svg):
def write_embroidery_file(file_path, stitch_plan, svg):
origin = get_origin(svg)
- pattern = libembroidery.embPattern_create()
+ pattern = pyembroidery.EmbPattern()
for color_block in stitch_plan:
- add_thread(pattern, make_thread(color_block.color))
+ pattern.add_thread(color_block.color.pyembroidery_thread)
for stitch in color_block:
- if stitch.stop:
- # This is the start of the extra color block added by the
- # "STOP after" handler (see stitch_plan/stop.py). Assign it
- # the same color.
- add_thread(pattern, make_thread(color_block.color))
+ command = get_command(stitch)
+ pattern.add_stitch_absolute(command, stitch.x, stitch.y)
- flags = get_flags(stitch)
- libembroidery.embPattern_addStitchAbs(pattern, stitch.x - origin.x, stitch.y - origin.y, flags, 1)
-
- libembroidery.embPattern_addStitchAbs(pattern, stitch.x - origin.x, stitch.y - origin.y, libembroidery.END, 1)
+ pattern.add_stitch_absolute(pyembroidery.END, stitch.x, stitch.y)
# convert from pixels to millimeters
- libembroidery.embPattern_scale(pattern, 1/PIXELS_PER_MM)
+ # also multiply by 10 to get tenths of a millimeter as required by pyembroidery
+ scale = 10 / PIXELS_PER_MM
+
+ settings = {
+ # correct for the origin
+ "translate": -origin,
+
+ # convert from pixels to millimeters
+ # also multiply by 10 to get tenths of a millimeter as required by pyembroidery
+ "scale": (scale, scale),
- # SVG and embroidery disagree on the direction of the Y axis
- libembroidery.embPattern_flipVertical(pattern)
+ # This forces a jump at the start of the design and after each trim,
+ # even if we're close enough not to need one.
+ "full_jump": True,
+ }
- libembroidery.embPattern_write(pattern, file_path)
+ pyembroidery.write(pattern, file_path, settings)
diff --git a/lib/stitch_plan/stitch.py b/lib/stitch_plan/stitch.py
index 12642a60..5230efec 100644
--- a/lib/stitch_plan/stitch.py
+++ b/lib/stitch_plan/stitch.py
@@ -2,7 +2,7 @@ from ..utils.geometry import Point
class Stitch(Point):
- def __init__(self, x, y, color=None, jump=False, stop=False, trim=False, color_change=False, no_ties=False):
+ def __init__(self, x, y, color=None, jump=False, stop=False, trim=False, color_change=False, fake_color_change=False, no_ties=False):
self.x = x
self.y = y
self.color = color
@@ -10,10 +10,20 @@ class Stitch(Point):
self.trim = trim
self.stop = stop
self.color_change = color_change
+ self.fake_color_change = fake_color_change
self.no_ties = no_ties
def __repr__(self):
- return "Stitch(%s, %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 " ", "NO TIES" if self.no_ties else " ")
+ return "Stitch(%s, %s, %s, %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 " ",
+ "NO TIES" if self.no_ties else " ",
+ "FAKE " if self.fake_color_change else "",
+ "COLOR CHANGE" if self.color_change else " "
+ )
def copy(self):
return Stitch(self.x, self.y, self.color, self.jump, self.stop, self.trim, self.color_change, self.no_ties)
diff --git a/lib/stitch_plan/stitch_plan.py b/lib/stitch_plan/stitch_plan.py
index 93bcd195..682ea09f 100644
--- a/lib/stitch_plan/stitch_plan.py
+++ b/lib/stitch_plan/stitch_plan.py
@@ -1,6 +1,4 @@
from .stitch import Stitch
-from .stop import process_stop
-from .trim import process_trim
from .ties import add_ties
from ..svg import PIXELS_PER_MM
from ..utils.geometry import Point
@@ -16,62 +14,45 @@ def patches_to_stitch_plan(patches, collapse_len=3.0 * PIXELS_PER_MM):
"""
stitch_plan = StitchPlan()
- color_block = stitch_plan.new_color_block()
- need_trim = False
+ if not patches:
+ return stitch_plan
+
+ color_block = stitch_plan.new_color_block(color=patches[0].color)
+
for patch in patches:
if not patch.stitches:
continue
- if not color_block.has_color():
- # set the color for the first color block
- color_block.color = patch.color
-
- if color_block.color == patch.color:
- if need_trim:
- process_trim(color_block, patch.stitches[0])
- need_trim = False
-
- # add a jump stitch between patches if the distance is more
- # than the collapse length
- if color_block.last_stitch:
- if (patch.stitches[0] - color_block.last_stitch).length() > collapse_len:
- color_block.add_stitch(patch.stitches[0].x, patch.stitches[0].y, jump=True)
-
- else:
- # add a color change (only if we didn't just do a "STOP after")
- if not color_block.last_stitch.color_change:
- stitch = color_block.last_stitch.copy()
- stitch.color_change = True
- color_block.add_stitch(stitch)
+ if color_block.color != patch.color:
+ if len(color_block) == 0:
+ # We just processed a stop, which created a new color block.
+ # We'll just claim this new block as ours:
+ color_block.color = patch.color
+ else:
+ # end the previous block with a color change
+ color_block.add_stitch(color_change=True)
- color_block = stitch_plan.new_color_block()
- color_block.color = patch.color
+ # make a new block of our color
+ color_block = stitch_plan.new_color_block(color=patch.color)
- color_block.filter_duplicate_stitches()
color_block.add_stitches(patch.stitches, no_ties=patch.stitch_as_is)
if patch.trim_after:
- # a trim needs to be followed by a jump to the next stitch, so
- # we'll process it when we start the next patch
- need_trim = True
+ color_block.add_stitch(trim=True)
if patch.stop_after:
- process_stop(color_block)
-
- add_jumps(stitch_plan)
- add_ties(stitch_plan)
-
- return stitch_plan
+ color_block.add_stitch(stop=True)
+ color_block = stitch_plan.new_color_block(color_block.color)
+ if len(color_block) == 0:
+ # last block ended in a stop, so now we have an empty block
+ del stitch_plan.color_blocks[-1]
-def add_jumps(stitch_plan):
- """Add a JUMP stitch at the start of each color block."""
+ stitch_plan.filter_duplicate_stitches()
+ stitch_plan.add_ties()
- for color_block in stitch_plan:
- stitch = color_block.stitches[0].copy()
- stitch.jump = True
- color_block.stitches.insert(0, stitch)
+ return stitch_plan
class StitchPlan(object):
@@ -85,6 +66,17 @@ class StitchPlan(object):
self.color_blocks.append(color_block)
return color_block
+ def add_color_block(self, color_block):
+ self.color_blocks.append(color_block)
+
+ def filter_duplicate_stitches(self):
+ for color_block in self:
+ color_block.filter_duplicate_stitches()
+
+ def add_ties(self):
+ # see ties.py
+ add_ties(self)
+
def __iter__(self):
return iter(self.color_blocks)
@@ -100,8 +92,12 @@ class StitchPlan(object):
return len({block.color for block in self})
@property
+ def num_color_blocks(self):
+ return len(self.color_blocks)
+
+ @property
def num_stops(self):
- return sum(block.num_stops for block in self)
+ return sum(1 for block in self if block.stop_after)
@property
def num_trims(self):
@@ -137,6 +133,13 @@ class StitchPlan(object):
dimensions = self.dimensions
return (dimensions[0] / PIXELS_PER_MM, dimensions[1] / PIXELS_PER_MM)
+ @property
+ def last_color_block(self):
+ if self.color_blocks:
+ return self.color_blocks[-1]
+ else:
+ return None
+
class ColorBlock(object):
"""Holds a set of stitches, all with the same thread color."""
@@ -148,6 +151,9 @@ class ColorBlock(object):
def __iter__(self):
return iter(self.stitches)
+ def __len__(self):
+ return len(self.stitches)
+
def __repr__(self):
return "ColorBlock(%s, %s)" % (self.color, self.stitches)
@@ -180,20 +186,18 @@ class ColorBlock(object):
return len(self.stitches)
@property
- def num_stops(self):
- """Number of pauses in this color block."""
-
- # Stops are encoded using two STOP stitches each. See the comment in
- # stop.py for an explanation.
-
- return sum(1 for stitch in self if stitch.stop) / 2
-
- @property
def num_trims(self):
"""Number of trims in this color block."""
return sum(1 for stitch in self if stitch.trim)
+ @property
+ def stop_after(self):
+ if self.last_stitch is not None:
+ return self.last_stitch.stop
+ else:
+ return False
+
def filter_duplicate_stitches(self):
if not self.stitches:
return
@@ -201,12 +205,12 @@ class ColorBlock(object):
stitches = [self.stitches[0]]
for stitch in self.stitches[1:]:
- if stitches[-1].jump or stitch.stop or stitch.trim:
- # Don't consider jumps, stops, or trims as candidates for filtering
+ if stitches[-1].jump or stitch.stop or stitch.trim or stitch.color_change:
+ # Don't consider jumps, stops, color changes, or trims as candidates for filtering
pass
else:
l = (stitch - stitches[-1]).length()
- if l <= 0.1:
+ if l <= 0.1 * PIXELS_PER_MM:
# duplicate stitch, skip this one
continue
@@ -215,11 +219,21 @@ class ColorBlock(object):
self.stitches = stitches
def add_stitch(self, *args, **kwargs):
+ if not args:
+ # They're adding a command, e.g. `color_block.add_stitch(stop=True)``.
+ # Use the position from the last stitch.
+ if self.last_stitch:
+ args = (self.last_stitch.x, self.last_stitch.y)
+ else:
+ raise ValueError("internal error: can't add a command to an empty stitch block")
+
if isinstance(args[0], Stitch):
self.stitches.append(args[0])
elif isinstance(args[0], Point):
self.stitches.append(Stitch(args[0].x, args[0].y, *args[1:], **kwargs))
else:
+ if not args and self.last_stitch:
+ args = (self.last_stitch.x, self.last_stitch.y)
self.stitches.append(Stitch(*args, **kwargs))
def add_stitches(self, stitches, *args, **kwargs):
diff --git a/lib/stitch_plan/stop.py b/lib/stitch_plan/stop.py
deleted file mode 100644
index 81dec1da..00000000
--- a/lib/stitch_plan/stop.py
+++ /dev/null
@@ -1,43 +0,0 @@
-def process_stop(color_block):
- """Handle the "stop after" checkbox.
-
- The user wants the machine to pause after this patch. This can
- be useful for applique and similar on multi-needle machines that
- normally would not stop between colors.
-
- In machine embroidery files, there's no such thing as an actual
- "STOP" instruction. All that exists is a "color change" command
- (which libembroidery calls STOP just to be confusing).
-
- On multi-needle machines, the user assigns needles to the colors in
- the design before starting stitching. C01, C02, etc are normal
- needles, but C00 is special. For a block of stitches assigned
- to C00, the machine will continue sewing with the last color it
- had and pause after it completes the C00 block.
-
- That means we need to add an artificial color change instruction
- shortly before the current stitch so that the user can set that color
- block to C00. We'll go back 3 stitches and mark the start of the C00
- block:
- """
-
- if len(color_block.stitches) >= 3:
- # make a copy of the stitch and set it as a color change
- stitch = color_block.stitches[-3].copy()
- stitch.color_change = True
-
- # mark this stitch as a "stop" so that we can avoid
- # adding tie stitches in ties.py
- stitch.stop = True
-
- # insert it after the stitch
- color_block.stitches.insert(-2, stitch)
-
- # and also add a color change on this stitch, completing the C00
- # block:
-
- stitch = color_block.stitches[-1].copy()
- stitch.color_change = True
- color_block.add_stitch(stitch)
-
- # reference for the above: https://github.com/lexelby/inkstitch/pull/29#issuecomment-359175447
diff --git a/lib/stitch_plan/ties.py b/lib/stitch_plan/ties.py
index 6d07ac71..573469f5 100644
--- a/lib/stitch_plan/ties.py
+++ b/lib/stitch_plan/ties.py
@@ -30,15 +30,16 @@ def add_tie_in(stitches, upcoming_stitches):
def add_ties(stitch_plan):
"""Add tie-off before and after trims, jumps, and color changes."""
+ need_tie_in = True
for color_block in stitch_plan:
- need_tie_in = True
new_stitches = []
for i, stitch in enumerate(color_block.stitches):
- # Tie before and after TRIMs, JUMPs, and color changes, but ignore
- # the fake color change introduced by a "STOP after" (see stop.py).
- is_special = stitch.trim or stitch.jump or (stitch.color_change and not stitch.stop)
+ is_special = stitch.trim or stitch.jump or stitch.color_change or stitch.stop
- if is_special and not need_tie_in:
+ # see stop.py for an explanation of the fake color change
+ is_fake = stitch.fake_color_change
+
+ if is_special and not is_fake and not need_tie_in:
add_tie_off(new_stitches)
new_stitches.append(stitch)
need_tie_in = True
@@ -49,7 +50,8 @@ def add_ties(stitch_plan):
else:
new_stitches.append(stitch)
- if not need_tie_in:
- add_tie_off(new_stitches)
-
color_block.replace_stitches(new_stitches)
+
+ if not need_tie_in:
+ # tie off at the end if we haven't already
+ add_tie_off(color_block.stitches)
diff --git a/lib/stitch_plan/trim.py b/lib/stitch_plan/trim.py
deleted file mode 100644
index f692a179..00000000
--- a/lib/stitch_plan/trim.py
+++ /dev/null
@@ -1,23 +0,0 @@
-def process_trim(color_block, next_stitch):
- """Handle the "trim after" checkbox.
-
- DST (and maybe other formats?) has no actual TRIM instruction.
- Instead, 3 sequential JUMPs cause the machine to trim the thread.
-
- To support both DST and other formats, we'll add a TRIM and two
- JUMPs. The TRIM will be converted to a JUMP by libembroidery
- if saving to DST, resulting in the 3-jump sequence.
- """
-
- delta = next_stitch - color_block.last_stitch
- delta = delta * (1/4.0)
-
- pos = color_block.last_stitch
-
- for i in xrange(3):
- pos += delta
- color_block.add_stitch(pos.x, pos.y, jump=True)
-
- # first one should be TRIM instead of JUMP
- color_block.stitches[-3].jump = False
- color_block.stitches[-3].trim = True
diff --git a/lib/stitches/auto_fill.py b/lib/stitches/auto_fill.py
index 518a2812..6326ced2 100644
--- a/lib/stitches/auto_fill.py
+++ b/lib/stitches/auto_fill.py
@@ -2,7 +2,7 @@ import sys
import shapely
import networkx
import math
-from itertools import groupby
+from itertools import groupby, izip
from collections import deque
from .fill import intersect_region_with_grating, row_num, stitch_row
@@ -14,18 +14,38 @@ from ..utils.geometry import Point as InkstitchPoint
class MaxQueueLengthExceeded(Exception):
pass
+class PathEdge(object):
+ OUTLINE_KEYS = ("outline", "extra", "initial")
+ SEGMENT_KEY = "segment"
-def auto_fill(shape, angle, row_spacing, end_row_spacing, max_stitch_length, running_stitch_length, staggers, starting_point=None):
+ def __init__(self, nodes, key):
+ self.nodes = nodes
+ self._sorted_nodes = tuple(sorted(self.nodes))
+ self.key = key
+
+ def __getitem__(self, item):
+ return self.nodes[item]
+
+ def __hash__(self):
+ return hash((self._sorted_nodes, self.key))
+
+ def __eq__(self, other):
+ return self._sorted_nodes == other._sorted_nodes and self.key == other.key
+
+ def is_outline(self):
+ return self.key in self.OUTLINE_KEYS
+
+ def is_segment(self):
+ return self.key == self.SEGMENT_KEY
+
+def auto_fill(shape, angle, row_spacing, end_row_spacing, max_stitch_length, running_stitch_length, staggers, starting_point, ending_point=None):
stitches = []
rows_of_segments = intersect_region_with_grating(shape, angle, row_spacing, end_row_spacing)
segments = [segment for row in rows_of_segments for segment in row]
graph = build_graph(shape, segments, angle, row_spacing)
- path = find_stitch_path(graph, segments)
-
- if starting_point:
- stitches.extend(connect_points(shape, starting_point, path[0][0], running_stitch_length))
+ path = find_stitch_path(graph, segments, starting_point, ending_point)
stitches.extend(path_to_stitches(graph, path, shape, angle, row_spacing, max_stitch_length, running_stitch_length, staggers))
@@ -134,8 +154,6 @@ def build_graph(shape, segments, angle, row_spacing):
else:
edge_set = 1
- #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]])):
graph.add_edge(node1, node2, key="outline")
@@ -157,14 +175,20 @@ def node_list_to_edge_list(node_list):
def bfs_for_loop(graph, starting_node, max_queue_length=2000):
to_search = deque()
- to_search.appendleft(([starting_node], set(), 0))
+ to_search.append((None, set()))
while to_search:
if len(to_search) > max_queue_length:
raise MaxQueueLengthExceeded()
- path, visited_edges, visited_segments = to_search.pop()
- ending_node = path[-1]
+ path, visited_edges = to_search.pop()
+
+ if path is None:
+ # This is the very first time through the loop, so initialize.
+ path = []
+ ending_node = starting_node
+ else:
+ ending_node = path[-1][-1]
# get a list of neighbors paired with the key of the edge I can follow to get there
neighbors = [
@@ -178,26 +202,21 @@ def bfs_for_loop(graph, starting_node, max_queue_length=2000):
for next_node, key in neighbors:
# skip if I've already followed this edge
- edge = (tuple(sorted((ending_node, next_node))), key)
+ edge = PathEdge((ending_node, next_node), key)
if edge in visited_edges:
continue
- new_path = path + [next_node]
-
- if key == "segment":
- new_visited_segments = visited_segments + 1
- else:
- new_visited_segments = visited_segments
+ new_path = path + [edge]
if next_node == starting_node:
# ignore trivial loops (down and back a doubled edge)
if len(new_path) > 3:
- return node_list_to_edge_list(new_path), new_visited_segments
+ return new_path
new_visited_edges = visited_edges.copy()
new_visited_edges.add(edge)
- to_search.appendleft((new_path, new_visited_edges, new_visited_segments))
+ to_search.appendleft((new_path, new_visited_edges))
def find_loop(graph, starting_nodes):
@@ -216,14 +235,6 @@ def find_loop(graph, starting_nodes):
somewhere else.
"""
- #loop = self.simple_loop(graph, starting_nodes[-2])
-
- #if loop:
- # print >> sys.stderr, "simple_loop success"
- # starting_nodes.pop()
- # starting_nodes.pop()
- # return loop
-
loop = None
retry = []
max_queue_length = 2000
@@ -231,7 +242,6 @@ def find_loop(graph, starting_nodes):
while not loop:
while not loop and starting_nodes:
starting_node = starting_nodes.pop()
- #print >> sys.stderr, "find loop from", starting_node
try:
# Note: if bfs_for_loop() returns None, no loop can be
@@ -240,12 +250,7 @@ def find_loop(graph, starting_nodes):
# case we discard that node and try the next.
loop = bfs_for_loop(graph, starting_node, max_queue_length)
- #if not loop:
- #print >> dbg, "failed on", starting_node
- #dbg.flush()
except MaxQueueLengthExceeded:
- #print >> dbg, "gave up on", starting_node
- #dbg.flush()
# We're giving up on this node for now. We could try
# this node again later, so add it to the bottm of the
# stack.
@@ -272,7 +277,7 @@ def insert_loop(path, loop):
start and end point. The points will be specified in order, such
that they will look like this:
- ((p1, p2), (p2, p3), (p3, p4) ... (pn, p1))
+ ((p1, p2), (p2, p3), (p3, p4), ...)
path will be modified in place.
"""
@@ -282,11 +287,59 @@ def insert_loop(path, loop):
for i, (start, end) in enumerate(path):
if start == loop_start:
break
+ else:
+ # if we didn't find the start of the loop in the list at all, it must
+ # be the endpoint of the last segment
+ i += 1
path[i:i] = loop
+def nearest_node_on_outline(graph, point, outline_index=0):
+ point = shapely.geometry.Point(*point)
+ outline_nodes = [node for node, data in graph.nodes(data=True) if data['index'] == outline_index]
+ nearest = min(outline_nodes, key=lambda node: shapely.geometry.Point(*node).distance(point))
-def find_stitch_path(graph, segments):
+ return nearest
+
+def get_outline_nodes(graph, outline_index=0):
+ outline_nodes = [(node, data['projection']) \
+ for node, data \
+ in graph.nodes(data=True) \
+ if data['index'] == outline_index]
+ outline_nodes.sort(key=lambda (node, projection): projection)
+ outline_nodes = [node for node, data in outline_nodes]
+
+ return outline_nodes
+
+def find_initial_path(graph, starting_point, ending_point=None):
+ starting_node = nearest_node_on_outline(graph, starting_point)
+
+ if ending_point is None:
+ # If they didn't give an ending point, pick either neighboring node
+ # along the outline -- doesn't matter which. We do this because
+ # the algorithm requires we start with _some_ path.
+ neighbors = [n for n, keys in graph.adj[starting_node].iteritems() if 'outline' in keys]
+ return [PathEdge((starting_node, neighbors[0]), "initial")]
+ else:
+ ending_node = nearest_node_on_outline(graph, ending_point)
+ outline_nodes = get_outline_nodes(graph)
+
+ # Multiply the outline_nodes list by 2 (duplicate it) because
+ # the ending_node may occur first.
+ outline_nodes *= 2
+ start_index = outline_nodes.index(starting_node)
+ end_index = outline_nodes.index(ending_node, start_index)
+ nodes = outline_nodes[start_index:end_index + 1]
+
+ # we have a series of sequential points, but we need to
+ # turn it into an edge list
+ path = []
+ for start, end in izip(nodes[:-1], nodes[1:]):
+ path.append(PathEdge((start, end), "initial"))
+
+ return path
+
+def find_stitch_path(graph, segments, starting_point=None, ending_point=None):
"""find a path that visits every grating segment exactly once
Theoretically, we just need to find an Eulerian Path in the graph.
@@ -294,13 +347,14 @@ def find_stitch_path(graph, segments):
The edges on the outline of the region are only there to help us get
from one grating segment to the next.
- We'll build a "cycle" (a path that ends where it starts) using
- Hierholzer's algorithm. We'll stop once we've visited every grating
- segment.
+ We'll build a Eulerian Path using Hierholzer's algorithm. A true
+ Eulerian Path would visit every single edge (including all the extras
+ we inserted in build_graph()),but we'll stop short once we've visited
+ every grating segment since that's all we really care about.
Hierholzer's algorithm says to select an arbitrary starting node at
each step. In order to produce a reasonable stitch path, we'll select
- the vertex carefully such that we get back-and-forth traversal like
+ the starting node carefully such that we get back-and-forth traversal like
mowing a lawn.
To do this, we'll use a simple heuristic: try to start from nodes in
@@ -313,40 +367,42 @@ def find_stitch_path(graph, segments):
segments_visited = 0
nodes_visited = deque()
- # start with a simple loop: down one segment and then back along the
- # outer border to the starting point.
- path = [segments[0], list(reversed(segments[0]))]
+ if starting_point is None:
+ starting_point = segments[0][0]
+
+ path = find_initial_path(graph, starting_point, ending_point)
- graph.remove_edges_from(path)
+ # Our graph is Eulerian: every node has an even degree. An Eulerian graph
+ # must have an Eulerian Circuit which visits every edge and ends where it
+ # starts.
+ #
+ # However, we're starting with a path and _not_ removing the edges of that
+ # path from the graph. By doing this, we're implicitly adding those edges
+ # to the graph, after which the starting and ending point (and only those
+ # two) will now have odd degree. A graph that's Eulerian except for two
+ # nodes must have an Eulerian Path that starts and ends at those two nodes.
+ # That's how we force the starting and ending point.
- segments_visited += 1
- nodes_visited.extend(segments[0])
+ nodes_visited.append(path[0][0])
while segments_visited < num_segments:
- result = find_loop(graph, nodes_visited)
+ loop = find_loop(graph, nodes_visited)
- if not result:
+ if not loop:
print >> sys.stderr, _("Unexpected error while generating fill stitches. Please send your SVG file to lexelby@github.")
break
- loop, segments = result
-
- #print >> dbg, "found loop:", loop
- #dbg.flush()
-
- segments_visited += segments
- nodes_visited += [edge[0] for edge in loop]
+ segments_visited += sum(1 for edge in loop if edge.is_segment())
+ nodes_visited.extend(edge[0] for edge in loop)
graph.remove_edges_from(loop)
insert_loop(path, loop)
- #if segments_visited >= 12:
- # break
-
- # Now we have a loop that covers every grating segment. It returns to
- # where it started, which is unnecessary, so we'll snip the last bit off.
- #while original_graph.has_edge(*path[-1], key="outline"):
- # path.pop()
+ if ending_point is None:
+ # If they didn't specify an ending point, then the end of the path travels
+ # around the outline back to the start (see find_initial_path()). This
+ # isn't necessary, so remove it.
+ trim_end(path)
return path
@@ -363,10 +419,10 @@ def collapse_sequential_outline_edges(graph, path):
new_path = []
for edge in path:
- if graph.has_edge(*edge, key="segment"):
+ if edge.is_segment():
if start_of_run:
# close off the last run
- new_path.append((start_of_run, edge[0]))
+ new_path.append(PathEdge((start_of_run, edge[0]), "collapsed"))
start_of_run = None
new_path.append(edge)
@@ -376,7 +432,7 @@ def collapse_sequential_outline_edges(graph, path):
if start_of_run:
# if we were still in a run, close it off
- new_path.append((start_of_run, edge[1]))
+ new_path.append(PathEdge((start_of_run, edge[1]), "collapsed"))
return new_path
@@ -416,9 +472,6 @@ def connect_points(shape, start, end, running_stitch_length):
direction = math.copysign(1.0, distance)
one_stitch = running_stitch_length * direction
- #print >> dbg, "connect_points:", outline_index, start, end, distance, stitches, direction
- #dbg.flush()
-
stitches = [InkstitchPoint(*outline.interpolate(pos).coords[0])]
for i in xrange(num_stitches):
@@ -430,11 +483,11 @@ def connect_points(shape, start, end, running_stitch_length):
if (end - stitches[-1]).length() > 0.1 * PIXELS_PER_MM:
stitches.append(end)
- #print >> dbg, "end connect_points"
- #dbg.flush()
-
return stitches
+def trim_end(path):
+ while path and path[-1].is_outline():
+ path.pop()
def path_to_stitches(graph, path, shape, angle, row_spacing, max_stitch_length, running_stitch_length, staggers):
path = collapse_sequential_outline_edges(graph, path)
@@ -442,7 +495,7 @@ def path_to_stitches(graph, path, shape, angle, row_spacing, max_stitch_length,
stitches = []
for edge in path:
- if graph.has_edge(*edge, key="segment"):
+ if edge.is_segment():
stitch_row(stitches, edge[0], edge[1], angle, row_spacing, max_stitch_length, staggers)
else:
stitches.extend(connect_points(shape, edge[0], edge[1], running_stitch_length))
diff --git a/lib/svg/__init__.py b/lib/svg/__init__.py
index 1895bba4..8e846555 100644
--- a/lib/svg/__init__.py
+++ b/lib/svg/__init__.py
@@ -1,2 +1,3 @@
from .svg import color_block_to_point_lists, render_stitch_plan
from .units import *
+from .path import apply_transforms, get_node_transform
diff --git a/lib/svg/path.py b/lib/svg/path.py
new file mode 100644
index 00000000..2d9c0ff3
--- /dev/null
+++ b/lib/svg/path.py
@@ -0,0 +1,25 @@
+import simpletransform
+import cubicsuperpath
+
+from .units import get_viewbox_transform
+
+def apply_transforms(path, node):
+ transform = get_node_transform(node)
+
+ # apply the combined transform to this node's path
+ simpletransform.applyTransformToPath(transform, path)
+
+ return path
+
+def get_node_transform(node):
+ # 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)
+
+ # add in the transform implied by the viewBox
+ viewbox_transform = get_viewbox_transform(node.getroottree().getroot())
+ transform = simpletransform.composeTransform(viewbox_transform, transform)
+
+ return transform
diff --git a/lib/svg/realistic_rendering.py b/lib/svg/realistic_rendering.py
new file mode 100644
index 00000000..e31534da
--- /dev/null
+++ b/lib/svg/realistic_rendering.py
@@ -0,0 +1,129 @@
+import simplepath
+import math
+
+from .units import PIXELS_PER_MM
+from ..utils import cache, Point
+
+# The stitch vector path looks like this:
+# _______
+# (_______)
+#
+# It's 0.32mm high, which is the approximate thickness of common machine
+# embroidery threads.
+
+# 1.216 pixels = 0.32mm
+stitch_height = 1.216
+
+# This vector path starts at the upper right corner of the stitch shape and
+# proceeds counter-clockwise.and contains a placeholder (%s) for the stitch
+# length.
+#
+# It contains two invisible "whiskers" of zero width that go above and below
+# to ensure that the SVG renderer allocates a large enough canvas area when
+# computing the gaussian blur steps. Otherwise, we'd have to expand the
+# width and height attributes of the <filter> tag to add more buffer space.
+# The width and height are specified in multiples of the bounding box
+# size, It's the bounding box aligned with the global SVG canvas's axes, not
+# the axes of the stitch itself. That means that having a big enough value
+# to add enough padding on the long sides of the stitch would waste a ton
+# of space on the short sides and significantly slow down rendering.
+stitch_path = "M0,0c0.4,0,0.4,0.3,0.4,0.6c0,0.3,-0.1,0.6,-0.4,0.6v0.2,-0.2h-%sc-0.4,0,-0.4,-0.3,-0.4,-0.6c0,-0.3,0.1,-0.6,0.4,-0.6v-0.2,0.2z"
+
+# This filter makes the above stitch path look like a real stitch with lighting.
+realistic_filter = """
+ <filter
+ style="color-interpolation-filters:sRGB"
+ id="realistic-stitch-filter"
+ x="-0.1"
+ width="1.2"
+ y="-0.1"
+ height="1.2">
+ <feGaussianBlur
+ stdDeviation="1.5"
+ id="feGaussianBlur1542-6"
+ in="SourceAlpha" />
+ <feComponentTransfer
+ id="feComponentTransfer1544-7"
+ result="result1">
+ <feFuncR
+ id="feFuncR1546-5"
+ type="identity" />
+ <feFuncG
+ id="feFuncG1548-3"
+ type="identity" />
+ <feFuncB
+ id="feFuncB1550-5"
+ type="identity"
+ slope="4.5300000000000002" />
+ <feFuncA
+ id="feFuncA1552-6"
+ type="gamma"
+ slope="0.14999999999999999"
+ intercept="0"
+ amplitude="3.1299999999999999"
+ offset="-0.33000000000000002" />
+ </feComponentTransfer>
+ <feComposite
+ in2="SourceAlpha"
+ id="feComposite1558-2"
+ operator="in" />
+ <feGaussianBlur
+ stdDeviation="0.089999999999999997"
+ id="feGaussianBlur1969" />
+ <feMorphology
+ id="feMorphology1971"
+ operator="dilate"
+ radius="0.10000000000000001" />
+ <feSpecularLighting
+ id="feSpecularLighting1973"
+ result="result2"
+ specularConstant="0.70899999"
+ surfaceScale="30">
+ <fePointLight
+ id="fePointLight1975"
+ z="10" />
+ </feSpecularLighting>
+ <feGaussianBlur
+ stdDeviation="0.040000000000000001"
+ id="feGaussianBlur1979" />
+ <feComposite
+ in2="SourceGraphic"
+ id="feComposite1977"
+ operator="arithmetic"
+ k2="1"
+ k3="1"
+ result="result3"
+ k1="0"
+ k4="0" />
+ <feComposite
+ in2="SourceAlpha"
+ id="feComposite1981"
+ operator="in" />
+ </filter>
+"""
+
+def realistic_stitch(start, end):
+ """Generate a stitch vector path given a start and end point."""
+
+ end = Point(*end)
+ start = Point(*start)
+
+ stitch_length = (end - start).length()
+ stitch_center = (end + start) / 2.0
+ stitch_direction = (end - start)
+ stitch_angle = math.atan2(stitch_direction.y, stitch_direction.x)
+
+ stitch_length = max(0, stitch_length - 0.2 * PIXELS_PER_MM)
+
+ # create the path by filling in the length in the template
+ path = simplepath.parsePath(stitch_path % stitch_length)
+
+ # rotate the path to match the stitch
+ rotation_center_x = -stitch_length / 2.0
+ rotation_center_y = stitch_height / 2.0
+ simplepath.rotatePath(path, stitch_angle, cx=rotation_center_x, cy=rotation_center_y)
+
+ # move the path to the location of the stitch
+ simplepath.translatePath(path, stitch_center.x - rotation_center_x, stitch_center.y - rotation_center_y)
+
+ return simplepath.formatPath(path)
diff --git a/lib/svg/svg.py b/lib/svg/svg.py
index 852215f2..48b1343a 100644
--- a/lib/svg/svg.py
+++ b/lib/svg/svg.py
@@ -1,7 +1,8 @@
import simpletransform, simplestyle, inkex
from .units import get_viewbox_transform
-from .tags import SVG_GROUP_TAG, INKSCAPE_LABEL, INKSCAPE_GROUPMODE, SVG_PATH_TAG
+from .tags import SVG_GROUP_TAG, INKSCAPE_LABEL, INKSCAPE_GROUPMODE, SVG_PATH_TAG, SVG_DEFS_TAG
+from .realistic_rendering import realistic_stitch, realistic_filter
from ..i18n import _
from ..utils import cache
@@ -32,6 +33,31 @@ def get_correction_transform(svg):
return transform
+def color_block_to_realistic_stitches(color_block, svg):
+ paths = []
+
+ for point_list in color_block_to_point_lists(color_block):
+ if not point_list:
+ continue
+
+ color = color_block.color.visible_on_white.darker.to_hex_str()
+ start = point_list[0]
+ for point in point_list[1:]:
+ paths.append(inkex.etree.Element(
+ SVG_PATH_TAG,
+ {'style': simplestyle.formatStyle(
+ {
+ 'fill': color,
+ 'stroke': 'none',
+ 'filter': 'url(#realistic-stitch-filter)'
+ }),
+ 'd': realistic_stitch(start, point),
+ 'transform': get_correction_transform(svg)
+ }))
+ start = point
+
+ return paths
+
def color_block_to_paths(color_block, svg):
paths = []
# We could emit just a single path with one subpath per point list, but
@@ -56,8 +82,7 @@ def color_block_to_paths(color_block, svg):
return paths
-
-def render_stitch_plan(svg, stitch_plan):
+def render_stitch_plan(svg, stitch_plan, realistic=False):
layer = svg.find(".//*[@id='__inkstitch_stitch_plan__']")
if layer is None:
layer = inkex.etree.Element(SVG_GROUP_TAG,
@@ -76,6 +101,17 @@ def render_stitch_plan(svg, stitch_plan):
SVG_GROUP_TAG,
{'id': '__color_block_%d__' % i,
INKSCAPE_LABEL: "color block %d" % (i + 1)})
- group.extend(color_block_to_paths(color_block, svg))
+ if realistic:
+ group.extend(color_block_to_realistic_stitches(color_block, svg))
+ else:
+ group.extend(color_block_to_paths(color_block, svg))
svg.append(layer)
+
+ if realistic:
+ defs = svg.find(SVG_DEFS_TAG)
+
+ if defs is None:
+ defs = inkex.etree.SubElement(svg, SVG_DEFS_TAG)
+
+ defs.append(inkex.etree.fromstring(realistic_filter))
diff --git a/lib/svg/tags.py b/lib/svg/tags.py
index fee59957..7eb87540 100644
--- a/lib/svg/tags.py
+++ b/lib/svg/tags.py
@@ -5,8 +5,14 @@ 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')
+SVG_SYMBOL_TAG = inkex.addNS('symbol', 'svg')
+SVG_USE_TAG = inkex.addNS('use', 'svg')
INKSCAPE_LABEL = inkex.addNS('label', 'inkscape')
INKSCAPE_GROUPMODE = inkex.addNS('groupmode', 'inkscape')
+CONNECTION_START = inkex.addNS('connection-start', 'inkscape')
+CONNECTION_END = inkex.addNS('connection-end', 'inkscape')
+CONNECTOR_TYPE = inkex.addNS('connector-type', 'inkscape')
+XLINK_HREF = inkex.addNS('href', 'xlink')
EMBROIDERABLE_TAGS = (SVG_PATH_TAG, SVG_POLYLINE_TAG)
diff --git a/lib/svg/units.py b/lib/svg/units.py
index 015da60e..126027bc 100644
--- a/lib/svg/units.py
+++ b/lib/svg/units.py
@@ -75,11 +75,24 @@ def convert_length(length):
raise ValueError(_("Unknown unit: %s") % units)
+@cache
+def get_viewbox(svg):
+ return svg.get('viewBox').strip().replace(',', ' ').split()
+
@cache
def get_doc_size(svg):
- doc_width = convert_length(svg.get('width'))
- doc_height = convert_length(svg.get('height'))
+ width = svg.get('width')
+ height = svg.get('height')
+
+ if width is None or height is None:
+ # fall back to the dimensions from the viewBox
+ viewbox = get_viewbox(svg)
+ width = viewbox[2]
+ height = viewbox[3]
+
+ doc_width = convert_length(width)
+ doc_height = convert_length(height)
return doc_width, doc_height
@@ -88,7 +101,7 @@ def get_viewbox_transform(node):
# somewhat cribbed from inkscape-silhouette
doc_width, doc_height = get_doc_size(node)
- viewbox = node.get('viewBox').strip().replace(',', ' ').split()
+ viewbox = get_viewbox(node)
dx = -float(viewbox[0])
dy = -float(viewbox[1])
diff --git a/lib/threads/color.py b/lib/threads/color.py
index af474127..cc6c0c48 100644
--- a/lib/threads/color.py
+++ b/lib/threads/color.py
@@ -1,7 +1,7 @@
import simplestyle
import re
import colorsys
-
+from pyembroidery.EmbThread import EmbThread
class ThreadColor(object):
hex_str_re = re.compile('#([0-9a-z]{3}|[0-9a-z]{6})', re.I)
@@ -9,6 +9,12 @@ class ThreadColor(object):
def __init__(self, color, name=None, number=None, manufacturer=None):
if color is None:
self.rgb = (0, 0, 0)
+ elif isinstance(color, EmbThread):
+ self.name = color.description
+ self.number = color.catalog_number
+ self.manufacturer = color.brand
+ self.rgb = (color.get_red(), color.get_green(), color.get_blue())
+ return
elif isinstance(color, (list, tuple)):
self.rgb = tuple(color)
elif self.hex_str_re.match(color):
@@ -39,6 +45,15 @@ class ThreadColor(object):
return "#%s" % self.hex_digits
@property
+ def pyembroidery_thread(self):
+ return {
+ "name": self.name,
+ "id": self.number,
+ "manufacturer": self.manufacturer,
+ "rgb": int(self.hex_digits, 16),
+ }
+
+ @property
def hex_digits(self):
return "%02X%02X%02X" % self.rgb
@@ -80,3 +95,18 @@ class ThreadColor(object):
color = tuple(value * 255 for value in color)
return ThreadColor(color, name=self.name, number=self.number, manufacturer=self.manufacturer)
+
+ @property
+ def darker(self):
+ hls = list(colorsys.rgb_to_hls(*self.rgb_normalized))
+
+ # Capping lightness should make the color visible without changing it
+ # too much.
+ hls[1] *= 0.75
+
+ color = colorsys.hls_to_rgb(*hls)
+
+ # convert back to values in the range of 0-255
+ color = tuple(value * 255 for value in color)
+
+ return ThreadColor(color, name=self.name, number=self.number, manufacturer=self.manufacturer)
diff --git a/lib/utils/__init__.py b/lib/utils/__init__.py
index ff06d4a9..78d037f1 100644
--- a/lib/utils/__init__.py
+++ b/lib/utils/__init__.py
@@ -2,3 +2,4 @@ from geometry import *
from cache import cache
from io import *
from inkscape import *
+from paths import *
diff --git a/lib/utils/geometry.py b/lib/utils/geometry.py
index 61b98bcb..d0cb96cf 100644
--- a/lib/utils/geometry.py
+++ b/lib/utils/geometry.py
@@ -65,12 +65,21 @@ class Point:
else:
raise ValueError("cannot multiply Point by %s" % type(other))
+ def __neg__(self):
+ return self * -1
+
def __rmul__(self, other):
if isinstance(other, (int, float)):
return self.__mul__(other)
else:
raise ValueError("cannot multiply Point by %s" % type(other))
+ def __div__(self, other):
+ if isinstance(other, (int, float)):
+ return self * (1.0 / other)
+ else:
+ raise ValueErorr("cannot divide Point by %s" % type(other))
+
def __repr__(self):
return "Point(%s,%s)" % (self.x, self.y)
diff --git a/lib/utils/io.py b/lib/utils/io.py
index be1fdf24..e5a246f3 100644
--- a/lib/utils/io.py
+++ b/lib/utils/io.py
@@ -7,12 +7,27 @@ def save_stderr():
# GTK likes to spam stderr, which inkscape will show in a dialog.
null = open(os.devnull, 'w')
sys.stderr_dup = os.dup(sys.stderr.fileno())
+ sys.real_stderr = os.fdopen(sys.stderr_dup, 'w')
os.dup2(null.fileno(), 2)
- sys.stderr_backup = sys.stderr
sys.stderr = StringIO()
def restore_stderr():
os.dup2(sys.stderr_dup, 2)
- sys.stderr_backup.write(sys.stderr.getvalue())
- sys.stderr = sys.stderr_backup
+ sys.real_stderr.write(sys.stderr.getvalue())
+ sys.stderr = sys.real_stderr
+
+# It's probably possible to generalize this code, but when I tried,
+# the result was incredibly unreadable.
+def save_stdout():
+ null = open(os.devnull, 'w')
+ sys.stdout_dup = os.dup(sys.stdout.fileno())
+ sys.real_stdout = os.fdopen(sys.stdout_dup, 'w')
+ os.dup2(null.fileno(), 1)
+ sys.stdout = StringIO()
+
+
+def restore_stdout():
+ os.dup2(sys.stdout_dup, 1)
+ sys.real_stdout.write(sys.stdout.getvalue())
+ sys.stdout = sys.real_stdout
diff --git a/lib/utils/paths.py b/lib/utils/paths.py
new file mode 100644
index 00000000..863e8e69
--- /dev/null
+++ b/lib/utils/paths.py
@@ -0,0 +1,10 @@
+import sys
+import os
+from os.path import dirname, realpath
+
+
+def get_bundled_dir(name):
+ if getattr(sys, 'frozen', None) is not None:
+ return realpath(os.path.join(sys._MEIPASS, "..", name))
+ else:
+ return realpath(os.path.join(dirname(realpath(__file__)), '..', '..', name))