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"]
@ -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,10 +83,8 @@ class Transform(object):
>>>
>>> t.scale(2, 3).transformPoint((100, 100))
(200, 300)
"""
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
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]>
@ -93,9 +92,59 @@ class Transform(object):
<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]>
"""
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,10 @@ class Transform(object):
>>> bool(Offset(2))
True
"""
return self.__affine != Identity.__affine
__nonzero__ = __bool__
return self != Identity
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()