Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit eda3015

Browse filesBrowse files
committed
Implement Affine3D
1 parent 64e3674 commit eda3015
Copy full SHA for eda3015

File tree

2 files changed

+325
-4
lines changed
Filter options

2 files changed

+325
-4
lines changed

‎lib/matplotlib/transforms.py

Copy file name to clipboardExpand all lines: lib/matplotlib/transforms.py
+302-4Lines 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

Copy file name to clipboardExpand all lines: lib/matplotlib/transforms.pyi
+23Lines 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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.