summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/commands.py52
-rw-r--r--lib/elements/auto_fill.py4
-rw-r--r--lib/elements/element.py72
-rw-r--r--lib/elements/fill.py3
-rw-r--r--lib/extensions/__init__.py9
-rw-r--r--lib/extensions/break_apart.py68
-rw-r--r--lib/extensions/convert_to_satin.py60
-rw-r--r--lib/extensions/import_threadlist.py102
-rw-r--r--lib/extensions/remove_embroidery_settings.py8
-rw-r--r--lib/extensions/stitch_plan_preview.py24
-rw-r--r--lib/extensions/zip.py56
-rwxr-xr-xlib/inx/extensions.py7
-rw-r--r--lib/inx/utils.py17
-rw-r--r--lib/stitches/auto_fill.py62
-rw-r--r--lib/stitches/auto_satin.py29
-rw-r--r--lib/svg/rendering.py8
-rw-r--r--lib/svg/tags.py59
-rw-r--r--lib/svg/units.py7
-rw-r--r--lib/threads/catalog.py36
-rw-r--r--lib/threads/palette.py17
20 files changed, 582 insertions, 118 deletions
diff --git a/lib/commands.py b/lib/commands.py
index 908c6e53..b92d79cf 100644
--- a/lib/commands.py
+++ b/lib/commands.py
@@ -1,54 +1,57 @@
-from copy import deepcopy
import os
-from random import random
import sys
+from copy import deepcopy
+from random import random
+
+from shapely import geometry as shgeo
import cubicsuperpath
import inkex
-from shapely import geometry as shgeo
import simpletransform
-from .i18n import _, N_
-from .svg import apply_transforms, get_node_transform, get_correction_transform, get_document, generate_unique_id
-from .svg.tags import SVG_DEFS_TAG, SVG_GROUP_TAG, SVG_PATH_TAG, SVG_USE_TAG, SVG_SYMBOL_TAG, \
- CONNECTION_START, CONNECTION_END, CONNECTOR_TYPE, XLINK_HREF, INKSCAPE_LABEL
-from .utils import cache, get_bundled_dir, Point
-
+from .i18n import N_, _
+from .svg import (apply_transforms, generate_unique_id,
+ get_correction_transform, get_document, get_node_transform)
+from .svg.tags import (CONNECTION_END, CONNECTION_START, CONNECTOR_TYPE,
+ INKSCAPE_LABEL, INKSTITCH_ATTRIBS, SVG_DEFS_TAG,
+ SVG_GROUP_TAG, SVG_PATH_TAG, SVG_SYMBOL_TAG,
+ SVG_USE_TAG, XLINK_HREF)
+from .utils import Point, cache, get_bundled_dir
COMMANDS = {
# L10N command attached to an object
- N_("fill_start"): N_("Fill stitch starting position"),
+ "fill_start": N_("Fill stitch starting position"),
# L10N command attached to an object
- N_("fill_end"): N_("Fill stitch ending position"),
+ "fill_end": N_("Fill stitch ending position"),
# L10N command attached to an object
- N_("satin_start"): N_("Auto-route satin stitch starting position"),
+ "satin_start": N_("Auto-route satin stitch starting position"),
# L10N command attached to an object
- N_("satin_end"): N_("Auto-route satin stitch ending position"),
+ "satin_end": N_("Auto-route satin stitch ending position"),
# L10N command attached to an object
- N_("stop"): N_("Stop (pause machine) after sewing this object"),
+ "stop": N_("Stop (pause machine) after sewing this object"),
# L10N command attached to an object
- N_("trim"): N_("Trim thread after sewing this object"),
+ "trim": N_("Trim thread after sewing this object"),
# L10N command attached to an object
- N_("ignore_object"): N_("Ignore this object (do not stitch)"),
+ "ignore_object": N_("Ignore this object (do not stitch)"),
# L10N command attached to an object
- N_("satin_cut_point"): N_("Satin cut point (use with Cut Satin Column)"),
+ "satin_cut_point": N_("Satin cut point (use with Cut Satin Column)"),
# L10N command that affects a layer
- N_("ignore_layer"): N_("Ignore layer (do not stitch any objects in this layer)"),
+ "ignore_layer": N_("Ignore layer (do not stitch any objects in this layer)"),
# L10N command that affects entire document
- N_("origin"): N_("Origin for exported embroidery files"),
+ "origin": N_("Origin for exported embroidery files"),
# L10N command that affects entire document
- N_("stop_position"): N_("Jump destination for Stop commands (a.k.a. \"Frame Out position\")."),
+ "stop_position": N_("Jump destination for Stop commands (a.k.a. \"Frame Out position\")."),
}
OBJECT_COMMANDS = ["fill_start", "fill_end", "satin_start", "satin_end", "stop", "trim", "ignore_object", "satin_cut_point"]
@@ -346,7 +349,7 @@ def get_command_pos(element, index, total):
def remove_legacy_param(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
+ # automatically delete them. The new commands will do the same
# thing.
#
# If we didn't delete these here, then things would get confusing.
@@ -359,6 +362,13 @@ def remove_legacy_param(element, command):
if attribute in element.node.attrib:
del element.node.attrib[attribute]
+ # Attributes have changed to be namespaced.
+ # Let's check for them as well, they might have automatically changed.
+ attribute = INKSTITCH_ATTRIBS["%s_after" % command]
+
+ if attribute in element.node.attrib:
+ del element.node.attrib[attribute]
+
def add_commands(element, commands):
document = get_document(element.node)
diff --git a/lib/elements/auto_fill.py b/lib/elements/auto_fill.py
index 04da3288..b574c8bf 100644
--- a/lib/elements/auto_fill.py
+++ b/lib/elements/auto_fill.py
@@ -53,9 +53,9 @@ class AutoFill(Fill):
return max(self.get_float_param("running_stitch_length_mm", 1.5), 0.01)
@property
- @param('fill_underlay', _('Underlay'), type='toggle', group=_('AutoFill Underlay'), default=False)
+ @param('fill_underlay', _('Underlay'), type='toggle', group=_('AutoFill Underlay'), default=True)
def fill_underlay(self):
- return self.get_boolean_param("fill_underlay", default=False)
+ return self.get_boolean_param("fill_underlay", default=True)
@property
@param('fill_underlay_angle',
diff --git a/lib/elements/element.py b/lib/elements/element.py
index dd6c9063..83e3978f 100644
--- a/lib/elements/element.py
+++ b/lib/elements/element.py
@@ -1,14 +1,14 @@
-from copy import deepcopy
import sys
+from copy import deepcopy
-from cspsubdiv import cspsubdiv
import cubicsuperpath
-import simplestyle
+import tinycss2
+from cspsubdiv import cspsubdiv
from ..commands import find_commands
from ..i18n import _
from ..svg import PIXELS_PER_MM, apply_transforms, convert_length, get_doc_size
-from ..svg.tags import INKSCAPE_LABEL
+from ..svg.tags import INKSCAPE_LABEL, INKSTITCH_ATTRIBS
from ..utils import cache
@@ -72,6 +72,16 @@ class EmbroideryElement(object):
def __init__(self, node):
self.node = node
+ legacy_attribs = False
+ for attrib in self.node.attrib:
+ if attrib.startswith('embroider_'):
+ # update embroider_ attributes to namespaced attributes
+ self.replace_legacy_param(attrib)
+ legacy_attribs = True
+ if legacy_attribs and not self.get_param('fill_underlay', ""):
+ # defaut setting for fill_underlay has changed
+ self.set_param('fill_underlay', False)
+
@property
def id(self):
return self.node.get('id')
@@ -85,13 +95,16 @@ class EmbroideryElement(object):
# The 'param' attribute is set by the 'param' decorator defined above.
if hasattr(prop.fget, 'param'):
params.append(prop.fget.param)
-
return params
+ def replace_legacy_param(self, param):
+ value = self.node.get(param, "").strip()
+ self.set_param(param[10:], value)
+ del self.node.attrib[param]
+
@cache
def get_param(self, param, default):
- value = self.node.get("embroider_" + param, "").strip()
-
+ value = self.node.get(INKSTITCH_ATTRIBS[param], "").strip()
return value or default
@cache
@@ -131,22 +144,28 @@ class EmbroideryElement(object):
return value
def set_param(self, name, value):
- self.node.set("embroider_%s" % name, str(value))
+ param = INKSTITCH_ATTRIBS[name]
+ self.node.set(param, str(value))
+ @property
@cache
+ def style(self):
+ declarations = tinycss2.parse_declaration_list(self.node.get("style", ""))
+ style = {declaration.lower_name: declaration.value[0].serialize() for declaration in declarations}
+
+ return style
+
def get_style(self, style_name, default=None):
- style = simplestyle.parseStyle(self.node.get("style"))
- if (style_name not in style):
- return default
- value = style[style_name]
- if value == 'none':
- return None
- return value
+ style = self.style.get(style_name)
+ # Style not found, let's see if it is set as a separate attribute
+ if style is None:
+ style = self.node.get(style_name, default)
+ if style == 'none':
+ style = None
+ return style
- @cache
def has_style(self, style_name):
- style = simplestyle.parseStyle(self.node.get("style"))
- return style_name in style
+ return style_name in self.style
@property
@cache
@@ -160,7 +179,7 @@ class EmbroideryElement(object):
@property
@cache
def stroke_width(self):
- width = self.get_style("stroke-width", "1")
+ width = self.get_style("stroke-width", None)
if width is None:
return 1.0
@@ -169,6 +188,17 @@ class EmbroideryElement(object):
return width * self.stroke_scale
@property
+ @param('ties',
+ _('Ties'),
+ tooltip=_('Add ties. Manual stitch will not add ties.'),
+ type='boolean',
+ default=True,
+ sort_index=4)
+ @cache
+ def ties(self):
+ return self.get_boolean_param("ties", True)
+
+ @property
def path(self):
# A CSP is a "cubic superpath".
#
@@ -269,6 +299,10 @@ class EmbroideryElement(object):
patches = self.to_patches(last_patch)
+ if not self.ties:
+ for patch in patches:
+ patch.stitch_as_is = True
+
if patches:
patches[-1].trim_after = self.has_command("trim") or self.trim_after
patches[-1].stop_after = self.has_command("stop") or self.stop_after
diff --git a/lib/elements/fill.py b/lib/elements/fill.py
index 7157cc46..a5b8bfc3 100644
--- a/lib/elements/fill.py
+++ b/lib/elements/fill.py
@@ -19,8 +19,7 @@ class UnconnectedError(ValidationError):
"Ink/Stitch doesn't know what order to stitch them in. Please break this "
"object up into separate shapes.")
steps_to_solve = [
- _('* Path > Break apart (Shift+Ctrl+K)'),
- _('* (Optional) Recombine shapes with holes (Ctrl+K).')
+ _('* Extensions > Ink/Stitch > Fill Tools > Break Apart and Retain Holes.')
]
diff --git a/lib/extensions/__init__.py b/lib/extensions/__init__.py
index 38276a64..6f9ef5c4 100644
--- a/lib/extensions/__init__.py
+++ b/lib/extensions/__init__.py
@@ -1,9 +1,11 @@
from auto_satin import AutoSatin
+from break_apart import BreakApart
from convert_to_satin import ConvertToSatin
from cut_satin import CutSatin
from embroider import Embroider
from flip import Flip
from global_commands import GlobalCommands
+from import_threadlist import ImportThreadlist
from input import Input
from install import Install
from layer_commands import LayerCommands
@@ -15,10 +17,11 @@ from params import Params
from print_pdf import Print
from remove_embroidery_settings import RemoveEmbroiderySettings
from simulate import Simulate
+from stitch_plan_preview import StitchPlanPreview
from zip import Zip
-
__all__ = extensions = [Embroider,
+ StitchPlanPreview,
Install,
Params,
Print,
@@ -35,4 +38,6 @@ __all__ = extensions = [Embroider,
AutoSatin,
Lettering,
Troubleshoot,
- RemoveEmbroiderySettings]
+ RemoveEmbroiderySettings,
+ BreakApart,
+ ImportThreadlist]
diff --git a/lib/extensions/break_apart.py b/lib/extensions/break_apart.py
new file mode 100644
index 00000000..625ace55
--- /dev/null
+++ b/lib/extensions/break_apart.py
@@ -0,0 +1,68 @@
+from copy import deepcopy
+
+from shapely.geometry import Polygon
+
+import inkex
+
+from ..elements import AutoFill, Fill
+from ..i18n import _
+from ..svg import get_correction_transform
+from .base import InkstitchExtension
+
+
+class BreakApart(InkstitchExtension):
+ def effect(self): # noqa: C901
+ if not self.get_elements():
+ return
+
+ if not self.selected:
+ inkex.errormsg(_("Please select one or more fill areas to break apart."))
+ return
+
+ for element in self.elements:
+ if not isinstance(element, AutoFill) and not isinstance(element, Fill):
+ continue
+ if len(element.paths) <= 1:
+ continue
+
+ polygons = []
+ multipolygons = []
+ holes = []
+
+ for path in element.paths:
+ polygons.append(Polygon(path))
+
+ # sort paths by size and convert to polygons
+ polygons.sort(key=lambda polygon: polygon.area, reverse=True)
+
+ for shape in polygons:
+ if shape in holes:
+ continue
+ polygon_list = [shape]
+
+ for other in polygons:
+ if shape != other and shape.contains(other) and other not in holes:
+ # check if "other" is inside a hole, before we add it to the list
+ if any(p.contains(other) for p in polygon_list[1:]):
+ continue
+ polygon_list.append(other)
+ holes.append(other)
+ multipolygons.append(polygon_list)
+ self.element_to_nodes(multipolygons, element)
+
+ def element_to_nodes(self, multipolygons, element):
+ for polygons in multipolygons:
+ el = deepcopy(element)
+ d = ""
+ for polygon in polygons:
+ # copy element and replace path
+ el.node.set('id', self.uniqueId(element.node.get('id') + "_"))
+ d += "M"
+ for x, y in polygon.exterior.coords:
+ d += "%s,%s " % (x, y)
+ d += " "
+ d += "Z"
+ el.node.set('d', d)
+ el.node.set('transform', get_correction_transform(element.node))
+ element.node.getparent().insert(0, el.node)
+ element.node.getparent().remove(element.node)
diff --git a/lib/extensions/convert_to_satin.py b/lib/extensions/convert_to_satin.py
index 1227b207..e2b287dd 100644
--- a/lib/extensions/convert_to_satin.py
+++ b/lib/extensions/convert_to_satin.py
@@ -1,16 +1,16 @@
-from copy import deepcopy
-from itertools import chain, groupby
import math
+from itertools import chain, groupby
-import inkex
-from numpy import diff, sign, setdiff1d
import numpy
+from numpy import diff, setdiff1d, sign
from shapely import geometry as shgeo
+import inkex
+
from ..elements import Stroke
from ..i18n import _
-from ..svg import get_correction_transform, PIXELS_PER_MM
-from ..svg.tags import SVG_PATH_TAG
+from ..svg import PIXELS_PER_MM, get_correction_transform
+from ..svg.tags import INKSTITCH_ATTRIBS, SVG_PATH_TAG
from ..utils import Point
from .base import InkstitchExtension
@@ -49,23 +49,31 @@ class ConvertToSatin(InkstitchExtension):
# ignore paths with just one point -- they're not visible to the user anyway
continue
- self.fix_loop(path)
+ for satin in self.convert_path_to_satins(path, element.stroke_width, style_args, correction_transform, path_style):
+ parent.insert(index, satin)
+ index += 1
- try:
- rails, rungs = self.path_to_satin(path, element.stroke_width, style_args)
- except SelfIntersectionError:
- inkex.errormsg(
- _("Cannot convert %s to a satin column because it intersects itself. Try breaking it up into multiple paths.") %
- element.node.get('id'))
+ parent.remove(element.node)
- # revert any changes we've made
- self.document = deepcopy(self.original_document)
+ def convert_path_to_satins(self, path, stroke_width, style_args, correction_transform, path_style, depth=0):
+ try:
+ rails, rungs = self.path_to_satin(path, stroke_width, style_args)
+ yield self.satin_to_svg_node(rails, rungs, correction_transform, path_style)
+ except SelfIntersectionError:
+ # The path intersects itself. Split it in two and try doing the halves
+ # individually.
- return
+ if depth >= 20:
+ # At this point we're slicing the path way too small and still
+ # getting nowhere. Just give up on this section of the path.
+ return
- parent.insert(index, self.satin_to_svg_node(rails, rungs, correction_transform, path_style))
+ half = int(len(path) / 2.0)
+ halves = [path[:half + 1], path[half:]]
- parent.remove(element.node)
+ for path in halves:
+ for satin in self.convert_path_to_satins(path, stroke_width, style_args, correction_transform, path_style, depth=depth + 1):
+ yield satin
def fix_loop(self, path):
if path[0] == path[-1]:
@@ -107,13 +115,16 @@ class ConvertToSatin(InkstitchExtension):
return args
def path_to_satin(self, path, stroke_width, style_args):
+ if Point(*path[0]).distance(Point(*path[-1])) < 1:
+ raise SelfIntersectionError()
+
path = shgeo.LineString(path)
left_rail = path.parallel_offset(stroke_width / 2.0, 'left', **style_args)
right_rail = path.parallel_offset(stroke_width / 2.0, 'right', **style_args)
if not isinstance(left_rail, shgeo.LineString) or \
- not isinstance(right_rail, shgeo.LineString):
+ not isinstance(right_rail, shgeo.LineString):
# If the parallel offsets come out as anything but a LineString, that means the
# path intersects itself, when taking its stroke width into consideration. See
# the last example for parallel_offset() in the Shapely documentation:
@@ -159,6 +170,13 @@ class ConvertToSatin(InkstitchExtension):
# The dot product of two vectors is |v1| * |v2| * cos(angle).
# These are unit vectors, so their magnitudes are 1.
cos_angle_between = prev_direction * direction
+
+ # Clamp to the valid range for a cosine. The above _should_
+ # already be in this range, but floating point inaccuracy can
+ # push it outside the range causing math.acos to throw
+ # ValueError ("math domain error").
+ cos_angle_between = max(-1.0, min(1.0, cos_angle_between))
+
angle = abs(math.degrees(math.acos(cos_angle_between)))
# Use the square of the angle, measured in degrees.
@@ -248,7 +266,7 @@ class ConvertToSatin(InkstitchExtension):
# arbitrarily rejects the rung if there was one less than 2
# millimeters before this one.
if last_rung_center is not None and \
- (rung_center - last_rung_center).length() < 2 * PIXELS_PER_MM:
+ (rung_center - last_rung_center).length() < 2 * PIXELS_PER_MM:
continue
else:
last_rung_center = rung_center
@@ -292,6 +310,6 @@ class ConvertToSatin(InkstitchExtension):
"style": path_style,
"transform": correction_transform,
"d": d,
- "embroider_satin_column": "true",
+ INKSTITCH_ATTRIBS['satin_column']: "true",
}
)
diff --git a/lib/extensions/import_threadlist.py b/lib/extensions/import_threadlist.py
new file mode 100644
index 00000000..d31c0d69
--- /dev/null
+++ b/lib/extensions/import_threadlist.py
@@ -0,0 +1,102 @@
+import os
+import re
+import sys
+
+import inkex
+
+from ..i18n import _
+from ..threads import ThreadCatalog
+from .base import InkstitchExtension
+
+
+class ImportThreadlist(InkstitchExtension):
+ def __init__(self, *args, **kwargs):
+ InkstitchExtension.__init__(self, *args, **kwargs)
+ self.OptionParser.add_option("-f", "--filepath", type="str", default="", dest="filepath")
+ self.OptionParser.add_option("-m", "--method", type="int", default=1, dest="method")
+ self.OptionParser.add_option("-t", "--palette", type="str", default=None, dest="palette")
+
+ def effect(self):
+ # Remove selection, we want all the elements in the document
+ self.selected = {}
+
+ if not self.get_elements():
+ return
+
+ path = self.options.filepath
+ if not os.path.exists(path):
+ print >> sys.stderr, _("File not found.")
+ sys.exit(1)
+
+ method = self.options.method
+ if method == 1:
+ colors = self.parse_inkstitch_threadlist(path)
+ else:
+ colors = self.parse_threadlist_by_catalog_number(path)
+
+ if all(c is None for c in colors):
+ print >>sys.stderr, _("Couldn't find any matching colors in the file.")
+ if method == 1:
+ print >>sys.stderr, _('Please try to import as "other threadlist" and specify a color palette below.')
+ else:
+ print >>sys.stderr, _("Please chose an other color palette for your design.")
+ sys.exit(1)
+
+ # Iterate through the color blocks to apply colors
+ element_color = ""
+ i = -1
+ for element in self.elements:
+ if element.color != element_color:
+ element_color = element.color
+ i += 1
+
+ # No more colors in the list, stop here
+ if i == len(colors):
+ break
+
+ style = element.node.get('style').replace("%s" % element_color, "%s" % colors[i])
+ element.node.set('style', style)
+
+ def parse_inkstitch_threadlist(self, path):
+ colors = []
+ with open(path) as threadlist:
+ for line in threadlist:
+ if line[0].isdigit():
+ m = re.search(r"\((#[0-9A-Fa-f]{6})\)", line)
+ if m:
+ colors.append(m.group(1))
+ else:
+ # Color not found
+ colors.append(None)
+ return colors
+
+ def parse_threadlist_by_catalog_number(self, path):
+ palette_name = self.options.palette
+ palette = ThreadCatalog().get_palette_by_name(palette_name)
+
+ colors = []
+ palette_numbers = []
+ palette_colors = []
+
+ for color in palette:
+ palette_numbers.append(color.number)
+ palette_colors.append('#%s' % color.hex_digits.lower())
+ with open(path) as threadlist:
+ for line in threadlist:
+ if line[0].isdigit():
+ # some threadlists may add a # in front of the catalof number
+ # let's remove it from the entire string before splitting it up
+ thread = line.replace('#', '').split()
+ catalog_number = set(thread[1:]).intersection(palette_numbers)
+ if catalog_number:
+ color_index = palette_numbers.index(next(iter(catalog_number)))
+ colors.append(palette_colors[color_index])
+ else:
+ # No color found
+ colors.append(None)
+ return colors
+
+ def find_elements(self, xpath):
+ svg = self.document.getroot()
+ elements = svg.xpath(xpath, namespaces=inkex.NSS)
+ return elements
diff --git a/lib/extensions/remove_embroidery_settings.py b/lib/extensions/remove_embroidery_settings.py
index d87a216a..d39c7e94 100644
--- a/lib/extensions/remove_embroidery_settings.py
+++ b/lib/extensions/remove_embroidery_settings.py
@@ -30,11 +30,11 @@ class RemoveEmbroiderySettings(InkstitchExtension):
if not self.selected:
xpath = ".//svg:path"
elements = self.find_elements(xpath)
- self.remove_embroider_attributes(elements)
+ self.remove_inkstitch_attributes(elements)
else:
for node in self.selected:
elements = self.get_selected_elements(node)
- self.remove_embroider_attributes(elements)
+ self.remove_inkstitch_attributes(elements)
def remove_commands(self):
if not self.selected:
@@ -83,8 +83,8 @@ class RemoveEmbroiderySettings(InkstitchExtension):
def remove_element(self, element):
element.getparent().remove(element)
- def remove_embroider_attributes(self, elements):
+ def remove_inkstitch_attributes(self, elements):
for element in elements:
for attrib in element.attrib:
- if attrib.startswith('embroider_'):
+ if attrib.startswith(inkex.NSS['inkstitch'], 1):
del element.attrib[attrib]
diff --git a/lib/extensions/stitch_plan_preview.py b/lib/extensions/stitch_plan_preview.py
new file mode 100644
index 00000000..b89e24a7
--- /dev/null
+++ b/lib/extensions/stitch_plan_preview.py
@@ -0,0 +1,24 @@
+from ..stitch_plan import patches_to_stitch_plan
+from ..svg import render_stitch_plan
+from .base import InkstitchExtension
+
+
+class StitchPlanPreview(InkstitchExtension):
+
+ def effect(self):
+ # delete old stitch plan
+ svg = self.document.getroot()
+ layer = svg.find(".//*[@id='__inkstitch_stitch_plan__']")
+ if layer is not None:
+ del layer[:]
+
+ # create new stitch plan
+ if not self.get_elements():
+ return
+ patches = self.elements_to_patches(self.elements)
+ stitch_plan = patches_to_stitch_plan(patches)
+ render_stitch_plan(svg, stitch_plan)
+
+ # translate stitch plan to the right side of the canvas
+ layer = svg.find(".//*[@id='__inkstitch_stitch_plan__']")
+ layer.set('transform', 'translate(%s)' % svg.get('viewBox', '0 0 800 0').split(' ')[2])
diff --git a/lib/extensions/zip.py b/lib/extensions/zip.py
index 2376f79a..aebff331 100644
--- a/lib/extensions/zip.py
+++ b/lib/extensions/zip.py
@@ -1,14 +1,17 @@
-import sys
import os
+import sys
import tempfile
from zipfile import ZipFile
+
+import inkex
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 PIXELS_PER_MM
+from .base import InkstitchExtension
+from ..threads import ThreadCatalog
class Zip(InkstitchExtension):
@@ -26,6 +29,10 @@ class Zip(InkstitchExtension):
extension = format['extension']
self.OptionParser.add_option('--format-%s' % extension, type="inkbool", dest=extension)
self.formats.append(extension)
+ self.OptionParser.add_option('--format-svg', type="inkbool", dest='svg')
+ self.OptionParser.add_option('--format-threadlist', type="inkbool", dest='threadlist')
+ self.formats.append('svg')
+ self.formats.append('threadlist')
def effect(self):
if not self.get_elements():
@@ -42,7 +49,17 @@ class Zip(InkstitchExtension):
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())
+ if format == 'svg':
+ output = open(output_file, 'w')
+ output.write(inkex.etree.tostring(self.document.getroot()))
+ output.close()
+ if format == 'threadlist':
+ output_file = os.path.join(path, "%s_%s.txt" % (base_file_name, _("threadlist")))
+ output = open(output_file, 'w')
+ output.write(self.get_threadlist(stitch_plan, base_file_name))
+ output.close()
+ else:
+ write_embroidery_file(output_file, stitch_plan, self.document.getroot())
files.append(output_file)
if not files:
@@ -69,3 +86,36 @@ class Zip(InkstitchExtension):
# don't let inkex output the SVG!
sys.exit(0)
+
+ def get_threadlist(self, stitch_plan, design_name):
+ ThreadCatalog().match_and_apply_palette(stitch_plan, self.get_inkstitch_metadata()['thread-palette'])
+ thread_used = []
+
+ thread_output = "%s\n" % _("Design Details")
+ thread_output += "==============\n\n"
+
+ thread_output += "%s: %s\n" % (_("Title"), design_name)
+ thread_output += "%s (mm): %.2f x %.2f\n" % (_("Size"), stitch_plan.dimensions_mm[0], stitch_plan.dimensions_mm[1])
+ thread_output += "%s: %s\n" % (_("Stitches"), stitch_plan.num_stitches)
+ thread_output += "%s: %s\n\n" % (_("Colors"), stitch_plan.num_colors)
+
+ thread_output += "%s\n" % _("Thread Order")
+ thread_output += "============\n\n"
+
+ for i, color_block in enumerate(stitch_plan):
+ thread = color_block.color
+
+ thread_output += str(i + 1) + " "
+ string = "%s #%s - %s (#%s)" % (thread.name, thread.number, thread.manufacturer, thread.hex_digits.lower())
+ thread_output += string + "\n"
+
+ thread_used.append(string)
+
+ thread_output += "\n"
+ thread_output += _("Thread Used") + "\n"
+ thread_output += "============" + "\n\n"
+
+ for thread in set(thread_used):
+ thread_output += thread + "\n"
+
+ return "%s" % thread_output
diff --git a/lib/inx/extensions.py b/lib/inx/extensions.py
index d1a0c7f3..030e8aa6 100755
--- a/lib/inx/extensions.py
+++ b/lib/inx/extensions.py
@@ -4,6 +4,7 @@ from .utils import build_environment, write_inx_file
from .outputs import pyembroidery_output_formats
from ..extensions import extensions, Input, Output
from ..commands import LAYER_COMMANDS, OBJECT_COMMANDS, GLOBAL_COMMANDS, COMMANDS
+from ..threads import ThreadCatalog
def layer_commands():
@@ -27,6 +28,11 @@ def pyembroidery_debug_formats():
yield format['extension'], format['description']
+def threadcatalog():
+ threadcatalog = ThreadCatalog().palette_names()
+ return threadcatalog
+
+
def generate_extension_inx_files():
env = build_environment()
@@ -38,6 +44,7 @@ def generate_extension_inx_files():
template = env.get_template('%s.inx' % name)
write_inx_file(name, template.render(formats=pyembroidery_output_formats(),
debug_formats=pyembroidery_debug_formats(),
+ threadcatalog=threadcatalog(),
layer_commands=layer_commands(),
object_commands=object_commands(),
global_commands=global_commands()))
diff --git a/lib/inx/utils.py b/lib/inx/utils.py
index 1dc96829..a7c98a60 100644
--- a/lib/inx/utils.py
+++ b/lib/inx/utils.py
@@ -1,11 +1,12 @@
import errno
-import os
import gettext
+import os
+import sys
from os.path import dirname
-from jinja2 import Environment, FileSystemLoader
-from ..i18n import translation as default_translation, locale_dir, N_
+from jinja2 import Environment, FileSystemLoader
+from ..i18n import N_, locale_dir, translation as default_translation
_top_path = dirname(dirname(dirname(os.path.realpath(__file__))))
inx_path = os.path.join(_top_path, "inx")
@@ -25,6 +26,16 @@ def build_environment():
env.install_gettext_translations(current_translation)
env.globals["locale"] = current_locale
+ if "BUILD" in os.environ:
+ # building a ZIP release, with inkstitch packaged as a binary
+ if sys.platform == "win32":
+ env.globals["command_tag"] = '<command reldir="extensions">inkstitch/bin/inkstitch.exe</command>'
+ else:
+ env.globals["command_tag"] = '<command reldir="extensions">inkstitch/bin/inkstitch</command>'
+ else:
+ # user is running inkstitch.py directly as a developer
+ env.globals["command_tag"] = '<command reldir="extensions" interpreter="python">inkstitch.py</command>'
+
return env
diff --git a/lib/stitches/auto_fill.py b/lib/stitches/auto_fill.py
index 5833b779..dbbef136 100644
--- a/lib/stitches/auto_fill.py
+++ b/lib/stitches/auto_fill.py
@@ -1,18 +1,18 @@
# -*- coding: UTF-8 -*-
-from itertools import groupby, chain
import math
+from itertools import chain, groupby
import networkx
from shapely import geometry as shgeo
from shapely.ops import snap
from shapely.strtree import STRtree
+from .fill import intersect_region_with_grating, stitch_row
+from .running_stitch import running_stitch
from ..debug import debug
from ..svg import PIXELS_PER_MM
from ..utils.geometry import Point as InkstitchPoint, line_string_to_point_list
-from .fill import intersect_region_with_grating, stitch_row
-from .running_stitch import running_stitch
class PathEdge(object):
@@ -52,8 +52,7 @@ def auto_fill(shape,
starting_point,
ending_point=None,
underpath=True):
-
- fill_stitch_graph = build_fill_stitch_graph(shape, angle, row_spacing, end_row_spacing)
+ fill_stitch_graph = build_fill_stitch_graph(shape, angle, row_spacing, end_row_spacing, starting_point, ending_point)
if not graph_is_valid(fill_stitch_graph, shape, max_stitch_length):
return fallback(shape, running_stitch_length)
@@ -95,7 +94,7 @@ def project(shape, coords, outline_index):
@debug.time
-def build_fill_stitch_graph(shape, angle, row_spacing, end_row_spacing):
+def build_fill_stitch_graph(shape, angle, row_spacing, end_row_spacing, starting_point=None, ending_point=None):
"""build a graph representation of the grating segments
This function builds a specialized graph (as in graph theory) that will
@@ -146,11 +145,39 @@ def build_fill_stitch_graph(shape, angle, row_spacing, end_row_spacing):
tag_nodes_with_outline_and_projection(graph, shape, graph.nodes())
add_edges_between_outline_nodes(graph, duplicate_every_other=True)
+ if starting_point:
+ insert_node(graph, shape, starting_point)
+
+ if ending_point:
+ insert_node(graph, shape, ending_point)
+
debug.log_graph(graph, "graph")
return graph
+def insert_node(graph, shape, point):
+ """Add node to graph, splitting one of the outline edges"""
+
+ point = tuple(point)
+ outline = which_outline(shape, point)
+ projection = project(shape, point, outline)
+ projected_point = list(shape.boundary)[outline].interpolate(projection)
+ node = (projected_point.x, projected_point.y)
+
+ edges = []
+ for start, end, key, data in graph.edges(keys=True, data=True):
+ if key == "outline":
+ edges.append(((start, end), data))
+
+ edge, data = min(edges, key=lambda (edge, data): shgeo.LineString(edge).distance(projected_point))
+
+ graph.remove_edge(*edge, key="outline")
+ graph.add_edge(edge[0], node, key="outline", **data)
+ graph.add_edge(node, edge[1], key="outline", **data)
+ tag_nodes_with_outline_and_projection(graph, shape, nodes=[node])
+
+
def tag_nodes_with_outline_and_projection(graph, shape, nodes):
for node in nodes:
outline_index = which_outline(shape, node)
@@ -159,6 +186,27 @@ def tag_nodes_with_outline_and_projection(graph, shape, nodes):
graph.add_node(node, outline=outline_index, projection=outline_projection)
+def add_boundary_travel_nodes(graph, shape):
+ for outline_index, outline in enumerate(shape.boundary):
+ prev = None
+ for point in outline.coords:
+ point = shgeo.Point(point)
+ if prev is not None:
+ # Subdivide long straight line segments. Otherwise we may not
+ # have a node near the user's chosen starting or ending point
+ length = point.distance(prev)
+ segment = shgeo.LineString((prev, point))
+ if length > 1:
+ # Just plot a point every pixel, that should be plenty of
+ # resolution. A pixel is around a quarter of a millimeter.
+ for i in xrange(1, int(length)):
+ subpoint = segment.interpolate(i)
+ graph.add_node((subpoint.x, subpoint.y), projection=outline.project(subpoint), outline=outline_index)
+
+ graph.add_node((point.x, point.y), projection=outline.project(point), outline=outline_index)
+ prev = point
+
+
def add_edges_between_outline_nodes(graph, duplicate_every_other=False):
"""Add edges around the outlines of the graph, connecting sequential nodes.
@@ -240,6 +288,8 @@ def build_travel_graph(fill_stitch_graph, shape, fill_stitch_angle, underpath):
# This will ensure that a path traveling inside the shape can reach its
# target on the outline, which will be one of the points added above.
tag_nodes_with_outline_and_projection(graph, shape, boundary_points)
+ else:
+ add_boundary_travel_nodes(graph, shape)
add_edges_between_outline_nodes(graph)
diff --git a/lib/stitches/auto_satin.py b/lib/stitches/auto_satin.py
index 4ce356ce..9edff53c 100644
--- a/lib/stitches/auto_satin.py
+++ b/lib/stitches/auto_satin.py
@@ -1,20 +1,23 @@
-from itertools import chain, izip
import math
+from itertools import chain, izip
-import cubicsuperpath
-import inkex
+import networkx as nx
from shapely import geometry as shgeo
from shapely.geometry import Point as ShapelyPoint
-import simplestyle
-import networkx as nx
+import cubicsuperpath
+import inkex
+import simplestyle
from ..commands import add_commands
-from ..elements import Stroke, SatinColumn
+from ..elements import SatinColumn, Stroke
from ..i18n import _
-from ..svg import PIXELS_PER_MM, line_strings_to_csp, get_correction_transform, generate_unique_id
-from ..svg.tags import SVG_PATH_TAG, SVG_GROUP_TAG, INKSCAPE_LABEL
-from ..utils import Point as InkstitchPoint, cut, cache
+from ..svg import (PIXELS_PER_MM, generate_unique_id, get_correction_transform,
+ line_strings_to_csp)
+from ..svg.tags import (INKSCAPE_LABEL, INKSTITCH_ATTRIBS, SVG_GROUP_TAG,
+ SVG_PATH_TAG)
+from ..utils import Point as InkstitchPoint
+from ..utils import cache, cut
class SatinSegment(object):
@@ -209,9 +212,9 @@ class RunningStitch(object):
self.original_element = original_element
self.style = original_element.node.get('style', '')
self.running_stitch_length = \
- original_element.node.get('embroider_running_stitch_length_mm', '') or \
- original_element.node.get('embroider_center_walk_underlay_stitch_length_mm', '') or \
- original_element.node.get('embroider_contour_underlay_stitch_length_mm', '')
+ original_element.node.get(INKSTITCH_ATTRIBS['running_stitch_length_mm'], '') or \
+ original_element.node.get(INKSTITCH_ATTRIBS['center_walk_underlay_stitch_length_mm'], '') or \
+ original_element.node.get(INKSTITCH_ATTRIBS['contour_underlay_stitch_length_mm'], '')
def to_element(self):
node = inkex.etree.Element(SVG_PATH_TAG)
@@ -222,7 +225,7 @@ class RunningStitch(object):
style['stroke-dasharray'] = "0.5,0.5"
style = simplestyle.formatStyle(style)
node.set("style", style)
- node.set("embroider_running_stitch_length_mm", self.running_stitch_length)
+ node.set(INKSTITCH_ATTRIBS['running_stitch_length_mm'], self.running_stitch_length)
stroke = Stroke(node)
diff --git a/lib/svg/rendering.py b/lib/svg/rendering.py
index 2711f12a..5860ceef 100644
--- a/lib/svg/rendering.py
+++ b/lib/svg/rendering.py
@@ -5,10 +5,11 @@ import simplepath
import simplestyle
import simpletransform
-from .tags import INKSCAPE_GROUPMODE, INKSCAPE_LABEL, SVG_DEFS_TAG, SVG_GROUP_TAG, SVG_PATH_TAG
-from .units import PIXELS_PER_MM, get_viewbox_transform
from ..i18n import _
from ..utils import Point, cache
+from .tags import (INKSCAPE_GROUPMODE, INKSCAPE_LABEL, INKSTITCH_ATTRIBS,
+ SVG_DEFS_TAG, SVG_GROUP_TAG, SVG_PATH_TAG)
+from .units import PIXELS_PER_MM, get_viewbox_transform
# The stitch vector path looks like this:
# _______
@@ -198,6 +199,7 @@ def color_block_to_paths(color_block, svg, destination, visual_commands):
add_commands(Stroke(destination[-1]), ["trim"])
color = color_block.color.visible_on_white.to_hex_str()
+
path = inkex.etree.Element(SVG_PATH_TAG, {
'style': simplestyle.formatStyle({
'stroke': color,
@@ -206,7 +208,7 @@ def color_block_to_paths(color_block, svg, destination, visual_commands):
}),
'd': "M" + " ".join(" ".join(str(coord) for coord in point) for point in point_list),
'transform': get_correction_transform(svg),
- 'embroider_manual_stitch': 'true'
+ INKSTITCH_ATTRIBS['manual_stitch']: 'true'
})
destination.append(path)
diff --git a/lib/svg/tags.py b/lib/svg/tags.py
index 55af113a..3e444513 100644
--- a/lib/svg/tags.py
+++ b/lib/svg/tags.py
@@ -1,5 +1,6 @@
import inkex
+
# This is used below and added to the document in ../extensions/base.py.
inkex.NSS['inkstitch'] = 'http://inkstitch.org/namespace'
@@ -13,15 +14,71 @@ SVG_GROUP_TAG = inkex.addNS('g', 'svg')
SVG_SYMBOL_TAG = inkex.addNS('symbol', 'svg')
SVG_USE_TAG = inkex.addNS('use', 'svg')
+EMBROIDERABLE_TAGS = (SVG_PATH_TAG, SVG_POLYLINE_TAG)
+
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')
+
SODIPODI_NAMEDVIEW = inkex.addNS('namedview', 'sodipodi')
SODIPODI_GUIDE = inkex.addNS('guide', 'sodipodi')
SODIPODI_ROLE = inkex.addNS('role', 'sodipodi')
+
INKSTITCH_LETTERING = inkex.addNS('lettering', 'inkstitch')
-EMBROIDERABLE_TAGS = (SVG_PATH_TAG, SVG_POLYLINE_TAG)
+INKSTITCH_ATTRIBS = {}
+# Fill
+inkstitch_attribs = [
+ 'ties',
+ 'trim_after',
+ 'stop_after',
+ # fill
+ 'angle',
+ 'auto_fill',
+ 'expand_mm',
+ 'fill_underlay',
+ 'fill_underlay_angle',
+ 'fill_underlay_inset_mm',
+ 'fill_underlay_max_stitch_length_mm',
+ 'fill_underlay_row_spacing_mm',
+ 'fill_underlay_skip_last',
+ 'max_stitch_length_mm',
+ 'row_spacing_mm',
+ 'end_row_spacing_mm',
+ 'skip_last',
+ 'staggers',
+ 'underlay_underpath',
+ 'underpath',
+ 'flip',
+ 'expand_mm',
+ # stroke
+ 'manual_stitch',
+ 'bean_stitch_repeats',
+ 'repeats',
+ 'running_stitch_length_mm',
+ # satin column
+ 'satin_column',
+ 'satin_column',
+ 'running_stitch_length_mm',
+ 'center_walk_underlay',
+ 'center_walk_underlay_stitch_length_mm',
+ 'contour_underlay',
+ 'contour_underlay_stitch_length_mm',
+ 'contour_underlay_inset_mm',
+ 'zigzag_underlay',
+ 'zigzag_spacing_mm',
+ 'zigzag_underlay_inset_mm',
+ 'zigzag_underlay_spacing_mm',
+ 'e_stitch',
+ 'pull_compensation_mm',
+ 'stroke_first',
+ # Legacy
+ 'embroider_trim_after',
+ 'embroider_stop_after'
+ ]
+for attrib in inkstitch_attribs:
+ INKSTITCH_ATTRIBS[attrib] = inkex.addNS(attrib, 'inkstitch')
diff --git a/lib/svg/units.py b/lib/svg/units.py
index 739dcbb4..319e018b 100644
--- a/lib/svg/units.py
+++ b/lib/svg/units.py
@@ -3,7 +3,6 @@ import simpletransform
from ..i18n import _
from ..utils import cache
-
# modern versions of Inkscape use 96 pixels per inch as per the CSS standard
PIXELS_PER_MM = 96 / 25.4
@@ -127,6 +126,12 @@ def get_viewbox_transform(node):
try:
sx = doc_width / float(viewbox[2])
sy = doc_height / float(viewbox[3])
+
+ # preserve aspect ratio
+ aspect_ratio = node.get('preserveAspectRatio', 'xMidYMid meet')
+ if aspect_ratio != 'none':
+ sx = sy = max(sx, sy) if 'slice' in aspect_ratio else min(sx, sy)
+
scale_transform = simpletransform.parseTransform("scale(%f, %f)" % (sx, sy))
transform = simpletransform.composeTransform(transform, scale_transform)
except ZeroDivisionError:
diff --git a/lib/threads/catalog.py b/lib/threads/catalog.py
index ece2f8ac..aba2696d 100644
--- a/lib/threads/catalog.py
+++ b/lib/threads/catalog.py
@@ -1,9 +1,10 @@
import os
-from os.path import dirname, realpath
import sys
-from glob import glob
from collections import Sequence
+from glob import glob
+from os.path import dirname, realpath
+from ..utils import guess_inkscape_config_path
from .palette import ThreadPalette
@@ -12,19 +13,32 @@ class _ThreadCatalog(Sequence):
def __init__(self):
self.palettes = []
- self.load_palettes(self.get_palettes_path())
+ self.load_palettes(self.get_palettes_paths())
+
+ def get_palettes_paths(self):
+ """Creates a list containing the path of two directories:
+ 1. Palette directory of Inkscape
+ 2. Palette directory of inkstitch
+ """
+ path = [os.path.join(guess_inkscape_config_path(), 'palettes')]
- def get_palettes_path(self):
if getattr(sys, 'frozen', None) is not None:
- path = os.path.join(sys._MEIPASS, "..")
+ inkstitch_path = os.path.join(sys._MEIPASS, "..")
else:
- path = dirname(dirname(dirname(realpath(__file__))))
+ inkstitch_path = dirname(dirname(dirname(realpath(__file__))))
- return os.path.join(path, 'palettes')
+ path.append(os.path.join(inkstitch_path, 'palettes'))
- def load_palettes(self, path):
- for palette_file in glob(os.path.join(path, '*.gpl')):
- self.palettes.append(ThreadPalette(palette_file))
+ return path
+
+ def load_palettes(self, paths):
+ palettes = []
+ for path in paths:
+ for palette_file in glob(os.path.join(path, 'InkStitch*.gpl')):
+ palette_basename = os.path.basename(palette_file)
+ if palette_basename not in palettes:
+ self.palettes.append(ThreadPalette(palette_file))
+ palettes.append(palette_basename)
def palette_names(self):
return list(sorted(palette.name for palette in self))
@@ -59,6 +73,8 @@ class _ThreadCatalog(Sequence):
chosen if more tha 80% of the thread colors in the stitch plan are
exact matches for threads in the palette.
"""
+ if not self.palettes:
+ return None
threads = [color_block.color for color_block in stitch_plan]
palettes_and_matches = [(palette, self._num_exact_color_matches(palette, threads))
diff --git a/lib/threads/palette.py b/lib/threads/palette.py
index 654c43e5..d685e5bb 100644
--- a/lib/threads/palette.py
+++ b/lib/threads/palette.py
@@ -48,13 +48,16 @@ class ThreadPalette(Set):
palette.readline()
for line in palette:
- fields = line.split("\t", 3)
- thread_color = [int(field) for field in fields[:3]]
- thread_name, thread_number = fields[3].strip().rsplit(" ", 1)
- thread_name = thread_name.strip()
-
- thread = ThreadColor(thread_color, thread_name, thread_number, manufacturer=self.name)
- self.threads[thread] = convert_color(sRGBColor(*thread_color, is_upscaled=True), LabColor)
+ try:
+ fields = line.split(None, 3)
+ thread_color = [int(field) for field in fields[:3]]
+ thread_name, thread_number = fields[3].strip().rsplit(" ", 1)
+ thread_name = thread_name.strip()
+
+ thread = ThreadColor(thread_color, thread_name, thread_number, manufacturer=self.name)
+ self.threads[thread] = convert_color(sRGBColor(*thread_color, is_upscaled=True), LabColor)
+ except ValueError:
+ continue
def __contains__(self, thread):
return thread in self.threads