Merge pull request #54 from playi/scale

add scale() for curves, and scaled() for paths
pull/48/merge
Andy Port 2018-05-30 21:20:22 -07:00 committed by GitHub
commit 55661d18a4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 183 additions and 36 deletions

View File

@ -169,7 +169,7 @@ def rotate(curve, degs, origin=None):
def transform(z): def transform(z):
return exp(1j*radians(degs))*(z - origin) + origin return exp(1j*radians(degs))*(z - origin) + origin
if origin == None: if origin is None:
if isinstance(curve, Arc): if isinstance(curve, Arc):
origin = curve.center origin = curve.center
else: else:
@ -207,6 +207,47 @@ def translate(curve, z0):
"QuadraticBezier, CubicBezier, or Arc object.") "QuadraticBezier, CubicBezier, or Arc object.")
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
"""
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(seg, sx, sy, origin) for seg in curve])
elif is_bezier_segment(curve):
return bpoints2bezier([transform(z) for z in curve.bpoints()])
elif isinstance(curve, Arc):
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.")
def bezier_unit_tangent(seg, t): def bezier_unit_tangent(seg, t):
"""Returns the unit tangent of the segment at t. """Returns the unit tangent of the segment at t.
@ -637,6 +678,10 @@ class Line(object):
that self.translated(z0).point(t) = self.point(t) + z0 for any t.""" that self.translated(z0).point(t) = self.point(t) + z0 for any t."""
return translate(self, z0) return translate(self, z0)
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): class QuadraticBezier(object):
# For compatibility with old pickle files. # For compatibility with old pickle files.
@ -881,6 +926,10 @@ class QuadraticBezier(object):
that self.translated(z0).point(t) = self.point(t) + z0 for any t.""" that self.translated(z0).point(t) = self.point(t) + z0 for any t."""
return translate(self, z0) return translate(self, z0)
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): class CubicBezier(object):
# For compatibility with old pickle files. # For compatibility with old pickle files.
@ -1121,6 +1170,10 @@ class CubicBezier(object):
that self.translated(z0).point(t) = self.point(t) + z0 for any t.""" that self.translated(z0).point(t) = self.point(t) + z0 for any t."""
return translate(self, z0) return translate(self, z0)
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): class Arc(object):
def __init__(self, start, radius, rotation, large_arc, sweep, end, def __init__(self, start, radius, rotation, large_arc, sweep, end,
@ -1686,6 +1739,10 @@ class Arc(object):
that self.translated(z0).point(t) = self.point(t) + z0 for any t.""" that self.translated(z0).point(t) = self.point(t) + z0 for any t."""
return translate(self, z0) return translate(self, z0)
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): def is_bezier_segment(x):
return (isinstance(x, Line) or return (isinstance(x, Line) or
@ -2242,3 +2299,7 @@ class Path(MutableSequence):
"""Returns a copy of self shifted by the complex quantity `z0` such """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.""" that self.translated(z0).point(t) = self.point(t) + z0 for any t."""
return translate(self, z0) return translate(self, z0)
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)

View File

@ -3,7 +3,7 @@ from __future__ import division, absolute_import, print_function
import unittest import unittest
from math import sqrt, pi from math import sqrt, pi
from operator import itemgetter from operator import itemgetter
from numpy import poly1d from numpy import poly1d, linspace
# Internal dependencies # Internal dependencies
from svgpathtools import * from svgpathtools import *
@ -48,12 +48,12 @@ class LineTest(unittest.TestCase):
# This is to test the __eq__ and __ne__ methods, so we can't use # This is to test the __eq__ and __ne__ methods, so we can't use
# assertEqual and assertNotEqual # assertEqual and assertNotEqual
line = Line(0j, 400 + 0j) 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(0, 400))
self.assertTrue(line != Line(100, 400)) self.assertTrue(line != Line(100, 400))
self.assertFalse(line == str(line)) self.assertFalse(line == str(line))
self.assertTrue(line != str(line)) self.assertTrue(line != str(line))
self.assertFalse( self.assertFalse(cubic == line)
CubicBezier(600 + 500j, 600 + 350j, 900 + 650j, 900 + 500j) == line)
class CubicBezierTest(unittest.TestCase): class CubicBezierTest(unittest.TestCase):
@ -233,8 +233,9 @@ class CubicBezierTest(unittest.TestCase):
self.assertAlmostEqual(cub.length(), sqrt(2 * 100 * 100)) self.assertAlmostEqual(cub.length(), sqrt(2 * 100 * 100))
# A quarter circle large_arc with radius 100: # A quarter circle large_arc with radius 100
kappa = 4 * (sqrt(2) - 1) / 3 # http://www.whizkidtech.redprince.net/bezier/circle/ # http://www.whizkidtech.redprince.net/bezier/circle/
kappa = 4 * (sqrt(2) - 1) / 3
cub = CubicBezier( cub = CubicBezier(
complex(0, 0), complex(0, 0),
@ -270,9 +271,9 @@ class CubicBezierTest(unittest.TestCase):
complex(900, 650), complex(900, 500)) complex(900, 650), complex(900, 500))
self.assertTrue(segment == self.assertTrue(segment ==
CubicBezier(600 + 500j, 600 + 350j, 900 + 650j, 900 + 500j)) CubicBezier(600 + 500j, 600 + 350j, 900 + 650j, 900 + 500j))
self.assertTrue(segment != 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)) 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 # This is to test the __eq__ and __ne__ methods, so we can't use
# assertEqual and assertNotEqual # assertEqual and assertNotEqual
segment = QuadraticBezier(200 + 300j, 400 + 50j, 600 + 300j) segment = QuadraticBezier(200 + 300j, 400 + 50j, 600 + 300j)
self.assertTrue(segment == QuadraticBezier(200 + 300j, 400 + 50j, 600 + 300j)) self.assertTrue(segment ==
self.assertTrue(segment != QuadraticBezier(200 + 301j, 400 + 50j, 600 + 300j)) 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.assertFalse(segment == Arc(0j, 100 + 50j, 0, 0, 0, 100 + 50j))
self.assertTrue(Arc(0j, 100 + 50j, 0, 0, 0, 100 + 50j) != segment) self.assertTrue(Arc(0j, 100 + 50j, 0, 0, 0, 100 + 50j) != segment)
@ -439,16 +442,26 @@ class ArcTest(unittest.TestCase):
225.6910319606926, 1, 1, 225.6910319606926, 1, 1,
(-624.6375539637027+896.5483089399895j)) (-624.6375539637027+896.5483089399895j))
self.assertAlmostEqual(arc5.point(0.0), (725.307482226-915.554819928j)) 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.0909090909091),
self.assertAlmostEqual(arc5.point(0.181818181818), (1242.80253007-232.251400124j)) (1023.47397369-597.730444283j))
self.assertAlmostEqual(arc5.point(0.272727272727), (1365.52445614+151.273373978j)) self.assertAlmostEqual(arc5.point(0.181818181818),
self.assertAlmostEqual(arc5.point(0.363636363636), (1381.69755131+521.772981736j)) (1242.80253007-232.251400124j))
self.assertAlmostEqual(arc5.point(0.454545454545), (1290.01156757+849.231748376j)) self.assertAlmostEqual(arc5.point(0.272727272727),
self.assertAlmostEqual(arc5.point(0.545454545455), (1097.89435807+1107.12091209j)) (1365.52445614+151.273373978j))
self.assertAlmostEqual(arc5.point(0.636363636364), (820.910116547+1274.54782658j)) self.assertAlmostEqual(arc5.point(0.363636363636),
self.assertAlmostEqual(arc5.point(0.727272727273), (481.49845896+1337.94855893j)) (1381.69755131+521.772981736j))
self.assertAlmostEqual(arc5.point(0.818181818182), (107.156499251+1292.18675889j)) self.assertAlmostEqual(arc5.point(0.454545454545),
self.assertAlmostEqual(arc5.point(0.909090909091), (-271.788803303+1140.96977533j)) (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): def test_length(self):
# I'll test the length calculations by making a circle, in two parts. # 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), path = Path(Line(300 + 200j, 150 + 200j),
Arc(150 + 200j, 150 + 150j, 0, 1, 0, 300 + 50j), Arc(150 + 200j, 150 + 150j, 0, 1, 0, 300 + 50j),
Line(300 + 50j, 300 + 200j)) 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.0), (300 + 200j))
self.assertAlmostEqual(path.point(0.14897825542), (150 + 200j)) self.assertAlmostEqual(path.point(0.14897825542), (150 + 200j))
self.assertAlmostEqual(path.point(0.5), (406.066017177 + 306.066017177j)) 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.14897825542), (300 + 50j))
self.assertAlmostEqual(path.point(1.0), (300 + 200j)) 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) self.assertAlmostEqual(path.length(), pi * 225 + 300, places=6)
# Little pie: M275,175 v-150 a150,150 0 0,0 -150,150 z # Little pie: M275,175 v-150 a150,150 0 0,0 -150,150 z
path = Path(Line(275 + 175j, 275 + 25j), path = Path(Line(275 + 175j, 275 + 25j),
Arc(275 + 25j, 150 + 150j, 0, 0, 0, 125 + 175j), Arc(275 + 25j, 150 + 150j, 0, 0, 0, 125 + 175j),
Line(125 + 175j, 275 + 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.0), (275 + 175j))
self.assertAlmostEqual(path.point(0.2800495767557787), (275 + 25j)) 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.2800495767557787), (125 + 175j))
self.assertAlmostEqual(path.point(1.0), (275 + 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) self.assertAlmostEqual(path.length(), pi * 75 + 300, places=6)
# Bumpy path: M600,350 l 50,-25 # 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.5), (827.730749264+147.824157418j))
# self.assertAlmostEqual(path.point(0.9), (971.284357806+106.302352605j)) # self.assertAlmostEqual(path.point(0.9), (971.284357806+106.302352605j))
# self.assertAlmostEqual(path.point(1), (1050+125j)) # 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) # self.assertAlmostEqual(path.length(), 928.3886394081095)
def test_repr(self): def test_repr(self):
path = Path( path = Path(
Line(start=600 + 350j, end=650 + 325j), Line(start=600 + 350j, end=650 + 325j),
Arc(start=650 + 325j, radius=25 + 25j, rotation=-30, large_arc=0, sweep=1, end=700 + 300j), Arc(start=650 + 325j, radius=25 + 25j, rotation=-30,
CubicBezier(start=700 + 300j, control1=800 + 400j, control2=750 + 200j, end=600 + 100j), 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)) QuadraticBezier(start=600 + 100j, control=600, end=600 + 300j))
self.assertEqual(eval(repr(path)), path) self.assertEqual(eval(repr(path)), path)
@ -547,13 +568,17 @@ class TestPath(unittest.TestCase):
# assertEqual and assertNotEqual # assertEqual and assertNotEqual
path1 = Path( path1 = Path(
Line(start=600 + 350j, end=650 + 325j), Line(start=600 + 350j, end=650 + 325j),
Arc(start=650 + 325j, radius=25 + 25j, rotation=-30, large_arc=0, sweep=1, end=700 + 300j), Arc(start=650 + 325j, radius=25 + 25j, rotation=-30,
CubicBezier(start=700 + 300j, control1=800 + 400j, control2=750 + 200j, end=600 + 100j), 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)) QuadraticBezier(start=600 + 100j, control=600, end=600 + 300j))
path2 = Path( path2 = Path(
Line(start=600 + 350j, end=650 + 325j), Line(start=600 + 350j, end=650 + 325j),
Arc(start=650 + 325j, radius=25 + 25j, rotation=-30, large_arc=0, sweep=1, end=700 + 300j), Arc(start=650 + 325j, radius=25 + 25j, rotation=-30,
CubicBezier(start=700 + 300j, control1=800 + 400j, control2=750 + 200j, end=600 + 100j), 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)) QuadraticBezier(start=600 + 100j, control=600, end=600 + 300j))
self.assertTrue(path1 == path2) self.assertTrue(path1 == path2)
@ -701,6 +726,63 @@ class TestPath(unittest.TestCase):
with self.assertRaises(AssertionError): with self.assertRaises(AssertionError):
p_open.cropped(1, 0) p_open.cropped(1, 0)
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)
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, arc2)
test_curves = ([bezpath, bezpathz, path, pathz, lpath, qpath, cpath, apath] +
[line1, arc1, arc2, cub1, cub2, quad3, linez])
for path_orig in test_curves:
# 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_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(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): class Test_ilength(unittest.TestCase):
# See svgpathtools.notes.inv_arclength.py for information on how these # See svgpathtools.notes.inv_arclength.py for information on how these
@ -992,14 +1074,18 @@ class Test_intersect(unittest.TestCase):
################################################################### ###################################################################
def test_line_line_0(self): def test_line_line_0(self):
l0 = Line(start=(25.389999999999997+99.989999999999995j), end=(25.389999999999997+90.484999999999999j)) l0 = Line(start=(25.389999999999997+99.989999999999995j),
l1 = Line(start=(25.390000000000001+84.114999999999995j), end=(25.389999999999997+74.604202137430320j)) end=(25.389999999999997+90.484999999999999j))
l1 = Line(start=(25.390000000000001+84.114999999999995j),
end=(25.389999999999997+74.604202137430320j))
i = l0.intersect(l1) i = l0.intersect(l1)
assert(len(i)) == 0 assert(len(i)) == 0
def test_line_line_1(self): def test_line_line_1(self):
l0 = Line(start=(-124.705378549+327.696674827j), end=(12.4926214511+121.261674827j)) l0 = Line(start=(-124.705378549+327.696674827j),
l1 = Line(start=(-12.4926214511+121.261674827j), end=(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) i = l0.intersect(l1)
assert(len(i)) == 1 assert(len(i)) == 1
assert(abs(l0.point(i[0][0])-l1.point(i[0][1])) < 1e-9) assert(abs(l0.point(i[0][0])-l1.point(i[0][1])) < 1e-9)