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:
parent
fc10f74a19
commit
dbc9d132c0
@ -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()
|
||||
|
Loading…
x
Reference in New Issue
Block a user