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

Skip to content

Commit eda3015

Browse files
committed
Implement Affine3D
1 parent 64e3674 commit eda3015

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},)"
@@ -1839,8 +1840,8 @@ class AffineImmutable(AffineBase):
18391840
b d f
18401841
0 0 1
18411842
1842-
This class provides the read-only interface. For a mutable 2D
1843-
affine transformation, use `Affine2D`.
1843+
This class provides the read-only interface. For a mutable
1844+
affine transformation, use `Affine2D` or `Affine3D`.
18441845
18451846
Subclasses of this class will generally only need to override a
18461847
constructor and `~.Transform.get_matrix` that generates a custom matrix
@@ -1942,6 +1943,8 @@ class Affine2DBase(AffineImmutable):
19421943
def _affine_factory(mtx, dims, *args, **kwargs):
19431944
if dims == 2:
19441945
return Affine2D(mtx, *args, **kwargs)
1946+
elif dims == 3:
1947+
return Affine3D(mtx, *args, **kwargs)
19451948
else:
19461949
return NotImplemented
19471950

@@ -2169,6 +2172,299 @@ def skew_deg(self, xShear, yShear):
21692172
return self.skew(math.radians(xShear), math.radians(yShear))
21702173

21712174

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

26202918

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)