diff --git a/src/svg/path/path.py b/src/svg/path/path.py index fffb4d3..c4283fc 100644 --- a/src/svg/path/path.py +++ b/src/svg/path/path.py @@ -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 @@ -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): + self.start -= other + self.end -= other + + def __mul__(self, other): + 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 @@ -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): @@ -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): @@ -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): @@ -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 @@ -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 @@ -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) @@ -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] diff --git a/src/svg/path/tests/test_paths.py b/src/svg/path/tests/test_paths.py index 1d19105..7315c3d 100644 --- a/src/svg/path/tests/test_paths.py +++ b/src/svg/path/tests/test_paths.py @@ -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 @@ -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)