1use fast_mesh_bvh::Aabb;
5
6use crate::{QueryError, Result};
7
8const TOLERANCE: f64 = 1e-6;
10
11#[derive(Debug, Clone, Copy, PartialEq)]
24pub struct Rigid {
25 pub rotation: [[f64; 3]; 3],
27 pub translation: [f64; 3],
29}
30
31impl Rigid {
32 pub const IDENTITY: Rigid = Rigid {
34 rotation: [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]],
35 translation: [0.0; 3],
36 };
37
38 pub fn inverse(&self) -> Rigid {
41 let r = &self.rotation;
42 let rotation: [[f64; 3]; 3] = std::array::from_fn(|i| std::array::from_fn(|j| r[j][i]));
43 let t = self.translation;
44 let translation = std::array::from_fn(|i| {
45 -(rotation[i][0] * t[0] + rotation[i][1] * t[1] + rotation[i][2] * t[2])
46 });
47 Rigid {
48 rotation,
49 translation,
50 }
51 }
52
53 pub fn validate(&self) -> Result<()> {
61 let r = &self.rotation;
62 if !r
63 .iter()
64 .flatten()
65 .chain(&self.translation)
66 .all(|v| v.is_finite())
67 {
68 return Err(QueryError::InvalidInput("transform has a non-finite value"));
69 }
70 for i in 0..3 {
71 for j in 0..3 {
72 let dot = r[0][i] * r[0][j] + r[1][i] * r[1][j] + r[2][i] * r[2][j];
73 let want = if i == j { 1.0 } else { 0.0 };
74 if (dot - want).abs() > TOLERANCE {
75 return Err(QueryError::InvalidInput(
76 "transform rotation is not orthonormal",
77 ));
78 }
79 }
80 }
81 let det = r[0][0] * (r[1][1] * r[2][2] - r[1][2] * r[2][1])
82 - r[0][1] * (r[1][0] * r[2][2] - r[1][2] * r[2][0])
83 + r[0][2] * (r[1][0] * r[2][1] - r[1][1] * r[2][0]);
84 if (det - 1.0).abs() > TOLERANCE {
85 return Err(QueryError::InvalidInput(
86 "transform rotation is a reflection",
87 ));
88 }
89 Ok(())
90 }
91
92 pub fn apply(&self, p: [f32; 3]) -> [f64; 3] {
116 self.apply_f64(p.map(f64::from))
117 }
118
119 pub(crate) fn apply_f64(&self, p: [f64; 3]) -> [f64; 3] {
122 let r = &self.rotation;
123 let t = self.translation;
124 std::array::from_fn(|i| r[i][0] * p[0] + r[i][1] * p[1] + r[i][2] * p[2] + t[i])
125 }
126
127 pub(crate) fn transform_box(&self, b: &Aabb) -> Aabb {
138 if b.is_empty() {
139 return Aabb::EMPTY;
140 }
141 let lo = b.min.map(f64::from);
142 let hi = b.max.map(f64::from);
143 let r = &self.rotation;
144 let t = self.translation;
145 let mut out = Aabb::EMPTY;
146 for i in 0..3 {
147 let low = |j: usize| if r[i][j] >= 0.0 { lo[j] } else { hi[j] };
148 let high = |j: usize| if r[i][j] >= 0.0 { hi[j] } else { lo[j] };
149 let min = r[i][0] * low(0) + r[i][1] * low(1) + r[i][2] * low(2) + t[i];
150 let max = r[i][0] * high(0) + r[i][1] * high(1) + r[i][2] * high(2) + t[i];
151 out.min[i] = round_down(min);
152 out.max[i] = round_up(max);
153 }
154 out
155 }
156}
157
158fn round_down(x: f64) -> f32 {
160 let f = x as f32;
161 if f64::from(f) > x { f.next_down() } else { f }
162}
163
164fn round_up(x: f64) -> f32 {
166 let f = x as f32;
167 if f64::from(f) < x { f.next_up() } else { f }
168}
169
170#[cfg(test)]
171mod tests {
172 use super::*;
173 use crate::test_util::{Lcg, random_rotation};
174
175 fn rot_z(deg: f64) -> [[f64; 3]; 3] {
176 let (s, c) = deg.to_radians().sin_cos();
177 [[c, -s, 0.0], [s, c, 0.0], [0.0, 0.0, 1.0]]
178 }
179
180 fn rigid(rotation: [[f64; 3]; 3], translation: [f64; 3]) -> Rigid {
181 Rigid {
182 rotation,
183 translation,
184 }
185 }
186
187 fn is_invalid(r: Rigid) -> bool {
188 matches!(r.validate(), Err(QueryError::InvalidInput(_)))
189 }
190
191 #[test]
192 fn rigid_validation() {
193 assert_eq!(Rigid::IDENTITY.validate(), Ok(()));
194 let r30 = rot_z(30.0);
195 assert_eq!(rigid(r30, [1.0, 2.0, 3.0]).validate(), Ok(()));
196 let r30_f32 = r30.map(|row| row.map(|v| f64::from(v as f32)));
197 assert_ne!(r30_f32, r30);
198 assert_eq!(rigid(r30_f32, [0.0; 3]).validate(), Ok(()));
199
200 let mut off = r30;
201 off[0][0] += 1e-3;
202 assert!(is_invalid(rigid(off, [0.0; 3])));
203 let reflection = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, -1.0]];
204 assert!(is_invalid(rigid(reflection, [0.0; 3])));
205 assert!(is_invalid(rigid(r30, [0.0, f64::NAN, 0.0])));
206 let mut inf = r30;
207 inf[2][1] = f64::INFINITY;
208 assert!(is_invalid(rigid(inf, [0.0; 3])));
209 }
210
211 #[test]
212 fn inverse_round_trips() {
213 let r = rigid(rot_z(30.0), [1.5, -2.0, 0.25]);
214 let inv = r.inverse();
215 assert_eq!(inv.validate(), Ok(()));
216 for p in [[0.0, 0.0, 0.0], [1.0, 2.0, 3.0], [-7.5, 0.125, 4.0]] {
217 let back = inv.apply_f64(r.apply(p));
218 for k in 0..3 {
219 assert!(
220 (back[k] - f64::from(p[k])).abs() <= 1e-12,
221 "{back:?} vs {p:?}"
222 );
223 }
224 }
225 let p = [0.5f32, -1.0, 2.0];
227 let q = inv.apply(p);
228 let q32 = q.map(|v| v as f32);
229 let back = r.apply(q32);
230 for k in 0..3 {
231 assert!((back[k] - f64::from(p[k])).abs() <= 1e-6);
232 }
233 }
234
235 fn assert_inside(r: &Rigid, b: &Aabb, t: &Aabb, extra: &[[f32; 3]]) {
237 let corners = (0..8).map(|c: u32| -> [f32; 3] {
238 std::array::from_fn(|k| if c >> k & 1 == 0 { b.min[k] } else { b.max[k] })
239 });
240 for p in corners.chain(extra.iter().copied()) {
241 let q = r.apply(p);
242 let inside = q
243 .iter()
244 .zip(t.min.iter().zip(&t.max))
245 .all(|(&v, (&lo, &hi))| f64::from(lo) <= v && v <= f64::from(hi));
246 assert!(inside, "{p:?} -> {q:?} outside {t:?}");
247 }
248 }
249
250 #[test]
251 fn transform_box_is_conservative() {
252 let mut rng = Lcg(42);
253 for _ in 0..1000 {
254 let r = rigid(
255 random_rotation(&mut rng),
256 std::array::from_fn(|_| f64::from(rng.next()) * 200.0 - 100.0),
257 );
258 assert_eq!(r.validate(), Ok(()));
259 let lo: [f32; 3] = std::array::from_fn(|_| rng.next() * 20.0 - 10.0);
260 let size: [f32; 3] = std::array::from_fn(|_| rng.next() * 5.0);
261 let b = Aabb::new(lo, std::array::from_fn(|k| lo[k] + size[k]));
262 let inner: Vec<[f32; 3]> = (0..4)
263 .map(|_| std::array::from_fn(|k| b.min[k] + rng.next() * size[k]))
264 .filter(|p: &[f32; 3]| (0..3).all(|k| p[k] <= b.max[k]))
265 .collect();
266 assert_inside(&r, &b, &r.transform_box(&b), &inner);
267 assert_eq!(Rigid::IDENTITY.transform_box(&b), b);
268 }
269 assert!(Rigid::IDENTITY.transform_box(&Aabb::EMPTY).is_empty());
270 let r = rigid(rot_z(30.0), [1.0, 2.0, 3.0]);
271 assert!(r.transform_box(&Aabb::EMPTY).is_empty());
272 }
273
274 #[test]
275 fn transform_box_rounds_outward() {
276 let r = rigid(rot_z(90.0), [0.1, 0.0, 0.0]);
279 let b = Aabb::new([1.0, 1.0, 1.0], [2.0, 2.0, 2.0]);
280 let t = r.transform_box(&b);
281 assert_inside(&r, &b, &t, &[]);
282 assert!((t.min[0] - (0.1 - 2.0)).abs() < 1e-5);
284 assert!((t.max[1] - 2.0).abs() < 1e-5);
285 }
286}