summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/elements/__init__.py7
-rw-r--r--lib/elements/utils.py47
-rw-r--r--lib/extensions/__init__.py23
-rw-r--r--lib/extensions/auto_satin.py24
-rw-r--r--lib/extensions/base.py48
-rw-r--r--lib/extensions/embroider.py4
-rw-r--r--lib/extensions/layer_commands.py14
-rw-r--r--lib/extensions/lettering.py39
-rw-r--r--lib/lettering/__init__.py1
-rw-r--r--lib/lettering/font.py185
-rw-r--r--lib/lettering/font_variant.py86
-rw-r--r--lib/lettering/glyph.py86
-rw-r--r--lib/stitches/auto_satin.py91
-rw-r--r--lib/svg/__init__.py1
-rw-r--r--lib/svg/guides.py43
-rw-r--r--lib/svg/path.py16
-rw-r--r--lib/svg/tags.py2
-rw-r--r--lib/utils/__init__.py1
-rw-r--r--lib/utils/string.py5
19 files changed, 629 insertions, 94 deletions
diff --git a/lib/elements/__init__.py b/lib/elements/__init__.py
index 22603217..5413ba04 100644
--- a/lib/elements/__init__.py
+++ b/lib/elements/__init__.py
@@ -1,6 +1,7 @@
+from auto_fill import AutoFill
from element import EmbroideryElement
+from fill import Fill
+from polyline import Polyline
from satin_column import SatinColumn
from stroke import Stroke
-from polyline import Polyline
-from fill import Fill
-from auto_fill import AutoFill
+from utils import node_to_elements, nodes_to_elements
diff --git a/lib/elements/utils.py b/lib/elements/utils.py
new file mode 100644
index 00000000..87dfa877
--- /dev/null
+++ b/lib/elements/utils.py
@@ -0,0 +1,47 @@
+
+from ..commands import is_command
+from ..svg.tags import SVG_POLYLINE_TAG, SVG_PATH_TAG
+
+from .auto_fill import AutoFill
+from .element import EmbroideryElement
+from .fill import Fill
+from .polyline import Polyline
+from .satin_column import SatinColumn
+from .stroke import Stroke
+
+
+def node_to_elements(node):
+ if node.tag == SVG_POLYLINE_TAG:
+ return [Polyline(node)]
+ elif node.tag == SVG_PATH_TAG:
+ element = EmbroideryElement(node)
+
+ if element.get_boolean_param("satin_column"):
+ return [SatinColumn(node)]
+ else:
+ elements = []
+
+ if element.get_style("fill", "black"):
+ if element.get_boolean_param("auto_fill", True):
+ elements.append(AutoFill(node))
+ else:
+ elements.append(Fill(node))
+
+ if element.get_style("stroke"):
+ if not is_command(element.node):
+ elements.append(Stroke(node))
+
+ if element.get_boolean_param("stroke_first", False):
+ elements.reverse()
+
+ return elements
+ else:
+ return []
+
+
+def nodes_to_elements(nodes):
+ elements = []
+ for node in nodes:
+ elements.extend(node_to_elements(node))
+
+ return elements
diff --git a/lib/extensions/__init__.py b/lib/extensions/__init__.py
index f70c0135..741973ab 100644
--- a/lib/extensions/__init__.py
+++ b/lib/extensions/__init__.py
@@ -1,18 +1,20 @@
+from auto_satin import AutoSatin
+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 input import Input
from install import Install
+from layer_commands import LayerCommands
+from lettering import Lettering
+from object_commands import ObjectCommands
+from output import Output
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 object_commands import ObjectCommands
-from layer_commands import LayerCommands
-from global_commands import GlobalCommands
-from convert_to_satin import ConvertToSatin
-from cut_satin import CutSatin
-from auto_satin import AutoSatin
+
__all__ = extensions = [Embroider,
Install,
@@ -28,4 +30,5 @@ __all__ = extensions = [Embroider,
GlobalCommands,
ConvertToSatin,
CutSatin,
- AutoSatin]
+ AutoSatin,
+ Lettering]
diff --git a/lib/extensions/auto_satin.py b/lib/extensions/auto_satin.py
index e5e9c40b..f846ac6b 100644
--- a/lib/extensions/auto_satin.py
+++ b/lib/extensions/auto_satin.py
@@ -41,14 +41,15 @@ class AutoSatin(CommandsExtension):
if not self.check_selection():
return
- group = self.create_group()
- new_elements, trim_indices = self.auto_satin()
-
- # The ordering is careful here. Some of the original satins may have
- # been used unmodified. That's why we remove all of the original
- # satins _first_ before adding new_elements back into the SVG.
- self.remove_original_satins()
- self.add_elements(group, new_elements)
+ if self.options.preserve_order:
+ # when preservering order, auto_satin() takes care of putting the
+ # newly-created elements into the existing group nodes in the SVG
+ # DOM
+ new_elements, trim_indices = self.auto_satin()
+ else:
+ group = self.create_group()
+ new_elements, trim_indices = self.auto_satin()
+ self.add_elements(group, new_elements)
self.add_trims(new_elements, trim_indices)
@@ -79,13 +80,6 @@ class AutoSatin(CommandsExtension):
ending_point = self.get_ending_point()
return auto_satin(self.elements, self.options.preserve_order, starting_point, ending_point)
- def remove_original_satins(self):
- for element in self.elements:
- for command in element.commands:
- command.connector.getparent().remove(command.connector)
- command.use.getparent().remove(command.use)
- element.node.getparent().remove(element.node)
-
def add_elements(self, group, new_elements):
for i, element in enumerate(new_elements):
if isinstance(element, SatinColumn):
diff --git a/lib/extensions/base.py b/lib/extensions/base.py
index b9bba617..279ca396 100644
--- a/lib/extensions/base.py
+++ b/lib/extensions/base.py
@@ -6,10 +6,10 @@ import re
import inkex
from stringcase import snakecase
-from ..commands import is_command, layer_commands
-from ..elements import AutoFill, Fill, Stroke, SatinColumn, Polyline, EmbroideryElement
+from ..commands import layer_commands
+from ..elements import EmbroideryElement, nodes_to_elements
from ..i18n import _
-from ..svg.tags import SVG_GROUP_TAG, INKSCAPE_GROUPMODE, SVG_DEFS_TAG, EMBROIDERABLE_TAGS, SVG_POLYLINE_TAG
+from ..svg.tags import SVG_GROUP_TAG, INKSCAPE_GROUPMODE, SVG_DEFS_TAG, EMBROIDERABLE_TAGS
SVG_METADATA_TAG = inkex.addNS("metadata", "svg")
@@ -109,6 +109,16 @@ class InkstitchExtension(inkex.Effect):
if g.get(INKSCAPE_GROUPMODE) == "layer":
g.set("style", "display:none")
+ def ensure_current_layer(self):
+ # if no layer is selected, inkex defaults to the root, which isn't
+ # particularly useful
+ if self.current_layer is self.document.getroot():
+ try:
+ self.current_layer = self.document.xpath(".//svg:g[@inkscape:groupmode='layer']", namespaces=inkex.NSS)[0]
+ except IndexError:
+ # No layers at all?? Fine, we'll stick with the default.
+ pass
+
def no_elements_error(self):
if self.selected:
inkex.errormsg(_("No embroiderable paths selected."))
@@ -151,38 +161,8 @@ class InkstitchExtension(inkex.Effect):
def get_nodes(self):
return self.descendants(self.document.getroot())
- def detect_classes(self, node):
- if node.tag == SVG_POLYLINE_TAG:
- return [Polyline]
- else:
- element = EmbroideryElement(node)
-
- if element.get_boolean_param("satin_column"):
- return [SatinColumn]
- else:
- classes = []
-
- 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"):
- if not is_command(element.node):
- classes.append(Stroke)
-
- if element.get_boolean_param("stroke_first", False):
- classes.reverse()
-
- return classes
-
def get_elements(self):
- self.elements = []
- for node in self.get_nodes():
- classes = self.detect_classes(node)
- self.elements.extend(cls(node) for cls in classes)
-
+ self.elements = nodes_to_elements(self.get_nodes())
if self.elements:
return True
else:
diff --git a/lib/extensions/embroider.py b/lib/extensions/embroider.py
index 7c8adfc9..1a578031 100644
--- a/lib/extensions/embroider.py
+++ b/lib/extensions/embroider.py
@@ -1,15 +1,15 @@
import os
-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 .base import InkstitchExtension
class Embroider(InkstitchExtension):
def __init__(self, *args, **kwargs):
- InkstitchExtension.__init__(self)
+ InkstitchExtension.__init__(self, *args, **kwargs)
self.OptionParser.add_option("-c", "--collapse_len_mm",
action="store", type="float",
dest="collapse_length_mm", default=3.0,
diff --git a/lib/extensions/layer_commands.py b/lib/extensions/layer_commands.py
index 60a5fab2..3a746fcf 100644
--- a/lib/extensions/layer_commands.py
+++ b/lib/extensions/layer_commands.py
@@ -1,25 +1,15 @@
import inkex
-from .commands import CommandsExtension
from ..commands import LAYER_COMMANDS, get_command_description
from ..i18n import _
-from ..svg.tags import SVG_USE_TAG, INKSCAPE_LABEL, XLINK_HREF
from ..svg import get_correction_transform
+from ..svg.tags import SVG_USE_TAG, INKSCAPE_LABEL, XLINK_HREF
+from .commands import CommandsExtension
class LayerCommands(CommandsExtension):
COMMANDS = LAYER_COMMANDS
- def ensure_current_layer(self):
- # if no layer is selected, inkex defaults to the root, which isn't
- # particularly useful
- if self.current_layer is self.document.getroot():
- try:
- self.current_layer = self.document.xpath(".//svg:g[@inkscape:groupmode='layer']", namespaces=inkex.NSS)[0]
- except IndexError:
- # No layers at all?? Fine, we'll stick with the default.
- pass
-
def effect(self):
commands = [command for command in self.COMMANDS if getattr(self.options, command)]
diff --git a/lib/extensions/lettering.py b/lib/extensions/lettering.py
new file mode 100644
index 00000000..0d6629f8
--- /dev/null
+++ b/lib/extensions/lettering.py
@@ -0,0 +1,39 @@
+import os
+
+from ..i18n import _
+from ..lettering import Font
+from ..svg.tags import SVG_PATH_TAG, SVG_GROUP_TAG, INKSCAPE_LABEL
+from ..utils import get_bundled_dir
+from .commands import CommandsExtension
+
+
+class Lettering(CommandsExtension):
+ COMMANDS = ["trim"]
+
+ def __init__(self, *args, **kwargs):
+ CommandsExtension.__init__(self, *args, **kwargs)
+
+ self.OptionParser.add_option("-t", "--text")
+
+ def effect(self):
+ font_path = os.path.join(get_bundled_dir("fonts"), "small_font")
+ font = Font(font_path)
+ self.ensure_current_layer()
+
+ lines = font.render_text(self.options.text.decode('utf-8'))
+ self.set_labels(lines)
+ self.current_layer.append(lines)
+
+ def set_labels(self, lines):
+ path = 1
+ for node in lines.iterdescendants():
+ if node.tag == SVG_PATH_TAG:
+ node.set("id", self.uniqueId("lettering"))
+
+ # L10N Label for an object created by the Lettering extension
+ node.set(INKSCAPE_LABEL, _("Lettering %d") % path)
+ path += 1
+ elif node.tag == SVG_GROUP_TAG:
+ node.set("id", self.uniqueId("letteringline"))
+
+ # lettering extension already set the label
diff --git a/lib/lettering/__init__.py b/lib/lettering/__init__.py
new file mode 100644
index 00000000..c6201223
--- /dev/null
+++ b/lib/lettering/__init__.py
@@ -0,0 +1 @@
+from font import Font \ No newline at end of file
diff --git a/lib/lettering/font.py b/lib/lettering/font.py
new file mode 100644
index 00000000..4a89df47
--- /dev/null
+++ b/lib/lettering/font.py
@@ -0,0 +1,185 @@
+# -*- coding: UTF-8 -*-
+
+from copy import deepcopy
+import json
+import os
+
+import inkex
+
+from ..elements import nodes_to_elements
+from ..i18n import _
+from ..stitches.auto_satin import auto_satin
+from ..svg import PIXELS_PER_MM
+from ..svg.tags import SVG_GROUP_TAG, SVG_PATH_TAG, INKSCAPE_LABEL
+from ..utils import Point
+from .font_variant import FontVariant
+
+
+def font_metadata(name, default=None, multiplier=None):
+ def getter(self):
+ value = self.metadata.get(name, default)
+
+ if multiplier is not None:
+ value *= multiplier
+
+ return value
+
+ return property(getter)
+
+
+class Font(object):
+ """Represents a font with multiple variants.
+
+ Each font may have multiple FontVariants for left-to-right, right-to-left,
+ etc. Each variant has a set of Glyphs, one per character.
+
+ Properties:
+ path -- the path to the directory containing this font
+ metadata -- A dict of information about the font.
+ name -- Shortcut property for metadata["name"]
+ license -- contents of the font's LICENSE file, or None if no LICENSE file exists.
+ variants -- A dict of FontVariants, with keys in FontVariant.VARIANT_TYPES.
+ """
+
+ def __init__(self, font_path):
+ self.path = font_path
+ self._load_metadata()
+ self._load_license()
+ self._load_variants()
+
+ def _load_metadata(self):
+ try:
+ with open(os.path.join(self.path, "font.json")) as metadata_file:
+ self.metadata = json.load(metadata_file)
+ except IOError:
+ self.metadata = {}
+
+ def _load_license(self):
+ try:
+ with open(os.path.join(self.path, "LICENSE")) as license_file:
+ self.license = license_file.read()
+ except IOError:
+ self.license = None
+
+ def _load_variants(self):
+ self.variants = {}
+
+ for variant in FontVariant.VARIANT_TYPES:
+ try:
+ self.variants[variant] = FontVariant(self.path, variant, self.default_glyph)
+ except IOError:
+ # we'll deal with missing variants when we apply lettering
+ pass
+
+ name = font_metadata('name', '')
+ description = font_metadata('description', '')
+ default_variant = font_metadata('default_variant', FontVariant.LEFT_TO_RIGHT)
+ default_glyph = font_metadata('defalt_glyph', u"�")
+ letter_spacing = font_metadata('letter_spacing', 1.5, multiplier=PIXELS_PER_MM)
+ leading = font_metadata('leading', 5, multiplier=PIXELS_PER_MM)
+ word_spacing = font_metadata('word_spacing', 3, multiplier=PIXELS_PER_MM)
+ kerning_pairs = font_metadata('kerning_pairs', {})
+ auto_satin = font_metadata('auto_satin', True)
+
+ def render_text(self, text, variant=None, back_and_forth=True):
+ if variant is None:
+ variant = self.default_variant
+
+ if back_and_forth:
+ glyph_sets = [self.get_variant(variant), self.get_variant(FontVariant.reversed_variant(variant))]
+ else:
+ glyph_sets = [self.get_variant(variant)] * 2
+
+ line_group = inkex.etree.Element(SVG_GROUP_TAG, {
+ INKSCAPE_LABEL: _("Ink/Stitch Text")
+ })
+ position = Point(0, 0)
+ for i, line in enumerate(text.splitlines()):
+ glyph_set = glyph_sets[i % 2]
+ line = line.strip()
+
+ letter_group = self._render_line(line, position, glyph_set)
+ if glyph_set.variant == FontVariant.RIGHT_TO_LEFT:
+ letter_group[:] = reversed(letter_group)
+ line_group.append(letter_group)
+
+ position.x = 0
+ position.y += self.leading
+
+ if self.auto_satin:
+ self._apply_auto_satin(line_group)
+
+ return line_group
+
+ def get_variant(self, variant):
+ return self.variants.get(variant, self.variants[self.default_variant])
+
+ def _render_line(self, line, position, glyph_set):
+ """Render a line of text.
+
+ An SVG XML node tree will be returned, with an svg:g at its root. If
+ the font metadata requests it, Auto-Satin will be applied.
+
+ Parameters:
+ line -- the line of text to render.
+ position -- Current position. Will be updated to point to the spot
+ immediately after the last character.
+ glyph_set -- a FontVariant instance.
+
+ Returns:
+ An svg:g element containing the rendered text.
+ """
+ group = inkex.etree.Element(SVG_GROUP_TAG, {
+ INKSCAPE_LABEL: line
+ })
+
+ last_character = None
+ for character in line:
+ if character == " ":
+ position.x += self.word_spacing
+ last_character = None
+ else:
+ glyph = glyph_set[character] or glyph_set[self.default_glyph]
+
+ if glyph is not None:
+ node = self._render_glyph(glyph, position, character, last_character)
+ group.append(node)
+
+ last_character = character
+
+ return group
+
+ def _render_glyph(self, glyph, position, character, last_character):
+ """Render a single glyph.
+
+ An SVG XML node tree will be returned, with an svg:g at its root.
+
+ Parameters:
+ glyph -- a Glyph instance
+ position -- Current position. Will be updated based on the width
+ of this character and the letter spacing.
+ character -- the current Unicode character.
+ last_character -- the previous character in the line, or None if
+ we're at the start of the line or a word.
+ """
+
+ node = deepcopy(glyph.node)
+
+ if last_character is not None:
+ position.x += self.letter_spacing + self.kerning_pairs.get(last_character + character, 0) * PIXELS_PER_MM
+
+ transform = "translate(%s, %s)" % position.as_tuple()
+ node.set('transform', transform)
+ position.x += glyph.width
+
+ return node
+
+ def _apply_auto_satin(self, group):
+ """Apply Auto-Satin to an SVG XML node tree with an svg:g at its root.
+
+ The group's contents will be replaced with the results of the auto-
+ satin operation. Any nested svg:g elements will be removed.
+ """
+
+ elements = nodes_to_elements(group.iterdescendants(SVG_PATH_TAG))
+ auto_satin(elements, preserve_order=True)
diff --git a/lib/lettering/font_variant.py b/lib/lettering/font_variant.py
new file mode 100644
index 00000000..445946e2
--- /dev/null
+++ b/lib/lettering/font_variant.py
@@ -0,0 +1,86 @@
+# -*- coding: UTF-8 -*-
+
+import os
+import inkex
+import simplestyle
+
+from ..svg.tags import INKSCAPE_GROUPMODE, INKSCAPE_LABEL
+from .glyph import Glyph
+
+
+class FontVariant(object):
+ """Represents a single variant of a font.
+
+ Each font may have multiple variants for left-to-right, right-to-left,
+ etc. Each variant has a set of Glyphs, one per character.
+
+ A FontVariant instance can be accessed as a dict by using a unicode
+ character as a key.
+
+ Properties:
+ path -- the path to the directory containing this font
+ variant -- the font variant, specified using one of the constants below
+ glyphs -- a dict of Glyphs, with the glyphs' unicode characters as keys.
+ """
+
+ # We use unicode characters rather than English strings for font file names
+ # in order to be more approachable for languages other than English.
+ LEFT_TO_RIGHT = u"→"
+ RIGHT_TO_LEFT = u"←"
+ TOP_TO_BOTTOM = u"↓"
+ BOTTOM_TO_TOP = u"↑"
+ VARIANT_TYPES = (LEFT_TO_RIGHT, RIGHT_TO_LEFT, TOP_TO_BOTTOM, BOTTOM_TO_TOP)
+
+ @classmethod
+ def reversed_variant(cls, variant):
+ if variant == cls.LEFT_TO_RIGHT:
+ return cls.RIGHT_TO_LEFT
+ elif variant == cls.RIGHT_TO_LEFT:
+ return cls.LEFT_TO_RIGHT
+ elif variant == cls.TOP_TO_BOTTOM:
+ return cls.BOTTOM_TO_TOP
+ elif variant == cls.BOTTOM_TO_TOP:
+ return cls.TOP_TO_BOTTOM
+ else:
+ return None
+
+ def __init__(self, font_path, variant, default_glyph=None):
+ self.path = font_path
+ self.variant = variant
+ self.default_glyph = default_glyph
+ self.glyphs = {}
+ self._load_glyphs()
+
+ def _load_glyphs(self):
+ svg_path = os.path.join(self.path, u"%s.svg" % self.variant)
+ svg = inkex.etree.parse(svg_path)
+
+ glyph_layers = svg.xpath(".//svg:g[starts-with(@inkscape:label, 'GlyphLayer-')]", namespaces=inkex.NSS)
+ for layer in glyph_layers:
+ self._clean_group(layer)
+ layer.attrib[INKSCAPE_LABEL] = layer.attrib[INKSCAPE_LABEL].replace("GlyphLayer-", "", 1)
+ glyph_name = layer.attrib[INKSCAPE_LABEL]
+ self.glyphs[glyph_name] = Glyph(layer)
+
+ def _clean_group(self, group):
+ # We'll repurpose the layer as a container group labelled with the
+ # glyph.
+ del group.attrib[INKSCAPE_GROUPMODE]
+
+ style_text = group.get('style')
+
+ if style_text:
+ # The layer may be marked invisible, so we'll clear the 'display'
+ # style.
+ style = simplestyle.parseStyle(group.get('style'))
+ style.pop('display')
+ group.set('style', simplestyle.formatStyle(style))
+
+ def __getitem__(self, character):
+ if character in self.glyphs:
+ return self.glyphs[character]
+ else:
+ return self.glyphs.get(self.default_glyph, None)
+
+ def __contains__(self, character):
+ return character in self.glyphs
diff --git a/lib/lettering/glyph.py b/lib/lettering/glyph.py
new file mode 100644
index 00000000..bb1a971c
--- /dev/null
+++ b/lib/lettering/glyph.py
@@ -0,0 +1,86 @@
+from copy import copy
+
+import cubicsuperpath
+import simpletransform
+
+from ..svg import apply_transforms, get_guides
+from ..svg.tags import SVG_GROUP_TAG, SVG_PATH_TAG
+
+
+class Glyph(object):
+ """Represents a single character in a single font variant.
+
+ For example, the font inkstitch_small may have variants for left-to-right,
+ right-to-left, etc. Each variant would have a set of Glyphs, one for each
+ character in that variant.
+
+ Properties:
+ width -- total width of this glyph including all component satins
+ node -- svg:g XML node containing the component satins in this character
+ """
+
+ def __init__(self, group):
+ """Create a Glyph.
+
+ The nodes will be copied out of their parent SVG document (with nested
+ transforms applied). The original nodes will be unmodified.
+
+ Arguments:
+ group -- an svg:g XML node containing all the paths that make up
+ this Glyph. Nested groups are allowed.
+ """
+
+ self._process_baseline(group.getroottree().getroot())
+ self.node = self._process_group(group)
+ self._process_bbox()
+ self._move_to_origin()
+
+ def _process_group(self, group):
+ new_group = copy(group)
+ new_group.attrib.pop('transform', None)
+ del new_group[:] # delete references to the original group's children
+
+ for node in group:
+ if node.tag == SVG_GROUP_TAG:
+ new_group.append(self._process_group(node))
+ else:
+ node_copy = copy(node)
+
+ if "d" in node.attrib:
+ # Convert the path to absolute coordinates, incorporating all
+ # nested transforms.
+ path = cubicsuperpath.parsePath(node.get("d"))
+ apply_transforms(path, node)
+ node_copy.set("d", cubicsuperpath.formatPath(path))
+
+ # Delete transforms from paths and groups, since we applied
+ # them to the paths already.
+ node_copy.attrib.pop('transform', None)
+
+ new_group.append(node_copy)
+
+ return new_group
+
+ def _process_baseline(self, svg):
+ for guide in get_guides(svg):
+ if guide.label == "baseline":
+ self._baseline = guide.position.y
+ break
+ else:
+ # no baseline guide found, assume 0 for lack of anything better to use...
+ self._baseline = 0
+
+ def _process_bbox(self):
+ left, right, top, bottom = simpletransform.computeBBox(self.node.iterdescendants())
+
+ self.width = right - left
+ self._min_x = left
+
+ def _move_to_origin(self):
+ translate_x = -self._min_x
+ translate_y = -self._baseline
+ transform = "translate(%s, %s)" % (translate_x, translate_y)
+
+ for node in self.node.iter(SVG_PATH_TAG):
+ node.set('transform', transform)
+ simpletransform.fuseTransform(node)
diff --git a/lib/stitches/auto_satin.py b/lib/stitches/auto_satin.py
index 59bf6b0a..7bc3e67c 100644
--- a/lib/stitches/auto_satin.py
+++ b/lib/stitches/auto_satin.py
@@ -1,4 +1,4 @@
-from itertools import chain
+from itertools import chain, izip
import math
import cubicsuperpath
@@ -9,8 +9,8 @@ import simplestyle
import networkx as nx
-from ..elements import Stroke
-from ..svg import PIXELS_PER_MM, line_strings_to_csp
+from ..elements import Stroke, SatinColumn
+from ..svg import PIXELS_PER_MM, line_strings_to_csp, get_correction_transform
from ..svg.tags import SVG_PATH_TAG
from ..utils import Point as InkstitchPoint, cut, cache
@@ -25,7 +25,7 @@ class SatinSegment(object):
reverse -- if True, reverse the direction of the satin
"""
- def __init__(self, satin, start=0.0, end=1.0, reverse=False):
+ def __init__(self, satin, start=0.0, end=1.0, reverse=False, original_satin=None):
"""Initialize a SatinEdge.
Arguments:
@@ -38,6 +38,7 @@ class SatinSegment(object):
"""
self.satin = satin
+ self.original_satin = original_satin or self.satin
self.reverse = reverse
# start and end are stored as normalized projections
@@ -74,17 +75,19 @@ class SatinSegment(object):
to_element = to_satin
def to_running_stitch(self):
- return RunningStitch(self.center_line, self.satin)
+ return RunningStitch(self.center_line, self.original_satin)
def break_up(self, segment_size):
"""Break this SatinSegment up into SatinSegments of the specified size."""
num_segments = int(math.ceil(self.center_line.length / segment_size))
segments = []
- satin = self.to_satin()
for i in xrange(num_segments):
- segments.append(SatinSegment(satin, float(
- i) / num_segments, float(i + 1) / num_segments, self.reverse))
+ segments.append(SatinSegment(self.satin,
+ float(i) / num_segments,
+ float(i + 1) / num_segments,
+ self.reverse,
+ self.original_satin))
if self.reverse:
segments.reverse()
@@ -121,6 +124,10 @@ class SatinSegment(object):
def end_point(self):
return self.satin.center_line.interpolate(self.end, normalized=True)
+ @property
+ def original_node(self):
+ return self.original_satin.node
+
def is_sequential(self, other):
"""Check if a satin segment immediately follows this one on the same satin."""
@@ -213,10 +220,11 @@ 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)
- return Stroke(node)
+ stroke = Stroke(node)
+
+ return stroke
@property
@cache
@@ -228,6 +236,10 @@ class RunningStitch(object):
def end_point(self):
return self.path.interpolate(1.0, normalized=True)
+ @property
+ def original_node(self):
+ return self.original_element.node
+
@cache
def reversed(self):
return RunningStitch(shgeo.LineString(reversed(self.path.coords)), self.style)
@@ -236,6 +248,9 @@ class RunningStitch(object):
if not isinstance(other, RunningStitch):
return False
+ if self.original_element is not other.original_element:
+ return False
+
return self.path.distance(other.path) < 0.5
def __add__(self, other):
@@ -277,6 +292,11 @@ def auto_satin(elements, preserve_order=False, starting_point=None, ending_point
instances (that are running stitch) can be included to indicate how to travel
between two SatinColumns. This works best when preserve_order is True.
+ If preserve_order is True, then the elements and any newly-created elements
+ will be in the same position in the SVG DOM. If preserve_order is False, then
+ the elements will be removed from the SVG DOM and it's up to the caller to
+ decide where to put the returned SVG path nodes.
+
Returns: a list of SVG path nodes making up the selected stitch order.
Jumps between objects are implied if they are not right next to each
other.
@@ -289,7 +309,13 @@ def auto_satin(elements, preserve_order=False, starting_point=None, ending_point
path = find_path(graph, starting_node, ending_node)
operations = path_to_operations(graph, path)
operations = collapse_sequential_segments(operations)
- return operations_to_elements_and_trims(operations)
+ new_elements, trims, original_parents = operations_to_elements_and_trims(operations, preserve_order)
+ remove_original_elements(elements)
+
+ if preserve_order:
+ preserve_original_groups(new_elements, original_parents)
+
+ return new_elements, trims
def build_graph(elements, preserve_order=False):
@@ -306,7 +332,7 @@ def build_graph(elements, preserve_order=False):
segments = []
if isinstance(element, Stroke):
segments.append(RunningStitch(element))
- else:
+ elif isinstance(element, SatinColumn):
whole_satin = SatinSegment(element)
segments = whole_satin.break_up(PIXELS_PER_MM)
@@ -548,26 +574,57 @@ def collapse_sequential_segments(old_operations):
return new_operations
-def operations_to_elements_and_trims(operations):
+def operations_to_elements_and_trims(operations, preserve_order):
"""Convert a list of operations to Elements and locations of trims.
Returns:
- (nodes, trims)
+ (elements, trims, original_parents)
- element -- a list of Element instances
+ elements -- a list of Element instances
trims -- indices of nodes after which the thread should be trimmed
+ original_parents -- a parallel list of the original SVG parent nodes that spawned each element
"""
elements = []
trims = []
+ original_parent_nodes = []
for operation in operations:
- # Ignore JumpStitch opertions. Jump stitches in Ink/Stitch are
+ # Ignore JumpStitch operations. Jump stitches in Ink/Stitch are
# implied and added by Embroider if needed.
if isinstance(operation, (SatinSegment, RunningStitch)):
elements.append(operation.to_element())
+ original_parent_nodes.append(operation.original_node.getparent())
elif isinstance(operation, (JumpStitch)):
if elements and operation.length > PIXELS_PER_MM:
trims.append(len(elements) - 1)
- return elements, list(set(trims))
+ return elements, list(set(trims)), original_parent_nodes
+
+
+def remove_original_elements(elements):
+ for element in elements:
+ for command in element.commands:
+ remove_from_parent(command.connector)
+ remove_from_parent(command.use)
+ remove_from_parent(element.node)
+
+
+def remove_from_parent(node):
+ if node.getparent() is not None:
+ node.getparent().remove(node)
+
+
+def preserve_original_groups(elements, original_parent_nodes):
+ """Ensure that all elements are contained in the original SVG group elements.
+
+ When preserve_order is True, no SatinColumn or Stroke elements will be
+ reordered in the XML tree. This makes it possible to preserve original SVG
+ group membership. We'll ensure that each newly-created Element is added
+ to the group that contained the original SatinColumn that spawned it.
+ """
+
+ for element, parent in izip(elements, original_parent_nodes):
+ if parent is not None:
+ parent.append(element.node)
+ element.node.set('transform', get_correction_transform(parent, child=True))
diff --git a/lib/svg/__init__.py b/lib/svg/__init__.py
index 74a409b6..df76c0d2 100644
--- a/lib/svg/__init__.py
+++ b/lib/svg/__init__.py
@@ -1,3 +1,4 @@
from .svg import color_block_to_point_lists, render_stitch_plan
from .units import *
from .path import apply_transforms, get_node_transform, get_correction_transform, line_strings_to_csp, point_lists_to_csp
+from .guides import get_guides
diff --git a/lib/svg/guides.py b/lib/svg/guides.py
new file mode 100644
index 00000000..3e26a90d
--- /dev/null
+++ b/lib/svg/guides.py
@@ -0,0 +1,43 @@
+import simpletransform
+
+from ..utils import string_to_floats, Point, cache
+from .tags import SODIPODI_NAMEDVIEW, SODIPODI_GUIDE, INKSCAPE_LABEL
+from .units import get_doc_size, get_viewbox_transform
+
+
+class InkscapeGuide(object):
+ def __init__(self, node):
+ self.node = node
+ self.svg = node.getroottree().getroot()
+
+ self._parse()
+
+ def _parse(self):
+ self.label = self.node.get(INKSCAPE_LABEL, "")
+
+ doc_size = list(get_doc_size(self.svg))
+
+ # convert the size from viewbox-relative to real-world pixels
+ viewbox_transform = get_viewbox_transform(self.svg)
+ simpletransform.applyTransformToPoint(simpletransform.invertTransform(viewbox_transform), doc_size)
+
+ self.position = Point(*string_to_floats(self.node.get('position')))
+
+ # inkscape's Y axis is reversed from SVG's, and the guide is in inkscape coordinates
+ self.position.y = doc_size[1] - self.position.y
+
+ # This one baffles me. I think inkscape might have gotten the order of
+ # their vector wrong?
+ parts = string_to_floats(self.node.get('orientation'))
+ self.direction = Point(parts[1], parts[0])
+
+
+@cache
+def get_guides(svg):
+ """Find all Inkscape guides and return as InkscapeGuide instances."""
+
+ namedview = svg.find(SODIPODI_NAMEDVIEW)
+ if namedview is None:
+ return []
+
+ return [InkscapeGuide(node) for node in namedview.findall(SODIPODI_GUIDE)]
diff --git a/lib/svg/path.py b/lib/svg/path.py
index 6212211f..d2b4aee1 100644
--- a/lib/svg/path.py
+++ b/lib/svg/path.py
@@ -1,3 +1,4 @@
+import inkex
import simpletransform
from .units import get_viewbox_transform
@@ -12,6 +13,19 @@ def apply_transforms(path, node):
return path
+def compose_parent_transforms(node, mat):
+ # This is adapted from Inkscape's simpletransform.py's composeParents()
+ # function. That one can't handle nodes that are detached from a DOM.
+
+ trans = node.get('transform')
+ if trans:
+ mat = simpletransform.composeTransform(simpletransform.parseTransform(trans), mat)
+ if node.getparent() is not None:
+ if node.getparent().tag == inkex.addNS('g', 'svg'):
+ mat = compose_parent_transforms(node.getparent(), mat)
+ return mat
+
+
def get_node_transform(node):
# start with the identity transform
transform = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]
@@ -19,7 +33,7 @@ def get_node_transform(node):
# this if is because sometimes inkscape likes to create paths outside of a layer?!
if node.getparent() is not None:
# combine this node's transform with all parent groups' transforms
- transform = simpletransform.composeParents(node, transform)
+ transform = compose_parent_transforms(node, transform)
# add in the transform implied by the viewBox
viewbox_transform = get_viewbox_transform(node.getroottree().getroot())
diff --git a/lib/svg/tags.py b/lib/svg/tags.py
index 7eb87540..55352be2 100644
--- a/lib/svg/tags.py
+++ b/lib/svg/tags.py
@@ -14,5 +14,7 @@ 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')
EMBROIDERABLE_TAGS = (SVG_PATH_TAG, SVG_POLYLINE_TAG)
diff --git a/lib/utils/__init__.py b/lib/utils/__init__.py
index 78d037f1..a6ae4374 100644
--- a/lib/utils/__init__.py
+++ b/lib/utils/__init__.py
@@ -3,3 +3,4 @@ from cache import cache
from io import *
from inkscape import *
from paths import *
+from string import *
diff --git a/lib/utils/string.py b/lib/utils/string.py
new file mode 100644
index 00000000..a7839f7d
--- /dev/null
+++ b/lib/utils/string.py
@@ -0,0 +1,5 @@
+def string_to_floats(string, delimiter=","):
+ """Convert a string of delimiter-separated floats into a list of floats."""
+
+ floats = string.split(delimiter)
+ return [float(num) for num in floats]