1use fast_mesh_query::{QueryError, Rigid};
12
13use crate::{Result, SceneError};
14
15#[derive(Debug, Clone, Copy, PartialEq)]
26pub struct Placement {
27 pub rotation: [[f64; 3]; 3],
29 pub translation: [f64; 3],
31 pub scale: [f64; 3],
33}
34
35impl Placement {
36 pub const IDENTITY: Placement = Placement {
38 rotation: [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]],
39 translation: [0.0; 3],
40 scale: [1.0; 3],
41 };
42
43 pub fn validate(&self) -> Result<()> {
50 let rigid = Rigid {
51 rotation: self.rotation,
52 translation: self.translation,
53 };
54 rigid.validate().map_err(|e| match e {
55 QueryError::InvalidInput(m) => SceneError::InvalidInput(m),
56 other => SceneError::Query(other),
57 })?;
58 if !self.scale.iter().all(|s| s.is_finite()) {
59 return Err(SceneError::InvalidInput("placement scale must be finite"));
60 }
61 if !self.scale.iter().all(|&s| s > 0.0) {
62 return Err(SceneError::InvalidInput("placement scale must be positive"));
63 }
64 Ok(())
65 }
66
67 pub fn is_scaled(&self) -> bool {
69 self.scale != [1.0; 3]
70 }
71
72 pub fn uniform_scale(&self) -> Option<f64> {
74 (self.scale[0] == self.scale[1] && self.scale[1] == self.scale[2]).then_some(self.scale[0])
75 }
76
77 pub(crate) fn max_scale(&self) -> f64 {
79 self.scale[0].max(self.scale[1]).max(self.scale[2])
80 }
81
82 pub fn apply(&self, p: [f64; 3]) -> [f64; 3] {
85 let (r, t, s) = (&self.rotation, &self.translation, &self.scale);
86 std::array::from_fn(|i| {
90 r[i][0] * (s[0] * p[0]) + r[i][1] * (s[1] * p[1]) + r[i][2] * (s[2] * p[2]) + t[i]
91 })
92 }
93
94 pub fn inverse_apply(&self, q: [f64; 3]) -> [f64; 3] {
97 let t = &self.translation;
98 let d = [q[0] - t[0], q[1] - t[1], q[2] - t[2]];
99 let r = &self.rotation;
100 let s = &self.scale;
101 std::array::from_fn(|i| (r[0][i] * d[0] + r[1][i] * d[1] + r[2][i] * d[2]) / s[i])
102 }
103
104 pub(crate) fn inverse_direction(&self, d: [f64; 3]) -> [f64; 3] {
109 let r = &self.rotation;
110 let s = &self.scale;
111 std::array::from_fn(|i| (r[0][i] * d[0] + r[1][i] * d[1] + r[2][i] * d[2]) / s[i])
112 }
113
114 pub fn apply_direction(&self, d: [f64; 3]) -> [f64; 3] {
118 let r = &self.rotation;
119 let s = &self.scale;
120 std::array::from_fn(|i| {
121 r[i][0] * (s[0] * d[0]) + r[i][1] * (s[1] * d[1]) + r[i][2] * (s[2] * d[2])
122 })
123 }
124
125 pub fn normal_to_world(&self, n: [f64; 3]) -> [f64; 3] {
130 unit(std::array::from_fn(|i| {
131 let r = &self.rotation;
132 let s = &self.scale;
133 r[i][0] * (n[0] / s[0]) + r[i][1] * (n[1] / s[1]) + r[i][2] * (n[2] / s[2])
134 }))
135 }
136
137 pub fn normal_to_local(&self, n: [f64; 3]) -> [f64; 3] {
142 let r = &self.rotation;
143 let s = &self.scale;
144 unit(std::array::from_fn(|i| {
145 s[i] * (r[0][i] * n[0] + r[1][i] * n[1] + r[2][i] * n[2])
146 }))
147 }
148}
149
150impl Default for Placement {
151 fn default() -> Self {
152 Placement::IDENTITY
153 }
154}
155
156pub(crate) fn local_to_world(p: &Placement, origin: [f64; 3], local: [f64; 3]) -> [f64; 3] {
158 p.apply([
159 origin[0] + local[0],
160 origin[1] + local[1],
161 origin[2] + local[2],
162 ])
163}
164
165pub(crate) fn world_to_local(p: &Placement, origin: [f64; 3], q: [f64; 3]) -> [f64; 3] {
167 let w = p.inverse_apply(q);
168 [w[0] - origin[0], w[1] - origin[1], w[2] - origin[2]]
169}
170
171fn dot(a: [f64; 3], b: [f64; 3]) -> f64 {
172 a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
173}
174
175fn unit(a: [f64; 3]) -> [f64; 3] {
176 let n = dot(a, a).sqrt();
177 [a[0] / n, a[1] / n, a[2] / n]
178}
179
180pub(crate) fn orthonormalize(m: [[f64; 3]; 3]) -> [[f64; 3]; 3] {
182 let r0 = unit(m[0]);
183 let d = dot(m[1], r0);
184 let r1 = unit([
185 m[1][0] - d * r0[0],
186 m[1][1] - d * r0[1],
187 m[1][2] - d * r0[2],
188 ]);
189 let r2 = [
191 r0[1] * r1[2] - r0[2] * r1[1],
192 r0[2] * r1[0] - r0[0] * r1[2],
193 r0[0] * r1[1] - r0[1] * r1[0],
194 ];
195 [r0, r1, r2]
196}
197
198#[derive(Debug, Clone, Copy, PartialEq)]
203pub(crate) struct Affine {
204 pub(crate) matrix: [[f64; 3]; 3],
205 pub(crate) translation: [f64; 3],
206}
207
208impl Affine {
209 pub(crate) fn apply(&self, p: [f64; 3]) -> [f64; 3] {
211 let (m, t) = (&self.matrix, &self.translation);
212 std::array::from_fn(|i| m[i][0] * p[0] + m[i][1] * p[1] + m[i][2] * p[2] + t[i])
213 }
214}
215
216pub(crate) fn relative_affine(
220 a: &Placement,
221 origin_a: [f64; 3],
222 b: &Placement,
223 origin_b: [f64; 3],
224) -> Affine {
225 let wb = b.apply(origin_b);
228 let w = [
229 wb[0] - a.translation[0],
230 wb[1] - a.translation[1],
231 wb[2] - a.translation[2],
232 ];
233 let ra = &a.rotation;
234 let unscaled: [f64; 3] =
235 std::array::from_fn(|i| ra[0][i] * w[0] + ra[1][i] * w[1] + ra[2][i] * w[2]);
236 let translation = std::array::from_fn(|i| unscaled[i] / a.scale[i] - origin_a[i]);
237 let (rb, sa, sb) = (&b.rotation, &a.scale, &b.scale);
239 let matrix: [[f64; 3]; 3] = std::array::from_fn(|i| {
240 std::array::from_fn(|j| {
241 (ra[0][i] * rb[0][j] + ra[1][i] * rb[1][j] + ra[2][i] * rb[2][j]) * sb[j] / sa[i]
242 })
243 });
244 Affine {
245 matrix,
246 translation,
247 }
248}
249
250pub(crate) fn is_relative_rigid(a: &Placement, b: &Placement) -> bool {
253 !a.is_scaled() && !b.is_scaled()
254}
255
256pub(crate) fn relative_rigid(
263 a: &Placement,
264 origin_a: [f64; 3],
265 b: &Placement,
266 origin_b: [f64; 3],
267) -> Rigid {
268 let (ra, rb) = (&a.rotation, &b.rotation);
269 let rotation: [[f64; 3]; 3] = std::array::from_fn(|i| {
270 std::array::from_fn(|j| ra[0][i] * rb[0][j] + ra[1][i] * rb[1][j] + ra[2][i] * rb[2][j])
271 });
272 let wb = b.apply(origin_b);
274 let w = [
275 wb[0] - a.translation[0],
276 wb[1] - a.translation[1],
277 wb[2] - a.translation[2],
278 ];
279 let translation =
280 std::array::from_fn(|i| ra[0][i] * w[0] + ra[1][i] * w[1] + ra[2][i] * w[2] - origin_a[i]);
281 Rigid {
282 rotation: orthonormalize(rotation),
283 translation,
284 }
285}
286
287#[cfg(test)]
288mod tests {
289 use super::*;
290
291 const RZ90: [[f64; 3]; 3] = [[0.0, -1.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 1.0]];
292 const RX90: [[f64; 3]; 3] = [[1.0, 0.0, 0.0], [0.0, 0.0, -1.0], [0.0, 1.0, 0.0]];
293
294 fn p(rotation: [[f64; 3]; 3], translation: [f64; 3]) -> Placement {
295 Placement {
296 rotation,
297 translation,
298 scale: [1.0; 3],
299 }
300 }
301
302 fn with_scale(translation: [f64; 3], scale: [f64; 3]) -> Placement {
303 Placement {
304 translation,
305 scale,
306 ..Placement::IDENTITY
307 }
308 }
309
310 #[test]
311 fn placement_validation() {
312 assert!(Placement::IDENTITY.validate().is_ok());
313 assert!(p(RZ90, [1.0, 2.0, 3.0]).validate().is_ok());
314 assert!(with_scale([0.0; 3], [2.0, 0.5, 3.0]).validate().is_ok());
315 let mut scaled = Placement::IDENTITY;
316 scaled.rotation[0][0] = 2.0;
317 assert!(matches!(
318 scaled.validate(),
319 Err(SceneError::InvalidInput(_))
320 ));
321 let mut mirror = Placement::IDENTITY;
322 mirror.rotation[2][2] = -1.0;
323 assert!(matches!(
324 mirror.validate(),
325 Err(SceneError::InvalidInput(_))
326 ));
327 let mut nan = Placement::IDENTITY;
328 nan.translation[1] = f64::NAN;
329 assert!(matches!(nan.validate(), Err(SceneError::InvalidInput(_))));
330 let mut inf = Placement::IDENTITY;
331 inf.rotation[0][1] = f64::INFINITY;
332 assert!(matches!(inf.validate(), Err(SceneError::InvalidInput(_))));
333 for scale in [
335 [0.0, 1.0, 1.0],
336 [1.0, 0.0, 1.0],
337 [-1.0, 1.0, 1.0],
338 [1.0, -2.0, 1.0],
339 [f64::NAN, 1.0, 1.0],
340 [1.0, f64::INFINITY, 1.0],
341 ] {
342 assert!(
343 matches!(
344 with_scale([0.0; 3], scale).validate(),
345 Err(SceneError::InvalidInput(_))
346 ),
347 "scale {scale:?}"
348 );
349 }
350 assert!(Placement::IDENTITY.uniform_scale().is_some());
351 assert_eq!(with_scale([0.0; 3], [2.0; 3]).uniform_scale(), Some(2.0));
352 assert_eq!(with_scale([0.0; 3], [2.0, 1.0, 1.0]).uniform_scale(), None);
353 assert!(!Placement::IDENTITY.is_scaled());
354 assert!(with_scale([0.0; 3], [1.0, 1.0, 2.0]).is_scaled());
355 }
356
357 #[test]
358 fn scaled_apply_round_trips() {
359 let pl = Placement {
360 rotation: RZ90,
361 translation: [10.0, -4.0, 7.0],
362 scale: [2.0, 0.5, 3.0],
363 };
364 assert!(pl.validate().is_ok());
365 assert_eq!(pl.apply([1.0, -1.0, 3.0]), [10.5, -2.0, 16.0]);
368 for p in [[0.0; 3], [1.0, 2.0, -3.0], [-100.5, 0.25, 999.0]] {
369 let back = pl.inverse_apply(pl.apply(p));
370 for (b, x) in back.iter().zip(p) {
371 assert!((b - x).abs() < 1e-12, "{back:?} vs {p:?}");
372 }
373 }
374 let o = [1.0, 2.0, 3.0];
378 let d = unit([1.0, -2.0, 0.5]);
379 let base = pl.apply(o);
380 let ol = pl.inverse_apply(base);
381 let dl = pl.inverse_direction(d);
382 for t in [0.0, 0.5, 10.0] {
383 let want: [f64; 3] = std::array::from_fn(|k| base[k] + t * d[k]);
384 let got: [f64; 3] = pl.apply(std::array::from_fn(|k| ol[k] + t * dl[k]));
385 for (g, w) in got.iter().zip(want) {
386 assert!((g - w).abs() < 1e-9, "{got:?} vs {want:?}");
387 }
388 }
389 let rigid = p(RZ90, [10.0, -4.0, 7.0]);
391 assert_eq!(
392 rigid.apply([1.0, 2.0, 3.0]),
393 [-2.0 + 10.0, 1.0 - 4.0, 3.0 + 7.0]
394 );
395 }
396
397 #[test]
398 fn normals_follow_inverse_transpose() {
399 let pl = with_scale([5.0, 0.0, 0.0], [2.0, 1.0, 1.0]);
402 let n = unit([1.0, 1.0, 0.0]);
403 let w = pl.normal_to_world(n);
404 assert!((dot(w, w) - 1.0).abs() < 1e-15);
405 let want = unit([0.5, 1.0, 0.0]);
407 for (g, x) in w.iter().zip(want) {
408 assert!((g - x).abs() < 1e-15, "{w:?} vs {want:?}");
409 }
410 let back = pl.normal_to_local(w);
412 for (b, x) in back.iter().zip(n) {
413 assert!((b - x).abs() < 1e-15, "{back:?} vs {n:?}");
414 }
415 let origin_w = pl.apply([0.0; 3]);
418 let local_origin = pl.inverse_apply(origin_w);
419 let nl = pl.normal_to_local([0.0, 0.0, 1.0]);
420 for q in [[1.0, 0.0, 5.0], [0.0, -3.0, -2.0], [7.0, 7.0, 0.5]] {
421 let l = pl.inverse_apply(q);
422 let side_w = dot(q, [0.0, 0.0, 1.0]) - dot(origin_w, [0.0, 0.0, 1.0]);
423 let side_l = dot(
424 [
425 l[0] - local_origin[0],
426 l[1] - local_origin[1],
427 l[2] - local_origin[2],
428 ],
429 nl,
430 );
431 assert!(side_w * side_l > 0.0 || (side_w == 0.0 && side_l == 0.0));
432 }
433 }
434
435 #[test]
436 fn relative_affine_matches_apply() {
437 let a = Placement {
440 rotation: RZ90,
441 translation: [10.0, -4.0, 7.0],
442 scale: [2.0, 0.5, 1.0],
443 };
444 let b = Placement {
445 rotation: RX90,
446 translation: [-3.0, 8.0, 2.0],
447 scale: [0.5, 3.0, 2.0],
448 };
449 let (oa, ob) = ([1.0, 2.0, 3.0], [-4.0, 5.0, -6.0]);
450 let rel = relative_affine(&a, oa, &b, ob);
451 for local in [[0.0; 3], [1.0, -1.0, 0.5], [-100.0, 200.0, -300.0]] {
452 let in_a = rel.apply(local);
456 let world_direct = b.apply([ob[0] + local[0], ob[1] + local[1], ob[2] + local[2]]);
457 let world_via_a = a.apply([oa[0] + in_a[0], oa[1] + in_a[1], oa[2] + in_a[2]]);
458 for (g, w) in world_via_a.iter().zip(world_direct) {
459 assert!((g - w).abs() < 1e-9, "{world_via_a:?} vs {world_direct:?}");
460 }
461 }
462 let c = p(RZ90, [1.0, 0.0, 0.0]);
464 let d = p(RX90, [0.0, 1.0, 0.0]);
465 let aff = relative_affine(&c, [0.0; 3], &d, [0.0; 3]);
466 let rig = relative_rigid(&c, [0.0; 3], &d, [0.0; 3]);
467 for p in [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [1.0, 2.0, 3.0]] {
468 let g = aff.apply(p.map(f64::from));
469 let w = rig.apply(p.map(|x| x as f32));
470 for (g, w) in g.iter().zip(w) {
471 assert!((g - w).abs() < 1e-6, "{g:?} vs {w:?}");
472 }
473 }
474 assert!(is_relative_rigid(&c, &d));
475 assert!(!is_relative_rigid(&a, &b));
476 }
477
478 #[test]
479 fn relative_rigid_is_exact_for_integer_placements() {
480 let a = p(RZ90, [10.0, -4.0, 7.0]);
481 let b = p(RX90, [-3.0, 8.0, 2.0]);
482 let oa = [1_000_000.0, 2_000_000.0, -1_500_000.0];
483 let ob = [-1_000_000.0, 1_000_001.0, 2_500_000.0];
484 let rel = relative_rigid(&a, oa, &b, ob);
485 for local in [[1.0f32, 2.0, 3.0], [-5.0, 0.0, 4.0], [0.5, 0.25, -8.0]] {
486 let got = rel.apply(local);
487 let l = local.map(f64::from);
488 let world = b.apply([ob[0] + l[0], ob[1] + l[1], ob[2] + l[2]]);
489 let d = [world[0] - 10.0, world[1] + 4.0, world[2] - 7.0];
491 let want = [d[1] - oa[0], -d[0] - oa[1], d[2] - oa[2]];
492 assert_eq!(got, want);
493 }
494 }
495
496 #[test]
497 fn relative_rotation_of_near_orthonormal_inputs_validates() {
498 let e = 0.9e-6;
501 let mut a = p(RZ90, [0.0; 3]);
502 a.rotation[0][0] = e;
503 let mut b = p(RZ90, [0.0; 3]);
504 b.rotation[0][0] = e;
505 assert!(a.validate().is_ok() && b.validate().is_ok());
506 let (ra, rb) = (&a.rotation, &b.rotation);
507 let raw: [[f64; 3]; 3] = std::array::from_fn(|i| {
508 std::array::from_fn(|j| ra[0][i] * rb[0][j] + ra[1][i] * rb[1][j] + ra[2][i] * rb[2][j])
509 });
510 let raw_rigid = Rigid {
511 rotation: raw,
512 translation: [0.0; 3],
513 };
514 assert!(raw_rigid.validate().is_err(), "raw product must be invalid");
515 let rel = relative_rigid(&a, [0.0; 3], &b, [0.0; 3]);
516 assert_eq!(rel.validate(), Ok(()));
517 for (row, raw_row) in rel.rotation.iter().zip(&raw) {
518 for (x, y) in row.iter().zip(raw_row) {
519 assert!((x - y).abs() < 2e-6, "{x} vs {y}");
520 }
521 }
522 }
523
524 #[test]
525 fn world_local_round_trip() {
526 let pl = p(RX90, [3.0, -2.0, 5.0]);
527 let origin = [100.0, -50.0, 25.0];
528 let local = [0.25, -1.5, 3.0];
529 let world = local_to_world(&pl, origin, local);
530 let back = world_to_local(&pl, origin, world);
531 for (b, l) in back.iter().zip(local) {
532 assert!((b - l).abs() < 1e-12);
533 }
534 }
535}