diff options
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/elements/__init__.py | 7 | ||||
| -rw-r--r-- | lib/elements/utils.py | 47 | ||||
| -rw-r--r-- | lib/extensions/__init__.py | 23 | ||||
| -rw-r--r-- | lib/extensions/auto_satin.py | 24 | ||||
| -rw-r--r-- | lib/extensions/base.py | 48 | ||||
| -rw-r--r-- | lib/extensions/embroider.py | 4 | ||||
| -rw-r--r-- | lib/extensions/layer_commands.py | 14 | ||||
| -rw-r--r-- | lib/extensions/lettering.py | 39 | ||||
| -rw-r--r-- | lib/lettering/__init__.py | 1 | ||||
| -rw-r--r-- | lib/lettering/font.py | 185 | ||||
| -rw-r--r-- | lib/lettering/font_variant.py | 86 | ||||
| -rw-r--r-- | lib/lettering/glyph.py | 86 | ||||
| -rw-r--r-- | lib/stitches/auto_satin.py | 91 | ||||
| -rw-r--r-- | lib/svg/__init__.py | 1 | ||||
| -rw-r--r-- | lib/svg/guides.py | 43 | ||||
| -rw-r--r-- | lib/svg/path.py | 16 | ||||
| -rw-r--r-- | lib/svg/tags.py | 2 | ||||
| -rw-r--r-- | lib/utils/__init__.py | 1 | ||||
| -rw-r--r-- | lib/utils/string.py | 5 |
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] |
