You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			517 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			Python
		
	
			
		
		
	
	
			517 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			Python
		
	
"""Affine 2D transformation matrix class.
 | 
						|
 | 
						|
The Transform class implements various transformation matrix operations,
 | 
						|
both on the matrix itself, as well as on 2D coordinates.
 | 
						|
 | 
						|
Transform instances are effectively immutable: all methods that operate on the
 | 
						|
transformation itself always return a new instance. This has as the
 | 
						|
interesting side effect that Transform instances are hashable, ie. they can be
 | 
						|
used as dictionary keys.
 | 
						|
 | 
						|
This module exports the following symbols:
 | 
						|
 | 
						|
Transform
 | 
						|
	this is the main class
 | 
						|
Identity
 | 
						|
	Transform instance set to the identity transformation
 | 
						|
Offset
 | 
						|
	Convenience function that returns a translating transformation
 | 
						|
Scale
 | 
						|
	Convenience function that returns a scaling transformation
 | 
						|
 | 
						|
The DecomposedTransform class implements a transformation with separate
 | 
						|
translate, rotation, scale, skew, and transformation-center components.
 | 
						|
 | 
						|
:Example:
 | 
						|
 | 
						|
	>>> t = Transform(2, 0, 0, 3, 0, 0)
 | 
						|
	>>> t.transformPoint((100, 100))
 | 
						|
	(200, 300)
 | 
						|
	>>> t = Scale(2, 3)
 | 
						|
	>>> t.transformPoint((100, 100))
 | 
						|
	(200, 300)
 | 
						|
	>>> t.transformPoint((0, 0))
 | 
						|
	(0, 0)
 | 
						|
	>>> t = Offset(2, 3)
 | 
						|
	>>> t.transformPoint((100, 100))
 | 
						|
	(102, 103)
 | 
						|
	>>> t.transformPoint((0, 0))
 | 
						|
	(2, 3)
 | 
						|
	>>> t2 = t.scale(0.5)
 | 
						|
	>>> t2.transformPoint((100, 100))
 | 
						|
	(52.0, 53.0)
 | 
						|
	>>> import math
 | 
						|
	>>> t3 = t2.rotate(math.pi / 2)
 | 
						|
	>>> t3.transformPoint((0, 0))
 | 
						|
	(2.0, 3.0)
 | 
						|
	>>> t3.transformPoint((100, 100))
 | 
						|
	(-48.0, 53.0)
 | 
						|
	>>> t = Identity.scale(0.5).translate(100, 200).skew(0.1, 0.2)
 | 
						|
	>>> t.transformPoints([(0, 0), (1, 1), (100, 100)])
 | 
						|
	[(50.0, 100.0), (50.550167336042726, 100.60135501775433), (105.01673360427253, 160.13550177543362)]
 | 
						|
	>>>
 | 
						|
"""
 | 
						|
 | 
						|
from __future__ import annotations
 | 
						|
 | 
						|
import math
 | 
						|
from typing import NamedTuple
 | 
						|
from dataclasses import dataclass
 | 
						|
 | 
						|
 | 
						|
__all__ = ["Transform", "Identity", "Offset", "Scale", "DecomposedTransform"]
 | 
						|
 | 
						|
 | 
						|
_EPSILON = 1e-15
 | 
						|
_ONE_EPSILON = 1 - _EPSILON
 | 
						|
_MINUS_ONE_EPSILON = -1 + _EPSILON
 | 
						|
 | 
						|
 | 
						|
def _normSinCos(v: float) -> float:
 | 
						|
    if abs(v) < _EPSILON:
 | 
						|
        v = 0
 | 
						|
    elif v > _ONE_EPSILON:
 | 
						|
        v = 1
 | 
						|
    elif v < _MINUS_ONE_EPSILON:
 | 
						|
        v = -1
 | 
						|
    return v
 | 
						|
 | 
						|
 | 
						|
