Commit 768ba690 authored by Michael Buschbeck's avatar Michael Buschbeck Committed by Martin Owens

Fix path transform with relative command after "z"

After encountering a "z" command, Path.transform() continued at the
closed path's untransformed start coordinates both in untransformed and
transformed space, messing up any relative command that followed.

This change keeps track of the transformed starting point of the current
sub-path (in addition to the untransformed) so that "z" can move the
current position to the correct place in transformed space, too.
parent 9b01a4a1
......@@ -29,6 +29,7 @@ from copy import deepcopy
from ..utils import addNS
from ..transforms import Transform
from ..tween import interpcoord, interp
from ..units import convert_unit
from ..styles import Style
from ._base import BaseElement
......@@ -168,7 +169,7 @@ class Gradient(BaseElement):
# interpolate orientation
for attr in self.orientation_attributes:
newattr = interpcoord(float(self.get(attr)), float(other.get(attr)), fraction)
newattr = interpcoord(convert_unit(self.get(attr), 'px'), convert_unit(other.get(attr), 'px'), fraction)
newgrad.set(attr, newattr)
# interpolate stops
......@@ -202,8 +203,8 @@ class LinearGradient(Gradient):
def apply_transform(self): # type: () -> None
"""Apply transform to orientation points and set it to identity."""
trans = self.pop('gradientTransform')
p1 = (float(self.get('x1')), float(self.get('y1')))
p2 = (float(self.get('x2')), float(self.get('y2')))
p1 = (convert_unit(self.get('x1'), 'px'), convert_unit(self.get('y1'), 'px'))
p2 = (convert_unit(self.get('x2'), 'px'), convert_unit(self.get('y2'), 'px'))
p1t = trans.apply_to_point(p1)
p2t = trans.apply_to_point(p2)
self.update(x1=p1t[0], y1=p1t[1], x2=p2t[0], y2=p2t[1])
......@@ -216,8 +217,8 @@ class RadialGradient(Gradient):
def apply_transform(self): # type: () -> None
"""Apply transform to orientation points and set it to identity."""
trans = self.pop('gradientTransform')
p1 = (float(self.get('cx')), float(self.get('cy')))
p2 = (float(self.get('fx')), float(self.get('fy')))
p1 = (convert_unit(self.get('cx'), 'px'), convert_unit(self.get('cy'), 'px'))
p2 = (convert_unit(self.get('fx'), 'px'), convert_unit(self.get('fy'), 'px'))
p1t = trans.apply_to_point(p1)
p2t = trans.apply_to_point(p2)
self.update(cx=p1t[0], cy=p1t[1], fx=p2t[0], fy=p2t[1])
......
......@@ -26,6 +26,7 @@ Interface for all shapes/polygons such as lines, paths, rectangles, circles etc.
from ..paths import Path
from ..transforms import Transform, ImmutableVector2d, Vector2d
from ..utils import addNS
from ..units import convert_unit
from ._base import ShapeElement
......@@ -113,14 +114,14 @@ class Line(ShapeElement):
class RectangleBase(ShapeElement):
"""Provide a useful extension for rectangle elements"""
left = property(lambda self: float(self.get('x', '0')))
top = property(lambda self: float(self.get('y', '0')))
left = property(lambda self: convert_unit(self.get('x', '0'), 'px'))
top = property(lambda self: convert_unit(self.get('y', '0'), 'px'))
right = property(lambda self: self.left + self.width)
bottom = property(lambda self: self.top + self.height)
width = property(lambda self: float(self.get('width', '0')))
height = property(lambda self: float(self.get('height', '0')))
rx = property(lambda self: float(self.get('rx', self.get('ry', 0.0))))
ry = property(lambda self: float(self.get('ry', self.get('rx', 0.0)))) # pylint: disable=invalid-name
width = property(lambda self: convert_unit(self.get('width', '0'), 'px'))
height = property(lambda self: convert_unit(self.get('height', '0'), 'px'))
rx = property(lambda self: convert_unit(self.get('rx', self.get('ry', 0.0)), 'px'))
ry = property(lambda self: convert_unit(self.get('ry', self.get('rx', 0.0)), 'px')) # pylint: disable=invalid-name
def get_path(self):
"""Calculate the path as the box around the rect"""
......@@ -159,7 +160,7 @@ class EllipseBase(ShapeElement):
@property
def center(self):
return ImmutableVector2d(float(self.get('cx', '0')), float(self.get('cy', '0')))
return ImmutableVector2d(convert_unit(self.get('cx', '0'), 'px'), convert_unit(self.get('cy', '0'), 'px'))
@center.setter
def center(self, value):
......@@ -186,7 +187,7 @@ class Circle(EllipseBase):
@property
def radius(self):
return float(self.get('r', '0'))
return convert_unit(self.get('r', '0'), 'px')
@radius.setter
def radius(self, value):
......@@ -203,7 +204,7 @@ class Ellipse(EllipseBase):
@property
def radius(self):
return ImmutableVector2d(float(self.get('rx', '0')), float(self.get('ry', '0')))
return ImmutableVector2d(convert_unit(self.get('rx', '0'), 'px'), convert_unit(self.get('ry', '0'), 'px'))
@radius.setter
def radius(self, value):
......
......@@ -1186,6 +1186,7 @@ class Path(list):
previous = Vector2d()
previous_new = Vector2d()
first = Vector2d()
first_new = Vector2d()
for i, seg in enumerate(self): # type: PathCommand
if i == 0:
......@@ -1199,12 +1200,15 @@ class Path(list):
else:
new_seg = seg.transform(transform)
if i == 0:
first_new = new_seg.end_point(first_new, previous_new)
if inplace:
self[i] = new_seg
else:
result.append(new_seg)
previous = seg.end_point(first, previous)
previous_new = new_seg.end_point(first, previous_new)
previous_new = new_seg.end_point(first_new, previous_new)
if inplace:
return self
return result
......
......@@ -170,6 +170,21 @@ class RectTest(ElementTestCase):
"""Test extra functionality on a rectangle element"""
tag = 'rect'
def test_parse(self):
"""Test Rectangle parsed from XML"""
rect = Rectangle(attrib={
"x": "10px", "y": "20px",
"width": "100px", "height": "200px",
"rx": "15px", "ry": "30px" })
self.assertEqual(rect.left, 10)
self.assertEqual(rect.top, 20)
self.assertEqual(rect.right, 10+100)
self.assertEqual(rect.bottom, 20+200)
self.assertEqual(rect.width, 100)
self.assertEqual(rect.height, 200)
self.assertEqual(rect.rx, 15)
self.assertEqual(rect.ry, 30)
def test_compose_transform(self):
"""Composed transformation"""
self.assertEqual(self.elem.transform, Transform('rotate(16.097889)'))
......@@ -212,6 +227,18 @@ class CircleTest(ElementTestCase):
"""Test extra functionality on a circle element"""
tag = 'circle'
def test_parse(self):
"""Test Circle parsed from XML"""
circle = Circle(attrib={"cx": "10px", "cy": "20px", "r": "30px"})
self.assertEqual(circle.center.x, 10)
self.assertEqual(circle.center.y, 20)
self.assertEqual(circle.radius, 30)
ellipse = Ellipse(attrib={"cx": "10px", "cy": "20px", "rx": "30px", "ry": "40px"})
self.assertEqual(ellipse.center.x, 10)
self.assertEqual(ellipse.center.y, 20)
self.assertEqual(ellipse.radius.x, 30)
self.assertEqual(ellipse.radius.y, 40)
def test_new(self):
"""Test new circles"""
elem = Circle.new((10, 10), 50)
......@@ -319,6 +346,27 @@ class GradientTests(ElementTestCase):
translate11 = Transform('translate(1.0, 1.0)')
translate22 = Transform('translate(2.0, 2.0)')
def test_parse(self):
"""Gradients parsed from XML"""
values = [
(LinearGradient,
{'x1': '0px', 'y1': '1px', 'x2': '2px', 'y2': '3px'},
{'x1': 0.0, 'y1': 1.0, 'x2': 2.0, 'y2': 3.0},
),
(RadialGradient,
{'cx': '0px', 'cy': '1px', 'fx': '2px', 'fy': '3px', 'r': '4px'},
{'cx': 0.0, 'cy': 1.0, 'fx': 2.0, 'fy': 3.0}
)]
for classname, attributes, expected in values:
grad = classname(attrib=attributes)
grad.apply_transform() # identity transform
for key, value in expected.items():
assert float(grad.get(key)) == pytest.approx(value, 1e-3)
grad = classname(attrib=attributes)
grad = grad.interpolate(grad, 0.0)
for key, value in expected.items():
assert float(grad.get(key)) == pytest.approx(value, 1e-3)
def test_apply_transform(self):
"""Transform gradients"""
values = [
......
......@@ -312,6 +312,18 @@ class PathTest(TestCase):
'A 17.8412 11.8942 0 0 1 18 69.452 '
'A 17.8412 11.8942 0 0 1 35.8412 81.3462 Z')
def test_scale_relative_after_close(self):
"""Zone close moves current position correctly after transform"""
# expected positions:
# - before scale:
# M to (10,10), l by (+10,+10), Z back to (10,10), l by (+10,+10)
# <=> M to (10,10), L to (20,20), Z back to (10,10), L to (20,20)
# - after scale:
# M to (20,20), L to (40,40), Z back to (20,20), L to (40,40)
# <=> M to (20,20), l by (+20,+20), Z back to (20,20), l by (+20,+20)
ret = Path('M 10,10 l 10,10 Z l 10,10').scale(2, 2)
self._assertPath(ret, 'M 20 20 l 20 20 Z l 20 20')
def test_absolute(self):
"""Paths can be converted to absolute"""
ret = Path("M 100 100 l 10 10 10 10 10 10")
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment