Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added basic transformations, reversing #46

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
183 changes: 179 additions & 4 deletions src/svg/path/path.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from __future__ import division
from math import sqrt, cos, sin, acos, degrees, radians, log
from math import sqrt, cos, sin, acos, degrees, radians, log, atan2
from collections import MutableSequence


Expand Down Expand Up @@ -49,6 +49,31 @@ def __ne__(self, other):
return NotImplemented
return not self == other

def __add__(self, other):
self.start += other
self.end += other

def __sub__(self, other):
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And this is just a negative transform, really...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is, but also dunder methods would allow you to say:
new_line = line - (6+3j)

Which is generally easy to understand as a command as to what you'd want.

self.start -= other
self.end -= other

def __mul__(self, other):
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although multiplications on complex numbers are cool, they don't really translate into anything particularly useful in graphics. I don't think we need it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Multiplication does scaling. If you take that element and you multiply by 2 it scales by 2.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It also moves it by two, so that's not what you want in most cases.

I think we probably should implement this by first implementing the matrix transformation, and then possibly implementing others as shortcuts.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't move it by 2. It multiplies the .imag and .real by that value which is a uniform scaling operation, relative to the origin. To scaled based on a given point you'd subtract by a complex, multiply by a real, then add the original complex again.

(x + yj) * s = (sx, sy). That's a properly scaled point.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, and that's what is necessary. If you have a path that makes a square on point 1+1j, 1+2j, 2+2j and 2+1j and you multiply that by 2 you get the points 2+2j, 2+4j, 4+4j and 4+2j.

That's scaled AND moved, and that's not a useful graphical operation.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@regebro, nope. That's scaled.

You are confusing scaled with scaled relative to a location. Whereas the operation scaled is relative to the origin. To perform a scale relative to the center you would do path
path -= 1.5 + 1.5j
path *= 2
path += 1.5 + 1.5j

That is scale and scale is by definition a very useful graphical operation. I think the confusion is in thinking scale cannot move elements. It totally can. I'm not sure how you would make all point relative to other points larger without moving the points around.

A rectangle at (1,1), (1,2), (2,2), (2,1) scaled by 2 has points (2,2), (2,4), (4,4), (4,2) that's not an argument for wrongness, that's the definition of scaled in a geometric sense. All scale operations are relative to the origin. You need to do "transform(-x,-y), scale(s), transform(x,y)" to do this relative to a point but that's really just move the origin, scale, move the origin back.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you want to wrap your head around it, consider the distance between any point and any other point in the original versus scaled version. You will find they are in every case exactly twice as far away.

from svg.elements import *
print(Path("M1,1 1,2 2,2 2,1z") * "scale(2)")

M 2,2 L 2,4 L 4,4 L 4,2 Z

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have wrapped my head around it. Thanks.

self.start *= other
self.end *= other

__rmul__ = __mul__

def __len__(self):
return 2

def __getitem__(self, item):
if item == 0:
return self.start
return self.end

def reverse(self):
return Line(self.end, self.start)

def point(self, pos):
distance = self.end - self.start
return self.start + distance * pos
Expand Down Expand Up @@ -80,6 +105,41 @@ def __ne__(self, other):
return NotImplemented
return not self == other

def __add__(self, other):
self.start += other
self.control1 += other
self.control2 += other
self.end += other

def __sub__(self, other):
self.start -= other
self.control1 -= other
self.control2 -= other
self.end -= other

def __mul__(self, other):
self.start *= other
self.control1 *= other
self.control2 *= other
self.end *= other

__rmul__ = __mul__

def __len__(self):
return 4

def __getitem__(self, item):
if item == 0:
return self.start
elif item == 1:
return self.control1
elif item == 2:
return self.control2
return self.end

def reverse(self):
return CubicBezier(self.end, self.control2, self.control1, self.start)

def is_smooth_from(self, previous):
"""Checks if this segment would be a smooth segment following the previous"""
if isinstance(previous, CubicBezier):
Expand Down Expand Up @@ -123,6 +183,36 @@ def __ne__(self, other):
return NotImplemented
return not self == other

def __add__(self, other):
self.start += other
self.control += other
self.end += other

def __sub__(self, other):
self.start -= other
self.control -= other
self.end -= other

def __mul__(self, other):
self.start *= other
self.control *= other
self.end *= other

__rmul__ = __mul__

def __len__(self):
return 3

def __getitem__(self, item):
if item == 0:
return self.start
elif item == 1:
return self.control
return self.end

def reverse(self):
return QuadraticBezier(self.end, self.control, self.start)

def is_smooth_from(self, previous):
"""Checks if this segment would be a smooth segment following the previous"""
if isinstance(previous, QuadraticBezier):
Expand Down Expand Up @@ -165,6 +255,7 @@ def length(self, error=None, min_depth=None):
log((2 * A2 + BA + Sabc) / (BA + C2))) / (4 * A32)
return s


class Arc(object):

def __init__(self, start, radius, rotation, arc, sweep, end):
Expand Down Expand Up @@ -196,6 +287,46 @@ def __ne__(self, other):
return NotImplemented
return not self == other

def __add__(self, other):
self.start += other
self.center += other
self.end += other

def __sub__(self, other):
self.start -= other
self.center -= other
self.end -= other

def __mul__(self, other):
# this will mess up non-centered parameterized info, especially with regard to radius and rotation.
self.start *= other
self.end *= other
self.center *= other
# this will fix radius
distance = (self.center - self.start)
self.radius = sqrt(distance.real ** 2 + distance.imag ** 2)
# this will fix rotation
cosr = cos(radians(self.rotation))
sinr = sin(radians(self.rotation))
zero_angle_point = cosr + sinr * 1j
zero_angle_point *= other
self.rotation = degrees((atan2(zero_angle_point.imag, zero_angle_point.real)))

__rmul__ = __mul__

def __len__(self):
return 3

def __getitem__(self, item):
if item == 0:
return self.start
if item == 1:
return self.center
return self.end

def reverse(self):
return Arc(self.end, self.radius, self.rotation, self.arc, self.sweep, self.start)

def _parameterize(self):
# Conversion from endpoint to center parameterization
# http://www.w3.org/TR/SVG/implnote.html#ArcImplementationNotes
Expand Down Expand Up @@ -314,6 +445,26 @@ def __ne__(self, other):
return NotImplemented
return not self == other

def __add__(self, other):
self.start += other

def __sub__(self, other):
self.start -= other

def __mul__(self, other):
self.start *= other

__rmul__ = __mul__

def __len__(self):
return 1

def __getitem__(self, item):
return self.start

def reverse(self):
return Move(self.start)

def point(self, pos):
return self.start

Expand Down Expand Up @@ -350,9 +501,16 @@ def insert(self, index, value):
self._length = None

def reverse(self):
# Reversing the order of a path would require reversing each element
# as well. That's not implemented.
raise NotImplementedError
reversed_segments = self._segments[::-1]
for i in range(0, len(reversed_segments)):
reversed_segments[i] = reversed_segments[i].reverse()
path = Path()
path._segments = reversed_segments
return path

def __reversed__(self):
for segment in reversed(self._segments):
yield segment.reverse()

def __len__(self):
return len(self._segments)
Expand Down Expand Up @@ -429,6 +587,23 @@ def closed(self, value):
raise ValueError("End does not coincide with a segment start.")
self._closed = value

def translate(self, dx, dy=None):
if isinstance(dx, complex):
translate = dx
else:
translate = dx + dy * 1j
for segment in self:
segment += translate

def scale(self, scale):
for segment in self:
segment *= scale

def rotate(self, theta):
rotate = cos(radians(theta)) + sin(radians(theta)) * 1j
for segment in self:
segment *= rotate

