summaryrefslogtreecommitdiff
path: root/lib/elements
diff options
context:
space:
mode:
Diffstat (limited to 'lib/elements')
-rw-r--r--lib/elements/clone.py81
-rw-r--r--lib/elements/element.py24
2 files changed, 79 insertions, 26 deletions
diff --git a/lib/elements/clone.py b/lib/elements/clone.py
index 9a89b6ff..91c69740 100644
--- a/lib/elements/clone.py
+++ b/lib/elements/clone.py
@@ -5,18 +5,20 @@
from math import degrees
from contextlib import contextmanager
-from typing import Generator, List
+from typing import Generator, List, Dict
from inkex import Transform, BaseElement
from shapely import MultiLineString
from ..stitch_plan.stitch_group import StitchGroup
-from ..commands import is_command_symbol
+from ..commands import is_command_symbol, find_commands
from ..i18n import _
+from ..svg.svg import copy_no_children
from ..svg.path import get_node_transform
from ..svg.tags import (EMBROIDERABLE_TAGS, INKSTITCH_ATTRIBS, SVG_USE_TAG,
- XLINK_HREF, SVG_GROUP_TAG)
+ XLINK_HREF, CONNECTION_START, CONNECTION_END,
+ SVG_GROUP_TAG)
from ..utils import cache
from .element import EmbroideryElement, param
from .validation import ValidationWarning
@@ -88,7 +90,8 @@ class Clone(EmbroideryElement):
stitch_groups = []
for element in elements:
- element_stitch_groups = element.to_stitch_groups(last_stitch_group)
+ # Using `embroider` here to get trim/stop after commands, etc.
+ element_stitch_groups = element.embroider(last_stitch_group)
if len(element_stitch_groups):
last_stitch_group = element_stitch_groups[-1]
stitch_groups.extend(element_stitch_groups)
@@ -104,40 +107,40 @@ class Clone(EmbroideryElement):
Could possibly be refactored into just a generator - being a context manager is mainly to control the lifecycle of the elements
that are cloned (again, for testing convenience primarily)
"""
- parent: BaseElement = self.node.getparent()
- cloned_node = self.resolve_clone()
+ cloned_nodes = self.resolve_clone()
try:
# In a try block so we can ensure that the cloned_node is removed from the tree in the event of an exception.
# Otherwise, it might be left around on the document if we throw for some reason.
- yield self.clone_to_elements(cloned_node)
+ yield self.clone_to_elements(cloned_nodes[0])
finally:
# Remove the "manually cloned" tree.
- parent.remove(cloned_node)
+ for cloned_node in cloned_nodes:
+ cloned_node.delete()
- def resolve_clone(self, recursive=True) -> BaseElement:
+ def resolve_clone(self, recursive=True) -> List[BaseElement]:
"""
"Resolve" this clone element by copying the node it hrefs as if unlinking the clone in Inkscape.
The node will be added as a sibling of this element's node, with its transform and style applied.
The fill angles for resolved elements will be rotated per the transform and clone_fill_angle properties of the clone.
:param recursive: Recursively "resolve" all child clones in the same manner
- :returns: The "resolved" node
+ :returns: A list where the first element is the "resolved" node, and zero or more commands attached to that node
"""
parent: BaseElement = self.node.getparent()
source_node: BaseElement = self.node.href
source_parent: BaseElement = source_node.getparent()
- cloned_node = source_node.copy()
+ cloned_node = clone_with_fixup(parent, source_node)
if recursive:
# Recursively resolve all clones as if the clone was in the same place as its source
source_parent.add(cloned_node)
if is_clone(cloned_node):
- cloned_node = cloned_node.replace_with(Clone(cloned_node).resolve_clone())
+ cloned_node = cloned_node.replace_with(Clone(cloned_node).resolve_clone()[0])
else:
clones: List[BaseElement] = [n for n in cloned_node.iterdescendants() if is_clone(n)]
for clone in clones:
- clone.replace_with(Clone(clone).resolve_clone())
+ clone.replace_with(Clone(clone).resolve_clone()[0])
source_parent.remove(cloned_node)
@@ -153,12 +156,18 @@ class Clone(EmbroideryElement):
# Compute angle transform:
# Effectively, this is (local clone transform) * (to parent space) * (from clone's parent space)
# There is a translation component here that will be ignored.
- source_transform = source_parent.composed_transform()
- clone_transform = self.node.composed_transform()
+ source_transform: Transform = source_parent.composed_transform()
+ clone_transform: Transform = self.node.composed_transform()
angle_transform = clone_transform @ -source_transform
self.apply_angles(cloned_node, angle_transform)
- return cloned_node
+ ret = [cloned_node]
+
+ # We need to copy all commands that were attached directly to the href'd node
+ for command in find_commands(source_node):
+ ret.append(command.clone(cloned_node))
+
+ return ret
def apply_angles(self, cloned_node: BaseElement, transform: Transform) -> None:
"""
@@ -174,6 +183,11 @@ class Clone(EmbroideryElement):
if node.tag not in EMBROIDERABLE_TAGS:
continue
+ # Only need to adjust angles on fill elements.
+ element = EmbroideryElement(node)
+ if not (element.get_style("fill", "black") and not element.get_style('fill-opacity', 1) == "0"):
+ continue
+
# Normally, rotate the cloned element's angle by the clone's rotation.
if self.clone_fill_angle is None:
element_angle = float(node.get(INKSTITCH_ATTRIBS['angle'], 0))
@@ -212,3 +226,38 @@ def is_clone(node):
if node.tag == SVG_USE_TAG and node.get(XLINK_HREF) and not is_command_symbol(node):
return True
return False
+
+
+def clone_with_fixup(parent: BaseElement, node: BaseElement) -> BaseElement:
+ """
+ Clone the node, placing the clone as a child of parent, and fix up
+ references in the cloned subtree to point to elements from the clone subtree.
+ """
+ # A map of "#id" -> "#corresponding-id-in-the-cloned-subtree"
+ id_map: Dict[str, str] = {}
+
+ def clone_children(parent: BaseElement, node: BaseElement) -> BaseElement:
+ # Copy the node without copying its children.
+ cloned = copy_no_children(node)
+ parent.append(cloned)
+ id_map[f"#{node.get_id()}"] = f"#{cloned.get_id()}"
+
+ for child in node.getchildren():
+ clone_children(cloned, child)
+
+ return cloned
+
+ ret = clone_children(parent, node)
+
+ def fixup_id_attr(node: BaseElement, attr: str):
+ # Replace the id value for this attrib with the corresponding one in the clone subtree, if applicable.
+ val = node.get(attr)
+ if val is not None:
+ node.set(attr, id_map.get(val, val))
+
+ for n in ret.iter():
+ fixup_id_attr(n, XLINK_HREF)
+ fixup_id_attr(n, CONNECTION_START)
+ fixup_id_attr(n, CONNECTION_END)
+
+ return ret
diff --git a/lib/elements/element.py b/lib/elements/element.py
index 5cc13174..8884bc2a 100644
--- a/lib/elements/element.py
+++ b/lib/elements/element.py
@@ -9,8 +9,10 @@ from copy import deepcopy
import inkex
import numpy as np
from inkex import bezier, BaseElement
+from typing import List, Optional
-from ..commands import find_commands
+from ..commands import Command, find_commands
+from ..stitch_plan import StitchGroup
from ..debug.debug import debug
from ..exceptions import InkstitchException, format_uncaught_exception
from ..i18n import _
@@ -436,19 +438,19 @@ class EmbroideryElement(object):
@property
@cache
- def commands(self):
+ def commands(self) -> List[Command]:
return find_commands(self.node)
@cache
- def get_commands(self, command):
+ def get_commands(self, command: str) -> List[Command]:
return [c for c in self.commands if c.command == command]
@cache
- def has_command(self, command):
+ def has_command(self, command: str) -> bool:
return len(self.get_commands(command)) > 0
@cache
- def get_command(self, command):
+ def get_command(self, command: str) -> Optional[Command]:
commands = self.get_commands(command)
if commands:
@@ -495,7 +497,7 @@ class EmbroideryElement(object):
return lock_start, lock_end
- def to_stitch_groups(self, last_stitch_group):
+ def to_stitch_groups(self, last_stitch_group: Optional[StitchGroup]) -> List[StitchGroup]:
raise NotImplementedError("%s must implement to_stitch_groups()" % self.__class__.__name__)
@debug.time
@@ -517,7 +519,7 @@ class EmbroideryElement(object):
return stitch_groups
- def uses_previous_stitch(self):
+ def uses_previous_stitch(self) -> bool:
"""Returns True if the previous stitch can affect this Element's stitches.
This function may be overridden in a subclass.
@@ -589,7 +591,7 @@ class EmbroideryElement(object):
return cache_key
- def embroider(self, last_stitch_group):
+ def embroider(self, last_stitch_group: Optional[StitchGroup]) -> List[StitchGroup]:
debug.log(f"starting {self.node.get('id')} {self.node.get(INKSCAPE_LABEL)}")
with self.handle_unexpected_exceptions():
@@ -606,8 +608,10 @@ class EmbroideryElement(object):
apply_patterns(stitch_groups, self.node)
if stitch_groups:
- stitch_groups[-1].trim_after = self.has_command("trim") or self.trim_after
- stitch_groups[-1].stop_after = self.has_command("stop") or self.stop_after
+ # In some cases (clones) the last stitch group may have trim_after or stop_after already set,
+ # and we shouldn't override that with this element's values, hence the use of or-equals
+ stitch_groups[-1].trim_after |= self.has_command("trim") or self.trim_after
+ stitch_groups[-1].stop_after |= self.has_command("stop") or self.stop_after
for stitch_group in stitch_groups:
stitch_group.min_jump_stitch_length = self.min_jump_stitch_length