diff options
| author | Lex Neva <lexelby@users.noreply.github.com> | 2018-10-30 17:43:21 -0600 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2018-10-30 17:43:21 -0600 |
| commit | be833f898ff4912b4f1e54be37e6b8ff3c3f2c42 (patch) | |
| tree | f9ea2a1e69c6ea916e7933f9d84cc4178bbec75f /lib/extensions | |
| parent | d9525968a2462270ed5ef0f2ec1742c8ae325079 (diff) | |
new extension: Auto-Route Satin Columns (#330)
**video demo:** https://www.youtube.com/watch?v=tbghtqziB1g
This branch adds a new extension, Auto-Route Satin Columns, implementing #214! This is a huge new feature that opens the door wide for exciting stuff like lettering (#142).
To use it, select some satin columns and run the extension. After a few seconds, it will replace your satins with a new set with a logical stitching order. Under-pathing and jump-stitches will be added as necessary, and satins will be broken to facilitate jumps. The resulting satins will retain all of the parameters you had set on the original satins, including underlay, zig-zag spacing, etc.
By default, it will choose the left-most extreme as the starting point and the right-most extreme as the ending point (even if these occur partway through a satin such as the left edge of a letter "o"). You can override this by attaching the new "Auto-route satin stitch starting/ending position" commands.
There's also an option to add trims instead of jump stitches. Any jump stitch over 1mm is trimmed. I might make this configurable in the future but in my tests it seems to do a good job. Trim commands are added to the SVG, so it's easy enough to modify/delete as you see fit.
Diffstat (limited to 'lib/extensions')
| -rw-r--r-- | lib/extensions/__init__.py | 4 | ||||
| -rw-r--r-- | lib/extensions/auto_satin.py | 108 | ||||
| -rw-r--r-- | lib/extensions/base.py | 29 | ||||
| -rw-r--r-- | lib/extensions/commands.py | 91 | ||||
| -rw-r--r-- | lib/extensions/cut_satin.py | 3 | ||||
| -rw-r--r-- | lib/extensions/layer_commands.py | 16 | ||||
| -rw-r--r-- | lib/extensions/object_commands.py | 86 |
7 files changed, 234 insertions, 103 deletions
diff --git a/lib/extensions/__init__.py b/lib/extensions/__init__.py index 56cd774b..f70c0135 100644 --- a/lib/extensions/__init__.py +++ b/lib/extensions/__init__.py @@ -12,6 +12,7 @@ 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, @@ -26,4 +27,5 @@ __all__ = extensions = [Embroider, LayerCommands, GlobalCommands, ConvertToSatin, - CutSatin] + CutSatin, + AutoSatin] diff --git a/lib/extensions/auto_satin.py b/lib/extensions/auto_satin.py new file mode 100644 index 00000000..e5e9c40b --- /dev/null +++ b/lib/extensions/auto_satin.py @@ -0,0 +1,108 @@ +import sys + +import inkex + +from ..elements import SatinColumn +from ..i18n import _ +from ..stitches.auto_satin import auto_satin +from ..svg import get_correction_transform +from ..svg.tags import SVG_GROUP_TAG, INKSCAPE_LABEL +from .commands import CommandsExtension + + +class AutoSatin(CommandsExtension): + COMMANDS = ["trim"] + + def __init__(self, *args, **kwargs): + CommandsExtension.__init__(self, *args, **kwargs) + + self.OptionParser.add_option("-p", "--preserve_order", dest="preserve_order", type="inkbool", default=False) + + def get_starting_point(self): + return self.get_point("satin_start") + + def get_ending_point(self): + return self.get_point("satin_end") + + def get_point(self, command_type): + command = None + for satin in self.elements: + this_command = satin.get_command(command_type) + if command is not None and this_command: + inkex.errormsg(_("Please ensure that at most one start and end command is attached to the selected satin columns.")) + sys.exit(0) + elif this_command: + command = this_command + + if command is not None: + return command.target_point + + def effect(self): + 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) + + self.add_trims(new_elements, trim_indices) + + def check_selection(self): + if not self.get_elements(): + return + + if not self.selected: + # L10N auto-route satin columns extension + inkex.errormsg(_("Please select one or more satin columns.")) + return False + + return True + + def create_group(self): + first = self.elements[0].node + parent = first.getparent() + insert_index = parent.index(first) + group = inkex.etree.Element(SVG_GROUP_TAG, { + "transform": get_correction_transform(parent, child=True) + }) + parent.insert(insert_index, group) + + return group + + def auto_satin(self): + starting_point = self.get_starting_point() + 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): + element.node.set("id", self.uniqueId("autosatin")) + + # L10N Label for a satin column created by Auto-Route Satin Columns extension + element.node.set(INKSCAPE_LABEL, _("AutoSatin %d") % (i + 1)) + else: + element.node.set("id", self.uniqueId("autosatinrun")) + + # L10N Label for running stitch (underpathing) created by Auto-Route Satin Columns extension + element.node.set(INKSCAPE_LABEL, _("AutoSatin Running Stitch %d") % (i + 1)) + + group.append(element.node) + + def add_trims(self, new_elements, trim_indices): + if self.options.trim and trim_indices: + self.ensure_symbol("trim") + for i in trim_indices: + self.add_commands(new_elements[i], ["trim"]) diff --git a/lib/extensions/base.py b/lib/extensions/base.py index 25de441f..b9bba617 100644 --- a/lib/extensions/base.py +++ b/lib/extensions/base.py @@ -1,14 +1,15 @@ -import inkex -import re -import json -from copy import deepcopy from collections import MutableMapping +from copy import deepcopy +import json +import re + +import inkex from stringcase import snakecase -from ..svg.tags import SVG_GROUP_TAG, INKSCAPE_GROUPMODE, SVG_DEFS_TAG, EMBROIDERABLE_TAGS, SVG_POLYLINE_TAG -from ..elements import AutoFill, Fill, Stroke, SatinColumn, Polyline, EmbroideryElement from ..commands import is_command, layer_commands +from ..elements import AutoFill, Fill, Stroke, SatinColumn, Polyline, EmbroideryElement from ..i18n import _ +from ..svg.tags import SVG_GROUP_TAG, INKSCAPE_GROUPMODE, SVG_DEFS_TAG, EMBROIDERABLE_TAGS, SVG_POLYLINE_TAG SVG_METADATA_TAG = inkex.addNS("metadata", "svg") @@ -21,7 +22,7 @@ def strip_namespace(tag): <<< namedview """ - match = re.match('^\{[^}]+\}(.+)$', tag) + match = re.match(r'^\{[^}]+\}(.+)$', tag) if match: return match.group(1) @@ -211,8 +212,20 @@ class InkstitchExtension(inkex.Effect): return svg_filename + def uniqueId(self, prefix, make_new_id=True): + """Override inkex.Effect.uniqueId with a nicer naming scheme.""" + i = 1 + while True: + new_id = "%s%d" % (prefix, i) + if new_id not in self.doc_ids: + break + i += 1 + self.doc_ids[new_id] = 1 + + return new_id + def parse(self): - """Override inkex.Effect to add Ink/Stitch xml namespace""" + """Override inkex.Effect.parse to add Ink/Stitch xml namespace""" # SVG parsers don't actually look for anything at this URL. They just # care that it's unique. That defines a "namespace" of element and diff --git a/lib/extensions/commands.py b/lib/extensions/commands.py index fb6f7874..07b450e1 100644 --- a/lib/extensions/commands.py +++ b/lib/extensions/commands.py @@ -1,13 +1,21 @@ import os import inkex from copy import deepcopy +from random import random + from .base import InkstitchExtension from ..utils import get_bundled_dir, cache -from ..svg.tags import SVG_DEFS_TAG +from ..commands import get_command_description +from ..i18n import _ +from ..svg.tags import SVG_DEFS_TAG, SVG_PATH_TAG, CONNECTION_START, CONNECTION_END, \ + CONNECTOR_TYPE, INKSCAPE_LABEL, SVG_GROUP_TAG, SVG_USE_TAG, XLINK_HREF +from ..svg import get_correction_transform class CommandsExtension(InkstitchExtension): + """Base class for extensions that manipulate commands.""" + def __init__(self, *args, **kwargs): InkstitchExtension.__init__(self, *args, **kwargs) for command in self.COMMANDS: @@ -37,3 +45,84 @@ class CommandsExtension(InkstitchExtension): path = "./*[@id='inkstitch_%s']" % command if self.defs.find(path) is None: self.defs.append(deepcopy(self.symbol_defs.find(path))) + + def add_connector(self, symbol, element): + # I'd like it if I could position the connector endpoint nicely but inkscape just + # moves it to the element's center immediately after the extension runs. + start_pos = (symbol.get('x'), symbol.get('y')) + end_pos = element.shape.centroid + + path = inkex.etree.Element(SVG_PATH_TAG, + { + "id": self.uniqueId("connector"), + "d": "M %s,%s %s,%s" % (start_pos[0], start_pos[1], end_pos.x, end_pos.y), + "style": "stroke:#000000;stroke-width:1px;stroke-opacity:0.5;fill:none;", + CONNECTION_START: "#%s" % symbol.get('id'), + CONNECTION_END: "#%s" % element.node.get('id'), + CONNECTOR_TYPE: "polyline", + + # l10n: the name of the line that connects a command to the object it applies to + INKSCAPE_LABEL: _("connector") + } + ) + + symbol.getparent().insert(0, path) + + def get_command_pos(self, element, index, total): + # Put command symbols 30 pixels out from the shape, spaced evenly around it. + + # get a line running 30 pixels out from the shape + outline = element.shape.buffer(30).exterior + + # pick this item's spot arond the outline and perturb it a bit to avoid + # stacking up commands if they run the extension multiple times + position = index / float(total) + position += random() * 0.1 + + return outline.interpolate(position, normalized=True) + + def remove_legacy_param(self, element, command): + if command == "trim" or command == "stop": + # If they had the old "TRIM after" or "STOP after" attributes set, + # automatically delete them. THe new commands will do the same + # thing. + # + # If we didn't delete these here, then things would get confusing. + # If the user were to delete a "trim" symbol added by this extension + # but the "embroider_trim_after" attribute is still set, then the + # trim would keep happening. + + attribute = "embroider_%s_after" % command + + if attribute in element.node.attrib: + del element.node.attrib[attribute] + + def add_commands(self, element, commands): + for i, command in enumerate(commands): + self.remove_legacy_param(element, command) + + pos = self.get_command_pos(element, i, len(commands)) + + group = inkex.etree.SubElement(element.node.getparent(), SVG_GROUP_TAG, + { + "id": self.uniqueId("group"), + INKSCAPE_LABEL: _("Ink/Stitch Command") + ": %s" % get_command_description(command), + "transform": get_correction_transform(element.node) + } + ) + + symbol = inkex.etree.SubElement(group, SVG_USE_TAG, + { + "id": self.uniqueId("use"), + XLINK_HREF: "#inkstitch_%s" % command, + "height": "100%", + "width": "100%", + "x": str(pos.x), + "y": str(pos.y), + + # l10n: the name of a command symbol (example: scissors icon for trim command) + INKSCAPE_LABEL: _("command marker"), + } + ) + + self.add_connector(symbol, element) diff --git a/lib/extensions/cut_satin.py b/lib/extensions/cut_satin.py index 0bef794e..b776a68c 100644 --- a/lib/extensions/cut_satin.py +++ b/lib/extensions/cut_satin.py @@ -3,6 +3,7 @@ import inkex from .base import InkstitchExtension from ..i18n import _ from ..elements import SatinColumn +from ..svg import get_correction_transform class CutSatin(InkstitchExtension): @@ -29,9 +30,11 @@ class CutSatin(InkstitchExtension): command.connector.getparent().remove(command.connector) new_satins = satin.split(split_point) + transform = get_correction_transform(satin.node) parent = satin.node.getparent() index = parent.index(satin.node) parent.remove(satin.node) for new_satin in new_satins: + new_satin.node.set('transform', transform) parent.insert(index, new_satin.node) index += 1 diff --git a/lib/extensions/layer_commands.py b/lib/extensions/layer_commands.py index dbafc39f..60a5fab2 100644 --- a/lib/extensions/layer_commands.py +++ b/lib/extensions/layer_commands.py @@ -35,12 +35,12 @@ class LayerCommands(CommandsExtension): inkex.etree.SubElement(self.current_layer, SVG_USE_TAG, { - "id": self.uniqueId("use"), - INKSCAPE_LABEL: _("Ink/Stitch Command") + ": %s" % get_command_description(command), - XLINK_HREF: "#inkstitch_%s" % command, - "height": "100%", - "width": "100%", - "x": str(i * 20), - "y": "-10", - "transform": correction_transform + "id": self.uniqueId("use"), + INKSCAPE_LABEL: _("Ink/Stitch Command") + ": %s" % get_command_description(command), + XLINK_HREF: "#inkstitch_%s" % command, + "height": "100%", + "width": "100%", + "x": str(i * 20), + "y": "-10", + "transform": correction_transform }) diff --git a/lib/extensions/object_commands.py b/lib/extensions/object_commands.py index e678890d..47fb361d 100644 --- a/lib/extensions/object_commands.py +++ b/lib/extensions/object_commands.py @@ -1,97 +1,13 @@ import inkex -from random import random from .commands import CommandsExtension -from ..commands import OBJECT_COMMANDS, get_command_description +from ..commands import OBJECT_COMMANDS from ..i18n import _ -from ..svg.tags import SVG_PATH_TAG, CONNECTION_START, CONNECTION_END, CONNECTOR_TYPE, INKSCAPE_LABEL, SVG_GROUP_TAG, SVG_USE_TAG, XLINK_HREF -from ..svg import get_correction_transform class ObjectCommands(CommandsExtension): COMMANDS = OBJECT_COMMANDS - def add_connector(self, symbol, element): - # I'd like it if I could position the connector endpoint nicely but inkscape just - # moves it to the element's center immediately after the extension runs. - start_pos = (symbol.get('x'), symbol.get('y')) - end_pos = element.shape.centroid - - path = inkex.etree.Element(SVG_PATH_TAG, - { - "id": self.uniqueId("connector"), - "d": "M %s,%s %s,%s" % (start_pos[0], start_pos[1], end_pos.x, end_pos.y), - "style": "stroke:#000000;stroke-width:1px;stroke-opacity:0.5;fill:none;", - CONNECTION_START: "#%s" % symbol.get('id'), - CONNECTION_END: "#%s" % element.node.get('id'), - CONNECTOR_TYPE: "polyline", - - # l10n: the name of the line that connects a command to the object it applies to - INKSCAPE_LABEL: _("connector") - } - ) - - symbol.getparent().insert(0, path) - - def get_command_pos(self, element, index, total): - # Put command symbols 30 pixels out from the shape, spaced evenly around it. - - # get a line running 30 pixels out from the shape - outline = element.shape.buffer(30).exterior - - # pick this item's spot arond the outline and perturb it a bit to avoid - # stacking up commands if they run the extension multiple times - position = index / float(total) - position += random() * 0.1 - - return outline.interpolate(position, normalized=True) - - def remove_legacy_param(self, element, command): - if command == "trim" or command == "stop": - # If they had the old "TRIM after" or "STOP after" attributes set, - # automatically delete them. THe new commands will do the same - # thing. - # - # If we didn't delete these here, then things would get confusing. - # If the user were to delete a "trim" symbol added by this extension - # but the "embroider_trim_after" attribute is still set, then the - # trim would keep happening. - - attribute = "embroider_%s_after" % command - - if attribute in element.node.attrib: - del element.node.attrib[attribute] - - def add_commands(self, element, commands): - for i, command in enumerate(commands): - self.remove_legacy_param(element, command) - - pos = self.get_command_pos(element, i, len(commands)) - - group = inkex.etree.SubElement(element.node.getparent(), SVG_GROUP_TAG, - { - "id": self.uniqueId("group"), - INKSCAPE_LABEL: _("Ink/Stitch Command") + ": %s" % get_command_description(command), - "transform": get_correction_transform(element.node) - } - ) - - symbol = inkex.etree.SubElement(group, SVG_USE_TAG, - { - "id": self.uniqueId("use"), - XLINK_HREF: "#inkstitch_%s" % command, - "height": "100%", - "width": "100%", - "x": str(pos.x), - "y": str(pos.y), - - # l10n: the name of a command symbol (example: scissors icon for trim command) - INKSCAPE_LABEL: _("command marker"), - } - ) - - self.add_connector(symbol, element) - def effect(self): if not self.get_elements(): return |
