Coverage for trimesh/voxel/transforms.py: 83%
72 statements
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-24 04:40 +0000
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-24 04:40 +0000
1import numpy as np
3from .. import caching, util
4from .. import transformations as tr
7class Transform:
8 """
9 Class for caching metadata associated with 4x4 transformations.
11 The transformation matrix is used to define relevant properties
12 for the voxels, including pitch and origin.
13 """
15 def __init__(self, matrix, datastore: caching.DataStore | None = None):
16 """
17 Initialize with a transform.
19 Parameters
20 -----------
21 matrix : (4, 4) float
22 Homogeneous transformation matrix
23 datastore
24 If passed store the actual values in a reference to
25 another datastore.
26 """
27 matrix = np.asanyarray(matrix, dtype=np.float64)
28 if matrix.shape != (4, 4) or not np.allclose(matrix[3, :], [0, 0, 0, 1]):
29 raise ValueError("matrix is invalid!")
31 # store matrix as data
32 if datastore is None:
33 self._data = caching.DataStore()
34 elif isinstance(datastore, caching.DataStore):
35 self._data = datastore
36 else:
37 raise ValueError(f"{type(datastore)} != caching.DataStore")
39 self._data["transform_matrix"] = matrix
40 # dump cache when matrix changes
41 self._cache = caching.Cache(id_function=self._data.__hash__)
43 def __hash__(self):
44 """
45 Get the hash of the current transformation matrix.
47 Returns
48 ------------
49 hash : str
50 Hash of transformation matrix
51 """
52 return self._data.__hash__()
54 @property
55 def translation(self):
56 """
57 Get the translation component of the matrix
59 Returns
60 ------------
61 translation : (3,) float
62 Cartesian translation
63 """
64 return self._data["transform_matrix"][:3, 3]
66 @property
67 def matrix(self):
68 """
69 Get the homogeneous transformation matrix.
71 Returns
72 -------------
73 matrix : (4, 4) float
74 Transformation matrix
75 """
76 return self._data["transform_matrix"]
78 @matrix.setter
79 def matrix(self, values):
80 """
81 Set the homogeneous transformation matrix.
83 Parameters
84 -------------
85 matrix : (4, 4) float
86 Transformation matrix
87 """
88 values = np.asanyarray(values, dtype=np.float64)
89 if values.shape != (4, 4):
90 raise ValueError("matrix must be (4, 4)!")
91 self._data["transform_matrix"] = values
93 @caching.cache_decorator
94 def scale(self):
95 """
96 Get the scale factor of the current transformation.
98 Returns
99 -------------
100 scale : (3,) float
101 Scale factor from the matrix
102 """
103 # get the current transformation
104 matrix = self.matrix
105 # get the (3,) diagonal of the rotation component
106 scale = np.diag(matrix[:3, :3])
107 if not np.allclose(matrix[:3, :3], scale * np.eye(3), scale * 1e-6 + 1e-8):
108 raise RuntimeError("transform features a shear or rotation")
109 return scale
111 @caching.cache_decorator
112 def pitch(self):
113 scale = self.scale
114 if not util.allclose(scale[0], scale[1:], np.max(np.abs(scale)) * 1e-6 + 1e-8):
115 raise RuntimeError("transform features non-uniform scaling")
116 return scale
118 @caching.cache_decorator
119 def unit_volume(self):
120 """Volume of a transformed unit cube."""
121 return np.linalg.det(self._data["transform_matrix"][:3, :3])
123 def apply_transform(self, matrix):
124 """Mutate the transform in-place and return self."""
125 self.matrix = np.matmul(matrix, self.matrix)
126 return self
128 def apply_translation(self, translation):
129 """Mutate the transform in-place and return self."""
130 self.matrix[:3, 3] += translation
131 return self
133 def apply_scale(self, scale):
134 """Mutate the transform in-place and return self."""
135 self.matrix[:3] *= scale
136 return self
138 def transform_points(self, points):
139 """
140 Apply the transformation to points (not in-place).
142 Parameters
143 ----------
144 points: (n, 3) float
145 Points in cartesian space
147 Returns
148 ----------
149 transformed : (n, 3) float
150 Points transformed by matrix
151 """
152 if self.is_identity:
153 return points.copy()
154 return tr.transform_points(points.reshape(-1, 3), self.matrix).reshape(
155 points.shape
156 )
158 def inverse_transform_points(self, points):
159 """Apply the inverse transformation to points (not in-place)."""
160 if self.is_identity:
161 return points
162 return tr.transform_points(points.reshape(-1, 3), self.inverse_matrix).reshape(
163 points.shape
164 )
166 @caching.cache_decorator
167 def inverse_matrix(self):
168 inv = np.linalg.inv(self.matrix)
169 inv.flags.writeable = False
170 return inv
172 def copy(self):
173 return Transform(matrix=self.matrix)
175 @caching.cache_decorator
176 def is_identity(self):
177 """
178 Flags this transformation being sufficiently close to eye(4).
179 """
180 return util.allclose(self.matrix, np.eye(4), 1e-8)