transform: make Transform class a NamedTuple

This removes some boilerplate code, and also helps when using static type checkers like mypy.
The typing.NamedTuple class was added with python 3.6 which is our min required python, so we are good.
This commit is contained in:
Cosimo Lupo 2020-04-29 11:11:39 +01:00
parent fc10f74a19
commit dbc9d132c0
No known key found for this signature in database
GPG Key ID: 179A8F0895A02F4F

View File

@ -45,7 +45,8 @@ Examples:
>>>
"""
from fontTools.misc.py23 import *
from typing import NamedTuple
__all__ = ["Transform", "Identity", "Offset", "Scale"]
@ -65,7 +66,7 @@ def _normSinCos(v):
return v
class Transform(object):
class Transform(NamedTuple):
"""2x2 transformation matrix plus offset, a.k.a. Affine transform.
Transform instances are immutable: all transforming methods, eg.
@ -82,20 +83,68 @@ class Transform(object):
>>>
>>> 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]>
"""
def __init__(self, xx=1, xy=0, yx=0, yy=1, dx=0, dy=0):
"""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]>
>>>
"""
self.__affine = xx, xy, yx, yy, dx, dy
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.
@ -107,7 +156,7 @@ class Transform(object):
(250.0, 550.0)
"""
(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)
def transformPoints(self, points):
@ -119,7 +168,7 @@ class Transform(object):
[(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]
def translate(self, x=0, y=0):
@ -188,7 +237,7 @@ class Transform(object):
>>>
"""
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__(
xx1*xx2 + xy1*yx2,
xx1*xy2 + xy1*yy2,
@ -210,7 +259,7 @@ class Transform(object):
<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
return self.__class__(
xx1*xx2 + xy1*yx2,
@ -232,9 +281,9 @@ class Transform(object):
(10.0, 20.0)
>>>
"""
if self.__affine == (1, 0, 0, 1, 0, 0):
if self == Identity:
return self
xx, xy, yx, yy, dx, dy = self.__affine
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
@ -247,77 +296,7 @@ class Transform(object):
'[2 0 0 3 8 15]'
>>>
"""
return "[%s %s %s %s %s %s]" % self.__affine
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)
return "[%s %s %s %s %s %s]" % self
def __bool__(self):
"""Returns True if transform is not identity, False otherwise.
@ -336,13 +315,12 @@ class Transform(object):
>>> bool(Offset(2))
True
"""
return self.__affine != Identity.__affine
return self != Identity
__nonzero__ = __bool__
def __repr__(self):
return "<%s [%g %g %g %g %g %g]>" % ((self.__class__.__name__,) \
+ self.__affine)
return "<%s [%g %g %g %g %g %g]>" % ((self.__class__.__name__,) + self)
Identity = Transform()