def d(self):
if self.closed:
segments = self[:-1]
Expand Down
63 changes: 59 additions & 4 deletions src/svg/path/tests/test_paths.py
Original file line number Diff line number Diff line change
Expand Up @@ -509,10 +509,6 @@ def test_repr(self):
QuadraticBezier(start=600 + 100j, control=600, end=600 + 300j))
self.assertEqual(eval(repr(path)), path)

def test_reverse(self):
# Currently you can't reverse paths.
self.assertRaises(NotImplementedError, Path().reverse)

def test_equality(self):
# This is to test the __eq__ and __ne__ methods, so we can't use
# assertEqual and assertNotEqual
Expand Down Expand Up @@ -550,3 +546,62 @@ def test_non_arc(self):
self.assertEqual(segment.length(), 0)
self.assertEqual(segment.point(0.5), segment.start)


class TestTransform(unittest.TestCase):

def test_transforms(self):
"""The paths that are in the SVG specs"""

path = Path(Line(300 + 200j, 300 + 50j),
QuadraticBezier(300 + 50j, 0+0j, 75 + 100j),
Line(75 + 100j, 300 + 200j))
self.assertAlmostEqual(path.point(0.0), (300 + 200j))
self.assertAlmostEqual(path.point(1.0), (300 + 200j))
self.assertAlmostEqual(path.point(0.5), (66.93528751308301 + 49.735092953567715j))
path.translate(300 + 0j)
self.assertAlmostEqual(path.point(0.0), (600 + 200j))
self.assertAlmostEqual(path.point(1.0), (600 + 200j))
path.rotate(90)
self.assertAlmostEqual(path.point(0.0), (-200 + 600j))
self.assertAlmostEqual(path.point(1.0), (-200 + 600j))
path.rotate(90)
self.assertAlmostEqual(path.point(0.0), (-600 + -200j))
self.assertAlmostEqual(path.point(1.0), (-600 + -200j))
path.rotate(90)
self.assertAlmostEqual(path.point(0.0), (200 + -600j))
self.assertAlmostEqual(path.point(1.0), (200 + -600j))
path.rotate(90)
self.assertAlmostEqual(path.point(0.0), (600 + 200j))
self.assertAlmostEqual(path.point(1.0), (600 + 200j))
path.translate(-300 + 0j)
self.assertAlmostEqual(path.point(0.0), (300 + 200j))
self.assertAlmostEqual(path.point(1.0), (300 + 200j))
path.scale(2)
self.assertAlmostEqual(path.point(0.0), (600 + 400j))
self.assertAlmostEqual(path.point(1.0), (600 + 400j))
path.scale(0.5)
self.assertAlmostEqual(path.point(0.0), (300 + 200j))
self.assertAlmostEqual(path.point(1.0), (300 + 200j))
self.assertAlmostEqual(path.point(0.5), (66.93528751308301+49.735092953567715j))
path.translate(-300 + -200j)
path.scale(2)
path.translate(300 + 200j)
self.assertAlmostEqual(path.point(0.0), (300 + 200j))
self.assertAlmostEqual(path.point(1.0), (300 + 200j))
self.assertAlmostEqual(path.point(0.5), (-166.12942497383398 - 100.52981409286478j))
path.translate(-300 + -200j)
path.scale(0.5)
path.translate(300 + 200j)
self.assertAlmostEqual(path.point(0.0), (300 + 200j))
self.assertAlmostEqual(path.point(1.0), (300 + 200j))
self.assertAlmostEqual(path.point(0.5), (66.93528751308301+49.735092953567715j))


def test_reverse(self):
path = Path(Line(300 + 200j, 300 + 50j),
QuadraticBezier(300 + 50j, 0 + 0j, 75 + 100j),
Line(75 + 100j, 300 + 200j))
rpath = path.reverse()
self.assertTrue(path != rpath)
rpath = rpath.reverse()
self.assertTrue(path == rpath)