Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit eaaf335

Browse files
committed
Implement Affine3D
1 parent b5f0066 commit eaaf335

File tree

2 files changed

+325
-4
lines changed

2 files changed

+325
-4
lines changed

lib/matplotlib/transforms.py

Lines changed: 302 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1289,7 +1289,8 @@ class Transform(TransformNode):
12891289
actually perform a transformation.
12901290
12911291
All non-affine transformations should be subclasses of this class.
1292-
New affine transformations should be subclasses of `Affine2D`.
1292+
New affine transformations should be subclasses of `Affine2D` or
1293+
`Affine3D`.
12931294
12941295
Subclasses of this class should override the following members (at
12951296
minimum):
@@ -1510,7 +1511,7 @@ def transform(self, values):
15101511
return res[0, 0]
15111512
if ndim == 1:
15121513
return res.reshape(-1)
1513-
elif ndim == 2:
1514+
elif ndim == 2 or ndim == 3:
15141515
return res
15151516
raise ValueError(
15161517
"Input values must have shape (N, {dims}) or ({dims},)"
@@ -1833,8 +1834,8 @@ class AffineImmutable(AffineBase):
18331834
b d f
18341835
0 0 1
18351836
1836-
This class provides the read-only interface. For a mutable 2D
1837-
affine transformation, use `Affine2D`.
1837+
This class provides the read-only interface. For a mutable
1838+
affine transformation, use `Affine2D` or `Affine3D`.
18381839
18391840
Subclasses of this class will generally only need to override a
18401841
constructor and `~.Transform.get_matrix` that generates a custom matrix
@@ -1936,6 +1937,8 @@ class Affine2DBase(AffineImmutable):
19361937
def _affine_factory(mtx, dims, *args, **kwargs):
19371938
if dims == 2:
19381939
return Affine2D(mtx, *args, **kwargs)
1940+
elif dims == 3:
1941+
return Affine3D(mtx, *args, **kwargs)
19391942
else:
19401943
return NotImplemented
19411944

@@ -2163,6 +2166,299 @@ def skew_deg(self, xShear, yShear):
21632166
return self.skew(math.radians(xShear), math.radians(yShear))
21642167

21652168

2169+
class Affine3D(AffineImmutable):
2170+
"""
2171+
A mutable 3D affine transformation.
2172+
"""
2173+
2174+
def __init__(self, matrix=None, **kwargs):
2175+
"""
2176+
Initialize an Affine transform from a 4x4 numpy float array::
2177+
2178+
a d g j
2179+
b e h k
2180+
c f i l
2181+
0 0 0 1
2182+
2183+
If *matrix* is None, initialize with the identity transform.
2184+
"""
2185+
super().__init__(dims=3, **kwargs)
2186+
if matrix is None:
2187+
matrix = np.identity(4)
2188+
self._mtx = matrix.copy()
2189+
self._invalid = 0
2190+
2191+
_base_str = _make_str_method("_mtx")
2192+
2193+
def __str__(self):
2194+
return (self._base_str()
2195+
if (self._mtx != np.diag(np.diag(self._mtx))).any()
2196+
else f"Affine3D().scale("
2197+
f"{self._mtx[0, 0]}, "
2198+
f"{self._mtx[1, 1]}, "
2199+
f"{self._mtx[2, 2]})"
2200+
if self._mtx[0, 0] != self._mtx[1, 1] or
2201+
self._mtx[0, 0] != self._mtx[2, 2]
2202+
else f"Affine3D().scale({self._mtx[0, 0]})")
2203+
2204+
@staticmethod
2205+
def from_values(a, b, c, d, e, f, g, h, i, j, k, l):
2206+
"""
2207+
Create a new Affine2D instance from the given values::
2208+
2209+
a d g j
2210+
b e h k
2211+
c f i l
2212+
0 0 0 1
2213+
2214+
.
2215+
"""
2216+
return Affine3D(np.array([
2217+
a, d, g, j,
2218+
b, e, h, k,
2219+
c, f, i, l,
2220+
0.0, 0.0, 0.0, 1.0
2221+
], float).reshape((4, 4)))
2222+
2223+
def get_matrix(self):
2224+
"""
2225+
Get the underlying transformation matrix as a 4x4 array::
2226+
2227+
a d g j
2228+
b e h k
2229+
c f i l
2230+
0 0 0 1
2231+
2232+
.
2233+
"""
2234+
if self._invalid:
2235+
self._inverted = None
2236+
self._invalid = 0
2237+
return self._mtx
2238+
2239+
def set_matrix(self, mtx):
2240+
"""
2241+
Set the underlying transformation matrix from a 4x4 array::
2242+
2243+
a d g j
2244+
b e h k
2245+
c f i l
2246+
0 0 0 1
2247+
2248+
.
2249+
"""
2250+
self._mtx = mtx
2251+
self.invalidate()
2252+
2253+
def set(self, other):
2254+
"""
2255+
Set this transformation from the frozen copy of another
2256+
`AffineImmutable` object with input and output dimension of 3.
2257+
"""
2258+
_api.check_isinstance(AffineImmutable, other=other)
2259+
if (other.input_dims != 3):
2260+
raise TypeError("Mismatch between dimensions of AffineImmutable"
2261+
"and Affine3D")
2262+
self._mtx = other.get_matrix()
2263+
self.invalidate()
2264+
2265+
def clear(self):
2266+
"""
2267+
Reset the underlying matrix to the identity transform.
2268+
"""
2269+
self._mtx = np.identity(4)
2270+
self.invalidate()
2271+
return self
2272+
2273+
def rotate(self, theta, dim=0):
2274+
"""
2275+
Add a rotation (in radians) to this transform in place, along
2276+
the dimension denoted by *dim*.
2277+
2278+
Returns *self*, so this method can easily be chained with more
2279+
calls to :meth:`rotate`, :meth:`rotate_deg`, :meth:`translate`
2280+
and :meth:`scale`.
2281+
"""
2282+
a = math.cos(theta)
2283+
b = math.sin(theta)
2284+
mtx = self._mtx
2285+
# Operating and assigning one scalar at a time is much faster.
2286+
(xx, xy, xz, x0), (yx, yy, yz, y0), (zx, zy, zz, z0), _ = mtx.tolist()
2287+
# mtx = [[a -b 0], [b a 0], [0 0 1]] * mtx
2288+
2289+
if dim == 0:
2290+
mtx[1, 0] = (a * yx) - (b * zx)
2291+
mtx[1, 1] = (a * yy) - (b * zy)
2292+
mtx[1, 2] = (a * yz) - (b * zz)
2293+
mtx[1, 3] = (a * y0) - (b * z0)
2294+
mtx[2, 0] = (b * yx) + (a * zx)
2295+
mtx[2, 1] = (b * yy) + (a * zy)
2296+
mtx[2, 2] = (b * yz) + (a * zz)
2297+
mtx[2, 3] = (b * y0) + (a * z0)
2298+
2299+
elif dim == 1:
2300+
mtx[0, 0] = (a * xx) + (b * zx)
2301+
mtx[0, 1] = (a * xy) + (b * zy)
2302+
mtx[0, 2] = (a * xz) + (b * zz)
2303+
mtx[0, 3] = (a * x0) + (b * z0)
2304+
mtx[2, 0] = (a * zx) - (b * xx)
2305+
mtx[2, 1] = (a * zy) - (b * xy)
2306+
mtx[2, 2] = (a * zz) - (b * xz)
2307+
mtx[2, 3] = (a * z0) - (b * x0)
2308+
2309+
elif dim == 2:
2310+
mtx[0, 0] = (a * xx) - (b * yx)
2311+
mtx[0, 1] = (a * xy) - (b * yy)
2312+
mtx[0, 2] = (a * xz) - (b * yz)
2313+
mtx[0, 3] = (a * x0) - (b * y0)
2314+
mtx[1, 0] = (b * xx) + (a * yx)
2315+
mtx[1, 1] = (b * xy) + (a * yy)
2316+
mtx[1, 2] = (b * xz) + (a * yz)
2317+
mtx[1, 3] = (b * x0) + (a * y0)
2318+
2319+
self.invalidate()
2320+
return self
2321+
2322+
def rotate_deg(self, degrees, dim=0):
2323+
"""
2324+
Add a rotation (in degrees) to this transform in place, along
2325+
the dimension denoted by *dim*.
2326+
2327+
Returns *self*, so this method can easily be chained with more
2328+
calls to :meth:`rotate`, :meth:`rotate_deg`, :meth:`translate`
2329+
and :meth:`scale`.
2330+
"""
2331+
return self.rotate(math.radians(degrees), dim)
2332+
2333+
def rotate_around(self, x, y, z, theta, dim=0):
2334+
"""
2335+
Add a rotation (in radians) around the point (x, y, z) in place,
2336+
along the dimension denoted by *dim*.
2337+
2338+
Returns *self*, so this method can easily be chained with more
2339+
calls to :meth:`rotate`, :meth:`rotate_deg`, :meth:`translate`
2340+
and :meth:`scale`.
2341+
"""
2342+
return self.translate(-x, -y, -z).rotate(theta, dim).translate(x, y, z)
2343+
2344+
def rotate_deg_around(self, x, y, z, degrees, dim=0):
2345+
"""
2346+
Add a rotation (in degrees) around the point (x, y, z) in place,
2347+
along the dimension denoted by *dim*.
2348+
2349+
Returns *self*, so this method can easily be chained with more
2350+
calls to :meth:`rotate`, :meth:`rotate_deg`, :meth:`translate`
2351+
and :meth:`scale`.
2352+
"""
2353+
# Cast to float to avoid wraparound issues with uint8's
2354+
x, y = float(x), float(y)
2355+
return self.translate(-x, -y, -z).rotate_deg(degrees, dim).translate(x, y, z)
2356+
2357+
def translate(self, tx, ty, tz):
2358+
"""
2359+
Add a translation in place.
2360+
2361+
Returns *self*, so this method can easily be chained with more
2362+
calls to :meth:`rotate`, :meth:`rotate_deg`, :meth:`translate`
2363+
and :meth:`scale`.
2364+
"""
2365+
self._mtx[0, 3] += tx
2366+
self._mtx[1, 3] += ty
2367+
self._mtx[2, 3] += tz
2368+
self.invalidate()
2369+
return self
2370+
2371+
def scale(self, sx, sy=None, sz=None):
2372+
"""
2373+
Add a scale in place.
2374+
2375+
If a scale is not provided in the *y* or *z* directions, *sx*
2376+
will be applied for that direction.
2377+
2378+
Returns *self*, so this method can easily be chained with more
2379+
calls to :meth:`rotate`, :meth:`rotate_deg`, :meth:`translate`
2380+
and :meth:`scale`.
2381+
"""
2382+
if sy is None:
2383+
sy = sx
2384+
2385+
if sz is None:
2386+
sz = sx
2387+
# explicit element-wise scaling is fastest
2388+
self._mtx[0, 0] *= sx
2389+
self._mtx[0, 1] *= sx
2390+
self._mtx[0, 2] *= sx
2391+
self._mtx[0, 3] *= sx
2392+
self._mtx[1, 0] *= sy
2393+
self._mtx[1, 1] *= sy
2394+
self._mtx[1, 2] *= sy
2395+
self._mtx[1, 3] *= sy
2396+
self._mtx[2, 0] *= sz
2397+
self._mtx[2, 1] *= sz
2398+
self._mtx[2, 2] *= sz
2399+
self._mtx[2, 3] *= sz
2400+
2401+
self.invalidate()
2402+
return self
2403+
2404+
def skew(self, xyShear, xzShear, yxShear, yzShear, zxShear, zyShear):
2405+
"""
2406+
Add a skew in place along for each plane in the 3rd dimension.
2407+
2408+
For example *zxShear* is the shear angle along the *zx* plane,
2409+
in radians.
2410+
2411+
Returns *self*, so this method can easily be chained with more
2412+
calls to :meth:`rotate`, :meth:`rotate_deg`, :meth:`translate`
2413+
and :meth:`scale`.
2414+
"""
2415+
rxy = math.tan(xyShear)
2416+
rxz = math.tan(xzShear)
2417+
ryx = math.tan(yxShear)
2418+
ryz = math.tan(yzShear)
2419+
rzx = math.tan(zxShear)
2420+
rzy = math.tan(zyShear)
2421+
mtx = self._mtx
2422+
# Operating and assigning one scalar at a time is much faster.
2423+
(xx, xy, xz, x0), (yx, yy, yz, y0), (zx, zy, zz, z0), _ = mtx.tolist()
2424+
# mtx = [[1 rx 0], [ry 1 0], [0 0 1]] * mtx
2425+
2426+
mtx[0, 0] += (rxy * yx) + (rxz * zx)
2427+
mtx[0, 1] += (rxy * yy) + (rxz * zy)
2428+
mtx[0, 2] += (rxy * yz) + (rxz * zz)
2429+
mtx[0, 3] += (rxy * y0) + (rxz * z0)
2430+
mtx[1, 0] = (ryx * xx) + yx + (ryz * zx)
2431+
mtx[1, 1] = (ryx * xy) + yy + (ryz * zy)
2432+
mtx[1, 2] = (ryx * xz) + yz + (ryz * zz)
2433+
mtx[1, 3] = (ryx * x0) + y0 + (ryz * z0)
2434+
mtx[2, 0] = (rzx * xx) + (rzy * yx) + zx
2435+
mtx[2, 1] = (rzx * xy) + (rzy * yy) + zy
2436+
mtx[2, 2] = (rzx * xz) + (rzy * yz) + zz
2437+
mtx[2, 3] = (rzx * x0) + (rzy * y0) + z0
2438+
2439+
self.invalidate()
2440+
return self
2441+
2442+
def skew_deg(self, xyShear, xzShear, yxShear, yzShear, zxShear, zyShear):
2443+
"""
2444+
Add a skew in place along for each plane in the 3rd dimension.
2445+
2446+
For example *zxShear* is the shear angle along the *zx* plane,
2447+
in radians.
2448+
2449+
Returns *self*, so this method can easily be chained with more
2450+
calls to :meth:`rotate`, :meth:`rotate_deg`, :meth:`translate`
2451+
and :meth:`scale`.
2452+
"""
2453+
return self.skew(
2454+
math.radians(xyShear),
2455+
math.radians(xzShear),
2456+
math.radians(yxShear),
2457+
math.radians(yzShear),
2458+
math.radians(zxShear),
2459+
math.radians(zyShear))
2460+
2461+
21662462
class IdentityTransform(AffineImmutable):
21672463
"""
21682464
A special class that does one thing, the identity transform, in a
@@ -2609,6 +2905,8 @@ def composite_transform_factory(a, b):
26092905
return a
26102906
elif isinstance(a, Affine2D) and isinstance(b, Affine2D):
26112907
return CompositeAffine(a, b)
2908+
elif isinstance(a, Affine3D) and isinstance(b, Affine3D):
2909+
return CompositeAffine(a, b)
26122910
return CompositeGenericTransform(a, b)
26132911

26142912

lib/matplotlib/transforms.pyi

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,29 @@ class Affine2D(AffineImmutable):
251251
def skew(self, xShear: float, yShear: float) -> Affine2D: ...
252252
def skew_deg(self, xShear: float, yShear: float) -> Affine2D: ...
253253

254+
class Affine3D(AffineImmutable):
255+
def __init__(self, matrix: ArrayLike | None = ..., **kwargs) -> None: ...
256+
@staticmethod
257+
def from_values(
258+
a: float, b: float, c: float, d: float, e: float, f: float, g: float,
259+
h: float, i: float, j: float, k: float, l: float
260+
) -> Affine3D: ...
261+
def set_matrix(self, mtx: ArrayLike) -> None: ...
262+
def clear(self) -> Affine3D: ...
263+
def rotate(self, theta: float, dim: int = ...) -> Affine3D: ...
264+
def rotate_deg(self, degrees: float, dim: int = ...) -> Affine3D: ...
265+
def rotate_around(self, x: float, y: float, z: float, theta: float, dim: int = ...
266+
) -> Affine3D: ...
267+
def rotate_deg_around(self, x: float, y: float, z: float, degrees: float, dim: int = ...
268+
) -> Affine3D: ...
269+
def translate(self, tx: float, ty: float, tz: float) -> Affine3D: ...
270+
def scale(self, sx: float, sy: float | None = ..., sz: float | None = ...
271+
) -> Affine3D: ...
272+
def skew(self, xyShear: float, xzShear: float, yxShear: float, yzShear: float,
273+
zxShear: float, zyShear: float) -> Affine3D: ...
274+
def skew_deg(self, xyShear: float, xzShear: float, yxShear: float, yzShear: float,
275+
zxShear: float, zyShear: float) -> Affine3D: ...
276+
254277
class IdentityTransform(AffineImmutable): ...
255278

256279
class _BlendedMixin:

0 commit comments

Comments
 (0)