diff options
| author | Lex Neva <lexelby@users.noreply.github.com> | 2018-07-13 19:51:25 -0400 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2018-07-13 19:51:25 -0400 |
| commit | c6ebfea5425229f19e12d40391ff1d6253ea4c55 (patch) | |
| tree | 58a53c0730c516b9ce53ec873fd23e9463cb631f /lib | |
| parent | 67342608b0d059bc3ec79001e62d0bc0cdb251d4 (diff) | |
| parent | 6caba7b839e9f4e90ab9f3ff1110c8759e30337d (diff) | |
Merge pull request #224 from inkstitch/lexelby-trim-stop-commands
trim and stop commands
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/commands.py | 3 | ||||
| -rw-r--r-- | lib/elements/element.py | 24 | ||||
| -rw-r--r-- | lib/elements/polyline.py | 7 | ||||
| -rw-r--r-- | lib/elements/satin_column.py | 11 | ||||
| -rw-r--r-- | lib/elements/stroke.py | 7 | ||||
| -rw-r--r-- | lib/extensions/__init__.py | 1 | ||||
| -rw-r--r-- | lib/extensions/commands.py | 161 | ||||
| -rw-r--r-- | lib/extensions/install.py | 10 | ||||
| -rw-r--r-- | lib/stitch_plan/stitch_plan.py | 5 | ||||
| -rw-r--r-- | lib/svg/__init__.py | 2 | ||||
| -rw-r--r-- | lib/svg/path.py | 13 | ||||
| -rw-r--r-- | lib/svg/tags.py | 1 | ||||
| -rw-r--r-- | lib/utils/__init__.py | 1 | ||||
| -rw-r--r-- | lib/utils/paths.py | 10 |
14 files changed, 222 insertions, 34 deletions
diff --git a/lib/commands.py b/lib/commands.py index ec62d716..02c13b25 100644 --- a/lib/commands.py +++ b/lib/commands.py @@ -77,9 +77,6 @@ def find_commands(node): try: commands.append(Command(connector)) except ValueError: - import sys - import traceback - print >> sys.stderr, "not a Command:", connector.get('id'), traceback.format_exc() # Parsing the connector failed, meaning it's not actually an Ink/Stitch command. pass diff --git a/lib/elements/element.py b/lib/elements/element.py index 3c31f1b0..ebca90a4 100644 --- a/lib/elements/element.py +++ b/lib/elements/element.py @@ -206,6 +206,10 @@ class EmbroideryElement(object): return apply_transforms(self.path, self.node) @property + def shape(self): + raise NotImplementedError("INTERNAL ERROR: %s must implement shape()", self.__class__) + + @property @cache def commands(self): return find_commands(self.node) @@ -215,6 +219,10 @@ class EmbroideryElement(object): return [c for c in self.commands if c.command == command] @cache + def has_command(self, command): + return len(self.get_commands(command)) > 0 + + @cache def get_command(self, command): commands = self.get_commands(command) @@ -238,22 +246,10 @@ class EmbroideryElement(object): return [self.strip_control_points(subpath) for subpath in path] @property - @param('trim_after', - _('TRIM after'), - tooltip=_('Trim thread after this object (for supported machines and file formats)'), - type='boolean', - default=False, - sort_index=1000) def trim_after(self): return self.get_boolean_param('trim_after', False) @property - @param('stop_after', - _('STOP after'), - tooltip=_('Add STOP instruction after this object (for supported machines and file formats)'), - type='boolean', - default=False, - sort_index=1000) def stop_after(self): return self.get_boolean_param('stop_after', False) @@ -264,8 +260,8 @@ class EmbroideryElement(object): patches = self.to_patches(last_patch) if patches: - patches[-1].trim_after = self.trim_after - patches[-1].stop_after = self.stop_after + patches[-1].trim_after = self.has_command("trim") or self.trim_after + patches[-1].stop_after = self.has_command("stop") or self.stop_after return patches diff --git a/lib/elements/polyline.py b/lib/elements/polyline.py index 5c474237..b9ffdc0b 100644 --- a/lib/elements/polyline.py +++ b/lib/elements/polyline.py @@ -1,3 +1,5 @@ +from shapely import geometry as shgeo + from .element import param, EmbroideryElement, Patch from ..i18n import _ from ..utils.geometry import Point @@ -28,6 +30,11 @@ class Polyline(EmbroideryElement): return points @property + @cache + def shape(self): + return shgeo.LineString(self.points) + + @property def path(self): # A polyline is a series of connected line segments described by their # points. In order to make use of the existing logic for incorporating diff --git a/lib/elements/satin_column.py b/lib/elements/satin_column.py index 1d13c5e0..2ceb38de 100644 --- a/lib/elements/satin_column.py +++ b/lib/elements/satin_column.py @@ -89,6 +89,17 @@ class SatinColumn(EmbroideryElement): @property @cache + def shape(self): + # This isn't used for satins at all, but other parts of the code + # may need to know the general shape of a satin column. + + flattened = self.flatten(self.parse_path()) + line_strings = [shgeo.LineString(path) for path in flattened] + + return shgeo.MultiLineString(line_strings) + + @property + @cache def csp(self): return self.parse_path() diff --git a/lib/elements/stroke.py b/lib/elements/stroke.py index eca9e0ba..e8eb4783 100644 --- a/lib/elements/stroke.py +++ b/lib/elements/stroke.py @@ -1,4 +1,5 @@ import sys +import shapely.geometry from .element import param, EmbroideryElement, Patch from ..i18n import _ @@ -51,6 +52,12 @@ class Stroke(EmbroideryElement): return self.flatten(path) @property + @cache + def shape(self): + line_strings = [shapely.geometry.LineString(path) for path in self.paths] + return shapely.geometry.MultiLineString(line_strings) + + @property @param('manual_stitch', _('Manual stitch placement'), tooltip=_("Stitch every node in the path. Stitch length and zig-zag spacing are ignored."), type='boolean', default=False) def manual_stitch_mode(self): return self.get_boolean_param('manual_stitch') diff --git a/lib/extensions/__init__.py b/lib/extensions/__init__.py index b11ba1a4..8b243176 100644 --- a/lib/extensions/__init__.py +++ b/lib/extensions/__init__.py @@ -7,3 +7,4 @@ from input import Input from output import Output from zip import Zip from flip import Flip +from commands import Commands diff --git a/lib/extensions/commands.py b/lib/extensions/commands.py new file mode 100644 index 00000000..2f3006ff --- /dev/null +++ b/lib/extensions/commands.py @@ -0,0 +1,161 @@ +import os +import sys +import inkex +import simpletransform +import cubicsuperpath +from copy import deepcopy +from random import random +from shapely import geometry as shgeo + +from .base import InkstitchExtension +from ..i18n import _ +from ..elements import SatinColumn +from ..utils import get_bundled_dir, cache +from ..svg.tags import SVG_DEFS_TAG, SVG_GROUP_TAG, SVG_USE_TAG, SVG_PATH_TAG, INKSCAPE_GROUPMODE, XLINK_HREF, CONNECTION_START, CONNECTION_END, CONNECTOR_TYPE +from ..svg import get_node_transform + + +class Commands(InkstitchExtension): + COMMANDS = ["fill_start", "fill_end", "stop", "trim"] + + def __init__(self, *args, **kwargs): + InkstitchExtension.__init__(self, *args, **kwargs) + for command in self.COMMANDS: + self.OptionParser.add_option("--%s" % command, type="inkbool") + + @property + def symbols_path(self): + return os.path.join(get_bundled_dir("symbols"), "inkstitch.svg") + + @property + @cache + def symbols_svg(self): + with open(self.symbols_path) as symbols_file: + return inkex.etree.parse(symbols_file) + + @property + @cache + def symbol_defs(self): + return self.symbols_svg.find(SVG_DEFS_TAG) + + @property + @cache + def defs(self): + return self.document.find(SVG_DEFS_TAG) + + def ensure_symbol(self, command): + path = "./*[@id='inkstitch_%s']" % command + if self.defs.find(path) is None: + self.defs.append(deepcopy(self.symbol_defs.find(path))) + + def get_correction_transform(self, node): + # if we want to place our new nodes in the same group as this node, + # then we'll need to factor in the effects of any transforms set on + # the parents of this node. + + # we can ignore the transform on the node itself since it won't apply + # to the objects we add + transform = get_node_transform(node.getparent()) + + # now invert it, so that we can position our objects in absolute + # coordinates + transform = simpletransform.invertTransform(transform) + + return simpletransform.formatTransform(transform) + + def add_connector(self, symbol, element): + # I'd like it if I could position the connector endpoint nicely but inkscape just + # moves it to the element's center immediately after the extension runs. + start_pos = (symbol.get('x'), symbol.get('y')) + end_pos = element.shape.centroid + + path = inkex.etree.Element(SVG_PATH_TAG, + { + "id": self.uniqueId("connector"), + "d": "M %s,%s %s,%s" % (start_pos[0], start_pos[1], end_pos.x, end_pos.y), + "style": "stroke:#000000;stroke-width:1px;stroke-opacity:0.5;fill:none;", + "transform": self.get_correction_transform(symbol), + CONNECTION_START: "#%s" % symbol.get('id'), + CONNECTION_END: "#%s" % element.node.get('id'), + CONNECTOR_TYPE: "polyline", + } + ) + + symbol.getparent().insert(symbol.getparent().index(symbol), path) + + def get_command_pos(self, element, index, total): + # Put command symbols 30 pixels out from the shape, spaced evenly around it. + + # get a line running 30 pixels out from the shape + outline = element.shape.buffer(30).exterior + + # pick this item's spot arond the outline and perturb it a bit to avoid + # stacking up commands if they run the extension multiple times + position = index / float(total) + position += random() * 0.1 + + return outline.interpolate(position, normalized=True) + + def remove_legacy_param(self, element, command): + if command == "trim" or command == "stop": + # If they had the old "TRIM after" or "STOP after" attributes set, + # automatically delete them. THe new commands will do the same + # thing. + # + # If we didn't delete these here, then things would get confusing. + # If the user were to delete a "trim" symbol added by this extension + # but the "embroider_trim_after" attribute is still set, then the + # trim would keep happening. + + attribute = "embroider_%s_after" % command + + if attribute in element.node.attrib: + del element.node.attrib[attribute] + + def add_command(self, element, commands): + for i, command in enumerate(commands): + self.remove_legacy_param(element, command) + + pos = self.get_command_pos(element, i, len(commands)) + + symbol = inkex.etree.SubElement(element.node.getparent(), SVG_USE_TAG, + { + "id": self.uniqueId("use"), + XLINK_HREF: "#inkstitch_%s" % command, + "height": "100%", + "width": "100%", + "x": str(pos.x), + "y": str(pos.y), + "transform": self.get_correction_transform(element.node) + } + ) + + self.add_connector(symbol, element) + + def effect(self): + if not self.get_elements(): + return + + if not self.selected: + inkex.errormsg(_("Please select one or more objects to which to attach commands.")) + return + + self.svg = self.document.getroot() + + commands = [command for command in self.COMMANDS if getattr(self.options, command)] + + if not commands: + inkex.errormsg(_("Please choose one or more commands to attach.")) + return + + for command in commands: + self.ensure_symbol(command) + + # Each object (node) in the SVG may correspond to multiple Elements of different + # types (e.g. stroke + fill). We only want to process each one once. + seen_nodes = set() + + for element in self.elements: + if element.node not in seen_nodes: + self.add_command(element, commands) + seen_nodes.add(element.node) diff --git a/lib/extensions/install.py b/lib/extensions/install.py index d55b96d0..42a92113 100644 --- a/lib/extensions/install.py +++ b/lib/extensions/install.py @@ -13,7 +13,7 @@ import logging import wx import inkex -from ..utils import guess_inkscape_config_path +from ..utils import guess_inkscape_config_path, get_bundled_dir class InstallerFrame(wx.Frame): @@ -78,15 +78,9 @@ class InstallerFrame(wx.Frame): def install_addons(self, type): path = os.path.join(self.path, type) - src_dir = self.get_bundled_dir(type) + src_dir = get_bundled_dir(type) self.copy_files(glob(os.path.join(src_dir, "*")), path) - def get_bundled_dir(self, name): - if getattr(sys, 'frozen', None) is not None: - return realpath(os.path.join(sys._MEIPASS, '..', name)) - else: - return realpath(os.path.join(dirname(realpath(__file__)), '..', '..', name)) - if (sys.platform == "win32"): # If we try to just use shutil.copy it says the operation requires elevation. def copy_files(self, files, dest): diff --git a/lib/stitch_plan/stitch_plan.py b/lib/stitch_plan/stitch_plan.py index 93bcd195..742916f0 100644 --- a/lib/stitch_plan/stitch_plan.py +++ b/lib/stitch_plan/stitch_plan.py @@ -183,10 +183,7 @@ class ColorBlock(object): def num_stops(self): """Number of pauses in this color block.""" - # Stops are encoded using two STOP stitches each. See the comment in - # stop.py for an explanation. - - return sum(1 for stitch in self if stitch.stop) / 2 + return sum(1 for stitch in self if stitch.stop) @property def num_trims(self): diff --git a/lib/svg/__init__.py b/lib/svg/__init__.py index 50543b1b..8e846555 100644 --- a/lib/svg/__init__.py +++ b/lib/svg/__init__.py @@ -1,3 +1,3 @@ from .svg import color_block_to_point_lists, render_stitch_plan from .units import * -from .path import apply_transforms +from .path import apply_transforms, get_node_transform diff --git a/lib/svg/path.py b/lib/svg/path.py index a8012774..2d9c0ff3 100644 --- a/lib/svg/path.py +++ b/lib/svg/path.py @@ -4,6 +4,14 @@ import cubicsuperpath from .units import get_viewbox_transform def apply_transforms(path, node): + transform = get_node_transform(node) + + # apply the combined transform to this node's path + simpletransform.applyTransformToPath(transform, path) + + return path + +def get_node_transform(node): # start with the identity transform transform = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]] @@ -14,7 +22,4 @@ def apply_transforms(path, node): viewbox_transform = get_viewbox_transform(node.getroottree().getroot()) transform = simpletransform.composeTransform(viewbox_transform, transform) - # apply the combined transform to this node's path - simpletransform.applyTransformToPath(transform, path) - - return path + return transform diff --git a/lib/svg/tags.py b/lib/svg/tags.py index 5488608c..7eb87540 100644 --- a/lib/svg/tags.py +++ b/lib/svg/tags.py @@ -12,6 +12,7 @@ INKSCAPE_LABEL = inkex.addNS('label', 'inkscape') INKSCAPE_GROUPMODE = inkex.addNS('groupmode', 'inkscape') CONNECTION_START = inkex.addNS('connection-start', 'inkscape') CONNECTION_END = inkex.addNS('connection-end', 'inkscape') +CONNECTOR_TYPE = inkex.addNS('connector-type', 'inkscape') XLINK_HREF = inkex.addNS('href', 'xlink') EMBROIDERABLE_TAGS = (SVG_PATH_TAG, SVG_POLYLINE_TAG) diff --git a/lib/utils/__init__.py b/lib/utils/__init__.py index ff06d4a9..78d037f1 100644 --- a/lib/utils/__init__.py +++ b/lib/utils/__init__.py @@ -2,3 +2,4 @@ from geometry import * from cache import cache from io import * from inkscape import * +from paths import * diff --git a/lib/utils/paths.py b/lib/utils/paths.py new file mode 100644 index 00000000..863e8e69 --- /dev/null +++ b/lib/utils/paths.py @@ -0,0 +1,10 @@ +import sys +import os +from os.path import dirname, realpath + + +def get_bundled_dir(name): + if getattr(sys, 'frozen', None) is not None: + return realpath(os.path.join(sys._MEIPASS, "..", name)) + else: + return realpath(os.path.join(dirname(realpath(__file__)), '..', '..', name)) |
