Clones now also clone commands attached to element and its children. (#3032, #3121) (#3086)

pull/3141/head dev-build-kaalleen-fix-pull-comp-shape
capellancitizen 2024-08-14 19:40:42 -04:00 zatwierdzone przez GitHub
rodzic 744da960b3
commit f3a3cde71e
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: B5690EEEBB952194
9 zmienionych plików z 415 dodań i 44 usunięć

Wyświetl plik

@ -7,13 +7,16 @@ import os
import sys
from copy import deepcopy
from random import random
from typing import List
import inkex
from shapely import geometry as shgeo
from shapely import get_coordinates
from .i18n import N_, _
from .svg import (apply_transforms, generate_unique_id,
get_correction_transform, get_document, get_node_transform)
from .svg.svg import copy_no_children, point_upwards
from .svg.tags import (CONNECTION_END, CONNECTION_START, CONNECTOR_TYPE,
INKSCAPE_LABEL, INKSTITCH_ATTRIBS, SVG_SYMBOL_TAG,
SVG_USE_TAG, XLINK_HREF)
@ -113,7 +116,7 @@ class BaseCommand(object):
class Command(BaseCommand):
def __init__(self, connector):
self.connector = connector
self.connector: inkex.Path = connector
self.svg = self.connector.getroottree().getroot()
self.parse_command()
@ -132,7 +135,8 @@ class Command(BaseCommand):
(self.get_node_by_url(self.connector.get(CONNECTION_END)), path[0][-1][1])
]
if neighbors[0][0].tag != SVG_USE_TAG:
self.symbol_is_end = neighbors[0][0].tag != SVG_USE_TAG
if self.symbol_is_end:
neighbors.reverse()
if neighbors[0][0].tag != SVG_USE_TAG:
@ -143,12 +147,41 @@ class Command(BaseCommand):
self.symbol = self.get_node_by_url(neighbors[0][0].get(XLINK_HREF))
self.parse_symbol()
self.target = neighbors[1][0]
self.target: inkex.BaseElement = neighbors[1][0]
self.target_point = neighbors[1][1]
def __repr__(self):
return "Command('%s', %s)" % (self.command, self.target_point)
def clone(self, new_target: inkex.BaseElement) -> inkex.BaseElement:
"""
Clone this command and point it to the new target, positioning it relative to the new target the same as the target
"""
relative_transform = new_target.composed_transform() @ -self.target.composed_transform()
# Clone group
cloned_group = copy_no_children(self.connector.getparent())
cloned_group.transform = relative_transform @ cloned_group.transform
new_target.getparent().append(cloned_group)
symbol = copy_no_children(self.use)
cloned_group.append(symbol)
point_upwards(symbol)
# Copy connector
connector = copy_no_children(self.connector)
cloned_group.append(connector)
if self.symbol_is_end:
symbol_attr = CONNECTION_END
target_attr = CONNECTION_START
else:
symbol_attr = CONNECTION_START
target_attr = CONNECTION_END
connector.set(symbol_attr, f"#{symbol.get_id()}")
connector.set(target_attr, f"#{new_target.get_id()}")
return cloned_group
class StandaloneCommand(BaseCommand):
def __init__(self, use):
@ -175,11 +208,11 @@ class StandaloneCommand(BaseCommand):
return Point(*pos)
def get_command_description(command):
def get_command_description(command: str):
return COMMANDS[command]
def find_commands(node):
def find_commands(node: BaseCommand) -> List[Command]:
"""Find the symbols this node is connected to and return them as Commands"""
# find all paths that have this object as a connection
@ -307,10 +340,24 @@ def add_group(document, node, command):
def add_connector(document, symbol, command, 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.
# "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." - Lex Neva, rev. 4baced7085
# "Maybe we should have the target point be a seperately-moveable node? Sometimes moving the command
# node so the line drawn from the command to the centroid of the target is awkward anyway?" - CapellanCitizen
# Inkscape will draw this connector line from the bounding box center of the two nodes, but
# will stop at the first intersection with the path it's pointing to. It is necessary to
# compute the target point accurately to what inkscape will do.
# If not, then the target position will change when the document is loaded by inkscape and break.
# For example, not doing this caused issues when implementing commands attached to clones.
start_pos = (symbol.get('x'), symbol.get('y'))
end_pos = element.shape.centroid
centroid_pos = element.node.bounding_box(inkex.Transform(get_node_transform(element.node.getparent()))).center
connector_line = shgeo.LineString([start_pos, centroid_pos])
if connector_line.intersects(element.shape):
end_pos = get_coordinates(connector_line.intersection(element.shape))[0]
else:
# Sometimes the line won't intersect anything and will go straight to the centroid.
end_pos = centroid_pos
# Make sure the element's XML node has an id so that we can reference it.
if element.node.get('id') is None:
@ -318,10 +365,10 @@ def add_connector(document, symbol, command, element):
path = inkex.PathElement(attrib={
"id": generate_unique_id(document, "command_connector"),
"d": "M %s,%s %s,%s" % (start_pos[0], start_pos[1], end_pos.x, end_pos.y),
"d": f"M {start_pos[0]},{start_pos[1]} {end_pos[0]},{end_pos[1]}",
"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'),
CONNECTION_START: f"#{symbol.get('id')}",
CONNECTION_END: f"#{element.node.get('id')}",
# l10n: the name of the line that connects a command to the object it applies to
INKSCAPE_LABEL: _("connector")

Wyświetl plik

@ -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

Wyświetl plik

@ -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

Wyświetl plik

@ -5,9 +5,10 @@
from inkex import Boolean, errormsg, BaseElement
from ..elements import Clone
from ..elements import Clone, EmbroideryElement
from ..i18n import _
from .base import InkstitchExtension
from ..svg.tags import CONNECTION_END, CONNECTION_START
from typing import List, Tuple
@ -34,8 +35,14 @@ class UnlinkClone(InkstitchExtension):
for element in self.elements:
if isinstance(element, Clone):
resolved = element.resolve_clone(recursive=recursive)
clones_resolved.append((element.node, resolved))
clones_resolved.append((element.node, resolved[0]))
for (clone, resolved) in clones_resolved:
clone.getparent().remove(clone)
resolved.set_id(clone.get_id())
clone.delete()
orig_id = resolved.get_id()
new_id = clone.get_id()
# Fix up command backlinks - note this has to happen before we rename so they can actually be found.
for command in EmbroideryElement(resolved).commands:
backlink_attrib = CONNECTION_START if command.connector.get(CONNECTION_START) == ("#"+orig_id) else CONNECTION_END
command.connector.set(backlink_attrib, "#"+new_id)
resolved.set_id(new_id)

Wyświetl plik

@ -31,7 +31,7 @@ def compose_parent_transforms(node, mat):
return mat
def get_node_transform(node):
def get_node_transform(node: inkex.BaseElement):
"""
if getattr(node, "composed_transform", None):
return node.composed_transform()

Wyświetl plik

@ -3,7 +3,9 @@
# Copyright (c) 2010 Authors
# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details.
from inkex import NSS
import math
from inkex import NSS, BaseElement, Transform
from lxml import etree
from ..utils import cache
@ -29,3 +31,28 @@ def find_elements(node, xpath):
document = get_document(node)
elements = document.xpath(xpath, namespaces=NSS)
return elements
def copy_no_children(node: BaseElement) -> BaseElement:
return type(node)(attrib=node.attrib)
def point_upwards(node: BaseElement) -> None:
"""
Given a node, adjust the transform such that it is in the same spot, but pointing upwards (e.g. for command symbols)
"""
# Adjust the transform of the node so it's face-up and the right way around.
node_transform = node.composed_transform()
compensation = -Transform((node_transform.a, node_transform.b, node_transform.c, node_transform.d, 0, 0))
scale_vector = compensation.capply_to_point(1+1j)
scale_factor = math.sqrt(2)/math.sqrt(scale_vector.real*scale_vector.real + scale_vector.imag*scale_vector.imag)
compensation.add_scale(scale_factor, scale_factor)
node_correction = Transform().add_translate(float(node.get('x', 0)), float(node.get('y', 0)))
node_correction @= compensation
# Quick hack to compute the rotational angle - node_transform @ (1,0) = (a, b)
node.transform = node.transform @ node_correction
# Clear the x and y coords, they've been incorporated to the transform above
node.set('x', None)
node.set('y', None)

Wyświetl plik

@ -3,6 +3,7 @@
# Copyright (c) 2010 Authors
# Licensed under the GNU GPL version 3.0 or later. See the file LICENSE for details.
from . import cache as cache_module # Slight hack to allow cache to be imported for monkeypatching
from .cache import cache
from .dotdict import DotDict
from .geometry import *

Wyświetl plik

@ -1,5 +1,7 @@
from lib.elements import Clone, EmbroideryElement
from lib.elements import Clone, EmbroideryElement, FillStitch
from lib.commands import add_commands
from lib.svg.tags import INKSTITCH_ATTRIBS, SVG_RECT_TAG
from lib.utils import cache_module
from inkex import SvgDocumentElement, Rectangle, Circle, Group, Use, Transform, TextElement
from inkex.tester import TestCase
from inkex.tester.svg import svg
@ -17,6 +19,15 @@ def element_fill_angle(element: EmbroideryElement) -> Optional[float]:
class CloneElementTest(TestCase):
def setUp(self):
from pytest import MonkeyPatch
self.monkeypatch = MonkeyPatch()
self.monkeypatch.setattr(cache_module, "is_cache_disabled", lambda: True)
def tearDown(self):
self.monkeypatch.undo()
return super().tearDown()
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
@ -180,6 +191,23 @@ class CloneElementTest(TestCase):
# Angle goes from 0 -> -30
self.assertAngleAlmostEqual(element_fill_angle(elements[0]), -30)
def test_angle_not_applied_to_non_fills(self):
"""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), 1) # One for the stroke, one for the fill
self.assertIsNone(elements[0].get_param("angle", None)) # Angle as not set, as this isn't a fill
def test_style_inherits(self):
root: SvgDocumentElement = svg()
rect = root.add(Rectangle(attrib={
@ -331,6 +359,8 @@ class CloneElementTest(TestCase):
self.assertEqual(len(elements), 1)
self.assertAngleAlmostEqual(element_fill_angle(elements[0]), -10)
# Recursive use tests
def test_recursive_uses(self):
root: SvgDocumentElement = svg()
g1 = root.add(Group())
@ -440,3 +470,169 @@ class CloneElementTest(TestCase):
self.assertEqual(len(elements), 1)
# 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):
"""
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, ["fill_end"])
clone = Clone(use)
with clone.clone_elements() as elements:
self.assertEqual(len(elements), 1)
cmd_orig = original.get_command("fill_end")
cmd_clone = elements[0].get_command("fill_end")
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):
"""
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, ["fill_end"])
clone = Clone(use)
with clone.clone_elements() as elements:
self.assertEqual(len(elements), 1)
cmd_orig = original.get_command("fill_end")
cmd_clone = elements[0].get_command("fill_end")
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):
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):
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):
"""
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):
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):
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):
"""
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)

Wyświetl plik

@ -0,0 +1,40 @@
from lib.svg.svg import point_upwards
from inkex import Rectangle, Transform, PathElement
from inkex.tester import TestCase
from inkex.tester.svg import svg
class LibSvgSvgTest(TestCase):
def test_point_upwards(self):
root = svg()
rect = root.add(Rectangle(attrib={
"width": "10",
"height": "10",
"x": "10",
"y": "20"
}))
rect.transform = Transform().add_rotate(-45)
point_upwards(rect)
self.assertTransformEqual(
rect.transform,
Transform().add_translate(Transform().add_rotate(-45).apply_to_point((10, 20))),
4
)
def test_point_upwards_mirrored(self):
root = svg()
rect = root.add(PathElement(attrib={
"d": "M 0,0 L 10,0 0,5 Z",
}))
rect.transform = Transform().add_rotate(-45).add_scale(-1, 1)
point_upwards(rect)
self.assertTransformEqual(
rect.transform,
Transform(),
4
)