class Transform(NamedTuple):
 | 
						|
    """2x2 transformation matrix plus offset, a.k.a. Affine transform.
 | 
						|
    Transform instances are immutable: all transforming methods, eg.
 | 
						|
    rotate(), return a new Transform instance.
 | 
						|
 | 
						|
    :Example:
 | 
						|
 | 
						|
            >>> t = Transform()
 | 
						|
            >>> t
 | 
						|
            <Transform [1 0 0 1 0 0]>
 | 
						|
            >>> t.scale(2)
 | 
						|
            <Transform [2 0 0 2 0 0]>
 | 
						|
            >>> t.scale(2.5, 5.5)
 | 
						|
            <Transform [2.5 0 0 5.5 0 0]>
 | 
						|
            >>>
 | 
						|
            >>> t.scale(2, 3).transformPoint((100, 100))
 | 
						|
            (200, 300)
 | 
						|
 | 
						|
    Transform's constructor takes six arguments, all of which are
 | 
						|
    optional, and can be used as keyword arguments::
 | 
						|
 | 
						|
            >>> Transform(12)
 | 
						|
            <Transform [12 0 0 1 0 0]>
 | 
						|
            >>> Transform(dx=12)
 | 
						|
            <Transform [1 0 0 1 12 0]>
 | 
						|
            >>> Transform(yx=12)
 | 
						|
            <Transform [1 0 12 1 0 0]>
 | 
						|
 | 
						|
    Transform instances also behave like sequences of length 6::
 | 
						|
 | 
						|
            >>> len(Identity)
 | 
						|
            6
 | 
						|
            >>> list(Identity)
 | 
						|
            [1, 0, 0, 1, 0, 0]
 | 
						|
            >>> tuple(Identity)
 | 
						|
            (1, 0, 0, 1, 0, 0)
 | 
						|
 | 
						|
    Transform instances are comparable::
 | 
						|
 | 
						|
            >>> t1 = Identity.scale(2, 3).translate(4, 6)
 | 
						|
            >>> t2 = Identity.translate(8, 18).scale(2, 3)
 | 
						|
            >>> t1 == t2
 | 
						|
            1
 | 
						|
 | 
						|
    But beware of floating point rounding errors::
 | 
						|
 | 
						|
            >>> t1 = Identity.scale(0.2, 0.3).translate(0.4, 0.6)
 | 
						|
            >>> t2 = Identity.translate(0.08, 0.18).scale(0.2, 0.3)
 | 
						|
            >>> t1
 | 
						|
            <Transform [0.2 0 0 0.3 0.08 0.18]>
 | 
						|
            >>> t2
 | 
						|
            <Transform [0.2 0 0 0.3 0.08 0.18]>
 | 
						|
            >>> t1 == t2
 | 
						|
            0
 | 
						|
 | 
						|
    Transform instances are hashable, meaning you can use them as
 | 
						|
    keys in dictionaries::
 | 
						|
 | 
						|
            >>> d = {Scale(12, 13): None}
 | 
						|
            >>> d
 | 
						|
            {<Transform [12 0 0 13 0 0]>: None}
 | 
						|
 | 
						|
    But again, beware of floating point rounding errors::
 | 
						|
 | 
						|
            >>> t1 = Identity.scale(0.2, 0.3).translate(0.4, 0.6)
 | 
						|
            >>> t2 = Identity.translate(0.08, 0.18).scale(0.2, 0.3)
 | 
						|
            >>> t1
 | 
						|
            <Transform [0.2 0 0 0.3 0.08 0.18]>
 | 
						|
            >>> t2
 | 
						|
            <Transform [0.2 0 0 0.3 0.08 0.18]>
 | 
						|
            >>> d = {t1: None}
 | 
						|
            >>> d
 | 
						|
            {<Transform [0.2 0 0 0.3 0.08 0.18]>: None}
 | 
						|
            >>> d[t2]
 | 
						|
            Traceback (most recent call last):
 | 
						|
              File "<stdin>", line 1, in ?
 | 
						|
            KeyError: <Transform [0.2 0 0 0.3 0.08 0.18]>
 | 
						|
    """
 | 
						|
 | 
						|
    xx: float = 1
 | 
						|
    xy: float = 0
 | 
						|
    yx: float = 0
 | 
						|
    yy: float = 1
 | 
						|
    dx: float = 0
 | 
						|
    dy: float = 0
 | 
						|
 | 
						|
    def transformPoint(self, p):
 | 
						|
        """Transform a point.
 | 
						|
 | 
						|
        :Example:
 | 
						|
 | 
						|
                >>> t = Transform()
 | 
						|
                >>> t = t.scale(2.5, 5.5)
 | 
						|
                >>> t.transformPoint((100, 100))
 | 
						|
                (250.0, 550.0)
 | 
						|
        """
 | 
						|
        (x, y) = p
 | 
						|
        xx, xy, yx, yy, dx, dy = self
 | 
						|
        return (xx * x + yx * y + dx, xy * x + yy * y + dy)
 | 
						|
 | 
						|
    def transformPoints(self, points):
 | 
						|
        """Transform a list of points.
 | 
						|
 | 
						|
        :Example:
 | 
						|
 | 
						|
                >>> t = Scale(2, 3)
 | 
						|
                >>> t.transformPoints([(0, 0), (0, 100), (100, 100), (100, 0)])
 | 
						|
                [(0, 0), (0, 300), (200, 300), (200, 0)]
 | 
						|
                >>>
 | 
						|
        """
 | 
						|
        xx, xy, yx, yy, dx, dy = self
 | 
						|
        return [(xx * x + yx * y + dx, xy * x + yy * y + dy) for x, y in points]
 | 
						|
 | 
						|
    def transformVector(self, v):
 | 
						|
        """Transform an (dx, dy) vector, treating translation as zero.
 | 
						|
 | 
						|
        :Example:
 | 
						|
 | 
						|
                >>> t = Transform(2, 0, 0, 2, 10, 20)
 | 
						|
                >>> t.transformVector((3, -4))
 | 
						|
                (6, -8)
 | 
						|
                >>>
 | 
						|
        """
 | 
						|
        (dx, dy) = v
 | 
						|
        xx, xy, yx, yy = self[:4]
 | 
						|
        return (xx * dx + yx * dy, xy * dx + yy * dy)
 | 
						|
 | 
						|
    def transformVectors(self, vectors):
 | 
						|
        """Transform a list of (dx, dy) vector, treating translation as zero.
 | 
						|
 | 
						|
        :Example:
 | 
						|
                >>> t = Transform(2, 0, 0, 2, 10, 20)
 | 
						|
                >>> t.transformVectors([(3, -4), (5, -6)])
 | 
						|
                [(6, -8), (10, -12)]
 | 
						|
                >>>
 | 
						|
        """
 | 
						|
        xx, xy, yx, yy = self[:4]
 | 
						|
        return [(xx * dx + yx * dy, xy * dx + yy * dy) for dx, dy in vectors]
 | 
						|
 | 
						|
    def translate(self, x: float = 0, y: float = 0):
 | 
						|
        """Return a new transformation, translated (offset) by x, y.
 | 
						|
 | 
						|
        :Example:
 | 
						|
                >>> t = Transform()
 | 
						|
                >>> t.translate(20, 30)
 | 
						|
                <Transform [1 0 0 1 20 30]>
 | 
						|
                >>>
 | 
						|
        """
 | 
						|
        return self.transform((1, 0, 0, 1, x, y))
 | 
						|
 | 
						|
    def scale(self, x: float = 1, y: float | None = None):
 | 
						|
        """Return a new transformation, scaled by x, y. The 'y' argument
 | 
						|
        may be None, which implies to use the x value for y as well.
 | 
						|
 | 
						|
        :Example:
 | 
						|
                >>> t = Transform()
 | 
						|
                >>> t.scale(5)
 | 
						|
                <Transform [5 0 0 5 0 0]>
 | 
						|
                >>> t.scale(5, 6)
 | 
						|
                <Transform [5 0 0 6 0 0]>
 | 
						|
                >>>
 | 
						|
        """
 | 
						|
        if y is None:
 | 
						|
            y = x
 | 
						|
        return self.transform((x, 0, 0, y, 0, 0))
 | 
						|
 | 
						|
    def rotate(self, angle: float):
 | 
						|
        """Return a new transformation, rotated by 'angle' (radians).
 | 
						|
 | 
						|
        :Example:
 | 
						|
                >>> import math
 | 
						|
                >>> t = Transform()
 | 
						|
                >>> t.rotate(math.pi / 2)
 | 
						|
                <Transform [0 1 -1 0 0 0]>
 | 
						|
                >>>
 | 
						|
        """
 | 
						|
        c = _normSinCos(math.cos(angle))
 | 
						|
        s = _normSinCos(math.sin(angle))
 | 
						|
        return self.transform((c, s, -s, c, 0, 0))
 | 
						|
 | 
						|
    def skew(self, x: float = 0, y: float = 0):
 | 
						|
        """Return a new transformation, skewed by x and y.
 | 
						|
 | 
						|
        :Example:
 | 
						|
                >>> import math
 | 
						|
                >>> t = Transform()
 | 
						|
                >>> t.skew(math.pi / 4)
 | 
						|
                <Transform [1 0 1 1 0 0]>
 | 
						|
                >>>
 | 
						|
        """
 | 
						|
        return self.transform((1, math.tan(y), math.tan(x), 1, 0, 0))
 | 
						|
 | 
						|
    def transform(self, other):
 | 
						|
        """Return a new transformation, transformed by another
 | 
						|
        transformation.
 | 
						|
 | 
						|
        :Example:
 | 
						|
                >>> t = Transform(2, 0, 0, 3, 1, 6)
 | 
						|
                >>> t.transform((4, 3, 2, 1, 5, 6))
 | 
						|
                <Transform [8 9 4 3 11 24]>
 | 
						|
                >>>
 | 
						|
        """
 | 
						|
        xx1, xy1, yx1, yy1, dx1, dy1 = other
 | 
						|
        xx2, xy2, yx2, yy2, dx2, dy2 = self
 | 
						|
        return self.__class__(
 | 
						|
            xx1 * xx2 + xy1 * yx2,
 | 
						|
            xx1 * xy2 + xy1 * yy2,
 | 
						|
            yx1 * xx2 + yy1 * yx2,
 | 
						|
            yx1 * xy2 + yy1 * yy2,
 | 
						|
            xx2 * dx1 + yx2 * dy1 + dx2,
 | 
						|
            xy2 * dx1 + yy2 * dy1 + dy2,
 | 
						|
        )
 | 
						|
 | 
						|
    def reverseTransform(self, other):
 | 
						|
        """Return a new transformation, which is the other transformation
 | 
						|
        transformed by self. self.reverseTransform(other) is equivalent to
 | 
						|
        other.transform(self).
 | 
						|
 | 
						|
        :Example:
 | 
						|
                >>> t = Transform(2, 0, 0, 3, 1, 6)
 | 
						|
                >>> t.reverseTransform((4, 3, 2, 1, 5, 6))
 | 
						|
                <Transform [8 6 6 3 21 15]>
 | 
						|
                >>> Transform(4, 3, 2, 1, 5, 6).transform((2, 0, 0, 3, 1, 6))
 | 
						|
                <Transform [8 6 6 3 21 15]>
 | 
						|
                >>>
 | 
						|
        """
 | 
						|
        xx1, xy1, yx1, yy1, dx1, dy1 = self
 | 
						|
        xx2, xy2, yx2, yy2, dx2, dy2 = other
 | 
						|
        return self.__class__(
 | 
						|
            xx1 * xx2 + xy1 * yx2,
 | 
						|
            xx1 * xy2 + xy1 * yy2,
 | 
						|
            yx1 * xx2 + yy1 * yx2,
 | 
						|
            yx1 * xy2 + yy1 * yy2,
 | 
						|
            xx2 * dx1 + yx2 * dy1 + dx2,
 | 
						|
            xy2 * dx1 + yy2 * dy1 + dy2,
 | 
						|
        )
 | 
						|
 | 
						|
    def inverse(self):
 | 
						|
        """Return the inverse transformation.
 | 
						|
 | 
						|
        :Example:
 | 
						|
                >>> t = Identity.translate(2, 3).scale(4, 5)
 | 
						|
                >>> t.transformPoint((10, 20))
 | 
						|
                (42, 103)
 | 
						|
                >>> it = t.inverse()
 | 
						|
                >>> it.transformPoint((42, 103))
 | 
						|
                (10.0, 20.0)
 | 
						|
                >>>
 | 
						|
        """
 | 
						|
        if self == Identity:
 | 
						|
            return self
 | 
						|
        xx, xy, yx, yy, dx, dy = self
 | 
						|
        det = xx * yy - yx * xy
 | 
						|
        xx, xy, yx, yy = yy / det, -xy / det, -yx / det, xx / det
 | 
						|
        dx, dy = -xx * dx - yx * dy, -xy * dx - yy * dy
 | 
						|
        return self.__class__(xx, xy, yx, yy, dx, dy)
 | 
						|
 | 
						|
    def toPS(self) -> str:
 | 
						|
        """Return a PostScript representation
 | 
						|
 | 
						|
        :Example:
 | 
						|
 | 
						|
                >>> t = Identity.scale(2, 3).translate(4, 5)
 | 
						|
                >>> t.toPS()
 | 
						|
                '[2 0 0 3 8 15]'
 | 
						|
                >>>
 | 
						|
        """
 | 
						|
        return "[%s %s %s %s %s %s]" % self
 | 
						|
 | 
						|
    def toDecomposed(self) -> "DecomposedTransform":
 | 
						|
        """Decompose into a DecomposedTransform."""
 | 
						|
        return DecomposedTransform.fromTransform(self)
 | 
						|
 | 
						|
    def __bool__(self) -> bool:
 | 
						|
        """Returns True if transform is not identity, False otherwise.
 | 
						|
 | 
						|
        :Example:
 | 
						|
 | 
						|
                >>> bool(Identity)
 | 
						|
                False
 | 
						|
                >>> bool(Transform())
 | 
						|
                False
 | 
						|
                >>> bool(Scale(1.))
 | 
						|
                False
 | 
						|
                >>> bool(Scale(2))
 | 
						|
                True
 | 
						|
                >>> bool(Offset())
 | 
						|
                False
 | 
						|
                >>> bool(Offset(0))
 | 
						|
                False
 | 
						|
                >>> bool(Offset(2))
 | 
						|
                True
 | 
						|
        """
 | 
						|
        return self != Identity
 | 
						|
 | 
						|
    def __repr__(self) -> str:
 | 
						|
        return "<%s [%g %g %g %g %g %g]>" % ((self.__class__.__name__,) + self)
 | 
						|
 | 
						|
 | 
						|
