inkstitch/tests/test_clone.py

676 wiersze
24 KiB
Python

from math import sqrt
from typing import Optional
from inkex import (Circle, Group, Rectangle, SvgDocumentElement, TextElement,
Transform, Use)
from inkex.tester import TestCase
from inkex.tester.svg import svg
from lib.commands import add_commands
from lib.elements import Clone, EmbroideryElement, FillStitch
from lib.svg.tags import INKSCAPE_LABEL, INKSTITCH_ATTRIBS, SVG_RECT_TAG
from lib.utils import cache_module
from .utils import element_count
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):
# Monkey-patch the cahce to forcibly disable it: We may need to refactor this out for tests.
def setUp(self) -> None:
from pytest import MonkeyPatch
self.monkeypatch = MonkeyPatch()
self.monkeypatch.setattr(cache_module, "is_cache_disabled", lambda: True)
def tearDown(self) -> None:
self.monkeypatch.undo()
return super().tearDown()
def assertAngleAlmostEqual(self, a, b) -> None:
# 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) -> None:
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)
def test_not_clone(self) -> None:
root: SvgDocumentElement = svg()
rect = root.add(Rectangle(attrib={
"width": "10",
"height": "10",
INKSTITCH_ATTRIBS["angle"]: "30"
}))
use = root.add(Use(attrib={
INKSTITCH_ATTRIBS["clone"]: "false"
}))
use.href = rect
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) -> None:
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), element_count())
self.assertAngleAlmostEqual(element_fill_angle(elements[0]), 30)
def test_hidden_cloned_elements_not_embroidered(self) -> None:
root = svg()
g = root.add(Group())
g.add(Rectangle(attrib={
INKSCAPE_LABEL: "NotHidden",
"width": "10",
"height": "10"
}))
g.add(Rectangle(attrib={
INKSCAPE_LABEL: "Hidden",
"width": "10",
"height": "10",
"style": "display:none"
}))
hidden_group = g.add(Group(attrib={
"style": "display:none"
}))
hidden_group.add(Rectangle(attrib={
INKSCAPE_LABEL: "ChildOfHidden",
"width": "10",
"height": "10",
}))
use = root.add(Use())
use.href = g
clone = Clone(use)
with clone.clone_elements() as elements:
self.assertEqual(len(elements), element_count())
self.assertEqual(elements[0].node.get(INKSCAPE_LABEL), "NotHidden")
def test_angle_rotated(self) -> None:
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), element_count())
self.assertAngleAlmostEqual(element_fill_angle(elements[0]), 10)
def test_angle_flipped(self) -> None:
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), element_count())
self.assertAngleAlmostEqual(element_fill_angle(elements[0]), -30)
def test_angle_flipped_rotated(self) -> None:
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), element_count())
# 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) -> None:
"""
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), element_count())
# 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) -> None:
"""
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), element_count())
# Angle goes from 30 -> 40 (g1 -> g2) -> 29 (use)
self.assertAngleAlmostEqual(element_fill_angle(elements[0]), 29)
def test_angle_not_applied_twice(self) -> None:
"""Make sure that angle changes are not applied twice to an element with both stroke and fill."""
root: SvgDocumentElement = svg()
rect = root.add(Rectangle(attrib={
"width": "10",
"height": "10",
"style": "stroke: skyblue; fill: red;"
}))
use = root.add(Use())
use.href = rect
use.set('transform', Transform().add_rotate(30))
clone = Clone(use)
with clone.clone_elements() as elements:
self.assertEqual(len(elements), element_count()+1) # One for the stroke, one for the fill, one for the SewStack
self.assertEqual(elements[0].node, elements[1].node)
# Angle goes from 0 -> -30
self.assertAngleAlmostEqual(element_fill_angle(elements[0]), -30)
def test_angle_not_applied_to_non_fills(self) -> None:
"""Make sure that angle changes are not applied to non-fill elements."""
root: SvgDocumentElement = svg()
rect = root.add(Rectangle(attrib={
"width": "10",
"height": "10",
"style": "stroke: skyblue; fill-opacity: 0;"
}))
use = root.add(Use())
use.href = rect
use.set('transform', Transform().add_rotate(30))
clone = Clone(use)
with clone.clone_elements() as elements:
self.assertEqual(len(elements), element_count()) # One for the stroke, one for the fill, one for the SewStack
self.assertIsNone(elements[0].get_param("angle", None)) # Angle as not set, as this isn't a fill
def test_style_inherits(self) -> None:
root: SvgDocumentElement = svg()
rect = root.add(Rectangle(attrib={
"width": "10",
"height": "10"
}))
rect.set('style', 'stroke: skyblue; fill-opacity: 0;')
use = root.add(Use())
use.href = rect
use.set('style', 'stroke: red; stroke-width: 2;')
clone = Clone(use)
with clone.clone_elements() as elements:
self.assertEqual(len(elements), element_count())
style = elements[0].node.cascaded_style()
# Source style takes precedence over any attributes specified in the clone
self.assertEqual(style["stroke"], "skyblue")
self.assertEqual(style["stroke-width"], "2")
def test_transform_inherits_from_cloned_element(self) -> None:
"""
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), element_count())
self.assertTransformEqual(
elements[0].node.composed_transform(),
Transform().add_translate((5, 10)).add_scale(2, 2))
def test_transform_inherits_from_tree(self) -> None:
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), element_count())
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) -> None:
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), element_count()*2) # FillStitch, SewStack, FillStitch, SewStack
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[element_count()].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) -> None:
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) -> None:
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), element_count())
self.assertAngleAlmostEqual(element_fill_angle(elements[0]), 42)
def test_angle_manually_flipped(self) -> None:
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), element_count())
self.assertAngleAlmostEqual(element_fill_angle(elements[0]), -10)
# Recursive use tests
def test_recursive_uses(self) -> None:
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), element_count()*2)
self.assertEqual(type(elements[0]), FillStitch)
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(type(elements[element_count()]), FillStitch)
self.assertEqual(elements[element_count()].node.tag, SVG_RECT_TAG)
self.assertTransformEqual(elements[element_count()].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) -> None:
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
clone = Clone(u1)
with clone.clone_elements() as elements:
self.assertEqual(len(elements), element_count())
# Angle goes from 30 -> -30
self.assertAngleAlmostEqual(element_fill_angle(elements[0]), -30)
g = root.add(Group())
g.set('transform', Transform().add_rotate(-10))
u2 = g.add(Use())
u2.href = u1
clone = Clone(u2)
with clone.clone_elements() as elements:
self.assertEqual(len(elements), element_count())
# Angle goes from -30 -> -20 (u1 -> g)
self.assertAngleAlmostEqual(element_fill_angle(elements[0]), -20)
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), element_count())
# Angle goes from -20 -> -27
self.assertAngleAlmostEqual(element_fill_angle(elements[0]), -27)
# Cloning u2 directly, the relative transform of g does not apply
u4 = root.add(Use())
u4.set('transform', Transform().add_rotate(7))
u4.href = u2
clone = Clone(u4)
with clone.clone_elements() as elements:
self.assertEqual(len(elements), element_count())
# Angle goes from -30 -> -37
self.assertAngleAlmostEqual(element_fill_angle(elements[0]), -37)
def test_recursive_uses_angle_with_specified_angle(self) -> None:
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
u2.set(INKSTITCH_ATTRIBS["angle"], "0")
u3 = root.add(Use())
u3.set_id('U3')
u3.set('transform', Transform().add_rotate(7))
u3.href = g
clone = Clone(u3)
with clone.clone_elements() as elements:
self.assertEqual(len(elements), element_count())
# Angle goes from 0 (g -> u2) -> -7 (u3)
self.assertAngleAlmostEqual(element_fill_angle(elements[0]), -7)
# Command clone tests
def test_copies_directly_attached_commands(self) -> None:
"""
Check that commands attached to the clone target directly are applied to clones.
"""
root: SvgDocumentElement = svg()
rect = root.add(Rectangle(attrib={
"width": "10",
"height": "10",
}))
use = root.add(Use())
use.href = rect
use.set('transform', Transform().add_translate(10, 10))
original = FillStitch(rect)
add_commands(original, ["ending_point"])
clone = Clone(use)
with clone.clone_elements() as elements:
self.assertEqual(len(elements), element_count())
cmd_orig = original.get_command("ending_point")
cmd_clone = elements[0].get_command("ending_point")
self.assertIsNotNone(cmd_clone)
self.assertAlmostEqual(cmd_orig.target_point[0]+10, cmd_clone.target_point[0], 4)
self.assertAlmostEqual(cmd_orig.target_point[1]+10, cmd_clone.target_point[1], 4)
def test_copies_indirectly_attached_commands(self) -> None:
"""
Check that commands attached to children of the clone target are copied to clones.
"""
root: SvgDocumentElement = svg()
group = root.add(Group())
rect = group.add(Rectangle(attrib={
"width": "10",
"height": "10",
}))
use = root.add(Use())
use.href = group
use.set('transform', Transform().add_translate(10, 10))
original = FillStitch(rect)
add_commands(original, ["ending_point"])
clone = Clone(use)
with clone.clone_elements() as elements:
self.assertEqual(len(elements), element_count())
cmd_orig = original.get_command("ending_point")
cmd_clone = elements[0].get_command("ending_point")
self.assertIsNotNone(cmd_clone)
self.assertAlmostEqual(cmd_orig.target_point[0]+10, cmd_clone.target_point[0], 4)
self.assertAlmostEqual(cmd_orig.target_point[1]+10, cmd_clone.target_point[1], 4)
# Checks that trim_after and stop_after commands and settings in cloned elements aren't overridden
def test_trim_after(self) -> None:
root: SvgDocumentElement = svg()
rect = root.add(Rectangle(attrib={
"width": "10",
"height": "10",
INKSTITCH_ATTRIBS["trim_after"]: "true"
}))
use = root.add(Use())
use.href = rect
clone = Clone(use)
stitch_groups = clone.embroider(None)
self.assertGreater(len(stitch_groups), 0)
self.assertTrue(stitch_groups[-1].trim_after)
def test_trim_after_command(self) -> None:
root: SvgDocumentElement = svg()
rect = root.add(Rectangle(attrib={
"width": "10",
"height": "10",
}))
add_commands(FillStitch(rect), ["trim"])
use = root.add(Use())
use.href = rect
clone = Clone(use)
stitch_groups = clone.embroider(None)
self.assertGreater(len(stitch_groups), 0)
self.assertTrue(stitch_groups[-1].trim_after)
def test_trim_after_command_on_clone(self) -> None:
"""
If the clone element has a trim command, it should apply!
"""
root: SvgDocumentElement = svg()
rect = root.add(Rectangle(attrib={
"width": "10",
"height": "10",
}))
use = root.add(Use())
use.href = rect
clone = Clone(use)
add_commands(clone, ["trim"])
stitch_groups = clone.embroider(None)
self.assertGreater(len(stitch_groups), 0)
self.assertTrue(stitch_groups[-1].trim_after)
def test_stop_after(self) -> None:
root: SvgDocumentElement = svg()
rect = root.add(Rectangle(attrib={
"width": "10",
"height": "10",
INKSTITCH_ATTRIBS["stop_after"]: "true"
}))
use = root.add(Use())
use.href = rect
clone = Clone(use)
stitch_groups = clone.embroider(None)
self.assertGreater(len(stitch_groups), 0)
self.assertTrue(stitch_groups[-1].stop_after)
def test_stop_after_command(self) -> None:
root: SvgDocumentElement = svg()
rect = root.add(Rectangle(attrib={
"width": "10",
"height": "10",
}))
fill_stitch = FillStitch(rect)
add_commands(fill_stitch, ["stop"])
use = root.add(Use())
use.href = rect
clone = Clone(use)
stitch_groups = clone.embroider(None)
self.assertGreater(len(stitch_groups), 0)
self.assertTrue(stitch_groups[-1].stop_after)
def test_stop_after_command_on_clone(self) -> None:
"""
If the clone element has a stop command, it should still apply!
"""
root: SvgDocumentElement = svg()
rect = root.add(Rectangle(attrib={
"width": "10",
"height": "10",
}))
use = root.add(Use())
use.href = rect
clone = Clone(use)
add_commands(clone, ["stop"])
stitch_groups = clone.embroider(None)
self.assertGreater(len(stitch_groups), 0)
self.assertTrue(stitch_groups[-1].stop_after)