From d21a66aff060ea7cdeb11038272ec76aa6d3d3f2 Mon Sep 17 00:00:00 2001 From: Orion Elenzil Date: Tue, 22 May 2018 12:45:04 -0700 Subject: [PATCH 1/8] add scale() for curves, and scaled() for paths --- svgpathtools/path.py | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/svgpathtools/path.py b/svgpathtools/path.py index d21b061..72d0525 100644 --- a/svgpathtools/path.py +++ b/svgpathtools/path.py @@ -206,6 +206,28 @@ def translate(curve, z0): raise TypeError("Input `curve` should be a Path, Line, " "QuadraticBezier, CubicBezier, or Arc object.") +def scale(curve, s, origin=None): + """Scales the curve by the factor s around the origin such that + scale(curve, s, origin).point(t) = ((curve.point(t) - origin) * s) + origin""" + + def _scale_point(z, s, origin): + return ((z - origin) * s) + origin + + if origin == None: + origin = 0 + 0j + if isinstance(curve, Path): + return Path(*[scale(seg, s, origin) for seg in curve]) + elif is_bezier_segment(curve): + return bpoints2bezier([_scale_point(bpt, s, origin) for bpt in curve.bpoints()]) + elif isinstance(curve, Arc): + new_start = _scale_point(curve.start, s, origin) + new_end = _scale_point(curve.end, s, origin) + return Arc(new_start, radius=curve.radius * s, rotation=curve.rotation, + large_arc=curve.large_arc, sweep=curve.sweep, end=new_end) + else: + raise TypeError("Input `curve` should be a Path, Line, " + "QuadraticBezier, CubicBezier, or Arc object.") + def bezier_unit_tangent(seg, t): """Returns the unit tangent of the segment at t. @@ -637,6 +659,10 @@ class Line(object): that self.translated(z0).point(t) = self.point(t) + z0 for any t.""" return translate(self, z0) + def scaled(self, factor, origin=None): + """Returns a copy of self scaled by the scalar `factor`, about the complex point `origin`.""" + return scale(self, factor, origin=origin) + class QuadraticBezier(object): # For compatibility with old pickle files. @@ -881,6 +907,10 @@ class QuadraticBezier(object): that self.translated(z0).point(t) = self.point(t) + z0 for any t.""" return translate(self, z0) + def scaled(self, factor, origin=None): + """Returns a copy of self scaled by the scalar `factor`, about the complex point `origin`.""" + return scale(self, factor, origin=origin) + class CubicBezier(object): # For compatibility with old pickle files. @@ -1121,6 +1151,10 @@ class CubicBezier(object): that self.translated(z0).point(t) = self.point(t) + z0 for any t.""" return translate(self, z0) + def scaled(self, factor, origin=None): + """Returns a copy of self scaled by the scalar `factor`, about the complex point `origin`.""" + return scale(self, factor, origin=origin) + class Arc(object): def __init__(self, start, radius, rotation, large_arc, sweep, end, @@ -1686,6 +1720,9 @@ class Arc(object): that self.translated(z0).point(t) = self.point(t) + z0 for any t.""" return translate(self, z0) + def scaled(self, factor, origin=None): + """Returns a copy of self scaled by the scalar `factor`, about the complex point `origin`.""" + return scale(self, factor, origin=origin) def is_bezier_segment(x): return (isinstance(x, Line) or @@ -2242,3 +2279,7 @@ class Path(MutableSequence): """Returns a copy of self shifted by the complex quantity `z0` such that self.translated(z0).point(t) = self.point(t) + z0 for any t.""" return translate(self, z0) + + def scaled(self, factor, origin=None): + """Returns a copy of self scaled by the scalar `factor`, about the complex point `origin`.""" + return scale(self, factor, origin=origin) From 1ba9d45b35b63c5d378478606692468f6064c066 Mon Sep 17 00:00:00 2001 From: Orion Elenzil Date: Tue, 22 May 2018 15:48:45 -0700 Subject: [PATCH 2/8] unit test for new scale() and scaled() path transformation. tests all current segment types, composite paths, etc --- test/test_path.py | 67 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/test/test_path.py b/test/test_path.py index 8474b26..e9e1914 100644 --- a/test/test_path.py +++ b/test/test_path.py @@ -4,6 +4,7 @@ import unittest from math import sqrt, pi from operator import itemgetter from numpy import poly1d +from numpy import linspace # Internal dependencies from svgpathtools import * @@ -701,6 +702,72 @@ class TestPath(unittest.TestCase): with self.assertRaises(AssertionError): p_open.cropped(1, 0) + def test_transform_scale(self): + line1 = Line(600 + 350j, 650 + 325j) + arc1 = Arc(650 + 325j, 25 + 25j, -30, 0, 1, 700 + 300j) + cub1 = CubicBezier(650 + 325j, 25 + 25j, -30, 700 + 300j) + cub2 = CubicBezier(700 + 300j, 800 + 400j, 750 + 200j, 600 + 100j) + quad3 = QuadraticBezier(600 + 100j, 600, 600 + 300j) + linez = Line(600 + 300j, 600 + 350j) + + bezpath = Path(line1, cub1, cub2, quad3) + bezpathz = Path(line1, cub1, cub2, quad3, linez) + path = Path(line1, arc1, cub2, quad3) + pathz = Path(line1, arc1, cub2, quad3, linez) + lpath = Path(linez) + qpath = Path(quad3) + cpath = Path(cub1) + apath = Path(arc1) + + test_paths = [ + bezpath, + bezpathz, + path, + pathz, + lpath, + qpath, + cpath, + apath, + ] + + for path_orig in test_paths: + + # scale by 2 around (100, 100) + path_trns = path_orig.scaled(2.0, complex(100, 100)) + + # expected length + len_orig = path_orig.length() + len_trns = path_trns.length() + self.assertAlmostEqual(len_orig * 2.0, len_trns) + + # expected positions + for T in linspace(0.0, 1.0, num=100): + pt_orig = path_orig.point(T) + pt_trns = path_trns.point(T) + pt_xpct = (pt_orig - complex(100, 100)) * 2.0 + complex(100, 100) + self.assertAlmostEqual(pt_xpct, pt_trns) + + for path_orig in test_paths: + + # scale by 0.3 around (0, -100) + # the 'almost equal' test fails at the 7th decimal place for some length and position tests here. + path_trns = path_orig.scaled(0.3, complex(0, -100)) + + # expected length + len_orig = path_orig.length() + len_trns = path_trns.length() + self.assertAlmostEqual(len_orig * 0.3, len_trns, delta = 0.000001) + + # expected positions + for T in linspace(0.0, 1.0, num=100): + pt_orig = path_orig.point(T) + pt_trns = path_trns.point(T) + pt_xpct = (pt_orig - complex(0, -100)) * 0.3 + complex(0, -100) + self.assertAlmostEqual(pt_xpct, pt_trns, delta = 0.000001) + + + + class Test_ilength(unittest.TestCase): # See svgpathtools.notes.inv_arclength.py for information on how these From 63944151088d30820c69acaf1b4dd9c312fb0452 Mon Sep 17 00:00:00 2001 From: Andy Port Date: Tue, 22 May 2018 19:22:09 -0700 Subject: [PATCH 3/8] minor aesthetic change --- test/test_path.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/test_path.py b/test/test_path.py index e9e1914..94e7406 100644 --- a/test/test_path.py +++ b/test/test_path.py @@ -3,8 +3,7 @@ from __future__ import division, absolute_import, print_function import unittest from math import sqrt, pi from operator import itemgetter -from numpy import poly1d -from numpy import linspace +from numpy import poly1d, linspace # Internal dependencies from svgpathtools import * From eafe3682b987fd0e39c0d9956e340e51c247813e Mon Sep 17 00:00:00 2001 From: Andy Port Date: Tue, 22 May 2018 19:34:56 -0700 Subject: [PATCH 4/8] altered so lines aren't (much) over 79 characters Note: this is mostly unrelated to changes requested by @playi --- test/test_path.py | 100 ++++++++++++++++++++++++++++++---------------- 1 file changed, 65 insertions(+), 35 deletions(-) diff --git a/test/test_path.py b/test/test_path.py index 94e7406..b8df0de 100644 --- a/test/test_path.py +++ b/test/test_path.py @@ -48,12 +48,12 @@ class LineTest(unittest.TestCase): # This is to test the __eq__ and __ne__ methods, so we can't use # assertEqual and assertNotEqual line = Line(0j, 400 + 0j) + cubic = CubicBezier(600 + 500j, 600 + 350j, 900 + 650j, 900 + 500j) self.assertTrue(line == Line(0, 400)) self.assertTrue(line != Line(100, 400)) self.assertFalse(line == str(line)) self.assertTrue(line != str(line)) - self.assertFalse( - CubicBezier(600 + 500j, 600 + 350j, 900 + 650j, 900 + 500j) == line) + self.assertFalse(cubic == line) class CubicBezierTest(unittest.TestCase): @@ -233,8 +233,9 @@ class CubicBezierTest(unittest.TestCase): self.assertAlmostEqual(cub.length(), sqrt(2 * 100 * 100)) - # A quarter circle large_arc with radius 100: - kappa = 4 * (sqrt(2) - 1) / 3 # http://www.whizkidtech.redprince.net/bezier/circle/ + # A quarter circle large_arc with radius 100 + # http://www.whizkidtech.redprince.net/bezier/circle/ + kappa = 4 * (sqrt(2) - 1) / 3 cub = CubicBezier( complex(0, 0), @@ -270,9 +271,9 @@ class CubicBezierTest(unittest.TestCase): complex(900, 650), complex(900, 500)) self.assertTrue(segment == - CubicBezier(600 + 500j, 600 + 350j, 900 + 650j, 900 + 500j)) + CubicBezier(600 + 500j, 600 + 350j, 900 + 650j, 900 + 500j)) self.assertTrue(segment != - CubicBezier(600 + 501j, 600 + 350j, 900 + 650j, 900 + 500j)) + CubicBezier(600 + 501j, 600 + 350j, 900 + 650j, 900 + 500j)) self.assertTrue(segment != Line(0, 400)) @@ -345,8 +346,10 @@ class QuadraticBezierTest(unittest.TestCase): # This is to test the __eq__ and __ne__ methods, so we can't use # assertEqual and assertNotEqual segment = QuadraticBezier(200 + 300j, 400 + 50j, 600 + 300j) - self.assertTrue(segment == QuadraticBezier(200 + 300j, 400 + 50j, 600 + 300j)) - self.assertTrue(segment != QuadraticBezier(200 + 301j, 400 + 50j, 600 + 300j)) + self.assertTrue(segment == + QuadraticBezier(200 + 300j, 400 + 50j, 600 + 300j)) + self.assertTrue(segment != + QuadraticBezier(200 + 301j, 400 + 50j, 600 + 300j)) self.assertFalse(segment == Arc(0j, 100 + 50j, 0, 0, 0, 100 + 50j)) self.assertTrue(Arc(0j, 100 + 50j, 0, 0, 0, 100 + 50j) != segment) @@ -439,16 +442,26 @@ class ArcTest(unittest.TestCase): 225.6910319606926, 1, 1, (-624.6375539637027+896.5483089399895j)) self.assertAlmostEqual(arc5.point(0.0), (725.307482226-915.554819928j)) - self.assertAlmostEqual(arc5.point(0.0909090909091), (1023.47397369-597.730444283j)) - self.assertAlmostEqual(arc5.point(0.181818181818), (1242.80253007-232.251400124j)) - self.assertAlmostEqual(arc5.point(0.272727272727), (1365.52445614+151.273373978j)) - self.assertAlmostEqual(arc5.point(0.363636363636), (1381.69755131+521.772981736j)) - self.assertAlmostEqual(arc5.point(0.454545454545), (1290.01156757+849.231748376j)) - self.assertAlmostEqual(arc5.point(0.545454545455), (1097.89435807+1107.12091209j)) - self.assertAlmostEqual(arc5.point(0.636363636364), (820.910116547+1274.54782658j)) - self.assertAlmostEqual(arc5.point(0.727272727273), (481.49845896+1337.94855893j)) - self.assertAlmostEqual(arc5.point(0.818181818182), (107.156499251+1292.18675889j)) - self.assertAlmostEqual(arc5.point(0.909090909091), (-271.788803303+1140.96977533j)) + self.assertAlmostEqual(arc5.point(0.0909090909091), + (1023.47397369-597.730444283j)) + self.assertAlmostEqual(arc5.point(0.181818181818), + (1242.80253007-232.251400124j)) + self.assertAlmostEqual(arc5.point(0.272727272727), + (1365.52445614+151.273373978j)) + self.assertAlmostEqual(arc5.point(0.363636363636), + (1381.69755131+521.772981736j)) + self.assertAlmostEqual(arc5.point(0.454545454545), + (1290.01156757+849.231748376j)) + self.assertAlmostEqual(arc5.point(0.545454545455), + (1097.89435807+1107.12091209j)) + self.assertAlmostEqual(arc5.point(0.636363636364), + (820.910116547+1274.54782658j)) + self.assertAlmostEqual(arc5.point(0.727272727273), + (481.49845896+1337.94855893j)) + self.assertAlmostEqual(arc5.point(0.818181818182), + (107.156499251+1292.18675889j)) + self.assertAlmostEqual(arc5.point(0.909090909091), + (-271.788803303+1140.96977533j)) def test_length(self): # I'll test the length calculations by making a circle, in two parts. @@ -485,26 +498,31 @@ class TestPath(unittest.TestCase): path = Path(Line(300 + 200j, 150 + 200j), Arc(150 + 200j, 150 + 150j, 0, 1, 0, 300 + 50j), Line(300 + 50j, 300 + 200j)) - # The points and length for this path are calculated and not regression tests. + # The points and length for this path are calculated and not + # regression tests. self.assertAlmostEqual(path.point(0.0), (300 + 200j)) self.assertAlmostEqual(path.point(0.14897825542), (150 + 200j)) self.assertAlmostEqual(path.point(0.5), (406.066017177 + 306.066017177j)) self.assertAlmostEqual(path.point(1 - 0.14897825542), (300 + 50j)) self.assertAlmostEqual(path.point(1.0), (300 + 200j)) - # The errors seem to accumulate. Still 6 decimal places is more than good enough. + # The errors seem to accumulate. Still 6 decimal places is more + # than good enough. self.assertAlmostEqual(path.length(), pi * 225 + 300, places=6) # Little pie: M275,175 v-150 a150,150 0 0,0 -150,150 z path = Path(Line(275 + 175j, 275 + 25j), Arc(275 + 25j, 150 + 150j, 0, 0, 0, 125 + 175j), Line(125 + 175j, 275 + 175j)) - # The points and length for this path are calculated and not regression tests. + # The points and length for this path are calculated and not + # regression tests. self.assertAlmostEqual(path.point(0.0), (275 + 175j)) self.assertAlmostEqual(path.point(0.2800495767557787), (275 + 25j)) - self.assertAlmostEqual(path.point(0.5), (168.93398282201787 + 68.93398282201787j)) + self.assertAlmostEqual(path.point(0.5), + (168.93398282201787 + 68.93398282201787j)) self.assertAlmostEqual(path.point(1 - 0.2800495767557787), (125 + 175j)) self.assertAlmostEqual(path.point(1.0), (275 + 175j)) - # The errors seem to accumulate. Still 6 decimal places is more than good enough. + # The errors seem to accumulate. Still 6 decimal places is more + # than good enough. self.assertAlmostEqual(path.length(), pi * 75 + 300, places=6) # Bumpy path: M600,350 l 50,-25 @@ -531,14 +549,17 @@ class TestPath(unittest.TestCase): # self.assertAlmostEqual(path.point(0.5), (827.730749264+147.824157418j)) # self.assertAlmostEqual(path.point(0.9), (971.284357806+106.302352605j)) # self.assertAlmostEqual(path.point(1), (1050+125j)) - # # The errors seem to accumulate. Still 6 decimal places is more than good enough. + # # The errors seem to accumulate. Still 6 decimal places is more + # # than good enough. # self.assertAlmostEqual(path.length(), 928.3886394081095) def test_repr(self): path = Path( Line(start=600 + 350j, end=650 + 325j), - Arc(start=650 + 325j, radius=25 + 25j, rotation=-30, large_arc=0, sweep=1, end=700 + 300j), - CubicBezier(start=700 + 300j, control1=800 + 400j, control2=750 + 200j, end=600 + 100j), + Arc(start=650 + 325j, radius=25 + 25j, rotation=-30, + large_arc=0, sweep=1, end=700 + 300j), + CubicBezier(start=700 + 300j, control1=800 + 400j, + control2=750 + 200j, end=600 + 100j), QuadraticBezier(start=600 + 100j, control=600, end=600 + 300j)) self.assertEqual(eval(repr(path)), path) @@ -547,13 +568,17 @@ class TestPath(unittest.TestCase): # assertEqual and assertNotEqual path1 = Path( Line(start=600 + 350j, end=650 + 325j), - Arc(start=650 + 325j, radius=25 + 25j, rotation=-30, large_arc=0, sweep=1, end=700 + 300j), - CubicBezier(start=700 + 300j, control1=800 + 400j, control2=750 + 200j, end=600 + 100j), + Arc(start=650 + 325j, radius=25 + 25j, rotation=-30, + large_arc=0, sweep=1, end=700 + 300j), + CubicBezier(start=700 + 300j, control1=800 + 400j, + control2=750 + 200j, end=600 + 100j), QuadraticBezier(start=600 + 100j, control=600, end=600 + 300j)) path2 = Path( Line(start=600 + 350j, end=650 + 325j), - Arc(start=650 + 325j, radius=25 + 25j, rotation=-30, large_arc=0, sweep=1, end=700 + 300j), - CubicBezier(start=700 + 300j, control1=800 + 400j, control2=750 + 200j, end=600 + 100j), + Arc(start=650 + 325j, radius=25 + 25j, rotation=-30, + large_arc=0, sweep=1, end=700 + 300j), + CubicBezier(start=700 + 300j, control1=800 + 400j, + control2=750 + 200j, end=600 + 100j), QuadraticBezier(start=600 + 100j, control=600, end=600 + 300j)) self.assertTrue(path1 == path2) @@ -749,7 +774,8 @@ class TestPath(unittest.TestCase): for path_orig in test_paths: # scale by 0.3 around (0, -100) - # the 'almost equal' test fails at the 7th decimal place for some length and position tests here. + # the 'almost equal' test fails at the 7th decimal place for + # some length and position tests here. path_trns = path_orig.scaled(0.3, complex(0, -100)) # expected length @@ -1058,14 +1084,18 @@ class Test_intersect(unittest.TestCase): ################################################################### def test_line_line_0(self): - l0 = Line(start=(25.389999999999997+99.989999999999995j), end=(25.389999999999997+90.484999999999999j)) - l1 = Line(start=(25.390000000000001+84.114999999999995j), end=(25.389999999999997+74.604202137430320j)) + l0 = Line(start=(25.389999999999997+99.989999999999995j), + end=(25.389999999999997+90.484999999999999j)) + l1 = Line(start=(25.390000000000001+84.114999999999995j), + end=(25.389999999999997+74.604202137430320j)) i = l0.intersect(l1) assert(len(i)) == 0 def test_line_line_1(self): - l0 = Line(start=(-124.705378549+327.696674827j), end=(12.4926214511+121.261674827j)) - l1 = Line(start=(-12.4926214511+121.261674827j), end=(124.705378549+327.696674827j)) + l0 = Line(start=(-124.705378549+327.696674827j), + end=(12.4926214511+121.261674827j)) + l1 = Line(start=(-12.4926214511+121.261674827j), + end=(124.705378549+327.696674827j)) i = l0.intersect(l1) assert(len(i)) == 1 assert(abs(l0.point(i[0][0])-l1.point(i[0][1])) < 1e-9) From 08272069531ebc317d0d6dab0633714e82c20103 Mon Sep 17 00:00:00 2001 From: Andy Port Date: Tue, 22 May 2018 19:59:03 -0700 Subject: [PATCH 5/8] style changes --- svgpathtools/path.py | 35 +++++++++++++++++++---------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/svgpathtools/path.py b/svgpathtools/path.py index 72d0525..27bb4d5 100644 --- a/svgpathtools/path.py +++ b/svgpathtools/path.py @@ -169,7 +169,7 @@ def rotate(curve, degs, origin=None): def transform(z): return exp(1j*radians(degs))*(z - origin) + origin - if origin == None: + if origin is None: if isinstance(curve, Arc): origin = curve.center else: @@ -206,23 +206,25 @@ def translate(curve, z0): raise TypeError("Input `curve` should be a Path, Line, " "QuadraticBezier, CubicBezier, or Arc object.") -def scale(curve, s, origin=None): - """Scales the curve by the factor s around the origin such that - scale(curve, s, origin).point(t) = ((curve.point(t) - origin) * s) + origin""" + +def scale(curve, factor, origin=0j): + """Scales `curve` by scalar `factor` around `origin`. + + Note: scale(curve, s, origin).point(t) == + ((curve.point(t) - origin) * factor) + origin + """ def _scale_point(z, s, origin): return ((z - origin) * s) + origin - if origin == None: - origin = 0 + 0j if isinstance(curve, Path): - return Path(*[scale(seg, s, origin) for seg in curve]) + return Path(*[scale(seg, factor, origin) for seg in curve]) elif is_bezier_segment(curve): - return bpoints2bezier([_scale_point(bpt, s, origin) for bpt in curve.bpoints()]) + return bpoints2bezier([_scale_point(bpt, factor, origin) for bpt in curve.bpoints()]) elif isinstance(curve, Arc): - new_start = _scale_point(curve.start, s, origin) - new_end = _scale_point(curve.end, s, origin) - return Arc(new_start, radius=curve.radius * s, rotation=curve.rotation, + new_start = _scale_point(curve.start, factor, origin) + new_end = _scale_point(curve.end, factor, origin) + return Arc(new_start, radius=curve.radius * factor, rotation=curve.rotation, large_arc=curve.large_arc, sweep=curve.sweep, end=new_end) else: raise TypeError("Input `curve` should be a Path, Line, " @@ -660,7 +662,7 @@ class Line(object): return translate(self, z0) def scaled(self, factor, origin=None): - """Returns a copy of self scaled by the scalar `factor`, about the complex point `origin`.""" + """Returns copy of self scaled by `factor` about `origin`.""" return scale(self, factor, origin=origin) @@ -908,7 +910,7 @@ class QuadraticBezier(object): return translate(self, z0) def scaled(self, factor, origin=None): - """Returns a copy of self scaled by the scalar `factor`, about the complex point `origin`.""" + """Returns copy of self scaled by `factor` about `origin`.""" return scale(self, factor, origin=origin) @@ -1152,7 +1154,7 @@ class CubicBezier(object): return translate(self, z0) def scaled(self, factor, origin=None): - """Returns a copy of self scaled by the scalar `factor`, about the complex point `origin`.""" + """Returns copy of self scaled by `factor` about `origin`.""" return scale(self, factor, origin=origin) @@ -1721,9 +1723,10 @@ class Arc(object): return translate(self, z0) def scaled(self, factor, origin=None): - """Returns a copy of self scaled by the scalar `factor`, about the complex point `origin`.""" + """Returns copy of self scaled by `factor` about `origin`.""" return scale(self, factor, origin=origin) + def is_bezier_segment(x): return (isinstance(x, Line) or isinstance(x, QuadraticBezier) or @@ -2281,5 +2284,5 @@ class Path(MutableSequence): return translate(self, z0) def scaled(self, factor, origin=None): - """Returns a copy of self scaled by the scalar `factor`, about the complex point `origin`.""" + """Returns copy of self scaled by `factor` about `origin`.""" return scale(self, factor, origin=origin) From ee656c7de049d415962ecb9f2ac12a7fd8205d28 Mon Sep 17 00:00:00 2001 From: Orion Elenzil Date: Wed, 30 May 2018 10:54:56 -0700 Subject: [PATCH 6/8] refactor `scale()` and `scaled()` to `scale_uniform()` and `scaled_uniform()` --- svgpathtools/path.py | 28 ++++++++++++++-------------- test/test_path.py | 6 +++--- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/svgpathtools/path.py b/svgpathtools/path.py index 27bb4d5..8108042 100644 --- a/svgpathtools/path.py +++ b/svgpathtools/path.py @@ -207,10 +207,10 @@ def translate(curve, z0): "QuadraticBezier, CubicBezier, or Arc object.") -def scale(curve, factor, origin=0j): - """Scales `curve` by scalar `factor` around `origin`. +def scale_uniform(curve, factor, origin=0j): + """Uniformly scales `curve` by scalar `factor` around `origin`. - Note: scale(curve, s, origin).point(t) == + Note: scale_uniform(curve, s, origin).point(t) == ((curve.point(t) - origin) * factor) + origin """ @@ -218,7 +218,7 @@ def scale(curve, factor, origin=0j): return ((z - origin) * s) + origin if isinstance(curve, Path): - return Path(*[scale(seg, factor, origin) for seg in curve]) + return Path(*[scale_uniform(seg, factor, origin) for seg in curve]) elif is_bezier_segment(curve): return bpoints2bezier([_scale_point(bpt, factor, origin) for bpt in curve.bpoints()]) elif isinstance(curve, Arc): @@ -661,9 +661,9 @@ class Line(object): that self.translated(z0).point(t) = self.point(t) + z0 for any t.""" return translate(self, z0) - def scaled(self, factor, origin=None): + def scaled_uniform(self, factor, origin=None): """Returns copy of self scaled by `factor` about `origin`.""" - return scale(self, factor, origin=origin) + return scale_uniform(self, factor, origin=origin) class QuadraticBezier(object): @@ -909,9 +909,9 @@ class QuadraticBezier(object): that self.translated(z0).point(t) = self.point(t) + z0 for any t.""" return translate(self, z0) - def scaled(self, factor, origin=None): + def scaled_uniform(self, factor, origin=None): """Returns copy of self scaled by `factor` about `origin`.""" - return scale(self, factor, origin=origin) + return scale_uniform(self, factor, origin=origin) class CubicBezier(object): @@ -1153,9 +1153,9 @@ class CubicBezier(object): that self.translated(z0).point(t) = self.point(t) + z0 for any t.""" return translate(self, z0) - def scaled(self, factor, origin=None): + def scaled_uniform(self, factor, origin=None): """Returns copy of self scaled by `factor` about `origin`.""" - return scale(self, factor, origin=origin) + return scale_uniform(self, factor, origin=origin) class Arc(object): @@ -1722,9 +1722,9 @@ class Arc(object): that self.translated(z0).point(t) = self.point(t) + z0 for any t.""" return translate(self, z0) - def scaled(self, factor, origin=None): + def scaled_uniform(self, factor, origin=None): """Returns copy of self scaled by `factor` about `origin`.""" - return scale(self, factor, origin=origin) + return scale_uniform(self, factor, origin=origin) def is_bezier_segment(x): @@ -2283,6 +2283,6 @@ class Path(MutableSequence): that self.translated(z0).point(t) = self.point(t) + z0 for any t.""" return translate(self, z0) - def scaled(self, factor, origin=None): + def scaled_uniform(self, factor, origin=None): """Returns copy of self scaled by `factor` about `origin`.""" - return scale(self, factor, origin=origin) + return scale_uniform(self, factor, origin=origin) diff --git a/test/test_path.py b/test/test_path.py index b8df0de..8e5cc24 100644 --- a/test/test_path.py +++ b/test/test_path.py @@ -726,7 +726,7 @@ class TestPath(unittest.TestCase): with self.assertRaises(AssertionError): p_open.cropped(1, 0) - def test_transform_scale(self): + def test_transform_scale_uniform(self): line1 = Line(600 + 350j, 650 + 325j) arc1 = Arc(650 + 325j, 25 + 25j, -30, 0, 1, 700 + 300j) cub1 = CubicBezier(650 + 325j, 25 + 25j, -30, 700 + 300j) @@ -757,7 +757,7 @@ class TestPath(unittest.TestCase): for path_orig in test_paths: # scale by 2 around (100, 100) - path_trns = path_orig.scaled(2.0, complex(100, 100)) + path_trns = path_orig.scaled_uniform(2.0, complex(100, 100)) # expected length len_orig = path_orig.length() @@ -776,7 +776,7 @@ class TestPath(unittest.TestCase): # scale by 0.3 around (0, -100) # the 'almost equal' test fails at the 7th decimal place for # some length and position tests here. - path_trns = path_orig.scaled(0.3, complex(0, -100)) + path_trns = path_orig.scaled_uniform(0.3, complex(0, -100)) # expected length len_orig = path_orig.length() From 72d7467896ef165549c29ae4a2fc84f5da3128dc Mon Sep 17 00:00:00 2001 From: Andy Port Date: Wed, 30 May 2018 19:07:58 -0700 Subject: [PATCH 7/8] implemented (almost) full SVG scale transform functionality --- svgpathtools/path.py | 73 +++++++++++++++++++++++++++----------------- 1 file changed, 45 insertions(+), 28 deletions(-) diff --git a/svgpathtools/path.py b/svgpathtools/path.py index 8108042..9eec4c3 100644 --- a/svgpathtools/path.py +++ b/svgpathtools/path.py @@ -207,25 +207,42 @@ def translate(curve, z0): "QuadraticBezier, CubicBezier, or Arc object.") -def scale_uniform(curve, factor, origin=0j): - """Uniformly scales `curve` by scalar `factor` around `origin`. - - Note: scale_uniform(curve, s, origin).point(t) == - ((curve.point(t) - origin) * factor) + origin +def scale(curve, sx, sy=None, origin=0j): + """Scales `curve`, about `origin`, by diagonal matrix `[[sx,0],[0,sy]]`. + + Notes: + ------ + * If `sy` is not specified, it is assumed to be equal to `sx` and + a scalar transformation of `curve` about `origin` will be returned. + I.e. + scale(curve, sx, origin).point(t) == + ((curve.point(t) - origin) * sx) + origin """ - def _scale_point(z, s, origin): - return ((z - origin) * s) + origin + if sy is None: + isy = 1j*sx + else: + isy = 1j*sy + + def transform(z, sx=sx, sy=sy, origin=origin): + zeta = z - origin + return x*zeta.real + isy*zeta.imag + origin if isinstance(curve, Path): - return Path(*[scale_uniform(seg, factor, origin) for seg in curve]) + return Path(*[scale(seg, sx, sy, origin) for seg in curve]) elif is_bezier_segment(curve): - return bpoints2bezier([_scale_point(bpt, factor, origin) for bpt in curve.bpoints()]) + return bpoints2bezier([transform(z) for z in curve.bpoints()]) elif isinstance(curve, Arc): - new_start = _scale_point(curve.start, factor, origin) - new_end = _scale_point(curve.end, factor, origin) - return Arc(new_start, radius=curve.radius * factor, rotation=curve.rotation, - large_arc=curve.large_arc, sweep=curve.sweep, end=new_end) + if y is None or y == x: + return Arc(start=transform(curve.start), + radius=transform(radius, origin=0), + rotation=curve.rotation, + large_arc=curve.large_arc, + sweep=curve.sweep, + end=transform(curve.end)) + else: + raise Excpetion("For `Arc` objects, only scale transforms " + "with sx==sy are implemenented.") else: raise TypeError("Input `curve` should be a Path, Line, " "QuadraticBezier, CubicBezier, or Arc object.") @@ -661,9 +678,9 @@ class Line(object): that self.translated(z0).point(t) = self.point(t) + z0 for any t.""" return translate(self, z0) - def scaled_uniform(self, factor, origin=None): - """Returns copy of self scaled by `factor` about `origin`.""" - return scale_uniform(self, factor, origin=origin) + def scaled(self, sx, sy=None, origin=0j): + """Scale transform. See `scale` function for further explanation.""" + return scale(self, sx=sx, sy=sy, origin=origin) class QuadraticBezier(object): @@ -909,9 +926,9 @@ class QuadraticBezier(object): that self.translated(z0).point(t) = self.point(t) + z0 for any t.""" return translate(self, z0) - def scaled_uniform(self, factor, origin=None): - """Returns copy of self scaled by `factor` about `origin`.""" - return scale_uniform(self, factor, origin=origin) + def scaled(self, sx, sy=None, origin=0j): + """Scale transform. See `scale` function for further explanation.""" + return scale(self, sx=sx, sy=sy, origin=origin) class CubicBezier(object): @@ -1153,9 +1170,9 @@ class CubicBezier(object): that self.translated(z0).point(t) = self.point(t) + z0 for any t.""" return translate(self, z0) - def scaled_uniform(self, factor, origin=None): - """Returns copy of self scaled by `factor` about `origin`.""" - return scale_uniform(self, factor, origin=origin) + def scaled(self, sx, sy=None, origin=0j): + """Scale transform. See `scale` function for further explanation.""" + return scale(self, sx=sx, sy=sy, origin=origin) class Arc(object): @@ -1722,9 +1739,9 @@ class Arc(object): that self.translated(z0).point(t) = self.point(t) + z0 for any t.""" return translate(self, z0) - def scaled_uniform(self, factor, origin=None): - """Returns copy of self scaled by `factor` about `origin`.""" - return scale_uniform(self, factor, origin=origin) + def scaled(self, sx, sy=None, origin=0j): + """Scale transform. See `scale` function for further explanation.""" + return scale(self, sx=sx, sy=sy, origin=origin) def is_bezier_segment(x): @@ -2283,6 +2300,6 @@ class Path(MutableSequence): that self.translated(z0).point(t) = self.point(t) + z0 for any t.""" return translate(self, z0) - def scaled_uniform(self, factor, origin=None): - """Returns copy of self scaled by `factor` about `origin`.""" - return scale_uniform(self, factor, origin=origin) + def scaled(self, sx, sy=None, origin=0j): + """Scale transform. See `scale` function for further explanation.""" + return scale(self, sx=sx, sy=sy, origin=origin) From 304c0bbe1d6e7b907548563668972cd7709b9f16 Mon Sep 17 00:00:00 2001 From: Andy Port Date: Wed, 30 May 2018 19:30:24 -0700 Subject: [PATCH 8/8] improved `scale` related tests --- test/test_path.py | 32 +++++++++++--------------------- 1 file changed, 11 insertions(+), 21 deletions(-) diff --git a/test/test_path.py b/test/test_path.py index 8e5cc24..d7d1e4b 100644 --- a/test/test_path.py +++ b/test/test_path.py @@ -726,9 +726,10 @@ class TestPath(unittest.TestCase): with self.assertRaises(AssertionError): p_open.cropped(1, 0) - def test_transform_scale_uniform(self): - line1 = Line(600 + 350j, 650 + 325j) + def test_transform_scale(self): + line1 = Line(600.5 + 350.5j, 650.5 + 325.5j) arc1 = Arc(650 + 325j, 25 + 25j, -30, 0, 1, 700 + 300j) + arc2 = Arc(650 + 325j, 30 + 25j, -30, 0, 0, 700 + 300j) cub1 = CubicBezier(650 + 325j, 25 + 25j, -30, 700 + 300j) cub2 = CubicBezier(700 + 300j, 800 + 400j, 750 + 200j, 600 + 100j) quad3 = QuadraticBezier(600 + 100j, 600, 600 + 300j) @@ -741,23 +742,15 @@ class TestPath(unittest.TestCase): lpath = Path(linez) qpath = Path(quad3) cpath = Path(cub1) - apath = Path(arc1) + apath = Path(arc1, arc2) - test_paths = [ - bezpath, - bezpathz, - path, - pathz, - lpath, - qpath, - cpath, - apath, - ] + test_curves = ([bezpath, bezpathz, path, pathz, lpath, qpath, cpath, apath] + + [line1, arc1, arc2, cub1, cub2, quad3, linez]) - for path_orig in test_paths: + for path_orig in test_curves: # scale by 2 around (100, 100) - path_trns = path_orig.scaled_uniform(2.0, complex(100, 100)) + path_trns = path_orig.scaled(2.0, complex(100, 100)) # expected length len_orig = path_orig.length() @@ -771,12 +764,12 @@ class TestPath(unittest.TestCase): pt_xpct = (pt_orig - complex(100, 100)) * 2.0 + complex(100, 100) self.assertAlmostEqual(pt_xpct, pt_trns) - for path_orig in test_paths: + for path_orig in test_curves: # scale by 0.3 around (0, -100) # the 'almost equal' test fails at the 7th decimal place for # some length and position tests here. - path_trns = path_orig.scaled_uniform(0.3, complex(0, -100)) + path_trns = path_orig.scaled(0.3, complex(0, -100)) # expected length len_orig = path_orig.length() @@ -789,10 +782,7 @@ class TestPath(unittest.TestCase): pt_trns = path_trns.point(T) pt_xpct = (pt_orig - complex(0, -100)) * 0.3 + complex(0, -100) self.assertAlmostEqual(pt_xpct, pt_trns, delta = 0.000001) - - - - + class Test_ilength(unittest.TestCase): # See svgpathtools.notes.inv_arclength.py for information on how these