blob: 565aa3c21538a3512def429887fd7b9aa9cbac91 [file] [log] [blame]
jvrf184f752003-09-16 11:01:40 +00001"""Affine 2D transformation matrix class.
2
3The Transform class implements various transformation matrix operations,
4both on the matrix itself, as well as on 2D coordinates.
5
6Transform instances are effectively immutable: all methods that operate on the
7transformation itself always return a new instance. This has as the
8interesting side effect that Transform instances are hashable, ie. they can be
9used as dictionary keys.
10
11This module exports the following symbols:
12
13 Transform -- this is the main class
14 Identity -- Transform instance set to the identity transformation
15 Offset -- Convenience function that returns a translating transformation
16 Scale -- Convenience function that returns a scaling transformation
17
18Examples:
19
20 >>> t = Transform(2, 0, 0, 3, 0, 0)
21 >>> t.transformPoint((100, 100))
22 (200, 300)
23 >>> t = Scale(2, 3)
24 >>> t.transformPoint((100, 100))
25 (200, 300)
26 >>> t.transformPoint((0, 0))
27 (0, 0)
28 >>> t = Offset(2, 3)
29 >>> t.transformPoint((100, 100))
30 (102, 103)
31 >>> t.transformPoint((0, 0))
32 (2, 3)
33 >>> t2 = t.scale(0.5)
34 >>> t2.transformPoint((100, 100))
35 (52.0, 53.0)
36 >>> import math
37 >>> t3 = t2.rotate(math.pi / 2)
38 >>> t3.transformPoint((0, 0))
39 (2.0, 3.0)
40 >>> t3.transformPoint((100, 100))
41 (-48.0, 53.0)
42 >>> t = Identity.scale(0.5).translate(100, 200).skew(0.1, 0.2)
43 >>> t.transformPoints([(0, 0), (1, 1), (100, 100)])
44 [(50.0, 100.0), (50.550167336042726, 100.60135501775433), (105.01673360427253, 160.13550177543362)]
45 >>>
46"""
47
48
49__all__ = ["Transform", "Identity", "Offset", "Scale"]
jvr9e58c902003-08-22 14:56:48 +000050
51
52_EPSILON = 1e-15
53_ONE_EPSILON = 1 - _EPSILON
54_MINUS_ONE_EPSILON = -1 + _EPSILON
55
56
57def _normSinCos(v):
58 if abs(v) < _EPSILON:
59 v = 0
60 elif v > _ONE_EPSILON:
61 v = 1
62 elif v < _MINUS_ONE_EPSILON:
63 v = -1
64 return v
65
66
67class Transform:
jvr6385a4e2003-08-24 16:17:11 +000068
jvr9e58c902003-08-22 14:56:48 +000069 """2x2 transformation matrix plus offset, a.k.a. Affine transform.
jvrf184f752003-09-16 11:01:40 +000070 Transform instances are immutable: all transforming methods, eg.
71 rotate(), return a new Transform instance.
72
73 Examples:
74 >>> t = Transform()
75 >>> t
76 <Transform [1 0 0 1 0 0]>
77 >>> t.scale(2)
78 <Transform [2 0 0 2 0 0]>
79 >>> t.scale(2.5, 5.5)
80 <Transform [2.5 0.0 0.0 5.5 0 0]>
81 >>>
82 >>> t.scale(2, 3).transformPoint((100, 100))
83 (200, 300)
84 """
jvr6385a4e2003-08-24 16:17:11 +000085
jvr9e58c902003-08-22 14:56:48 +000086 def __init__(self, xx=1, xy=0, yx=0, yy=1, dx=0, dy=0):
jvrdeca3982003-09-16 11:30:29 +000087 """Transform's constructor takes six arguments, all of which are
88 optional, and can be used as keyword arguments:
89 >>> Transform(12)
90 <Transform [12 0 0 1 0 0]>
91 >>> Transform(dx=12)
92 <Transform [1 0 0 1 12 0]>
93 >>> Transform(yx=12)
94 <Transform [1 0 12 1 0 0]>
95 >>>
96 """
jvr9e58c902003-08-22 14:56:48 +000097 self.__affine = xx, xy, yx, yy, dx, dy
jvr6385a4e2003-08-24 16:17:11 +000098
Behdad Esfahbod3a9fd302013-11-27 03:19:32 -050099 def transformPoint(self, p):
jvrf184f752003-09-16 11:01:40 +0000100 """Transform a point.
101
102 Example:
103 >>> t = Transform()
104 >>> t = t.scale(2.5, 5.5)
105 >>> t.transformPoint((100, 100))
106 (250.0, 550.0)
107 """
Behdad Esfahbod3a9fd302013-11-27 03:19:32 -0500108 (x, y) = p
jvr9e58c902003-08-22 14:56:48 +0000109 xx, xy, yx, yy, dx, dy = self.__affine
110 return (xx*x + yx*y + dx, xy*x + yy*y + dy)
jvr6385a4e2003-08-24 16:17:11 +0000111
jvrf184f752003-09-16 11:01:40 +0000112 def transformPoints(self, points):
113 """Transform a list of points.
114
115 Example:
116 >>> t = Scale(2, 3)
117 >>> t.transformPoints([(0, 0), (0, 100), (100, 100), (100, 0)])
118 [(0, 0), (0, 300), (200, 300), (200, 0)]
119 >>>
120 """
121 xx, xy, yx, yy, dx, dy = self.__affine
122 return [(xx*x + yx*y + dx, xy*x + yy*y + dy) for x, y in points]
123
jvr9e58c902003-08-22 14:56:48 +0000124 def translate(self, x=0, y=0):
jvrf184f752003-09-16 11:01:40 +0000125 """Return a new transformation, translated (offset) by x, y.
126
127 Example:
128 >>> t = Transform()
129 >>> t.translate(20, 30)
130 <Transform [1 0 0 1 20 30]>
131 >>>
132 """
jvr9e58c902003-08-22 14:56:48 +0000133 return self.transform((1, 0, 0, 1, x, y))
jvr6385a4e2003-08-24 16:17:11 +0000134
jvr9e58c902003-08-22 14:56:48 +0000135 def scale(self, x=1, y=None):
jvrf184f752003-09-16 11:01:40 +0000136 """Return a new transformation, scaled by x, y. The 'y' argument
137 may be None, which implies to use the x value for y as well.
138
139 Example:
140 >>> t = Transform()
141 >>> t.scale(5)
142 <Transform [5 0 0 5 0 0]>
143 >>> t.scale(5, 6)
144 <Transform [5 0 0 6 0 0]>
145 >>>
146 """
jvr9e58c902003-08-22 14:56:48 +0000147 if y is None:
148 y = x
149 return self.transform((x, 0, 0, y, 0, 0))
jvr6385a4e2003-08-24 16:17:11 +0000150
jvr9e58c902003-08-22 14:56:48 +0000151 def rotate(self, angle):
jvrf184f752003-09-16 11:01:40 +0000152 """Return a new transformation, rotated by 'angle' (radians).
153
154 Example:
155 >>> import math
156 >>> t = Transform()
157 >>> t.rotate(math.pi / 2)
158 <Transform [0 1 -1 0 0 0]>
159 >>>
160 """
jvr9e58c902003-08-22 14:56:48 +0000161 import math
162 c = _normSinCos(math.cos(angle))
163 s = _normSinCos(math.sin(angle))
164 return self.transform((c, s, -s, c, 0, 0))
jvr6385a4e2003-08-24 16:17:11 +0000165
jvr9e58c902003-08-22 14:56:48 +0000166 def skew(self, x=0, y=0):
jvrf184f752003-09-16 11:01:40 +0000167 """Return a new transformation, skewed by x and y.
168
169 Example:
170 >>> import math
171 >>> t = Transform()
172 >>> t.skew(math.pi / 4)
173 <Transform [1.0 0.0 1.0 1.0 0 0]>
174 >>>
175 """
jvr9e58c902003-08-22 14:56:48 +0000176 import math
177 return self.transform((1, math.tan(y), math.tan(x), 1, 0, 0))
jvr6385a4e2003-08-24 16:17:11 +0000178
jvr9e58c902003-08-22 14:56:48 +0000179 def transform(self, other):
jvrf184f752003-09-16 11:01:40 +0000180 """Return a new transformation, transformed by another
181 transformation.
182
183 Example:
184 >>> t = Transform(2, 0, 0, 3, 1, 6)
185 >>> t.transform((4, 3, 2, 1, 5, 6))
186 <Transform [8 9 4 3 11 24]>
187 >>>
188 """
jvr9e58c902003-08-22 14:56:48 +0000189 xx1, xy1, yx1, yy1, dx1, dy1 = other
190 xx2, xy2, yx2, yy2, dx2, dy2 = self.__affine
191 return self.__class__(
192 xx1*xx2 + xy1*yx2,
193 xx1*xy2 + xy1*yy2,
194 yx1*xx2 + yy1*yx2,
195 yx1*xy2 + yy1*yy2,
196 xx2*dx1 + yx2*dy1 + dx2,
197 xy2*dx1 + yy2*dy1 + dy2)
jvr6385a4e2003-08-24 16:17:11 +0000198
jvr9e58c902003-08-22 14:56:48 +0000199 def reverseTransform(self, other):
jvrf184f752003-09-16 11:01:40 +0000200 """Return a new transformation, which is the other transformation
201 transformed by self. self.reverseTransform(other) is equivalent to
202 other.transform(self).
203
204 Example:
205 >>> t = Transform(2, 0, 0, 3, 1, 6)
206 >>> t.reverseTransform((4, 3, 2, 1, 5, 6))
207 <Transform [8 6 6 3 21 15]>
208 >>> Transform(4, 3, 2, 1, 5, 6).transform((2, 0, 0, 3, 1, 6))
209 <Transform [8 6 6 3 21 15]>
210 >>>
211 """
jvr9e58c902003-08-22 14:56:48 +0000212 xx1, xy1, yx1, yy1, dx1, dy1 = self.__affine
213 xx2, xy2, yx2, yy2, dx2, dy2 = other
214 return self.__class__(
215 xx1*xx2 + xy1*yx2,
216 xx1*xy2 + xy1*yy2,
217 yx1*xx2 + yy1*yx2,
218 yx1*xy2 + yy1*yy2,
219 xx2*dx1 + yx2*dy1 + dx2,
220 xy2*dx1 + yy2*dy1 + dy2)
jvr6385a4e2003-08-24 16:17:11 +0000221
jvr9e58c902003-08-22 14:56:48 +0000222 def inverse(self):
jvrf184f752003-09-16 11:01:40 +0000223 """Return the inverse transformation.
224
225 Example:
jvrdeca3982003-09-16 11:30:29 +0000226 >>> t = Identity.translate(2, 3).scale(4, 5)
227 >>> t.transformPoint((10, 20))
228 (42, 103)
229 >>> it = t.inverse()
230 >>> it.transformPoint((42, 103))
231 (10.0, 20.0)
jvrf184f752003-09-16 11:01:40 +0000232 >>>
233 """
jvr9e58c902003-08-22 14:56:48 +0000234 if self.__affine == (1, 0, 0, 1, 0, 0):
235 return self
236 xx, xy, yx, yy, dx, dy = self.__affine
237 det = float(xx*yy - yx*xy)
238 xx, xy, yx, yy = yy/det, -xy/det, -yx/det, xx/det
239 dx, dy = -xx*dx - yx*dy, -xy*dx - yy*dy
240 return self.__class__(xx, xy, yx, yy, dx, dy)
jvr6385a4e2003-08-24 16:17:11 +0000241
jvr9e58c902003-08-22 14:56:48 +0000242 def toPS(self):
jvrdeca3982003-09-16 11:30:29 +0000243 """Return a PostScript representation:
244 >>> t = Identity.scale(2, 3).translate(4, 5)
245 >>> t.toPS()
246 '[2 0 0 3 8 15]'
247 >>>
248 """
jvr9e58c902003-08-22 14:56:48 +0000249 return "[%s %s %s %s %s %s]" % self.__affine
jvr6385a4e2003-08-24 16:17:11 +0000250
jvr9e58c902003-08-22 14:56:48 +0000251 def __len__(self):
jvrdeca3982003-09-16 11:30:29 +0000252 """Transform instances also behave like sequences of length 6:
253 >>> len(Identity)
254 6
255 >>>
256 """
jvr9e58c902003-08-22 14:56:48 +0000257 return 6
jvr6385a4e2003-08-24 16:17:11 +0000258
jvr9e58c902003-08-22 14:56:48 +0000259 def __getitem__(self, index):
jvrdeca3982003-09-16 11:30:29 +0000260 """Transform instances also behave like sequences of length 6:
261 >>> list(Identity)
262 [1, 0, 0, 1, 0, 0]
263 >>> tuple(Identity)
264 (1, 0, 0, 1, 0, 0)
265 >>>
266 """
jvr9e58c902003-08-22 14:56:48 +0000267 return self.__affine[index]
jvr6385a4e2003-08-24 16:17:11 +0000268
jvr9e58c902003-08-22 14:56:48 +0000269 def __getslice__(self, i, j):
jvrdeca3982003-09-16 11:30:29 +0000270 """Transform instances also behave like sequences and even support
271 slicing:
272 >>> t = Offset(100, 200)
273 >>> t
274 <Transform [1 0 0 1 100 200]>
275 >>> t[4:]
276 (100, 200)
277 >>>
278 """
jvr9e58c902003-08-22 14:56:48 +0000279 return self.__affine[i:j]
jvr6385a4e2003-08-24 16:17:11 +0000280
jvr9e58c902003-08-22 14:56:48 +0000281 def __cmp__(self, other):
jvrdeca3982003-09-16 11:30:29 +0000282 """Transform instances are comparable:
283 >>> t1 = Identity.scale(2, 3).translate(4, 6)
284 >>> t2 = Identity.translate(8, 18).scale(2, 3)
285 >>> t1 == t2
286 1
287 >>>
288
289 But beware of floating point rounding errors:
290 >>> t1 = Identity.scale(0.2, 0.3).translate(0.4, 0.6)
291 >>> t2 = Identity.translate(0.08, 0.18).scale(0.2, 0.3)
292 >>> t1
293 <Transform [0.2 0.0 0.0 0.3 0.08 0.18]>
294 >>> t2
295 <Transform [0.2 0.0 0.0 0.3 0.08 0.18]>
296 >>> t1 == t2
297 0
298 >>>
299 """
jvr9e58c902003-08-22 14:56:48 +0000300 xx1, xy1, yx1, yy1, dx1, dy1 = self.__affine
301 xx2, xy2, yx2, yy2, dx2, dy2 = other
302 return cmp((xx1, xy1, yx1, yy1, dx1, dy1),
303 (xx2, xy2, yx2, yy2, dx2, dy2))
jvr6385a4e2003-08-24 16:17:11 +0000304
jvr9e58c902003-08-22 14:56:48 +0000305 def __hash__(self):
jvrdeca3982003-09-16 11:30:29 +0000306 """Transform instances are hashable, meaning you can use them as
307 keys in dictionaries:
308 >>> d = {Scale(12, 13): None}
309 >>> d
310 {<Transform [12 0 0 13 0 0]>: None}
311 >>>
312
313 But again, beware of floating point rounding errors:
314 >>> t1 = Identity.scale(0.2, 0.3).translate(0.4, 0.6)
315 >>> t2 = Identity.translate(0.08, 0.18).scale(0.2, 0.3)
316 >>> t1
317 <Transform [0.2 0.0 0.0 0.3 0.08 0.18]>
318 >>> t2
319 <Transform [0.2 0.0 0.0 0.3 0.08 0.18]>
320 >>> d = {t1: None}
321 >>> d
322 {<Transform [0.2 0.0 0.0 0.3 0.08 0.18]>: None}
323 >>> d[t2]
324 Traceback (most recent call last):
325 File "<stdin>", line 1, in ?
326 KeyError: <Transform [0.2 0.0 0.0 0.3 0.08 0.18]>
327 >>>
328 """
jvr9e58c902003-08-22 14:56:48 +0000329 return hash(self.__affine)
jvr6385a4e2003-08-24 16:17:11 +0000330
jvr9e58c902003-08-22 14:56:48 +0000331 def __repr__(self):
332 return "<%s [%s %s %s %s %s %s]>" % ((self.__class__.__name__,)
333 + tuple(map(str, self.__affine)))
334
335
336Identity = Transform()
337
338def Offset(x=0, y=0):
jvrf184f752003-09-16 11:01:40 +0000339 """Return the identity transformation offset by x, y.
340
341 Example:
342 >>> Offset(2, 3)
343 <Transform [1 0 0 1 2 3]>
344 >>>
345 """
jvr9e58c902003-08-22 14:56:48 +0000346 return Transform(1, 0, 0, 1, x, y)
347
348def Scale(x, y=None):
jvrf184f752003-09-16 11:01:40 +0000349 """Return the identity transformation scaled by x, y. The 'y' argument
350 may be None, which implies to use the x value for y as well.
351
352 Example:
353 >>> Scale(2, 3)
354 <Transform [2 0 0 3 0 0]>
355 >>>
356 """
jvr9e58c902003-08-22 14:56:48 +0000357 if y is None:
358 y = x
359 return Transform(x, 0, 0, y, 0, 0)
jvrf184f752003-09-16 11:01:40 +0000360
361
362def _test():
jvrdeca3982003-09-16 11:30:29 +0000363 import doctest, transform
364 return doctest.testmod(transform)
jvrf184f752003-09-16 11:01:40 +0000365
366if __name__ == "__main__":
367 _test()