kopia lustrzana https://github.com/inkstitch/inkstitch
Fixed clones of group elements not appearing. (#2766)
rodzic
51f2746b90
commit
2bbebe56fd
|
@ -5,4 +5,4 @@
|
||||||
# Instead of files, "--diff" may be passed to check only the lines changed
|
# Instead of files, "--diff" may be passed to check only the lines changed
|
||||||
# by a diff piped to standard input.
|
# by a diff piped to standard input.
|
||||||
|
|
||||||
flake8 --count --max-complexity=10 --max-line-length=150 --statistics --exclude=pyembroidery,__init__.py,electron,build,src,dist "${@:-.}"
|
flake8 --count --max-complexity=10 --max-line-length=150 --statistics --exclude=pyembroidery,__init__.py,electron,build,src,dist,./*-metadata.py,./pyembroidery-format-descriptions.py "${@:-.}"
|
||||||
|
|
|
@ -3,19 +3,24 @@
|
||||||
# Copyright (c) 2010 Authors
|
# Copyright (c) 2010 Authors
|
||||||
# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details.
|
# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details.
|
||||||
|
|
||||||
from math import atan2, degrees, radians
|
from math import degrees
|
||||||
|
from copy import deepcopy
|
||||||
|
from contextlib import contextmanager
|
||||||
|
from typing import Generator, List, Tuple
|
||||||
|
|
||||||
from inkex import CubicSuperPath, Path, Transform
|
from inkex import Transform, BaseElement
|
||||||
from shapely import MultiLineString
|
from shapely import MultiLineString
|
||||||
|
|
||||||
|
from ..stitch_plan.stitch_group import StitchGroup
|
||||||
|
|
||||||
from ..commands import is_command_symbol
|
from ..commands import is_command_symbol
|
||||||
from ..i18n import _
|
from ..i18n import _
|
||||||
from ..svg.path import get_node_transform
|
from ..svg.path import get_node_transform
|
||||||
from ..svg.tags import (EMBROIDERABLE_TAGS, INKSTITCH_ATTRIBS, SVG_USE_TAG,
|
from ..svg.tags import (EMBROIDERABLE_TAGS, INKSTITCH_ATTRIBS, SVG_USE_TAG,
|
||||||
XLINK_HREF)
|
XLINK_HREF, SVG_GROUP_TAG)
|
||||||
from ..utils import cache
|
from ..utils import cache
|
||||||
from .element import EmbroideryElement, param
|
from .element import EmbroideryElement, param
|
||||||
from .validation import ObjectTypeWarning, ValidationWarning
|
from .validation import ValidationWarning
|
||||||
|
|
||||||
|
|
||||||
class CloneWarning(ValidationWarning):
|
class CloneWarning(ValidationWarning):
|
||||||
|
@ -29,21 +34,7 @@ class CloneWarning(ValidationWarning):
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class CloneSourceWarning(ObjectTypeWarning):
|
|
||||||
name = _("Clone is not embroiderable")
|
|
||||||
description = _("There are one ore more clone objects in this document. A clone must be a direct child of an embroiderable element. "
|
|
||||||
"Ink/Stitch cannot embroider clones of groups or other not embroiderable elements (text or image).")
|
|
||||||
steps_to_solve = [
|
|
||||||
_("Convert the clone into a real element:"),
|
|
||||||
_("* Select the clone."),
|
|
||||||
_("* Run: Edit > Clone > Unlink Clone (Alt+Shift+D)")
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class Clone(EmbroideryElement):
|
class Clone(EmbroideryElement):
|
||||||
# A clone embroidery element is linked to an embroiderable element.
|
|
||||||
# It will be ignored if the source element is not a direct child of the xlink attribute.
|
|
||||||
|
|
||||||
element_name = "Clone"
|
element_name = "Clone"
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
@ -62,86 +53,125 @@ class Clone(EmbroideryElement):
|
||||||
type='float')
|
type='float')
|
||||||
@cache
|
@cache
|
||||||
def clone_fill_angle(self):
|
def clone_fill_angle(self):
|
||||||
return self.get_float_param('angle') or None
|
return self.get_float_param('angle')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@param('flip_angle',
|
@param('flip_angle',
|
||||||
_('Flip angle'),
|
_('Flip angle'),
|
||||||
tooltip=_("Flip automatically calucalted angle if it appears to be wrong."),
|
tooltip=_(
|
||||||
|
"Flip automatically calculated angle if it appears to be wrong."),
|
||||||
type='boolean')
|
type='boolean')
|
||||||
@cache
|
@cache
|
||||||
def flip_angle(self):
|
def flip_angle(self):
|
||||||
return self.get_boolean_param('flip_angle')
|
return self.get_boolean_param('flip_angle', False)
|
||||||
|
|
||||||
def get_cache_key_data(self, previous_stitch):
|
def get_cache_key_data(self, previous_stitch):
|
||||||
source_node = get_clone_source(self.node)
|
source_node = self.node.href
|
||||||
source_elements = self.clone_to_element(source_node)
|
source_elements = self.clone_to_elements(source_node)
|
||||||
return [element.get_cache_key(previous_stitch) for element in source_elements]
|
return [element.get_cache_key(previous_stitch) for element in source_elements]
|
||||||
|
|
||||||
def clone_to_element(self, node):
|
def clone_to_elements(self, node):
|
||||||
from .utils import node_to_elements
|
from .utils import node_to_elements
|
||||||
return node_to_elements(node, True)
|
elements = []
|
||||||
|
if node.tag in EMBROIDERABLE_TAGS:
|
||||||
|
elements = node_to_elements(node, True)
|
||||||
|
elif node.tag == SVG_GROUP_TAG:
|
||||||
|
for child in node.iterdescendants():
|
||||||
|
elements.extend(node_to_elements(child, True))
|
||||||
|
return elements
|
||||||
|
|
||||||
def to_stitch_groups(self, last_patch=None):
|
def to_stitch_groups(self, last_patch=None) -> List[StitchGroup]:
|
||||||
patches = []
|
with self.clone_elements() as elements:
|
||||||
|
patches = []
|
||||||
|
|
||||||
source_node = get_clone_source(self.node)
|
for element in elements:
|
||||||
if source_node.tag not in EMBROIDERABLE_TAGS:
|
stitch_groups = element.to_stitch_groups(last_patch)
|
||||||
return []
|
if len(stitch_groups):
|
||||||
|
last_patch = stitch_groups[-1]
|
||||||
|
patches.extend(stitch_groups)
|
||||||
|
|
||||||
old_transform = source_node.get('transform', '')
|
return patches
|
||||||
source_transform = source_node.composed_transform()
|
|
||||||
source_path = Path(source_node.get_path()).transform(source_transform)
|
|
||||||
transform = Transform(source_node.get('transform', '')) @ -source_transform
|
|
||||||
transform @= self.node.composed_transform() @ Transform(source_node.get('transform', ''))
|
|
||||||
source_node.set('transform', transform)
|
|
||||||
|
|
||||||
old_angle = float(source_node.get(INKSTITCH_ATTRIBS['angle'], 0))
|
@contextmanager
|
||||||
if self.clone_fill_angle is None:
|
def clone_elements(self) -> Generator[List[EmbroideryElement], None, None]:
|
||||||
rot = transform.add_rotate(-old_angle)
|
"""
|
||||||
angle = self._get_rotation(rot, source_node, source_path)
|
A context manager method which yields a set of elements representing the cloned element(s) href'd by this clone's element.
|
||||||
if angle is not None:
|
Cleans up after itself afterwards.
|
||||||
source_node.set(INKSTITCH_ATTRIBS['angle'], angle)
|
This is broken out from to_stitch_groups for testing convenience, primarily.
|
||||||
else:
|
Could possibly be refactored into just a generator - being a context manager is mainly to control the lifecycle of the elements
|
||||||
source_node.set(INKSTITCH_ATTRIBS['angle'], self.clone_fill_angle)
|
that are cloned (again, for testing convenience primarily)
|
||||||
|
"""
|
||||||
|
source_node, local_transform = get_concrete_source(self.node)
|
||||||
|
|
||||||
elements = self.clone_to_element(source_node)
|
if source_node.tag not in EMBROIDERABLE_TAGS and source_node.tag != SVG_GROUP_TAG:
|
||||||
for element in elements:
|
yield []
|
||||||
stitch_groups = element.to_stitch_groups(last_patch)
|
return
|
||||||
patches.extend(stitch_groups)
|
|
||||||
|
|
||||||
source_node.set('transform', old_transform)
|
# Effectively, manually clone the href'd element: Place it into the tree at the same location
|
||||||
source_node.set(INKSTITCH_ATTRIBS['angle'], old_angle)
|
# as the use element this Clone represents, with the same transform
|
||||||
return patches
|
parent: BaseElement = self.node.getparent()
|
||||||
|
cloned_node = deepcopy(source_node)
|
||||||
def _get_rotation(self, transform, source_node, source_path):
|
cloned_node.set('transform', local_transform)
|
||||||
|
parent.add(cloned_node)
|
||||||
try:
|
try:
|
||||||
rotation = transform.rotation_degrees()
|
# In a try block so we can ensure that the cloned_node is removed from the tree in the event of an exception.
|
||||||
except ValueError:
|
# Otherwise, it might be left around on the document if we throw for some reason.
|
||||||
source_path = CubicSuperPath(source_path)[0]
|
self.resolve_all_clones(cloned_node)
|
||||||
clone_path = Path(source_node.get_path()).transform(source_node.composed_transform())
|
|
||||||
clone_path = CubicSuperPath(clone_path)[0]
|
|
||||||
|
|
||||||
angle_source = atan2(source_path[1][1][1] - source_path[0][1][1], source_path[1][1][0] - source_path[0][1][0])
|
source_parent_transform = source_node.getparent().composed_transform()
|
||||||
angle_clone = atan2(clone_path[1][1][1] - clone_path[0][1][1], clone_path[1][1][0] - clone_path[0][1][0])
|
clone_transform = cloned_node.composed_transform()
|
||||||
angle_embroidery = radians(-float(source_node.get(INKSTITCH_ATTRIBS['angle'], 0)))
|
global_transform = clone_transform @ -source_parent_transform
|
||||||
|
self.apply_angles(cloned_node, global_transform)
|
||||||
|
|
||||||
diff = angle_source - angle_embroidery
|
yield self.clone_to_elements(cloned_node)
|
||||||
rotation = degrees(diff + angle_clone)
|
finally:
|
||||||
|
# Remove the "manually cloned" tree.
|
||||||
|
parent.remove(cloned_node)
|
||||||
|
|
||||||
|
def resolve_all_clones(self, node: BaseElement) -> None:
|
||||||
|
"""
|
||||||
|
For a subtree, recursively replace all `use` tags with the elements they href.
|
||||||
|
"""
|
||||||
|
clones: List[BaseElement] = [n for n in node.iterdescendants() if n.tag == SVG_USE_TAG]
|
||||||
|
for clone in clones:
|
||||||
|
parent: BaseElement = clone.getparent()
|
||||||
|
source_node, local_transform = get_concrete_source(clone)
|
||||||
|
cloned_node = deepcopy(source_node)
|
||||||
|
parent.add(cloned_node)
|
||||||
|
cloned_node.set('transform', local_transform)
|
||||||
|
parent.remove(clone)
|
||||||
|
self.resolve_all_clones(cloned_node)
|
||||||
|
self.apply_angles(cloned_node, local_transform)
|
||||||
|
|
||||||
|
def apply_angles(self, cloned_node: BaseElement, transform: Transform) -> None:
|
||||||
|
"""
|
||||||
|
Adjust angles on a cloned tree based on their transform.
|
||||||
|
"""
|
||||||
|
if self.clone_fill_angle is None:
|
||||||
|
# Strip out the translation component to simplify the fill vector rotation angle calculation.
|
||||||
|
angle_transform = Transform((transform.a, transform.b, transform.c, transform.d, 0.0, 0.0))
|
||||||
|
|
||||||
|
elements = self.clone_to_elements(cloned_node)
|
||||||
|
for element in elements:
|
||||||
|
# We manipulate the element's node directly here instead of using get/set param methods, because otherwise
|
||||||
|
# we may run into issues due to those methods' use of caching not updating if the underlying param value is changed.
|
||||||
|
|
||||||
|
# Normally, rotate the cloned element's angle by the clone's rotation.
|
||||||
|
if self.clone_fill_angle is None:
|
||||||
|
element_angle = float(element.node.get(INKSTITCH_ATTRIBS['angle'], 0))
|
||||||
|
# We have to negate the angle because SVG/Inkscape's definition of rotation is clockwise, while Inkstitch uses counter-clockwise
|
||||||
|
fill_vector = (angle_transform @ Transform(f"rotate(${-element_angle})")).apply_to_point((1, 0))
|
||||||
|
# Same reason for negation here.
|
||||||
|
element_angle = -degrees(fill_vector.angle)
|
||||||
|
else: # If clone_fill_angle is specified, override the angle instead.
|
||||||
|
element_angle = self.clone_fill_angle
|
||||||
|
|
||||||
if self.flip_angle:
|
if self.flip_angle:
|
||||||
rotation = -degrees(diff - angle_clone)
|
element_angle = -element_angle
|
||||||
|
|
||||||
return -rotation
|
element.node.set(INKSTITCH_ATTRIBS['angle'], element_angle)
|
||||||
|
|
||||||
def get_clone_style(self, style_name, node, default=None):
|
return elements
|
||||||
style = node.style[style_name] or default
|
|
||||||
return style
|
|
||||||
|
|
||||||
def center(self, source_node):
|
|
||||||
transform = get_node_transform(self.node.getparent())
|
|
||||||
center = self.node.bounding_box(transform).center
|
|
||||||
return center
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def shape(self):
|
def shape(self):
|
||||||
|
@ -151,14 +181,15 @@ class Clone(EmbroideryElement):
|
||||||
path = path.to_superpath()
|
path = path.to_superpath()
|
||||||
return MultiLineString(path)
|
return MultiLineString(path)
|
||||||
|
|
||||||
|
def center(self, source_node):
|
||||||
|
transform = get_node_transform(self.node.getparent())
|
||||||
|
center = self.node.bounding_box(transform).center
|
||||||
|
return center
|
||||||
|
|
||||||
def validation_warnings(self):
|
def validation_warnings(self):
|
||||||
source_node = get_clone_source(self.node)
|
source_node = self.node.href
|
||||||
if source_node.tag not in EMBROIDERABLE_TAGS:
|
point = self.center(source_node)
|
||||||
point = self.center(source_node)
|
yield CloneWarning(point)
|
||||||
yield CloneSourceWarning(point)
|
|
||||||
else:
|
|
||||||
point = self.center(source_node)
|
|
||||||
yield CloneWarning(point)
|
|
||||||
|
|
||||||
|
|
||||||
def is_clone(node):
|
def is_clone(node):
|
||||||
|
@ -167,11 +198,21 @@ def is_clone(node):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def is_embroiderable_clone(node):
|
def get_concrete_source(node: BaseElement) -> Tuple[BaseElement, Transform]:
|
||||||
if is_clone(node) and get_clone_source(node).tag in EMBROIDERABLE_TAGS:
|
"""
|
||||||
return True
|
Given a use element, follow hrefs until finding an element that is not a use.
|
||||||
return False
|
Returns that non-use element, and a transform to apply to a copy of that element
|
||||||
|
which will place that copy in the same position as the use if added as a sibling of the use.
|
||||||
|
"""
|
||||||
def get_clone_source(node):
|
# Compute the transform that will be applied to the cloned element, which is based off of the cloned element.
|
||||||
return node.href
|
# This makes intuitive sense: The clone of a scaled element will also be scaled, the clone of a rotated element will also
|
||||||
|
# be rotated, etc. Any transforms from the use element will be applied on top of that.
|
||||||
|
transform = Transform(node.get('transform'))
|
||||||
|
source_node: BaseElement = node.href
|
||||||
|
while source_node.tag == SVG_USE_TAG:
|
||||||
|
# In case the source_node href's a use (and that href's a use...), iterate up the chain until we get a source node,
|
||||||
|
# applying the transforms as we go.
|
||||||
|
transform @= Transform(source_node.get('transform'))
|
||||||
|
source_node = source_node.href
|
||||||
|
transform @= Transform(source_node.get('transform'))
|
||||||
|
return (source_node, transform)
|
||||||
|
|
|
@ -8,7 +8,7 @@ from copy import deepcopy
|
||||||
|
|
||||||
import inkex
|
import inkex
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from inkex import bezier
|
from inkex import bezier, BaseElement
|
||||||
|
|
||||||
from ..commands import find_commands
|
from ..commands import find_commands
|
||||||
from ..debug import debug
|
from ..debug import debug
|
||||||
|
@ -58,7 +58,7 @@ def param(*args, **kwargs):
|
||||||
|
|
||||||
|
|
||||||
class EmbroideryElement(object):
|
class EmbroideryElement(object):
|
||||||
def __init__(self, node):
|
def __init__(self, node: BaseElement):
|
||||||
self.node = node
|
self.node = node
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
|
@ -0,0 +1,337 @@
|
||||||
|
from lib.elements import Clone, EmbroideryElement
|
||||||
|
from lib.svg.tags import INKSTITCH_ATTRIBS, SVG_RECT_TAG
|
||||||
|
from inkex import SvgDocumentElement, Rectangle, Circle, Group, Use, Transform, TextElement
|
||||||
|
from inkex.tester import TestCase
|
||||||
|
from inkex.tester.svg import svg
|
||||||
|
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from math import sqrt
|
||||||
|
|
||||||
|
|
||||||
|
def element_fill_angle(element: EmbroideryElement) -> Optional[float]:
|
||||||
|
angle = element.node.get(INKSTITCH_ATTRIBS['angle'])
|
||||||
|
if angle is not None:
|
||||||
|
angle = float(angle)
|
||||||
|
return angle
|
||||||
|
|
||||||
|
|
||||||
|
class CloneElementTest(TestCase):
|
||||||
|
def assertAngleAlmostEqual(self, a, b):
|
||||||
|
# Take the mod 180 of the returned angles, because e.g. -130deg and 50deg produce fills along the same angle.
|
||||||
|
# We have to use a precision of 4 decimal digits because of the precision of the matrices as they are stored in the svg trees
|
||||||
|
# generated by these tests.
|
||||||
|
self.assertAlmostEqual(a % 180, b % 180, 4)
|
||||||
|
|
||||||
|
def test_not_embroiderable(self):
|
||||||
|
root: SvgDocumentElement = svg()
|
||||||
|
text = root.add(TextElement())
|
||||||
|
text.text = "Can't embroider this!"
|
||||||
|
use = root.add(Use())
|
||||||
|
use.href = text
|
||||||
|
|
||||||
|
clone = Clone(use)
|
||||||
|
stitch_groups = clone.to_stitch_groups(None)
|
||||||
|
self.assertEqual(len(stitch_groups), 0)
|
||||||
|
|
||||||
|
# These tests make sure the element cloning works as expected, using the `clone_elements` method.
|
||||||
|
|
||||||
|
def test_basic(self):
|
||||||
|
root: SvgDocumentElement = svg()
|
||||||
|
rect = root.add(Rectangle(attrib={
|
||||||
|
"width": "10",
|
||||||
|
"height": "10",
|
||||||
|
INKSTITCH_ATTRIBS["angle"]: "30"
|
||||||
|
}))
|
||||||
|
use = root.add(Use())
|
||||||
|
use.href = rect
|
||||||
|
|
||||||
|
clone = Clone(use)
|
||||||
|
with clone.clone_elements() as elements:
|
||||||
|
self.assertEqual(len(elements), 1)
|
||||||
|
self.assertAlmostEqual(element_fill_angle(elements[0]), 30)
|
||||||
|
|
||||||
|
def test_angle_rotated(self):
|
||||||
|
root: SvgDocumentElement = svg()
|
||||||
|
rect = root.add(Rectangle(attrib={
|
||||||
|
"width": "10",
|
||||||
|
"height": "10",
|
||||||
|
INKSTITCH_ATTRIBS["angle"]: "30"
|
||||||
|
}))
|
||||||
|
use = root.add(Use())
|
||||||
|
use.href = rect
|
||||||
|
use.set('transform', Transform().add_rotate(20))
|
||||||
|
|
||||||
|
clone = Clone(use)
|
||||||
|
with clone.clone_elements() as elements:
|
||||||
|
self.assertEqual(len(elements), 1)
|
||||||
|
self.assertAngleAlmostEqual(element_fill_angle(elements[0]), 10)
|
||||||
|
|
||||||
|
def test_angle_flipped(self):
|
||||||
|
root: SvgDocumentElement = svg()
|
||||||
|
rect = root.add(Rectangle(attrib={
|
||||||
|
"width": "10",
|
||||||
|
"height": "10",
|
||||||
|
INKSTITCH_ATTRIBS["angle"]: "30"
|
||||||
|
}))
|
||||||
|
use = root.add(Use())
|
||||||
|
use.href = rect
|
||||||
|
use.set('transform', Transform().add_scale(-1, 1))
|
||||||
|
|
||||||
|
clone = Clone(use)
|
||||||
|
with clone.clone_elements() as elements:
|
||||||
|
self.assertEqual(len(elements), 1)
|
||||||
|
self.assertAngleAlmostEqual(element_fill_angle(elements[0]), -30)
|
||||||
|
|
||||||
|
def test_angle_flipped_rotated(self):
|
||||||
|
root: SvgDocumentElement = svg()
|
||||||
|
rect = root.add(Rectangle(attrib={
|
||||||
|
"width": "10",
|
||||||
|
"height": "10",
|
||||||
|
INKSTITCH_ATTRIBS["angle"]: "30"
|
||||||
|
}))
|
||||||
|
use = root.add(Use())
|
||||||
|
use.href = rect
|
||||||
|
use.set('transform', Transform().add_rotate(20).add_scale(-1, 1))
|
||||||
|
|
||||||
|
clone = Clone(use)
|
||||||
|
with clone.clone_elements() as elements:
|
||||||
|
self.assertEqual(len(elements), 1)
|
||||||
|
# Fill angle goes from 30 -> -30 after flip -> -50 after rotate
|
||||||
|
self.assertAngleAlmostEqual(element_fill_angle(elements[0]), -50)
|
||||||
|
|
||||||
|
def test_angle_non_uniform_scale(self):
|
||||||
|
"""
|
||||||
|
The angle isn't *as* well-defined for non-rotational scales, but we try to follow how the slope will be altered.
|
||||||
|
"""
|
||||||
|
root: SvgDocumentElement = svg()
|
||||||
|
rect = root.add(Rectangle(attrib={
|
||||||
|
"width": "10",
|
||||||
|
"height": "10",
|
||||||
|
INKSTITCH_ATTRIBS["angle"]: "30"
|
||||||
|
}))
|
||||||
|
use = root.add(Use())
|
||||||
|
use.href = rect
|
||||||
|
use.set('transform', Transform().add_rotate(10).add_scale(1, -sqrt(3)))
|
||||||
|
|
||||||
|
clone = Clone(use)
|
||||||
|
with clone.clone_elements() as elements:
|
||||||
|
self.assertEqual(len(elements), 1)
|
||||||
|
# Slope of the stitching goes from tan(30deg) = 1/sqrt(3) to -sqrt(3)/sqrt(3) = tan(-45deg),
|
||||||
|
# then rotated another -10 degrees to -55
|
||||||
|
self.assertAngleAlmostEqual(element_fill_angle(elements[0]), -55)
|
||||||
|
|
||||||
|
def test_angle_inherits_down_tree(self):
|
||||||
|
"""
|
||||||
|
The stitching angle of a clone is based in part on the relative transforms of the source and clone.
|
||||||
|
"""
|
||||||
|
root: SvgDocumentElement = svg()
|
||||||
|
g1 = root.add(Group())
|
||||||
|
g1.set('transform', Transform().add_rotate(3))
|
||||||
|
rect = g1.add(Rectangle(attrib={
|
||||||
|
"width": "10",
|
||||||
|
"height": "10",
|
||||||
|
INKSTITCH_ATTRIBS["angle"]: "30"
|
||||||
|
}))
|
||||||
|
g2 = root.add(Group())
|
||||||
|
g2.set('transform', Transform().add_translate((20, 0)).add_rotate(-7))
|
||||||
|
use = g2.add(Use())
|
||||||
|
use.href = rect
|
||||||
|
use.set('transform', Transform().add_rotate(11))
|
||||||
|
|
||||||
|
clone = Clone(use)
|
||||||
|
with clone.clone_elements() as elements:
|
||||||
|
self.assertEqual(len(elements), 1)
|
||||||
|
# Angle goes from 30 -> 40 (g1 -> g2) -> 29 (use)
|
||||||
|
self.assertAngleAlmostEqual(element_fill_angle(elements[0]), 29)
|
||||||
|
|
||||||
|
def test_transform_inherits_from_cloned_element(self):
|
||||||
|
"""
|
||||||
|
Elements cloned by cloned_elements need to inherit their transform from their href'd element and their use to match what's shown.
|
||||||
|
"""
|
||||||
|
root: SvgDocumentElement = svg()
|
||||||
|
rect = root.add(Rectangle(attrib={
|
||||||
|
"width": "10",
|
||||||
|
"height": "10",
|
||||||
|
INKSTITCH_ATTRIBS["angle"]: "30",
|
||||||
|
}))
|
||||||
|
rect.set('transform', Transform().add_scale(2, 2))
|
||||||
|
use = root.add(Use())
|
||||||
|
use.href = rect
|
||||||
|
use.set('transform', Transform().add_translate((5, 10)))
|
||||||
|
|
||||||
|
clone = Clone(use)
|
||||||
|
with clone.clone_elements() as elements:
|
||||||
|
self.assertEqual(len(elements), 1)
|
||||||
|
self.assertTransformEqual(
|
||||||
|
elements[0].node.composed_transform(),
|
||||||
|
Transform().add_translate((5, 10)).add_scale(2, 2))
|
||||||
|
|
||||||
|
def test_transform_inherits_from_tree(self):
|
||||||
|
root: SvgDocumentElement = svg()
|
||||||
|
g1 = root.add(Group())
|
||||||
|
g1.set('transform', Transform().add_translate((0, 5)).add_rotate(5))
|
||||||
|
rect = g1.add(Rectangle(attrib={
|
||||||
|
"width": "10",
|
||||||
|
"height": "10",
|
||||||
|
INKSTITCH_ATTRIBS["angle"]: "30",
|
||||||
|
}))
|
||||||
|
rect.set('transform', Transform().add_scale(2, 2))
|
||||||
|
use = root.add(Use())
|
||||||
|
use.href = g1
|
||||||
|
use.set('transform', Transform().add_translate((5, 10)))
|
||||||
|
|
||||||
|
clone = Clone(use)
|
||||||
|
with clone.clone_elements() as elements:
|
||||||
|
self.assertEqual(len(elements), 1)
|
||||||
|
self.assertTransformEqual(
|
||||||
|
elements[0].node.composed_transform(),
|
||||||
|
Transform().add_translate((5, 10)) # use
|
||||||
|
.add_translate((0, 5)).add_rotate(5) # g1
|
||||||
|
.add_scale(2, 2), # rect
|
||||||
|
5)
|
||||||
|
|
||||||
|
def test_transform_inherits_from_tree_up_tree(self):
|
||||||
|
root: SvgDocumentElement = svg()
|
||||||
|
g1 = root.add(Group())
|
||||||
|
g1.set('transform', Transform().add_translate((0, 5)).add_rotate(5))
|
||||||
|
rect = g1.add(Rectangle(attrib={
|
||||||
|
"width": "10",
|
||||||
|
"height": "10",
|
||||||
|
INKSTITCH_ATTRIBS["angle"]: "30",
|
||||||
|
}))
|
||||||
|
rect.set('transform', Transform().add_scale(2, 2))
|
||||||
|
circ = g1.add(Circle())
|
||||||
|
circ.radius = 5
|
||||||
|
g2 = root.add(Group())
|
||||||
|
g2.set('transform', Transform().add_translate((1, 2)).add_scale(0.5, 1))
|
||||||
|
use = g2.add(Use())
|
||||||
|
use.href = g1
|
||||||
|
use.set('transform', Transform().add_translate((5, 10)))
|
||||||
|
|
||||||
|
clone = Clone(use)
|
||||||
|
with clone.clone_elements() as elements:
|
||||||
|
self.assertEqual(len(elements), 2)
|
||||||
|
self.assertTransformEqual(
|
||||||
|
elements[0].node.composed_transform(),
|
||||||
|
Transform().add_translate((1, 2)).add_scale(0.5, 1) # g2
|
||||||
|
.add_translate((5, 10)) # use
|
||||||
|
.add_translate((0, 5)).add_rotate(5) # g1
|
||||||
|
.add_scale(2, 2), # rect
|
||||||
|
5)
|
||||||
|
self.assertTransformEqual(
|
||||||
|
elements[1].node.composed_transform(),
|
||||||
|
Transform().add_translate((1, 2)).add_scale(0.5, 1) # g2
|
||||||
|
.add_translate((5, 10)) # use
|
||||||
|
.add_translate((0, 5)).add_rotate(5), # g1
|
||||||
|
5)
|
||||||
|
|
||||||
|
def test_clone_fill_angle_not_specified(self):
|
||||||
|
root: SvgDocumentElement = svg()
|
||||||
|
rect = root.add(Rectangle(attrib={
|
||||||
|
"width": "10",
|
||||||
|
"height": "10",
|
||||||
|
INKSTITCH_ATTRIBS["angle"]: "30"
|
||||||
|
}))
|
||||||
|
use = root.add(Use())
|
||||||
|
use.href = rect
|
||||||
|
use.set('transform', Transform().add_rotate(20))
|
||||||
|
|
||||||
|
clone = Clone(use)
|
||||||
|
self.assertEqual(clone.clone_fill_angle, None)
|
||||||
|
|
||||||
|
def test_clone_fill_angle(self):
|
||||||
|
root: SvgDocumentElement = svg()
|
||||||
|
rect = root.add(Rectangle(attrib={
|
||||||
|
"width": "10",
|
||||||
|
"height": "10",
|
||||||
|
INKSTITCH_ATTRIBS["angle"]: "30"
|
||||||
|
}))
|
||||||
|
use = root.add(Use())
|
||||||
|
use.href = rect
|
||||||
|
use.set(INKSTITCH_ATTRIBS["angle"], 42)
|
||||||
|
use.set('transform', Transform().add_rotate(20))
|
||||||
|
|
||||||
|
clone = Clone(use)
|
||||||
|
self.assertEqual(clone.clone_fill_angle, 42)
|
||||||
|
|
||||||
|
with clone.clone_elements() as elements:
|
||||||
|
self.assertEqual(len(elements), 1)
|
||||||
|
self.assertAngleAlmostEqual(element_fill_angle(elements[0]), 42)
|
||||||
|
|
||||||
|
def test_angle_manually_flipped(self):
|
||||||
|
root: SvgDocumentElement = svg()
|
||||||
|
rect = root.add(Rectangle(attrib={
|
||||||
|
"width": "10",
|
||||||
|
"height": "10",
|
||||||
|
INKSTITCH_ATTRIBS["angle"]: "30"
|
||||||
|
}))
|
||||||
|
use = root.add(Use())
|
||||||
|
use.href = rect
|
||||||
|
use.set('transform', Transform().add_rotate(20))
|
||||||
|
use.set(INKSTITCH_ATTRIBS["flip_angle"], True)
|
||||||
|
|
||||||
|
clone = Clone(use)
|
||||||
|
self.assertTrue(clone.flip_angle)
|
||||||
|
with clone.clone_elements() as elements:
|
||||||
|
self.assertEqual(len(elements), 1)
|
||||||
|
self.assertAngleAlmostEqual(element_fill_angle(elements[0]), -10)
|
||||||
|
|
||||||
|
def test_recursive_uses(self):
|
||||||
|
root: SvgDocumentElement = svg()
|
||||||
|
g1 = root.add(Group())
|
||||||
|
rect = g1.add(Rectangle(attrib={
|
||||||
|
"width": "10",
|
||||||
|
"height": "10",
|
||||||
|
}))
|
||||||
|
u1 = g1.add(Use())
|
||||||
|
u1.set('transform', Transform().add_translate((20, 0)))
|
||||||
|
u1.href = rect
|
||||||
|
u2 = root.add(Use())
|
||||||
|
u2.set('transform', Transform().add_translate((0, 20)).add_scale(0.5, 0.5))
|
||||||
|
u2.href = g1
|
||||||
|
u3 = root.add(Use())
|
||||||
|
u3.set('transform', Transform().add_translate((0, 30)))
|
||||||
|
u3.href = u2
|
||||||
|
|
||||||
|
clone = Clone(u3)
|
||||||
|
with clone.clone_elements() as elements:
|
||||||
|
# There should be two elements cloned from u3, two rects, one corresponding to rect and one corresponding to u1.
|
||||||
|
# Their transforms should derive from the elements they href.
|
||||||
|
self.assertEqual(len(elements), 2)
|
||||||
|
self.assertEqual(elements[0].node.tag, SVG_RECT_TAG)
|
||||||
|
self.assertTransformEqual(elements[0].node.composed_transform(),
|
||||||
|
Transform().add_translate((0, 30)) # u3
|
||||||
|
.add_translate(0, 20).add_scale(0.5, 0.5) # u2
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(elements[1].node.tag, SVG_RECT_TAG)
|
||||||
|
self.assertTransformEqual(elements[1].node.composed_transform(),
|
||||||
|
Transform().add_translate((0, 30)) # u3
|
||||||
|
.add_translate((0, 20)).add_scale(0.5, 0.5) # u2
|
||||||
|
.add_translate((20, 0)) # u1
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_recursive_uses_angle(self):
|
||||||
|
root: SvgDocumentElement = svg()
|
||||||
|
rect = root.add(Rectangle(attrib={
|
||||||
|
"width": "10",
|
||||||
|
"height": "10",
|
||||||
|
INKSTITCH_ATTRIBS["angle"]: "30"
|
||||||
|
}))
|
||||||
|
u1 = root.add(Use())
|
||||||
|
u1.set('transform', Transform().add_rotate(60))
|
||||||
|
u1.href = rect
|
||||||
|
g = root.add(Group())
|
||||||
|
g.set('transform', Transform().add_rotate(-10))
|
||||||
|
u2 = g.add(Use())
|
||||||
|
u2.href = u1
|
||||||
|
u3 = root.add(Use())
|
||||||
|
u3.set('transform', Transform().add_rotate(7))
|
||||||
|
u3.href = g
|
||||||
|
|
||||||
|
clone = Clone(u3)
|
||||||
|
with clone.clone_elements() as elements:
|
||||||
|
self.assertEqual(len(elements), 1)
|
||||||
|
# Angle goes from 30 -> -30 (u1) -> -20 (g -> u2) -> -27 (u3)
|
||||||
|
self.assertAngleAlmostEqual(element_fill_angle(elements[0]), -27)
|
Ładowanie…
Reference in New Issue