Identity = Transform()
 | 
						|
 | 
						|
 | 
						|
def Offset(x: float = 0, y: float = 0) -> Transform:
 | 
						|
    """Return the identity transformation offset by x, y.
 | 
						|
 | 
						|
    :Example:
 | 
						|
            >>> Offset(2, 3)
 | 
						|
            <Transform [1 0 0 1 2 3]>
 | 
						|
            >>>
 | 
						|
    """
 | 
						|
    return Transform(1, 0, 0, 1, x, y)
 | 
						|
 | 
						|
 | 
						|
def Scale(x: float, y: float | None = None) -> Transform:
 | 
						|
    """Return the identity transformation scaled by x, y. The 'y' argument
 | 
						|
    may be None, which implies to use the x value for y as well.
 | 
						|
 | 
						|
    :Example:
 | 
						|
            >>> Scale(2, 3)
 | 
						|
            <Transform [2 0 0 3 0 0]>
 | 
						|
            >>>
 | 
						|
    """
 | 
						|
    if y is None:
 | 
						|
        y = x
 | 
						|
    return Transform(x, 0, 0, y, 0, 0)
 | 
						|
 | 
						|
 | 
						|
@dataclass
 | 
						|
class DecomposedTransform:
 | 
						|
    """The DecomposedTransform class implements a transformation with separate
 | 
						|
    translate, rotation, scale, skew, and transformation-center components.
 | 
						|
    """
 | 
						|
 | 
						|
    translateX: float = 0
 | 
						|
    translateY: float = 0
 | 
						|
    rotation: float = 0  # in degrees, counter-clockwise
 | 
						|
    scaleX: float = 1
 | 
						|
    scaleY: float = 1
 | 
						|
    skewX: float = 0  # in degrees, clockwise
 | 
						|
    skewY: float = 0  # in degrees, counter-clockwise
 | 
						|
    tCenterX: float = 0
 | 
						|
    tCenterY: float = 0
 | 
						|
 | 
						|
    def __bool__(self):
 | 
						|
        return (
 | 
						|
            self.translateX != 0
 | 
						|
            or self.translateY != 0
 | 
						|
            or self.rotation != 0
 | 
						|
            or self.scaleX != 1
 | 
						|
            or self.scaleY != 1
 | 
						|
            or self.skewX != 0
 | 
						|
            or self.skewY != 0
 | 
						|
            or self.tCenterX != 0
 | 
						|
            or self.tCenterY != 0
 | 
						|
        )
 | 
						|
 | 
						|
    @classmethod
 | 
						|
    def fromTransform(self, transform):
 | 
						|
        """Return a DecomposedTransform() equivalent of this transformation.
 | 
						|
        The returned solution always has skewY = 0, and angle in the (-180, 180].
 | 
						|
 | 
						|
        :Example:
 | 
						|
                >>> DecomposedTransform.fromTransform(Transform(3, 0, 0, 2, 0, 0))
 | 
						|
                DecomposedTransform(translateX=0, translateY=0, rotation=0.0, scaleX=3.0, scaleY=2.0, skewX=0.0, skewY=0.0, tCenterX=0, tCenterY=0)
 | 
						|
                >>> DecomposedTransform.fromTransform(Transform(0, 0, 0, 1, 0, 0))
 | 
						|
                DecomposedTransform(translateX=0, translateY=0, rotation=0.0, scaleX=0.0, scaleY=1.0, skewX=0.0, skewY=0.0, tCenterX=0, tCenterY=0)
 | 
						|
                >>> DecomposedTransform.fromTransform(Transform(0, 0, 1, 1, 0, 0))
 | 
						|
                DecomposedTransform(translateX=0, translateY=0, rotation=-45.0, scaleX=0.0, scaleY=1.4142135623730951, skewX=0.0, skewY=0.0, tCenterX=0, tCenterY=0)
 | 
						|
        """
 | 
						|
        # Adapted from an answer on
 | 
						|
        # https://math.stackexchange.com/questions/13150/extracting-rotation-scale-values-from-2d-transformation-matrix
 | 
						|
 | 
						|
        a, b, c, d, x, y = transform
 | 
						|
 | 
						|
        sx = math.copysign(1, a)
 | 
						|
        if sx < 0:
 | 
						|
            a *= sx
 | 
						|
            b *= sx
 | 
						|
 | 
						|
        delta = a * d - b * c
 | 
						|
 | 
						|
        rotation = 0
 | 
						|
        scaleX = scaleY = 0
 | 
						|
        skewX = 0
 | 
						|
 | 
						|
        # Apply the QR-like decomposition.
 | 
						|
        if a != 0 or b != 0:
 | 
						|
            r = math.sqrt(a * a + b * b)
 | 
						|
            rotation = math.acos(a / r) if b >= 0 else -math.acos(a / r)
 | 
						|
            scaleX, scaleY = (r, delta / r)
 | 
						|
            skewX = math.atan((a * c + b * d) / (r * r))
 | 
						|
        elif c != 0 or d != 0:
 | 
						|
            s = math.sqrt(c * c + d * d)
 | 
						|
            rotation = math.pi / 2 - (
 | 
						|
                math.acos(-c / s) if d >= 0 else -math.acos(c / s)
 | 
						|
            )
 | 
						|
            scaleX, scaleY = (delta / s, s)
 | 
						|
        else:
 | 
						|
            # a = b = c = d = 0
 | 
						|
            pass
 | 
						|
 | 
						|
        return DecomposedTransform(
 | 
						|
            x,
 | 
						|
            y,
 | 
						|
            math.degrees(rotation),
 | 
						|
            scaleX * sx,
 | 
						|
            scaleY,
 | 
						|
            math.degrees(skewX) * sx,
 | 
						|
            0.0,
 | 
						|
            0,
 | 
						|
            0,
 | 
						|
        )
 | 
						|
 | 
						|
    def toTransform(self) -> Transform:
 | 
						|
        """Return the Transform() equivalent of this transformation.
 | 
						|
 | 
						|
        :Example:
 | 
						|
                >>> DecomposedTransform(scaleX=2, scaleY=2).toTransform()
 | 
						|
                <Transform [2 0 0 2 0 0]>
 | 
						|
                >>>
 | 
						|
        """
 | 
						|
        t = Transform()
 | 
						|
        t = t.translate(
 | 
						|
            self.translateX + self.tCenterX, self.translateY + self.tCenterY
 | 
						|
        )
 | 
						|
        t = t.rotate(math.radians(self.rotation))
 | 
						|
        t = t.scale(self.scaleX, self.scaleY)
 | 
						|
        t = t.skew(math.radians(self.skewX), math.radians(self.skewY))
 | 
						|
        t = t.translate(-self.tCenterX, -self.tCenterY)
 | 
						|
        return t
 | 
						|
 | 
						|
 | 
						|
if __name__ == "__main__":
 | 
						|
    import sys
 | 
						|
    import doctest
 | 
						|
 | 
						|
    sys.exit(doctest.testmod().failed)
 |