Merge pull request #1904 from anthrotype/transform-named-tuple

transform: make Transform class a NamedTuple
This commit is contained in:
Cosimo Lupo 2020-04-29 11:36:38 +01:00 committed by GitHub
commit 70f0bb3570
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

View File

@ -45,7 +45,8 @@ Examples:
>>> >>>
""" """
from fontTools.misc.py23 import * from typing import NamedTuple
__all__ = ["Transform", "Identity", "Offset", "Scale"] __all__ = ["Transform", "Identity", "Offset", "Scale"]
@ -65,7 +66,7 @@ def _normSinCos(v):
return v return v
class Transform(object): class Transform(NamedTuple):
"""2x2 transformation matrix plus offset, a.k.a. Affine transform. """2x2 transformation matrix plus offset, a.k.a. Affine transform.
Transform instances are immutable: all transforming methods, eg. Transform instances are immutable: all transforming methods, eg.
@ -82,20 +83,68 @@ class Transform(object):
>>> >>>
>>> t.scale(2, 3).transformPoint((100, 100)) >>> t.scale(2, 3).transformPoint((100, 100))
(200, 300) (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]>
""" """
def __init__(self, xx=1, xy=0, yx=0, yy=1, dx=0, dy=0): xx: float = 1
"""Transform's constructor takes six arguments, all of which are xy: float = 0
optional, and can be used as keyword arguments: yx: float = 0
>>> Transform(12) yy: float = 1
<Transform [12 0 0 1 0 0]> dx: float = 0
>>> Transform(dx=12) dy: float = 0
<Transform [1 0 0 1 12 0]>
>>> Transform(yx=12)
<Transform [1 0 12 1 0 0]>
>>>
"""
self.__affine = xx, xy, yx, yy, dx, dy
def transformPoint(self, p): def transformPoint(self, p):
"""Transform a point. """Transform a point.
@ -107,7 +156,7 @@ class Transform(object):
(250.0, 550.0) (250.0, 550.0)
""" """
(x, y) = p (x, y) = p
xx, xy, yx, yy, dx, dy = self.__affine xx, xy, yx, yy, dx, dy = self
return (xx*x + yx*y + dx, xy*x + yy*y + dy) return (xx*x + yx*y + dx, xy*x + yy*y + dy)
def transformPoints(self, points): def transformPoints(self, points):
@ -119,7 +168,7 @@ class Transform(object):
[(0, 0), (0, 300), (200, 300), (200, 0)] [(0, 0), (0, 300), (200, 300), (200, 0)]
>>> >>>
""" """
xx, xy, yx, yy, dx, dy = self.__affine xx, xy, yx, yy, dx, dy = self
return [(xx*x + yx*y + dx, xy*x + yy*y + dy) for x, y in points] return [(xx*x + yx*y + dx, xy*x + yy*y + dy) for x, y in points]
def translate(self, x=0, y=0): def translate(self, x=0, y=0):
@ -188,7 +237,7 @@ class Transform(object):
>>> >>>
""" """
xx1, xy1, yx1, yy1, dx1, dy1 = other xx1, xy1, yx1, yy1, dx1, dy1 = other
xx2, xy2, yx2, yy2, dx2, dy2 = self.__affine xx2, xy2, yx2, yy2, dx2, dy2 = self
return self.__class__( return self.__class__(
xx1*xx2 + xy1*yx2, xx1*xx2 + xy1*yx2,
xx1*xy2 + xy1*yy2, xx1*xy2 + xy1*yy2,
@ -210,7 +259,7 @@ class Transform(object):
<Transform [8 6 6 3 21 15]> <Transform [8 6 6 3 21 15]>
>>> >>>
""" """
xx1, xy1, yx1, yy1, dx1, dy1 = self.__affine xx1, xy1, yx1, yy1, dx1, dy1 = self
xx2, xy2, yx2, yy2, dx2, dy2 = other xx2, xy2, yx2, yy2, dx2, dy2 = other
return self.__class__( return self.__class__(
xx1*xx2 + xy1*yx2, xx1*xx2 + xy1*yx2,
@ -232,9 +281,9 @@ class Transform(object):
(10.0, 20.0) (10.0, 20.0)
>>> >>>
""" """
if self.__affine == (1, 0, 0, 1, 0, 0): if self == Identity:
return self return self
xx, xy, yx, yy, dx, dy = self.__affine xx, xy, yx, yy, dx, dy = self
det = xx*yy - yx*xy det = xx*yy - yx*xy
xx, xy, yx, yy = yy/det, -xy/det, -yx/det, xx/det xx, xy, yx, yy = yy/det, -xy/det, -yx/det, xx/det
dx, dy = -xx*dx - yx*dy, -xy*dx - yy*dy dx, dy = -xx*dx - yx*dy, -xy*dx - yy*dy
@ -247,77 +296,7 @@ class Transform(object):
'[2 0 0 3 8 15]' '[2 0 0 3 8 15]'
>>> >>>
""" """
return "[%s %s %s %s %s %s]" % self.__affine return "[%s %s %s %s %s %s]" % self
def __len__(self):
"""Transform instances also behave like sequences of length 6:
>>> len(Identity)
6
>>>
"""
return 6
def __getitem__(self, index):
"""Transform instances also behave like sequences of length 6:
>>> list(Identity)
[1, 0, 0, 1, 0, 0]
>>> tuple(Identity)
(1, 0, 0, 1, 0, 0)
>>>
"""
return self.__affine[index]
def __ne__(self, other):
return not self.__eq__(other)
def __eq__(self, other):
"""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
>>>
"""
xx1, xy1, yx1, yy1, dx1, dy1 = self.__affine
xx2, xy2, yx2, yy2, dx2, dy2 = other
return (xx1, xy1, yx1, yy1, dx1, dy1) == \
(xx2, xy2, yx2, yy2, dx2, dy2)
def __hash__(self):
"""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]>
>>>
"""
return hash(self.__affine)
def __bool__(self): def __bool__(self):
"""Returns True if transform is not identity, False otherwise. """Returns True if transform is not identity, False otherwise.
@ -336,13 +315,10 @@ class Transform(object):
>>> bool(Offset(2)) >>> bool(Offset(2))
True True
""" """
return self.__affine != Identity.__affine return self != Identity
__nonzero__ = __bool__
def __repr__(self): def __repr__(self):
return "<%s [%g %g %g %g %g %g]>" % ((self.__class__.__name__,) \ return "<%s [%g %g %g %g %g %g]>" % ((self.__class__.__name__,) + self)
+ self.__affine)
Identity = Transform() Identity = Transform()