1*e1fe3e4aSElliott Hughes"""Affine 2D transformation matrix class. 2*e1fe3e4aSElliott Hughes 3*e1fe3e4aSElliott HughesThe Transform class implements various transformation matrix operations, 4*e1fe3e4aSElliott Hughesboth on the matrix itself, as well as on 2D coordinates. 5*e1fe3e4aSElliott Hughes 6*e1fe3e4aSElliott HughesTransform instances are effectively immutable: all methods that operate on the 7*e1fe3e4aSElliott Hughestransformation itself always return a new instance. This has as the 8*e1fe3e4aSElliott Hughesinteresting side effect that Transform instances are hashable, ie. they can be 9*e1fe3e4aSElliott Hughesused as dictionary keys. 10*e1fe3e4aSElliott Hughes 11*e1fe3e4aSElliott HughesThis module exports the following symbols: 12*e1fe3e4aSElliott Hughes 13*e1fe3e4aSElliott HughesTransform 14*e1fe3e4aSElliott Hughes this is the main class 15*e1fe3e4aSElliott HughesIdentity 16*e1fe3e4aSElliott Hughes Transform instance set to the identity transformation 17*e1fe3e4aSElliott HughesOffset 18*e1fe3e4aSElliott Hughes Convenience function that returns a translating transformation 19*e1fe3e4aSElliott HughesScale 20*e1fe3e4aSElliott Hughes Convenience function that returns a scaling transformation 21*e1fe3e4aSElliott Hughes 22*e1fe3e4aSElliott HughesThe DecomposedTransform class implements a transformation with separate 23*e1fe3e4aSElliott Hughestranslate, rotation, scale, skew, and transformation-center components. 24*e1fe3e4aSElliott Hughes 25*e1fe3e4aSElliott Hughes:Example: 26*e1fe3e4aSElliott Hughes 27*e1fe3e4aSElliott Hughes >>> t = Transform(2, 0, 0, 3, 0, 0) 28*e1fe3e4aSElliott Hughes >>> t.transformPoint((100, 100)) 29*e1fe3e4aSElliott Hughes (200, 300) 30*e1fe3e4aSElliott Hughes >>> t = Scale(2, 3) 31*e1fe3e4aSElliott Hughes >>> t.transformPoint((100, 100)) 32*e1fe3e4aSElliott Hughes (200, 300) 33*e1fe3e4aSElliott Hughes >>> t.transformPoint((0, 0)) 34*e1fe3e4aSElliott Hughes (0, 0) 35*e1fe3e4aSElliott Hughes >>> t = Offset(2, 3) 36*e1fe3e4aSElliott Hughes >>> t.transformPoint((100, 100)) 37*e1fe3e4aSElliott Hughes (102, 103) 38*e1fe3e4aSElliott Hughes >>> t.transformPoint((0, 0)) 39*e1fe3e4aSElliott Hughes (2, 3) 40*e1fe3e4aSElliott Hughes >>> t2 = t.scale(0.5) 41*e1fe3e4aSElliott Hughes >>> t2.transformPoint((100, 100)) 42*e1fe3e4aSElliott Hughes (52.0, 53.0) 43*e1fe3e4aSElliott Hughes >>> import math 44*e1fe3e4aSElliott Hughes >>> t3 = t2.rotate(math.pi / 2) 45*e1fe3e4aSElliott Hughes >>> t3.transformPoint((0, 0)) 46*e1fe3e4aSElliott Hughes (2.0, 3.0) 47*e1fe3e4aSElliott Hughes >>> t3.transformPoint((100, 100)) 48*e1fe3e4aSElliott Hughes (-48.0, 53.0) 49*e1fe3e4aSElliott Hughes >>> t = Identity.scale(0.5).translate(100, 200).skew(0.1, 0.2) 50*e1fe3e4aSElliott Hughes >>> t.transformPoints([(0, 0), (1, 1), (100, 100)]) 51*e1fe3e4aSElliott Hughes [(50.0, 100.0), (50.550167336042726, 100.60135501775433), (105.01673360427253, 160.13550177543362)] 52*e1fe3e4aSElliott Hughes >>> 53*e1fe3e4aSElliott Hughes""" 54*e1fe3e4aSElliott Hughes 55*e1fe3e4aSElliott Hughesimport math 56*e1fe3e4aSElliott Hughesfrom typing import NamedTuple 57*e1fe3e4aSElliott Hughesfrom dataclasses import dataclass 58*e1fe3e4aSElliott Hughes 59*e1fe3e4aSElliott Hughes 60*e1fe3e4aSElliott Hughes__all__ = ["Transform", "Identity", "Offset", "Scale", "DecomposedTransform"] 61*e1fe3e4aSElliott Hughes 62*e1fe3e4aSElliott Hughes 63*e1fe3e4aSElliott Hughes_EPSILON = 1e-15 64*e1fe3e4aSElliott Hughes_ONE_EPSILON = 1 - _EPSILON 65*e1fe3e4aSElliott Hughes_MINUS_ONE_EPSILON = -1 + _EPSILON 66*e1fe3e4aSElliott Hughes 67*e1fe3e4aSElliott Hughes 68*e1fe3e4aSElliott Hughesdef _normSinCos(v): 69*e1fe3e4aSElliott Hughes if abs(v) < _EPSILON: 70*e1fe3e4aSElliott Hughes v = 0 71*e1fe3e4aSElliott Hughes elif v > _ONE_EPSILON: 72*e1fe3e4aSElliott Hughes v = 1 73*e1fe3e4aSElliott Hughes elif v < _MINUS_ONE_EPSILON: 74*e1fe3e4aSElliott Hughes v = -1 75*e1fe3e4aSElliott Hughes return v 76*e1fe3e4aSElliott Hughes 77*e1fe3e4aSElliott Hughes 78*e1fe3e4aSElliott Hughesclass Transform(NamedTuple): 79*e1fe3e4aSElliott Hughes """2x2 transformation matrix plus offset, a.k.a. Affine transform. 80*e1fe3e4aSElliott Hughes Transform instances are immutable: all transforming methods, eg. 81*e1fe3e4aSElliott Hughes rotate(), return a new Transform instance. 82*e1fe3e4aSElliott Hughes 83*e1fe3e4aSElliott Hughes :Example: 84*e1fe3e4aSElliott Hughes 85*e1fe3e4aSElliott Hughes >>> t = Transform() 86*e1fe3e4aSElliott Hughes >>> t 87*e1fe3e4aSElliott Hughes <Transform [1 0 0 1 0 0]> 88*e1fe3e4aSElliott Hughes >>> t.scale(2) 89*e1fe3e4aSElliott Hughes <Transform [2 0 0 2 0 0]> 90*e1fe3e4aSElliott Hughes >>> t.scale(2.5, 5.5) 91*e1fe3e4aSElliott Hughes <Transform [2.5 0 0 5.5 0 0]> 92*e1fe3e4aSElliott Hughes >>> 93*e1fe3e4aSElliott Hughes >>> t.scale(2, 3).transformPoint((100, 100)) 94*e1fe3e4aSElliott Hughes (200, 300) 95*e1fe3e4aSElliott Hughes 96*e1fe3e4aSElliott Hughes Transform's constructor takes six arguments, all of which are 97*e1fe3e4aSElliott Hughes optional, and can be used as keyword arguments:: 98*e1fe3e4aSElliott Hughes 99*e1fe3e4aSElliott Hughes >>> Transform(12) 100*e1fe3e4aSElliott Hughes <Transform [12 0 0 1 0 0]> 101*e1fe3e4aSElliott Hughes >>> Transform(dx=12) 102*e1fe3e4aSElliott Hughes <Transform [1 0 0 1 12 0]> 103*e1fe3e4aSElliott Hughes >>> Transform(yx=12) 104*e1fe3e4aSElliott Hughes <Transform [1 0 12 1 0 0]> 105*e1fe3e4aSElliott Hughes 106*e1fe3e4aSElliott Hughes Transform instances also behave like sequences of length 6:: 107*e1fe3e4aSElliott Hughes 108*e1fe3e4aSElliott Hughes >>> len(Identity) 109*e1fe3e4aSElliott Hughes 6 110*e1fe3e4aSElliott Hughes >>> list(Identity) 111*e1fe3e4aSElliott Hughes [1, 0, 0, 1, 0, 0] 112*e1fe3e4aSElliott Hughes >>> tuple(Identity) 113*e1fe3e4aSElliott Hughes (1, 0, 0, 1, 0, 0) 114*e1fe3e4aSElliott Hughes 115*e1fe3e4aSElliott Hughes Transform instances are comparable:: 116*e1fe3e4aSElliott Hughes 117*e1fe3e4aSElliott Hughes >>> t1 = Identity.scale(2, 3).translate(4, 6) 118*e1fe3e4aSElliott Hughes >>> t2 = Identity.translate(8, 18).scale(2, 3) 119*e1fe3e4aSElliott Hughes >>> t1 == t2 120*e1fe3e4aSElliott Hughes 1 121*e1fe3e4aSElliott Hughes 122*e1fe3e4aSElliott Hughes But beware of floating point rounding errors:: 123*e1fe3e4aSElliott Hughes 124*e1fe3e4aSElliott Hughes >>> t1 = Identity.scale(0.2, 0.3).translate(0.4, 0.6) 125*e1fe3e4aSElliott Hughes >>> t2 = Identity.translate(0.08, 0.18).scale(0.2, 0.3) 126*e1fe3e4aSElliott Hughes >>> t1 127*e1fe3e4aSElliott Hughes <Transform [0.2 0 0 0.3 0.08 0.18]> 128*e1fe3e4aSElliott Hughes >>> t2 129*e1fe3e4aSElliott Hughes <Transform [0.2 0 0 0.3 0.08 0.18]> 130*e1fe3e4aSElliott Hughes >>> t1 == t2 131*e1fe3e4aSElliott Hughes 0 132*e1fe3e4aSElliott Hughes 133*e1fe3e4aSElliott Hughes Transform instances are hashable, meaning you can use them as 134*e1fe3e4aSElliott Hughes keys in dictionaries:: 135*e1fe3e4aSElliott Hughes 136*e1fe3e4aSElliott Hughes >>> d = {Scale(12, 13): None} 137*e1fe3e4aSElliott Hughes >>> d 138*e1fe3e4aSElliott Hughes {<Transform [12 0 0 13 0 0]>: None} 139*e1fe3e4aSElliott Hughes 140*e1fe3e4aSElliott Hughes But again, beware of floating point rounding errors:: 141*e1fe3e4aSElliott Hughes 142*e1fe3e4aSElliott Hughes >>> t1 = Identity.scale(0.2, 0.3).translate(0.4, 0.6) 143*e1fe3e4aSElliott Hughes >>> t2 = Identity.translate(0.08, 0.18).scale(0.2, 0.3) 144*e1fe3e4aSElliott Hughes >>> t1 145*e1fe3e4aSElliott Hughes <Transform [0.2 0 0 0.3 0.08 0.18]> 146*e1fe3e4aSElliott Hughes >>> t2 147*e1fe3e4aSElliott Hughes <Transform [0.2 0 0 0.3 0.08 0.18]> 148*e1fe3e4aSElliott Hughes >>> d = {t1: None} 149*e1fe3e4aSElliott Hughes >>> d 150*e1fe3e4aSElliott Hughes {<Transform [0.2 0 0 0.3 0.08 0.18]>: None} 151*e1fe3e4aSElliott Hughes >>> d[t2] 152*e1fe3e4aSElliott Hughes Traceback (most recent call last): 153*e1fe3e4aSElliott Hughes File "<stdin>", line 1, in ? 154*e1fe3e4aSElliott Hughes KeyError: <Transform [0.2 0 0 0.3 0.08 0.18]> 155*e1fe3e4aSElliott Hughes """ 156*e1fe3e4aSElliott Hughes 157*e1fe3e4aSElliott Hughes xx: float = 1 158*e1fe3e4aSElliott Hughes xy: float = 0 159*e1fe3e4aSElliott Hughes yx: float = 0 160*e1fe3e4aSElliott Hughes yy: float = 1 161*e1fe3e4aSElliott Hughes dx: float = 0 162*e1fe3e4aSElliott Hughes dy: float = 0 163*e1fe3e4aSElliott Hughes 164*e1fe3e4aSElliott Hughes def transformPoint(self, p): 165*e1fe3e4aSElliott Hughes """Transform a point. 166*e1fe3e4aSElliott Hughes 167*e1fe3e4aSElliott Hughes :Example: 168*e1fe3e4aSElliott Hughes 169*e1fe3e4aSElliott Hughes >>> t = Transform() 170*e1fe3e4aSElliott Hughes >>> t = t.scale(2.5, 5.5) 171*e1fe3e4aSElliott Hughes >>> t.transformPoint((100, 100)) 172*e1fe3e4aSElliott Hughes (250.0, 550.0) 173*e1fe3e4aSElliott Hughes """ 174*e1fe3e4aSElliott Hughes (x, y) = p 175*e1fe3e4aSElliott Hughes xx, xy, yx, yy, dx, dy = self 176*e1fe3e4aSElliott Hughes return (xx * x + yx * y + dx, xy * x + yy * y + dy) 177*e1fe3e4aSElliott Hughes 178*e1fe3e4aSElliott Hughes def transformPoints(self, points): 179*e1fe3e4aSElliott Hughes """Transform a list of points. 180*e1fe3e4aSElliott Hughes 181*e1fe3e4aSElliott Hughes :Example: 182*e1fe3e4aSElliott Hughes 183*e1fe3e4aSElliott Hughes >>> t = Scale(2, 3) 184*e1fe3e4aSElliott Hughes >>> t.transformPoints([(0, 0), (0, 100), (100, 100), (100, 0)]) 185*e1fe3e4aSElliott Hughes [(0, 0), (0, 300), (200, 300), (200, 0)] 186*e1fe3e4aSElliott Hughes >>> 187*e1fe3e4aSElliott Hughes """ 188*e1fe3e4aSElliott Hughes xx, xy, yx, yy, dx, dy = self 189*e1fe3e4aSElliott Hughes return [(xx * x + yx * y + dx, xy * x + yy * y + dy) for x, y in points] 190*e1fe3e4aSElliott Hughes 191*e1fe3e4aSElliott Hughes def transformVector(self, v): 192*e1fe3e4aSElliott Hughes """Transform an (dx, dy) vector, treating translation as zero. 193*e1fe3e4aSElliott Hughes 194*e1fe3e4aSElliott Hughes :Example: 195*e1fe3e4aSElliott Hughes 196*e1fe3e4aSElliott Hughes >>> t = Transform(2, 0, 0, 2, 10, 20) 197*e1fe3e4aSElliott Hughes >>> t.transformVector((3, -4)) 198*e1fe3e4aSElliott Hughes (6, -8) 199*e1fe3e4aSElliott Hughes >>> 200*e1fe3e4aSElliott Hughes """ 201*e1fe3e4aSElliott Hughes (dx, dy) = v 202*e1fe3e4aSElliott Hughes xx, xy, yx, yy = self[:4] 203*e1fe3e4aSElliott Hughes return (xx * dx + yx * dy, xy * dx + yy * dy) 204*e1fe3e4aSElliott Hughes 205*e1fe3e4aSElliott Hughes def transformVectors(self, vectors): 206*e1fe3e4aSElliott Hughes """Transform a list of (dx, dy) vector, treating translation as zero. 207*e1fe3e4aSElliott Hughes 208*e1fe3e4aSElliott Hughes :Example: 209*e1fe3e4aSElliott Hughes >>> t = Transform(2, 0, 0, 2, 10, 20) 210*e1fe3e4aSElliott Hughes >>> t.transformVectors([(3, -4), (5, -6)]) 211*e1fe3e4aSElliott Hughes [(6, -8), (10, -12)] 212*e1fe3e4aSElliott Hughes >>> 213*e1fe3e4aSElliott Hughes """ 214*e1fe3e4aSElliott Hughes xx, xy, yx, yy = self[:4] 215*e1fe3e4aSElliott Hughes return [(xx * dx + yx * dy, xy * dx + yy * dy) for dx, dy in vectors] 216*e1fe3e4aSElliott Hughes 217*e1fe3e4aSElliott Hughes def translate(self, x=0, y=0): 218*e1fe3e4aSElliott Hughes """Return a new transformation, translated (offset) by x, y. 219*e1fe3e4aSElliott Hughes 220*e1fe3e4aSElliott Hughes :Example: 221*e1fe3e4aSElliott Hughes >>> t = Transform() 222*e1fe3e4aSElliott Hughes >>> t.translate(20, 30) 223*e1fe3e4aSElliott Hughes <Transform [1 0 0 1 20 30]> 224*e1fe3e4aSElliott Hughes >>> 225*e1fe3e4aSElliott Hughes """ 226*e1fe3e4aSElliott Hughes return self.transform((1, 0, 0, 1, x, y)) 227*e1fe3e4aSElliott Hughes 228*e1fe3e4aSElliott Hughes def scale(self, x=1, y=None): 229*e1fe3e4aSElliott Hughes """Return a new transformation, scaled by x, y. The 'y' argument 230*e1fe3e4aSElliott Hughes may be None, which implies to use the x value for y as well. 231*e1fe3e4aSElliott Hughes 232*e1fe3e4aSElliott Hughes :Example: 233*e1fe3e4aSElliott Hughes >>> t = Transform() 234*e1fe3e4aSElliott Hughes >>> t.scale(5) 235*e1fe3e4aSElliott Hughes <Transform [5 0 0 5 0 0]> 236*e1fe3e4aSElliott Hughes >>> t.scale(5, 6) 237*e1fe3e4aSElliott Hughes <Transform [5 0 0 6 0 0]> 238*e1fe3e4aSElliott Hughes >>> 239*e1fe3e4aSElliott Hughes """ 240*e1fe3e4aSElliott Hughes if y is None: 241*e1fe3e4aSElliott Hughes y = x 242*e1fe3e4aSElliott Hughes return self.transform((x, 0, 0, y, 0, 0)) 243*e1fe3e4aSElliott Hughes 244*e1fe3e4aSElliott Hughes def rotate(self, angle): 245*e1fe3e4aSElliott Hughes """Return a new transformation, rotated by 'angle' (radians). 246*e1fe3e4aSElliott Hughes 247*e1fe3e4aSElliott Hughes :Example: 248*e1fe3e4aSElliott Hughes >>> import math 249*e1fe3e4aSElliott Hughes >>> t = Transform() 250*e1fe3e4aSElliott Hughes >>> t.rotate(math.pi / 2) 251*e1fe3e4aSElliott Hughes <Transform [0 1 -1 0 0 0]> 252*e1fe3e4aSElliott Hughes >>> 253*e1fe3e4aSElliott Hughes """ 254*e1fe3e4aSElliott Hughes import math 255*e1fe3e4aSElliott Hughes 256*e1fe3e4aSElliott Hughes c = _normSinCos(math.cos(angle)) 257*e1fe3e4aSElliott Hughes s = _normSinCos(math.sin(angle)) 258*e1fe3e4aSElliott Hughes return self.transform((c, s, -s, c, 0, 0)) 259*e1fe3e4aSElliott Hughes 260*e1fe3e4aSElliott Hughes def skew(self, x=0, y=0): 261*e1fe3e4aSElliott Hughes """Return a new transformation, skewed by x and y. 262*e1fe3e4aSElliott Hughes 263*e1fe3e4aSElliott Hughes :Example: 264*e1fe3e4aSElliott Hughes >>> import math 265*e1fe3e4aSElliott Hughes >>> t = Transform() 266*e1fe3e4aSElliott Hughes >>> t.skew(math.pi / 4) 267*e1fe3e4aSElliott Hughes <Transform [1 0 1 1 0 0]> 268*e1fe3e4aSElliott Hughes >>> 269*e1fe3e4aSElliott Hughes """ 270*e1fe3e4aSElliott Hughes import math 271*e1fe3e4aSElliott Hughes 272*e1fe3e4aSElliott Hughes return self.transform((1, math.tan(y), math.tan(x), 1, 0, 0)) 273*e1fe3e4aSElliott Hughes 274*e1fe3e4aSElliott Hughes def transform(self, other): 275*e1fe3e4aSElliott Hughes """Return a new transformation, transformed by another 276*e1fe3e4aSElliott Hughes transformation. 277*e1fe3e4aSElliott Hughes 278*e1fe3e4aSElliott Hughes :Example: 279*e1fe3e4aSElliott Hughes >>> t = Transform(2, 0, 0, 3, 1, 6) 280*e1fe3e4aSElliott Hughes >>> t.transform((4, 3, 2, 1, 5, 6)) 281*e1fe3e4aSElliott Hughes <Transform [8 9 4 3 11 24]> 282*e1fe3e4aSElliott Hughes >>> 283*e1fe3e4aSElliott Hughes """ 284*e1fe3e4aSElliott Hughes xx1, xy1, yx1, yy1, dx1, dy1 = other 285*e1fe3e4aSElliott Hughes xx2, xy2, yx2, yy2, dx2, dy2 = self 286*e1fe3e4aSElliott Hughes return self.__class__( 287*e1fe3e4aSElliott Hughes xx1 * xx2 + xy1 * yx2, 288*e1fe3e4aSElliott Hughes xx1 * xy2 + xy1 * yy2, 289*e1fe3e4aSElliott Hughes yx1 * xx2 + yy1 * yx2, 290*e1fe3e4aSElliott Hughes yx1 * xy2 + yy1 * yy2, 291*e1fe3e4aSElliott Hughes xx2 * dx1 + yx2 * dy1 + dx2, 292*e1fe3e4aSElliott Hughes xy2 * dx1 + yy2 * dy1 + dy2, 293*e1fe3e4aSElliott Hughes ) 294*e1fe3e4aSElliott Hughes 295*e1fe3e4aSElliott Hughes def reverseTransform(self, other): 296*e1fe3e4aSElliott Hughes """Return a new transformation, which is the other transformation 297*e1fe3e4aSElliott Hughes transformed by self. self.reverseTransform(other) is equivalent to 298*e1fe3e4aSElliott Hughes other.transform(self). 299*e1fe3e4aSElliott Hughes 300*e1fe3e4aSElliott Hughes :Example: 301*e1fe3e4aSElliott Hughes >>> t = Transform(2, 0, 0, 3, 1, 6) 302*e1fe3e4aSElliott Hughes >>> t.reverseTransform((4, 3, 2, 1, 5, 6)) 303*e1fe3e4aSElliott Hughes <Transform [8 6 6 3 21 15]> 304*e1fe3e4aSElliott Hughes >>> Transform(4, 3, 2, 1, 5, 6).transform((2, 0, 0, 3, 1, 6)) 305*e1fe3e4aSElliott Hughes <Transform [8 6 6 3 21 15]> 306*e1fe3e4aSElliott Hughes >>> 307*e1fe3e4aSElliott Hughes """ 308*e1fe3e4aSElliott Hughes xx1, xy1, yx1, yy1, dx1, dy1 = self 309*e1fe3e4aSElliott Hughes xx2, xy2, yx2, yy2, dx2, dy2 = other 310*e1fe3e4aSElliott Hughes return self.__class__( 311*e1fe3e4aSElliott Hughes xx1 * xx2 + xy1 * yx2, 312*e1fe3e4aSElliott Hughes xx1 * xy2 + xy1 * yy2, 313*e1fe3e4aSElliott Hughes yx1 * xx2 + yy1 * yx2, 314*e1fe3e4aSElliott Hughes yx1 * xy2 + yy1 * yy2, 315*e1fe3e4aSElliott Hughes xx2 * dx1 + yx2 * dy1 + dx2, 316*e1fe3e4aSElliott Hughes xy2 * dx1 + yy2 * dy1 + dy2, 317*e1fe3e4aSElliott Hughes ) 318*e1fe3e4aSElliott Hughes 319*e1fe3e4aSElliott Hughes def inverse(self): 320*e1fe3e4aSElliott Hughes """Return the inverse transformation. 321*e1fe3e4aSElliott Hughes 322*e1fe3e4aSElliott Hughes :Example: 323*e1fe3e4aSElliott Hughes >>> t = Identity.translate(2, 3).scale(4, 5) 324*e1fe3e4aSElliott Hughes >>> t.transformPoint((10, 20)) 325*e1fe3e4aSElliott Hughes (42, 103) 326*e1fe3e4aSElliott Hughes >>> it = t.inverse() 327*e1fe3e4aSElliott Hughes >>> it.transformPoint((42, 103)) 328*e1fe3e4aSElliott Hughes (10.0, 20.0) 329*e1fe3e4aSElliott Hughes >>> 330*e1fe3e4aSElliott Hughes """ 331*e1fe3e4aSElliott Hughes if self == Identity: 332*e1fe3e4aSElliott Hughes return self 333*e1fe3e4aSElliott Hughes xx, xy, yx, yy, dx, dy = self 334*e1fe3e4aSElliott Hughes det = xx * yy - yx * xy 335*e1fe3e4aSElliott Hughes xx, xy, yx, yy = yy / det, -xy / det, -yx / det, xx / det 336*e1fe3e4aSElliott Hughes dx, dy = -xx * dx - yx * dy, -xy * dx - yy * dy 337*e1fe3e4aSElliott Hughes return self.__class__(xx, xy, yx, yy, dx, dy) 338*e1fe3e4aSElliott Hughes 339*e1fe3e4aSElliott Hughes def toPS(self): 340*e1fe3e4aSElliott Hughes """Return a PostScript representation 341*e1fe3e4aSElliott Hughes 342*e1fe3e4aSElliott Hughes :Example: 343*e1fe3e4aSElliott Hughes 344*e1fe3e4aSElliott Hughes >>> t = Identity.scale(2, 3).translate(4, 5) 345*e1fe3e4aSElliott Hughes >>> t.toPS() 346*e1fe3e4aSElliott Hughes '[2 0 0 3 8 15]' 347*e1fe3e4aSElliott Hughes >>> 348*e1fe3e4aSElliott Hughes """ 349*e1fe3e4aSElliott Hughes return "[%s %s %s %s %s %s]" % self 350*e1fe3e4aSElliott Hughes 351*e1fe3e4aSElliott Hughes def toDecomposed(self) -> "DecomposedTransform": 352*e1fe3e4aSElliott Hughes """Decompose into a DecomposedTransform.""" 353*e1fe3e4aSElliott Hughes return DecomposedTransform.fromTransform(self) 354*e1fe3e4aSElliott Hughes 355*e1fe3e4aSElliott Hughes def __bool__(self): 356*e1fe3e4aSElliott Hughes """Returns True if transform is not identity, False otherwise. 357*e1fe3e4aSElliott Hughes 358*e1fe3e4aSElliott Hughes :Example: 359*e1fe3e4aSElliott Hughes 360*e1fe3e4aSElliott Hughes >>> bool(Identity) 361*e1fe3e4aSElliott Hughes False 362*e1fe3e4aSElliott Hughes >>> bool(Transform()) 363*e1fe3e4aSElliott Hughes False 364*e1fe3e4aSElliott Hughes >>> bool(Scale(1.)) 365*e1fe3e4aSElliott Hughes False 366*e1fe3e4aSElliott Hughes >>> bool(Scale(2)) 367*e1fe3e4aSElliott Hughes True 368*e1fe3e4aSElliott Hughes >>> bool(Offset()) 369*e1fe3e4aSElliott Hughes False 370*e1fe3e4aSElliott Hughes >>> bool(Offset(0)) 371*e1fe3e4aSElliott Hughes False 372*e1fe3e4aSElliott Hughes >>> bool(Offset(2)) 373*e1fe3e4aSElliott Hughes True 374*e1fe3e4aSElliott Hughes """ 375*e1fe3e4aSElliott Hughes return self != Identity 376*e1fe3e4aSElliott Hughes 377*e1fe3e4aSElliott Hughes def __repr__(self): 378*e1fe3e4aSElliott Hughes return "<%s [%g %g %g %g %g %g]>" % ((self.__class__.__name__,) + self) 379*e1fe3e4aSElliott Hughes 380*e1fe3e4aSElliott Hughes 381*e1fe3e4aSElliott HughesIdentity = Transform() 382*e1fe3e4aSElliott Hughes 383*e1fe3e4aSElliott Hughes 384*e1fe3e4aSElliott Hughesdef Offset(x=0, y=0): 385*e1fe3e4aSElliott Hughes """Return the identity transformation offset by x, y. 386*e1fe3e4aSElliott Hughes 387*e1fe3e4aSElliott Hughes :Example: 388*e1fe3e4aSElliott Hughes >>> Offset(2, 3) 389*e1fe3e4aSElliott Hughes <Transform [1 0 0 1 2 3]> 390*e1fe3e4aSElliott Hughes >>> 391*e1fe3e4aSElliott Hughes """ 392*e1fe3e4aSElliott Hughes return Transform(1, 0, 0, 1, x, y) 393*e1fe3e4aSElliott Hughes 394*e1fe3e4aSElliott Hughes 395*e1fe3e4aSElliott Hughesdef Scale(x, y=None): 396*e1fe3e4aSElliott Hughes """Return the identity transformation scaled by x, y. The 'y' argument 397*e1fe3e4aSElliott Hughes may be None, which implies to use the x value for y as well. 398*e1fe3e4aSElliott Hughes 399*e1fe3e4aSElliott Hughes :Example: 400*e1fe3e4aSElliott Hughes >>> Scale(2, 3) 401*e1fe3e4aSElliott Hughes <Transform [2 0 0 3 0 0]> 402*e1fe3e4aSElliott Hughes >>> 403*e1fe3e4aSElliott Hughes """ 404*e1fe3e4aSElliott Hughes if y is None: 405*e1fe3e4aSElliott Hughes y = x 406*e1fe3e4aSElliott Hughes return Transform(x, 0, 0, y, 0, 0) 407*e1fe3e4aSElliott Hughes 408*e1fe3e4aSElliott Hughes 409*e1fe3e4aSElliott Hughes@dataclass 410*e1fe3e4aSElliott Hughesclass DecomposedTransform: 411*e1fe3e4aSElliott Hughes """The DecomposedTransform class implements a transformation with separate 412*e1fe3e4aSElliott Hughes translate, rotation, scale, skew, and transformation-center components. 413*e1fe3e4aSElliott Hughes """ 414*e1fe3e4aSElliott Hughes 415*e1fe3e4aSElliott Hughes translateX: float = 0 416*e1fe3e4aSElliott Hughes translateY: float = 0 417*e1fe3e4aSElliott Hughes rotation: float = 0 # in degrees, counter-clockwise 418*e1fe3e4aSElliott Hughes scaleX: float = 1 419*e1fe3e4aSElliott Hughes scaleY: float = 1 420*e1fe3e4aSElliott Hughes skewX: float = 0 # in degrees, clockwise 421*e1fe3e4aSElliott Hughes skewY: float = 0 # in degrees, counter-clockwise 422*e1fe3e4aSElliott Hughes tCenterX: float = 0 423*e1fe3e4aSElliott Hughes tCenterY: float = 0 424*e1fe3e4aSElliott Hughes 425*e1fe3e4aSElliott Hughes @classmethod 426*e1fe3e4aSElliott Hughes def fromTransform(self, transform): 427*e1fe3e4aSElliott Hughes # Adapted from an answer on 428*e1fe3e4aSElliott Hughes # https://math.stackexchange.com/questions/13150/extracting-rotation-scale-values-from-2d-transformation-matrix 429*e1fe3e4aSElliott Hughes a, b, c, d, x, y = transform 430*e1fe3e4aSElliott Hughes 431*e1fe3e4aSElliott Hughes sx = math.copysign(1, a) 432*e1fe3e4aSElliott Hughes if sx < 0: 433*e1fe3e4aSElliott Hughes a *= sx 434*e1fe3e4aSElliott Hughes b *= sx 435*e1fe3e4aSElliott Hughes 436*e1fe3e4aSElliott Hughes delta = a * d - b * c 437*e1fe3e4aSElliott Hughes 438*e1fe3e4aSElliott Hughes rotation = 0 439*e1fe3e4aSElliott Hughes scaleX = scaleY = 0 440*e1fe3e4aSElliott Hughes skewX = skewY = 0 441*e1fe3e4aSElliott Hughes 442*e1fe3e4aSElliott Hughes # Apply the QR-like decomposition. 443*e1fe3e4aSElliott Hughes if a != 0 or b != 0: 444*e1fe3e4aSElliott Hughes r = math.sqrt(a * a + b * b) 445*e1fe3e4aSElliott Hughes rotation = math.acos(a / r) if b >= 0 else -math.acos(a / r) 446*e1fe3e4aSElliott Hughes scaleX, scaleY = (r, delta / r) 447*e1fe3e4aSElliott Hughes skewX, skewY = (math.atan((a * c + b * d) / (r * r)), 0) 448*e1fe3e4aSElliott Hughes elif c != 0 or d != 0: 449*e1fe3e4aSElliott Hughes s = math.sqrt(c * c + d * d) 450*e1fe3e4aSElliott Hughes rotation = math.pi / 2 - ( 451*e1fe3e4aSElliott Hughes math.acos(-c / s) if d >= 0 else -math.acos(c / s) 452*e1fe3e4aSElliott Hughes ) 453*e1fe3e4aSElliott Hughes scaleX, scaleY = (delta / s, s) 454*e1fe3e4aSElliott Hughes skewX, skewY = (0, math.atan((a * c + b * d) / (s * s))) 455*e1fe3e4aSElliott Hughes else: 456*e1fe3e4aSElliott Hughes # a = b = c = d = 0 457*e1fe3e4aSElliott Hughes pass 458*e1fe3e4aSElliott Hughes 459*e1fe3e4aSElliott Hughes return DecomposedTransform( 460*e1fe3e4aSElliott Hughes x, 461*e1fe3e4aSElliott Hughes y, 462*e1fe3e4aSElliott Hughes math.degrees(rotation), 463*e1fe3e4aSElliott Hughes scaleX * sx, 464*e1fe3e4aSElliott Hughes scaleY, 465*e1fe3e4aSElliott Hughes math.degrees(skewX) * sx, 466*e1fe3e4aSElliott Hughes math.degrees(skewY), 467*e1fe3e4aSElliott Hughes 0, 468*e1fe3e4aSElliott Hughes 0, 469*e1fe3e4aSElliott Hughes ) 470*e1fe3e4aSElliott Hughes 471*e1fe3e4aSElliott Hughes def toTransform(self): 472*e1fe3e4aSElliott Hughes """Return the Transform() equivalent of this transformation. 473*e1fe3e4aSElliott Hughes 474*e1fe3e4aSElliott Hughes :Example: 475*e1fe3e4aSElliott Hughes >>> DecomposedTransform(scaleX=2, scaleY=2).toTransform() 476*e1fe3e4aSElliott Hughes <Transform [2 0 0 2 0 0]> 477*e1fe3e4aSElliott Hughes >>> 478*e1fe3e4aSElliott Hughes """ 479*e1fe3e4aSElliott Hughes t = Transform() 480*e1fe3e4aSElliott Hughes t = t.translate( 481*e1fe3e4aSElliott Hughes self.translateX + self.tCenterX, self.translateY + self.tCenterY 482*e1fe3e4aSElliott Hughes ) 483*e1fe3e4aSElliott Hughes t = t.rotate(math.radians(self.rotation)) 484*e1fe3e4aSElliott Hughes t = t.scale(self.scaleX, self.scaleY) 485*e1fe3e4aSElliott Hughes t = t.skew(math.radians(self.skewX), math.radians(self.skewY)) 486*e1fe3e4aSElliott Hughes t = t.translate(-self.tCenterX, -self.tCenterY) 487*e1fe3e4aSElliott Hughes return t 488*e1fe3e4aSElliott Hughes 489*e1fe3e4aSElliott Hughes 490*e1fe3e4aSElliott Hughesif __name__ == "__main__": 491*e1fe3e4aSElliott Hughes import sys 492*e1fe3e4aSElliott Hughes import doctest 493*e1fe3e4aSElliott Hughes 494*e1fe3e4aSElliott Hughes sys.exit(doctest.testmod().failed) 495