1use fast_mesh_bvh::{Aabb, Bvh};
16use fast_mesh_query::{ClosestHit, ClosestOptions, Cull, Ray, RayHit, RayOptions, Rigid};
17use fast_mesh_store::{Mesh, StoreError, TriangleId};
18
19use crate::baked::baked_mesh;
20use crate::placement::{is_relative_rigid, local_to_world, relative_rigid, world_to_local};
21use crate::scene::Entry;
22use crate::{MeshId, Placement, Result, Scene, SceneError};
23
24pub(crate) const PLACEMENT_SLACK: f64 = 2e-5;
28
29const F32_SLACK: f64 = 16.0 * f32::EPSILON as f64;
31
32pub(crate) fn round_up(x: f64) -> f32 {
34 let r = x as f32;
35 if f64::from(r) < x { r.next_up() } else { r }
36}
37
38pub(crate) fn round_down(x: f64) -> f32 {
40 let r = x as f32;
41 if f64::from(r) > x { r.next_down() } else { r }
42}
43
44fn sub(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
45 [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
46}
47
48fn norm(v: [f64; 3]) -> f64 {
49 (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt()
50}
51
52fn max_abs(v: [f32; 3]) -> f64 {
54 v.iter().fold(0.0, |m, c| m.max(f64::from(c.abs())))
55}
56
57fn unscale_direction(p: &Placement, d: [f64; 3]) -> [f64; 3] {
60 p.inverse_direction(d)
61}
62
63fn oom() -> SceneError {
64 SceneError::Store(StoreError::OutOfMemory)
65}
66
67pub(crate) fn try_vec<T>(n: usize) -> Result<Vec<T>> {
69 let mut v = Vec::new();
70 v.try_reserve(n).map_err(|_| oom())?;
71 Ok(v)
72}
73
74fn check_finite(p: [f64; 3], what: &'static str) -> Result<()> {
75 if p.iter().all(|c| c.is_finite()) {
76 Ok(())
77 } else {
78 Err(SceneError::InvalidInput(what))
79 }
80}
81
82pub(crate) fn check_bound(x: f64, what: &'static str) -> Result<()> {
84 if x.is_nan() || x < 0.0 {
85 Err(SceneError::InvalidInput(what))
86 } else {
87 Ok(())
88 }
89}
90
91fn unit_dir(d: [f64; 3]) -> Result<[f64; 3]> {
93 let bad = SceneError::InvalidInput("ray direction must be finite and non-zero");
94 if !d.iter().all(|c| c.is_finite()) || d == [0.0; 3] {
95 return Err(bad);
96 }
97 let n = norm(d);
98 if n.is_finite() && n > 1e-150 {
100 return Ok(d.map(|c| c / n));
101 }
102 let m = d.iter().fold(0.0f64, |m, c| m.max(c.abs()));
103 let s = d.map(|c| c / m);
104 let n = norm(s);
105 Ok(s.map(|c| c / n))
106}
107
108fn local_box(entry: &Entry) -> Option<Aabb> {
110 entry.bvh.root_bounds().filter(|b| !b.is_empty())
111}
112
113pub(crate) fn local_mag(entry: &Entry) -> f64 {
115 local_box(entry).map_or(0.0, |b| max_abs(b.min).max(max_abs(b.max)))
116}
117
118struct LocalPoint {
120 p: [f32; 3],
121 err: f64,
123}
124
125fn local_point(entry: &Entry, q: [f64; 3]) -> Result<LocalPoint> {
126 let l = world_to_local(&entry.placement, entry.mesh.origin(), q);
127 let p = l.map(|c| c as f32);
128 if !p.iter().all(|c| c.is_finite()) {
129 return Err(SceneError::InvalidInput(
130 "the query point is out of f32 range in a mesh's local space",
131 ));
132 }
133 let err = norm(std::array::from_fn(|k| f64::from(p[k]) - l[k]));
134 Ok(LocalPoint { p, err })
135}
136
137fn query_pad(
151 placement: &Placement,
152 mesh_mag: f64,
153 wbox: &Aabb64,
154 q: [f64; 3],
155 local: [f32; 3],
156 err: f64,
157 err_dir: f64,
158) -> f64 {
159 let (c, h) = wbox.centre_half();
160 let far = norm(sub(q, c)) + norm(h);
161 let from_t = norm(sub(q, placement.translation));
162 let mag = max_abs(local) + mesh_mag + far;
163 let s = placement.max_scale().max(1.0);
164 s * (err + 2.0 * (far + err) * err_dir + PLACEMENT_SLACK * (from_t + far) + F32_SLACK * mag)
165}
166
167pub(crate) fn pair_pad(a: &Entry, wa: &Aabb64, b: &Entry, wb: &Aabb64) -> f64 {
173 let (ca, ha) = wa.centre_half();
174 let (cb, hb) = wb.centre_half();
175 let sa = a.placement.max_scale().max(1.0);
176 let sb = b.placement.max_scale().max(1.0);
177 let spread = norm(sub(cb, a.placement.translation))
178 + norm(sub(ca, b.placement.translation))
179 + norm(ha)
180 + norm(hb)
181 + sa * local_mag(a)
182 + sb * local_mag(b);
183 sa.max(sb) * PLACEMENT_SLACK * (wa.distance_to_box(wb) + spread)
184}
185
186fn to_world(entry: &Entry, p: [f32; 3]) -> [f64; 3] {
188 local_to_world(&entry.placement, entry.mesh.origin(), p.map(f64::from))
189}
190
191fn closest_hit(entry: &Entry, mesh: MeshId, hit: ClosestHit) -> SceneClosestHit {
192 SceneClosestHit {
193 mesh,
194 hit,
195 point: to_world(entry, hit.point),
196 distance: f64::from(hit.distance),
197 }
198}
199
200#[derive(Debug, Clone, Copy, PartialEq)]
202pub struct SceneRayOptions {
203 pub min_t: f64,
205 pub max_t: f64,
208 pub cull: Cull,
210}
211
212impl Default for SceneRayOptions {
213 fn default() -> Self {
214 SceneRayOptions {
215 min_t: 0.0,
216 max_t: f64::INFINITY,
217 cull: Cull::None,
218 }
219 }
220}
221
222#[derive(Debug, Clone, Copy, PartialEq)]
225pub struct SceneClosestOptions {
226 pub max_distance: f64,
229}
230
231impl Default for SceneClosestOptions {
232 fn default() -> Self {
233 SceneClosestOptions {
234 max_distance: f64::INFINITY,
235 }
236 }
237}
238
239pub(crate) type RayCandidate = (f64, MeshId, Ray);
242
243pub(crate) type ClosestCandidate = (f64, MeshId, [f32; 3]);
246
247type BakedPair = (Option<(Mesh, Bvh)>, (Mesh, Bvh), f64);
251
252#[derive(Debug, Clone, Copy, PartialEq)]
254pub struct SceneRayHit {
255 pub mesh: MeshId,
257 pub hit: RayHit,
259 pub point: [f64; 3],
262 pub t: f64,
267}
268
269#[derive(Debug, Clone, Copy, PartialEq)]
271pub struct SceneClosestHit {
272 pub mesh: MeshId,
274 pub hit: ClosestHit,
276 pub point: [f64; 3],
279 pub distance: f64,
284}
285
286#[derive(Debug, Clone, Copy, PartialEq)]
288pub struct SceneMeshDistance {
289 pub distance: f64,
292 pub point_a: [f64; 3],
294 pub point_b: [f64; 3],
296 pub triangle_a: TriangleId,
298 pub triangle_b: TriangleId,
300}
301
302impl Scene {
303 pub fn raycast(
328 &mut self,
329 origin: [f64; 3],
330 dir: [f64; 3],
331 opts: &SceneRayOptions,
332 ) -> Result<Option<SceneRayHit>> {
333 self.drop_field_unit();
334 check_finite(origin, "ray origin must be finite")?;
335 let d = unit_dir(dir)?;
336 if opts.min_t.is_nan() || opts.max_t.is_nan() || opts.min_t > opts.max_t {
337 return Err(SceneError::InvalidInput(
338 "min_t and max_t must not be NaN, and min_t must not exceed max_t",
339 ));
340 }
341 self.prepare(None)?;
342 let mut order = std::mem::take(&mut self.order_ray);
345 let result = self.raycast_inner(&mut order, origin, d, opts);
346 order.clear();
347 self.order_ray = order;
348 result
349 }
350
351 fn raycast_inner(
353 &mut self,
354 order: &mut Vec<RayCandidate>,
355 origin: [f64; 3],
356 d: [f64; 3],
357 opts: &SceneRayOptions,
358 ) -> Result<Option<SceneRayHit>> {
359 for (id, e) in self.entries.iter_mut() {
362 let (wbox, mag) = cached_world_box(e);
365 if wbox.is_empty() {
366 continue;
367 }
368 let lo = local_point(e, origin)?;
369 let dl = unscale_direction(&e.placement, d);
370 let dl32 = dl.map(|c| c as f32);
371 let err_dir = norm(std::array::from_fn(|k| f64::from(dl32[k]) - dl[k]));
372 let pad = query_pad(&e.placement, mag, &wbox, origin, lo.p, lo.err, err_dir);
373 if let Some(enter) = wbox.ray_enter(origin, d, pad, opts.min_t, opts.max_t) {
374 order.try_reserve(1).map_err(|_| oom())?;
375 let ray = Ray {
376 origin: lo.p,
377 dir: dl32,
378 };
379 order.push((enter, id, ray));
380 }
381 }
382 if order.len() > 1 {
383 order.sort_by(|x, y| x.0.total_cmp(&y.0).then(x.1.cmp(&y.1)));
384 }
385 let (min32, max32) = (round_down(opts.min_t), round_up(opts.max_t));
386 let mut best: Option<SceneRayHit> = None;
387 for &(enter, id, ray) in order.iter() {
388 if best.is_some_and(|b| enter > b.t) {
389 break;
390 }
391 let e = self.entry(id)?;
392 let mut local = RayOptions {
394 min_t: min32,
395 max_t: best.map_or(max32, |b| b.hit.t),
396 cull: opts.cull,
397 };
398 let hit = loop {
399 let Some(h) = fast_mesh_query::raycast(&e.mesh, &e.bvh, &ray, &local)? else {
400 break None;
401 };
402 if f64::from(h.t) >= opts.min_t {
403 break Some(h);
404 }
405 local.min_t = h.t.next_up();
407 if local.min_t > local.max_t {
408 break None;
409 }
410 };
411 let Some(h) = hit else { continue };
412 let t = f64::from(h.t);
413 if t > opts.max_t {
414 continue;
415 }
416 if best.is_none_or(|b| t < b.t || (t == b.t && id < b.mesh)) {
417 best = Some(SceneRayHit {
418 mesh: id,
419 hit: h,
420 point: to_world(e, h.point),
421 t,
422 });
423 }
424 }
425 Ok(best)
426 }
427
428 pub fn closest_point(
452 &mut self,
453 p: [f64; 3],
454 opts: &SceneClosestOptions,
455 ) -> Result<Option<SceneClosestHit>> {
456 self.drop_field_unit();
457 check_finite(p, "query point must be finite")?;
458 check_bound(opts.max_distance, MAX_DISTANCE)?;
459 self.prepare(None)?;
460 let mut order = std::mem::take(&mut self.order_closest);
463 let result = self.closest_inner(&mut order, p, opts);
464 order.clear();
465 self.order_closest = order;
466 result
467 }
468
469 fn closest_inner(
472 &mut self,
473 order: &mut Vec<ClosestCandidate>,
474 p: [f64; 3],
475 opts: &SceneClosestOptions,
476 ) -> Result<Option<SceneClosestHit>> {
477 for (id, e) in self.entries.iter_mut() {
478 let (wbox, mag) = cached_world_box(e);
481 if wbox.is_empty() {
482 continue;
483 }
484 let lp = local_point(e, p)?;
485 let pad = query_pad(&e.placement, mag, &wbox, p, lp.p, lp.err, 0.0);
486 let lower = (wbox.distance_to(p) - pad).max(0.0);
487 if lower <= opts.max_distance {
488 order.try_reserve(1).map_err(|_| oom())?;
489 order.push((lower, id, lp.p));
490 }
491 }
492 if order.len() > 1 {
493 order.sort_by(|x, y| x.0.total_cmp(&y.0).then(x.1.cmp(&y.1)));
494 }
495 let mut best: Option<SceneClosestHit> = None;
496 for &(lower, id, q) in order.iter() {
497 let bound = best.map_or(opts.max_distance, |b| b.distance);
498 if lower > bound {
499 break;
500 }
501 let Some(found) = self.closest_on(id, p, q, bound)? else {
502 continue;
503 };
504 if best.is_none_or(|b| {
505 found.distance < b.distance || (found.distance == b.distance && id < b.mesh)
506 }) {
507 best = Some(found);
508 }
509 }
510 Ok(best)
511 }
512
513 fn closest_on(
520 &self,
521 id: MeshId,
522 p: [f64; 3],
523 q: [f32; 3],
524 bound: f64,
525 ) -> Result<Option<SceneClosestHit>> {
526 let e = self.entry(id)?;
527 if e.placement.uniform_scale() == Some(1.0) {
528 let local = ClosestOptions {
529 max_distance: round_up(bound),
530 };
531 let hit = fast_mesh_query::closest_point(&e.mesh, &e.bvh, q, &local)?;
532 return Ok(hit
533 .filter(|h| f64::from(h.distance) <= bound)
534 .map(|h| closest_hit(e, id, h)));
535 }
536 if let Some(s) = e.placement.uniform_scale() {
537 let local = ClosestOptions {
540 max_distance: round_up(bound / s),
541 };
542 let hit = fast_mesh_query::closest_point(&e.mesh, &e.bvh, q, &local)?;
543 return Ok(hit.filter(|h| s * f64::from(h.distance) <= bound).map(|h| {
544 SceneClosestHit {
545 mesh: id,
546 hit: h,
547 point: to_world(e, h.point),
548 distance: s * f64::from(h.distance),
549 }
550 }));
551 }
552 let o = e.mesh.origin();
558 let pl = e.placement;
559 let (baked, bvh) = baked_mesh(
560 &e.mesh,
561 |l| {
562 pl.apply([
563 o[0] + f64::from(l[0]),
564 o[1] + f64::from(l[1]),
565 o[2] + f64::from(l[2]),
566 ])
567 },
568 e.bvh.options(),
569 )?;
570 let qw = p.map(|c| c as f32);
571 if !qw.iter().all(|c| c.is_finite()) {
572 return Err(SceneError::InvalidInput(
573 "the query point is out of f32 range in the world",
574 ));
575 }
576 let local = ClosestOptions {
577 max_distance: round_up(bound),
578 };
579 let hit = fast_mesh_query::closest_point(&baked, &bvh, qw, &local)?;
580 Ok(hit.filter(|h| f64::from(h.distance) <= bound).map(|h| {
581 let pw = h.point.map(f64::from);
582 let lp = world_to_local(&pl, o, pw);
583 let lq = world_to_local(&pl, o, p);
584 SceneClosestHit {
585 mesh: id,
586 hit: ClosestHit {
587 triangle: h.triangle,
588 point: lp.map(|c| c as f32),
589 distance: norm(sub(lp, lq)) as f32,
590 barycentric: h.barycentric,
591 feature: h.feature,
592 },
593 point: pw,
594 distance: f64::from(h.distance),
595 }
596 }))
597 }
598
599 pub fn closest_point_on(
607 &mut self,
608 id: MeshId,
609 p: [f64; 3],
610 opts: &SceneClosestOptions,
611 ) -> Result<Option<SceneClosestHit>> {
612 self.drop_field_unit();
613 self.entry(id)?;
614 check_finite(p, "query point must be finite")?;
615 check_bound(opts.max_distance, MAX_DISTANCE)?;
616 self.prepare(Some(&[id]))?;
617 let e = self.entry(id)?;
618 let lp = local_point(e, p)?;
619 self.closest_on(id, p, lp.p, opts.max_distance)
620 }
621
622 fn pair(&mut self, a: MeshId, b: MeshId) -> Result<(&Entry, &Entry, Rigid)> {
627 self.prepare(Some(&[a, b]))?;
628 let (ea, eb) = (self.entry(a)?, self.entry(b)?);
629 let xf = relative_rigid(
630 &ea.placement,
631 ea.mesh.origin(),
632 &eb.placement,
633 eb.mesh.origin(),
634 );
635 Ok((ea, eb, xf))
636 }
637
638 pub(crate) fn is_rigid_pair(&self, a: MeshId, b: MeshId) -> Result<bool> {
640 let (ea, eb) = (self.entry(a)?, self.entry(b)?);
641 Ok(is_relative_rigid(&ea.placement, &eb.placement))
642 }
643
644 pub(crate) fn bake_pair_inner(&self, a: MeshId, b: MeshId) -> Result<BakedPair> {
654 let (ea, eb) = (self.entry(a)?, self.entry(b)?);
655 let (pa, pb) = (ea.placement, eb.placement);
656 let (oa, ob) = (ea.mesh.origin(), eb.mesh.origin());
657 if let Some(s) = pa.uniform_scale() {
658 let baked_b = {
659 let eb = self.entry(b)?;
660 baked_mesh(
661 &eb.mesh,
662 |l| {
663 pa.inverse_apply(pb.apply([
664 ob[0] + f64::from(l[0]),
665 ob[1] + f64::from(l[1]),
666 ob[2] + f64::from(l[2]),
667 ]))
668 },
669 eb.bvh.options(),
670 )?
671 };
672 return Ok((None, baked_b, s));
673 }
674 let baked_a = {
675 let ea = self.entry(a)?;
676 baked_mesh(
677 &ea.mesh,
678 |l| {
679 pa.apply([
680 oa[0] + f64::from(l[0]),
681 oa[1] + f64::from(l[1]),
682 oa[2] + f64::from(l[2]),
683 ])
684 },
685 ea.bvh.options(),
686 )?
687 };
688 let baked_b = {
689 let eb = self.entry(b)?;
690 baked_mesh(
691 &eb.mesh,
692 |l| {
693 pb.apply([
694 ob[0] + f64::from(l[0]),
695 ob[1] + f64::from(l[1]),
696 ob[2] + f64::from(l[2]),
697 ])
698 },
699 eb.bvh.options(),
700 )?
701 };
702 Ok((Some(baked_a), baked_b, 1.0))
703 }
704
705 fn bake_pair(&mut self, a: MeshId, b: MeshId) -> Result<BakedPair> {
708 self.prepare(Some(&[a, b]))?;
709 self.bake_pair_inner(a, b)
710 }
711
712 fn frame_to_world(&self, a: MeshId, real_a: bool, p: [f32; 3]) -> Result<[f64; 3]> {
716 if real_a {
717 let e = self.entry(a)?;
718 Ok(to_world(e, p))
719 } else {
720 Ok(p.map(f64::from))
721 }
722 }
723
724 pub fn bvh(&self, id: MeshId) -> Result<&Bvh> {
733 let e = self.entry(id)?;
734 if !e.is_current() {
735 return Err(SceneError::StaleBvh(id));
736 }
737 Ok(&e.bvh)
738 }
739
740 pub fn relative_transform(&self, a: MeshId, b: MeshId) -> Result<Rigid> {
752 let (ea, eb) = (self.entry(a)?, self.entry(b)?);
753 if !is_relative_rigid(&ea.placement, &eb.placement) {
754 return Err(SceneError::InvalidInput(
755 "the relative transform of scaled placements is not rigid",
756 ));
757 }
758 Ok(relative_rigid(
759 &ea.placement,
760 ea.mesh.origin(),
761 &eb.placement,
762 eb.mesh.origin(),
763 ))
764 }
765
766 pub fn world_to_local(&self, id: MeshId, q: [f64; 3]) -> Result<[f64; 3]> {
772 let e = self.entry(id)?;
773 Ok(world_to_local(&e.placement, e.mesh.origin(), q))
774 }
775
776 pub fn local_to_world(&self, id: MeshId, p: [f64; 3]) -> Result<[f64; 3]> {
782 let e = self.entry(id)?;
783 Ok(local_to_world(&e.placement, e.mesh.origin(), p))
784 }
785
786 fn check_ids(&self, a: MeshId, b: MeshId) -> Result<()> {
787 self.entry(a)?;
788 self.entry(b)?;
789 Ok(())
790 }
791
792 pub fn intersects(&mut self, a: MeshId, b: MeshId) -> Result<bool> {
808 self.drop_field_unit();
809 self.check_ids(a, b)?;
810 if self.is_rigid_pair(a, b)? {
811 let (ea, eb, xf) = self.pair(a, b)?;
812 return Ok(fast_mesh_query::intersects(
813 &ea.mesh, &ea.bvh, &eb.mesh, &eb.bvh, &xf,
814 )?);
815 }
816 if a == b {
817 let (ea, _, _) = self.pair(a, b)?;
819 return Ok(fast_mesh_query::intersects(
820 &ea.mesh,
821 &ea.bvh,
822 &ea.mesh,
823 &ea.bvh,
824 &Rigid::IDENTITY,
825 )?);
826 }
827 let (ta, tb, _) = self.bake_pair(a, b)?;
828 let ea = self.entry(a)?;
829 let (ma, ba) = match &ta {
830 None => (&ea.mesh, &ea.bvh),
831 Some((m, b)) => (m, b),
832 };
833 Ok(fast_mesh_query::intersects(
834 ma,
835 ba,
836 &tb.0,
837 &tb.1,
838 &Rigid::IDENTITY,
839 )?)
840 }
841
842 pub fn mesh_distance(
859 &mut self,
860 a: MeshId,
861 b: MeshId,
862 max_distance: f64,
863 ) -> Result<Option<SceneMeshDistance>> {
864 self.drop_field_unit();
865 self.check_ids(a, b)?;
866 check_bound(max_distance, MAX_DISTANCE)?;
867 if self.is_rigid_pair(a, b)? {
868 let (ea, eb, xf) = self.pair(a, b)?;
869 let found = fast_mesh_query::mesh_distance(
870 &ea.mesh,
871 &ea.bvh,
872 &eb.mesh,
873 &eb.bvh,
874 &xf,
875 round_up(max_distance),
876 )?;
877 return Ok(found
878 .filter(|d| f64::from(d.distance) <= max_distance)
879 .map(|d| SceneMeshDistance {
880 distance: f64::from(d.distance),
881 point_a: to_world(ea, d.point_a),
883 point_b: to_world(ea, d.point_b),
884 triangle_a: d.triangle_a,
885 triangle_b: d.triangle_b,
886 }));
887 }
888 if a == b {
889 let (ea, _, _) = self.pair(a, b)?;
892 let found = fast_mesh_query::mesh_distance(
893 &ea.mesh,
894 &ea.bvh,
895 &ea.mesh,
896 &ea.bvh,
897 &Rigid::IDENTITY,
898 round_up(max_distance),
899 )?;
900 return Ok(found
901 .filter(|d| f64::from(d.distance) <= max_distance)
902 .map(|d| SceneMeshDistance {
903 distance: 0.0,
904 point_a: to_world(ea, d.point_a),
905 point_b: to_world(ea, d.point_b),
906 triangle_a: d.triangle_a,
907 triangle_b: d.triangle_b,
908 }));
909 }
910 let (ta, tb, s) = self.bake_pair(a, b)?;
911 let real_a = ta.is_none();
912 let ea = self.entry(a)?;
913 let (ma, ba) = match &ta {
914 None => (&ea.mesh, &ea.bvh),
915 Some((m, b)) => (m, b),
916 };
917 let found = fast_mesh_query::mesh_distance(
918 ma,
919 ba,
920 &tb.0,
921 &tb.1,
922 &Rigid::IDENTITY,
923 round_up(max_distance / s),
924 )?;
925 Ok(match found {
926 Some(d) if s * f64::from(d.distance) <= max_distance => Some(SceneMeshDistance {
927 distance: s * f64::from(d.distance),
928 point_a: self.frame_to_world(a, real_a, d.point_a)?,
930 point_b: self.frame_to_world(a, real_a, d.point_b)?,
931 triangle_a: d.triangle_a,
932 triangle_b: d.triangle_b,
933 }),
934 _ => None,
935 })
936 }
937
938 pub fn triangles_near(
958 &mut self,
959 a: MeshId,
960 b: MeshId,
961 margin: f64,
962 out_a: &mut Vec<TriangleId>,
963 out_b: &mut Vec<TriangleId>,
964 ) -> Result<()> {
965 self.drop_field_unit();
966 self.check_ids(a, b)?;
967 check_bound(margin, MARGIN)?;
968 if self.is_rigid_pair(a, b)? {
969 let (ea, eb, xf) = self.pair(a, b)?;
970 return Ok(fast_mesh_query::triangles_near(
971 &ea.mesh,
972 &ea.bvh,
973 &eb.mesh,
974 &eb.bvh,
975 &xf,
976 round_up(margin),
977 out_a,
978 out_b,
979 )?);
980 }
981 if a == b {
982 self.prepare(Some(&[a]))?;
984 let e = self.entry(a)?;
985 let mut all = Vec::new();
986 all.try_reserve(e.mesh.triangle_count())
987 .map_err(|_| oom())?;
988 for t in 0..e.mesh.triangle_slots() as TriangleId {
989 if e.mesh.triangle(t).is_some() {
990 all.push(t);
991 }
992 }
993 out_a.try_reserve(all.len()).map_err(|_| oom())?;
994 out_b.try_reserve(all.len()).map_err(|_| oom())?;
995 out_a.extend_from_slice(&all);
996 out_b.extend_from_slice(&all);
997 return Ok(());
998 }
999 let (ta, tb, s) = self.bake_pair(a, b)?;
1000 let ea = self.entry(a)?;
1001 let (ma, ba) = match &ta {
1002 None => (&ea.mesh, &ea.bvh),
1003 Some((m, b)) => (m, b),
1004 };
1005 Ok(fast_mesh_query::triangles_near(
1006 ma,
1007 ba,
1008 &tb.0,
1009 &tb.1,
1010 &Rigid::IDENTITY,
1011 round_up(margin / s),
1012 out_a,
1013 out_b,
1014 )?)
1015 }
1016}
1017
1018const MAX_DISTANCE: &str = "max_distance must be zero or greater, and not NaN";
1019pub(crate) const MARGIN: &str = "margin must be zero or greater, and not NaN";
1020
1021#[derive(Debug, Clone, Copy, PartialEq)]
1023pub(crate) struct Aabb64 {
1024 pub(crate) min: [f64; 3],
1025 pub(crate) max: [f64; 3],
1026}
1027
1028impl Aabb64 {
1029 pub(crate) const EMPTY: Aabb64 = Aabb64 {
1031 min: [f64::INFINITY; 3],
1032 max: [f64::NEG_INFINITY; 3],
1033 };
1034
1035 pub(crate) fn is_empty(&self) -> bool {
1037 (0..3).any(|k| self.min[k] > self.max[k])
1038 }
1039
1040 fn centre_half(&self) -> ([f64; 3], [f64; 3]) {
1042 let c = std::array::from_fn(|k| 0.5 * self.min[k] + 0.5 * self.max[k]);
1043 let h = std::array::from_fn(|k| 0.5 * self.max[k] - 0.5 * self.min[k]);
1044 (c, h)
1045 }
1046
1047 fn distance_to(&self, p: [f64; 3]) -> f64 {
1049 norm(std::array::from_fn(|k| {
1050 (self.min[k] - p[k]).max(p[k] - self.max[k]).max(0.0)
1051 }))
1052 }
1053
1054 pub(crate) fn distance_to_box(&self, other: &Aabb64) -> f64 {
1056 norm(std::array::from_fn(|k| {
1057 (self.min[k] - other.max[k])
1058 .max(other.min[k] - self.max[k])
1059 .max(0.0)
1060 }))
1061 }
1062
1063 fn ray_enter(&self, o: [f64; 3], d: [f64; 3], pad: f64, t0: f64, t1: f64) -> Option<f64> {
1066 let (mut lo, mut hi) = (t0, t1);
1067 for k in 0..3 {
1068 let (bmin, bmax) = (self.min[k] - pad, self.max[k] + pad);
1069 if d[k] == 0.0 {
1070 if o[k] < bmin || o[k] > bmax {
1071 return None;
1072 }
1073 continue;
1074 }
1075 let (a, b) = ((bmin - o[k]) / d[k], (bmax - o[k]) / d[k]);
1076 lo = lo.max(a.min(b));
1078 hi = hi.min(a.max(b));
1079 }
1080 (lo <= hi).then_some(lo)
1081 }
1082}
1083
1084#[derive(Debug, Clone, Copy, PartialEq)]
1089pub(crate) struct CachedBox {
1090 rev: u64,
1091 origin: [f64; 3],
1092 placement: Placement,
1093 bounds: Aabb64,
1094 mag: f64,
1097}
1098
1099pub(crate) fn cached_world_box(entry: &mut Entry) -> (Aabb64, f64) {
1106 let rev = entry.mesh.revision();
1107 let origin = entry.mesh.origin();
1108 if !entry.rebuild
1109 && let Some(c) = &entry.wbox
1110 && c.rev == rev
1111 && c.origin == origin
1112 && c.placement == entry.placement
1113 {
1114 return (c.bounds, c.mag);
1115 }
1116 let bounds = world_box(entry);
1117 let mag = local_mag(entry);
1118 entry.wbox = Some(CachedBox {
1119 rev,
1120 origin,
1121 placement: entry.placement,
1122 bounds,
1123 mag,
1124 });
1125 (bounds, mag)
1126}
1127
1128pub(crate) fn world_box(entry: &Entry) -> Aabb64 {
1133 let Some(b) = local_box(entry) else {
1134 return Aabb64::EMPTY;
1135 };
1136 let o = entry.mesh.origin();
1137 let lo: [f64; 3] = std::array::from_fn(|k| o[k] + f64::from(b.min[k]));
1138 let hi: [f64; 3] = std::array::from_fn(|k| o[k] + f64::from(b.max[k]));
1139 let c: [f64; 3] = std::array::from_fn(|k| 0.5 * lo[k] + 0.5 * hi[k]);
1140 let h: [f64; 3] = std::array::from_fn(|k| 0.5 * hi[k] - 0.5 * lo[k]);
1141 let p = &entry.placement;
1142 let wc = p.apply(c);
1143 let (r, s) = (&p.rotation, &p.scale);
1144 let mut out = Aabb64::EMPTY;
1145 for i in 0..3 {
1146 let a = r[i].map(f64::abs);
1147 let wh = a[0] * s[0] * h[0] + a[1] * s[1] * h[1] + a[2] * s[2] * h[2];
1148 let size = p.translation[i].abs()
1150 + a[0] * s[0] * (c[0].abs() + h[0])
1151 + a[1] * s[1] * (c[1].abs() + h[1])
1152 + a[2] * s[2] * (c[2].abs() + h[2]);
1153 let pad = 16.0 * f64::EPSILON * size + f64::MIN_POSITIVE;
1154 out.min[i] = wc[i] - wh - pad;
1155 out.max[i] = wc[i] + wh + pad;
1156 }
1157 out
1158}
1159
1160#[cfg(test)]
1161pub(crate) mod tests {
1162 use super::*;
1163 use crate::{MeshOptions, Placement, SceneOptions, UpdateMode};
1164 use fast_mesh_query::{ClosestOptions, Ray, RayOptions, Rigid};
1165 use fast_mesh_store::Mesh;
1166
1167 pub(crate) const RZ90: [[f64; 3]; 3] = [[0.0, -1.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 1.0]];
1168 pub(crate) const RX90: [[f64; 3]; 3] = [[1.0, 0.0, 0.0], [0.0, 0.0, -1.0], [0.0, 1.0, 0.0]];
1169
1170 fn tilted(deg: f64) -> [[f64; 3]; 3] {
1172 let (s, c) = deg.to_radians().sin_cos();
1173 let [x, y, z] = [1.0 / 3.0, 2.0 / 3.0, 2.0 / 3.0];
1174 let k = 1.0 - c;
1175 [
1176 [c + x * x * k, x * y * k - z * s, x * z * k + y * s],
1177 [y * x * k + z * s, c + y * y * k, y * z * k - x * s],
1178 [z * x * k - y * s, z * y * k + x * s, c + z * z * k],
1179 ]
1180 }
1181
1182 pub(crate) fn place(rotation: [[f64; 3]; 3], translation: [f64; 3]) -> MeshOptions {
1183 MeshOptions {
1184 placement: Placement {
1185 rotation,
1186 translation,
1187 ..Placement::IDENTITY
1188 },
1189 ..MeshOptions::default()
1190 }
1191 }
1192
1193 pub(crate) fn cube(min: [f32; 3], size: f32) -> Mesh {
1195 let mut pos = Vec::new();
1196 for i in 0..8u32 {
1197 let b = |bit: u32| if i & bit != 0 { size } else { 0.0 };
1198 pos.extend_from_slice(&[min[0] + b(1), min[1] + b(2), min[2] + b(4)]);
1199 }
1200 let idx = [
1201 0, 2, 1, 1, 2, 3, 4, 5, 6, 5, 7, 6, 0, 1, 4, 1, 5, 4, 2, 6, 3, 3, 6, 7, 0, 4, 2, 2, 4,
1202 6, 1, 3, 5, 3, 7, 5,
1203 ];
1204 Mesh::from_buffers(&pos, &idx).unwrap()
1205 }
1206
1207 pub(crate) fn wall(x0: f32, s: f32) -> Mesh {
1209 let pos = [x0, -s, -s, x0, s, -s, x0, -s, s, x0, s, s];
1210 Mesh::from_buffers(&pos, &[0, 1, 2, 1, 3, 2]).unwrap()
1211 }
1212
1213 fn far_cube(min: [f64; 3], size: f64) -> Mesh {
1216 let m = cube([0.0; 3], 1.0);
1217 let pos: Vec<f64> = m
1218 .positions()
1219 .iter()
1220 .flat_map(|p| (0..3).map(move |k| min[k] + size * f64::from(p[k])))
1221 .collect();
1222 let idx: Vec<u32> = m.triangles().iter().flatten().copied().collect();
1223 Mesh::from_buffers_f64(&pos, &idx).unwrap()
1224 }
1225
1226 fn to_local(p: &Placement, origin: [f64; 3], q: [f64; 3]) -> [f64; 3] {
1228 let d = [0, 1, 2].map(|i| q[i] - p.translation[i]);
1229 let r = &p.rotation;
1230 [0, 1, 2].map(|i| r[0][i] * d[0] + r[1][i] * d[1] + r[2][i] * d[2] - origin[i])
1231 }
1232
1233 fn rotate_back(p: &Placement, d: [f64; 3]) -> [f64; 3] {
1234 let r = &p.rotation;
1235 [0, 1, 2].map(|i| r[0][i] * d[0] + r[1][i] * d[1] + r[2][i] * d[2])
1236 }
1237
1238 fn to_world(p: &Placement, origin: [f64; 3], l: [f32; 3]) -> [f64; 3] {
1239 let q = [0, 1, 2].map(|i| origin[i] + f64::from(l[i]));
1240 let r = &p.rotation;
1241 [0, 1, 2].map(|i| r[i][0] * q[0] + r[i][1] * q[1] + r[i][2] * q[2] + p.translation[i])
1242 }
1243
1244 fn unit(d: [f64; 3]) -> [f64; 3] {
1245 let n = (d[0] * d[0] + d[1] * d[1] + d[2] * d[2]).sqrt();
1246 d.map(|c| c / n)
1247 }
1248
1249 fn three_cubes() -> (Scene, Vec<MeshId>) {
1251 let mut scene = Scene::new(SceneOptions::default());
1252 let ids = vec![
1253 scene
1254 .add_mesh(
1255 cube([0.0; 3], 2.0),
1256 place(Placement::IDENTITY.rotation, [5.0, 0.0, 0.0]),
1257 )
1258 .unwrap(),
1259 scene
1260 .add_mesh(cube([1.0, 0.0, -1.0], 1.0), place(RZ90, [9.0, 3.0, 0.5]))
1261 .unwrap(),
1262 scene
1263 .add_mesh(
1264 far_cube([1e5, -2e5, 3e5], 1.5),
1265 place(tilted(30.0), [-2.0, 1.0, 4.0]),
1266 )
1267 .unwrap(),
1268 ];
1269 (scene, ids)
1270 }
1271
1272 fn brute_raycast(scene: &Scene, o: [f64; 3], d: [f64; 3]) -> Option<SceneRayHit> {
1275 let d = unit(d);
1276 let mut best: Option<SceneRayHit> = None;
1277 for id in scene.ids() {
1278 let e = scene.entries.get(id).unwrap();
1279 let origin = e.mesh.origin();
1280 let ray = Ray {
1281 origin: to_local(&e.placement, origin, o).map(|c| c as f32),
1282 dir: rotate_back(&e.placement, d).map(|c| c as f32),
1283 };
1284 let Some(hit) =
1285 fast_mesh_query::raycast(&e.mesh, &e.bvh, &ray, &RayOptions::default()).unwrap()
1286 else {
1287 continue;
1288 };
1289 let t = f64::from(hit.t);
1290 if best.is_none_or(|b| t < b.t) {
1291 best = Some(SceneRayHit {
1292 mesh: id,
1293 hit,
1294 point: to_world(&e.placement, origin, hit.point),
1295 t,
1296 });
1297 }
1298 }
1299 best
1300 }
1301
1302 fn brute_closest(scene: &Scene, only: Option<MeshId>, p: [f64; 3]) -> Option<SceneClosestHit> {
1303 let mut best: Option<SceneClosestHit> = None;
1304 for id in scene.ids().filter(|i| only.is_none_or(|o| o == *i)) {
1305 let e = scene.entries.get(id).unwrap();
1306 let origin = e.mesh.origin();
1307 let q = to_local(&e.placement, origin, p).map(|c| c as f32);
1308 let Some(hit) =
1309 fast_mesh_query::closest_point(&e.mesh, &e.bvh, q, &ClosestOptions::default())
1310 .unwrap()
1311 else {
1312 continue;
1313 };
1314 let distance = f64::from(hit.distance);
1315 if best.is_none_or(|b| distance < b.distance) {
1316 best = Some(SceneClosestHit {
1317 mesh: id,
1318 hit,
1319 point: to_world(&e.placement, origin, hit.point),
1320 distance,
1321 });
1322 }
1323 }
1324 best
1325 }
1326
1327 #[test]
1328 fn raycast_matches_phase3() {
1329 let (mut scene, ids) = three_cubes();
1330 let c2 = to_world(
1331 &scene.placement(ids[2]).unwrap(),
1332 scene.mesh(ids[2]).unwrap().origin(),
1333 [0.0; 3],
1334 );
1335 let rays = [
1336 ([0.0, 1.0, 1.0], [1.0, 0.0, 0.0]),
1337 ([0.0, 0.7, 1.3], [3.0, 0.1, -0.05]),
1338 ([20.0, 3.5, 0.2], [-1.0, 0.0, 0.0]),
1339 ([9.0, 10.0, 0.0], [0.0, -1.0, 0.0]),
1340 ([8.5, 4.5, -5.0], [0.0, 0.0, 2.0]),
1341 ([0.0, 0.0, 0.0], [c2[0], c2[1], c2[2]]),
1342 ([0.0, 50.0, 0.0], [0.0, 1.0, 0.0]),
1343 ];
1344 let mut hits = 0;
1345 for (o, d) in rays {
1346 let got = scene.raycast(o, d, &SceneRayOptions::default()).unwrap();
1347 let want = brute_raycast(&scene, o, d);
1348 assert_eq!(got, want, "ray {o:?} {d:?}");
1349 hits += usize::from(got.is_some());
1350 }
1351 assert!(hits >= 5, "only {hits} rays hit");
1352 let a = scene.raycast(rays[0].0, [7.0, 0.0, 0.0], &SceneRayOptions::default());
1354 let b = scene.raycast(rays[0].0, [1.0, 0.0, 0.0], &SceneRayOptions::default());
1355 assert_eq!(a, b);
1356 assert_eq!(a.unwrap().unwrap().t, 5.0);
1357 }
1358
1359 #[test]
1360 fn closest_point_matches_phase3() {
1361 let (mut scene, ids) = three_cubes();
1362 let c2 = to_world(
1363 &scene.placement(ids[2]).unwrap(),
1364 scene.mesh(ids[2]).unwrap().origin(),
1365 [0.3, -0.2, 0.1],
1366 );
1367 let points = [
1368 [0.0, 0.0, 0.0],
1369 [6.0, 1.0, 1.0],
1370 [8.6, 4.4, 0.0],
1371 [9.0, 9.0, 9.0],
1372 c2,
1373 [c2[0] + 3.0, c2[1], c2[2] - 1.0],
1374 ];
1375 for p in points {
1376 let got = scene
1377 .closest_point(p, &SceneClosestOptions::default())
1378 .unwrap();
1379 assert_eq!(got, brute_closest(&scene, None, p), "point {p:?}");
1380 for &id in &ids {
1381 let got = scene
1382 .closest_point_on(id, p, &SceneClosestOptions::default())
1383 .unwrap();
1384 assert_eq!(got, brute_closest(&scene, Some(id), p), "{id:?} {p:?}");
1385 }
1386 }
1387 }
1388
1389 pub(crate) fn hand_rigid(scene: &Scene, a: MeshId, b: MeshId) -> Rigid {
1392 let (pa, pb) = (scene.placement(a).unwrap(), scene.placement(b).unwrap());
1393 let (oa, ob) = (
1394 scene.mesh(a).unwrap().origin(),
1395 scene.mesh(b).unwrap().origin(),
1396 );
1397 let (ra, rb) = (pa.rotation, pb.rotation);
1398 let rotation =
1399 [0, 1, 2].map(|i| [0, 1, 2].map(|j| (0..3).map(|k| ra[k][i] * rb[k][j]).sum::<f64>()));
1400 let wb = [0, 1, 2]
1401 .map(|i| rb[i][0] * ob[0] + rb[i][1] * ob[1] + rb[i][2] * ob[2] + pb.translation[i]);
1402 Rigid {
1403 rotation,
1404 translation: to_local(&pa, oa, wb),
1405 }
1406 }
1407
1408 #[test]
1409 fn pair_queries_match_phase3() {
1410 let mut scene = Scene::new(SceneOptions::default());
1411 let ids = [
1412 scene
1413 .add_mesh(
1414 cube([0.0; 3], 2.0),
1415 place(Placement::IDENTITY.rotation, [5.0, 0.0, 0.0]),
1416 )
1417 .unwrap(),
1418 scene
1420 .add_mesh(cube([0.0; 3], 1.0), place(RZ90, [7.5, 0.5, 0.5]))
1421 .unwrap(),
1422 scene
1424 .add_mesh(cube([0.0; 3], 1.0), place(RX90, [5.0, 3.75, 1.0]))
1425 .unwrap(),
1426 ];
1427 for &a in &ids {
1428 for &b in &ids {
1429 let xf = hand_rigid(&scene, a, b);
1430 let (ea, eb) = (scene.entries.get(a).unwrap(), scene.entries.get(b).unwrap());
1431 let (ma, ba, mb, bb) = (&ea.mesh, &ea.bvh, &eb.mesh, &eb.bvh);
1432 let want_i = fast_mesh_query::intersects(ma, ba, mb, bb, &xf).unwrap();
1433 let want_d = fast_mesh_query::mesh_distance(ma, ba, mb, bb, &xf, 10.0).unwrap();
1434 let (mut wa, mut wb) = (Vec::new(), Vec::new());
1435 fast_mesh_query::triangles_near(ma, ba, mb, bb, &xf, 1.0, &mut wa, &mut wb)
1436 .unwrap();
1437 let (pa, oa) = (ea.placement, ma.origin());
1438 let want_d = want_d.map(|d| SceneMeshDistance {
1439 distance: f64::from(d.distance),
1440 point_a: to_world(&pa, oa, d.point_a),
1441 point_b: to_world(&pa, oa, d.point_b),
1442 triangle_a: d.triangle_a,
1443 triangle_b: d.triangle_b,
1444 });
1445 assert_eq!(scene.intersects(a, b), Ok(want_i), "{a:?} {b:?}");
1446 assert_eq!(scene.mesh_distance(a, b, 10.0), Ok(want_d), "{a:?} {b:?}");
1447 let (mut ga, mut gb) = (vec![TriangleId::MAX], Vec::new());
1448 scene.triangles_near(a, b, 1.0, &mut ga, &mut gb).unwrap();
1449 assert_eq!(ga[0], TriangleId::MAX, "existing elements stay in front");
1450 assert_eq!((&ga[1..], &gb[..]), (&wa[..], &wb[..]), "{a:?} {b:?}");
1451 }
1452 }
1453 assert_eq!(scene.intersects(ids[0], ids[1]), Ok(true));
1454 assert_eq!(scene.intersects(ids[0], ids[2]), Ok(false));
1455 let d = scene.mesh_distance(ids[0], ids[2], 10.0).unwrap().unwrap();
1456 assert_eq!(d.distance, 0.75);
1457 assert_eq!(d.point_a[1], 2.0);
1458 assert_eq!(d.point_b[1], 2.75);
1459 assert_eq!(scene.mesh_distance(ids[0], ids[2], 0.5), Ok(None));
1460 }
1461
1462 #[test]
1463 fn raycast_picks_nearest_mesh_and_breaks_ties_by_id() {
1464 let mut scene = Scene::new(SceneOptions::default());
1465 let identity = Placement::IDENTITY.rotation;
1466 let low = scene
1469 .add_mesh(wall(5.0, 1.0), MeshOptions::default())
1470 .unwrap();
1471 let high_mesh = {
1472 let pos = [
1473 5.0, -1.0, -1.0, 5.0, 1.0, -1.0, 5.0, -1.0, 1.0, 5.0, 1.0, 1.0, 1.0, 5.0, 5.0, 1.0,
1474 6.0, 5.0, 1.0, 5.0, 6.0,
1475 ];
1476 Mesh::from_buffers(&pos, &[0, 1, 2, 1, 3, 2, 4, 5, 6]).unwrap()
1477 };
1478 let high = scene.add_mesh(high_mesh, MeshOptions::default()).unwrap();
1479 assert!(low < high);
1480 let o = [0.0, 0.0, 0.0];
1481 let x = [1.0, 0.0, 0.0];
1482 let hit = scene
1483 .raycast(o, x, &SceneRayOptions::default())
1484 .unwrap()
1485 .unwrap();
1486 assert_eq!((hit.mesh, hit.t), (low, 5.0));
1487 let near = scene
1490 .add_mesh(wall(0.0, 1.0), place(RX90, [3.0, 0.0, 0.0]))
1491 .unwrap();
1492 let hit = scene
1493 .raycast(o, x, &SceneRayOptions::default())
1494 .unwrap()
1495 .unwrap();
1496 assert_eq!((hit.mesh, hit.t, hit.point), (near, 3.0, [3.0, 0.0, 0.0]));
1497 let flat = scene
1500 .add_mesh(wall(2.0, 1.0), MeshOptions::default())
1501 .unwrap();
1502 let turned = scene
1503 .add_mesh(wall(1.0, 1.0), place(RX90, [1.0, 0.0, 0.0]))
1504 .unwrap();
1505 assert!(flat < turned);
1506 let hit = scene
1507 .raycast(o, x, &SceneRayOptions::default())
1508 .unwrap()
1509 .unwrap();
1510 assert_eq!((hit.mesh, hit.t), (flat, 2.0));
1511 scene
1512 .set_placement(
1513 flat,
1514 Placement {
1515 rotation: identity,
1516 translation: [0.5, 0.0, 0.0],
1517 ..Placement::IDENTITY
1518 },
1519 )
1520 .unwrap();
1521 let hit = scene
1522 .raycast(o, x, &SceneRayOptions::default())
1523 .unwrap()
1524 .unwrap();
1525 assert_eq!((hit.mesh, hit.t), (turned, 2.0));
1526 let opts = SceneRayOptions {
1529 min_t: 2.5,
1530 ..SceneRayOptions::default()
1531 };
1532 let hit = scene.raycast(o, x, &opts).unwrap().unwrap();
1533 assert_eq!((hit.mesh, hit.t), (flat, 2.5));
1534 let opts = SceneRayOptions {
1535 min_t: 2.75,
1536 ..SceneRayOptions::default()
1537 };
1538 let hit = scene.raycast(o, x, &opts).unwrap().unwrap();
1539 assert_eq!((hit.mesh, hit.t), (near, 3.0));
1540 }
1541
1542 #[test]
1543 fn closest_point_breaks_ties_by_id() {
1544 let mut scene = Scene::new(SceneOptions::default());
1545 let identity = Placement::IDENTITY.rotation;
1546 let low = scene
1549 .add_mesh(wall(2.0, 1.0), MeshOptions::default())
1550 .unwrap();
1551 let high = scene
1552 .add_mesh(wall(0.0, 1.0), place(identity, [-2.0, 0.0, 0.0]))
1553 .unwrap();
1554 assert!(low < high);
1555 let opts = SceneClosestOptions::default();
1556 let o = [0.0, 0.0, 0.0];
1557 let twin = scene
1559 .add_mesh(wall(2.0, 1.0), MeshOptions::default())
1560 .unwrap();
1561 let hit = scene.closest_point(o, &opts).unwrap().unwrap();
1562 assert_eq!((hit.mesh, hit.distance), (low, 2.0));
1563 scene
1565 .set_placement(
1566 twin,
1567 Placement {
1568 rotation: identity,
1569 translation: [-1.0, 0.0, 0.0],
1570 ..Placement::IDENTITY
1571 },
1572 )
1573 .unwrap();
1574 let hit = scene.closest_point(o, &opts).unwrap().unwrap();
1575 assert_eq!((hit.mesh, hit.distance), (twin, 1.0));
1576 scene
1577 .set_placement(
1578 low,
1579 Placement {
1580 rotation: identity,
1581 translation: [1.0, 0.0, 0.0],
1582 ..Placement::IDENTITY
1583 },
1584 )
1585 .unwrap();
1586 let hit = scene.closest_point(o, &opts).unwrap().unwrap();
1587 assert_eq!((hit.mesh, hit.distance), (twin, 1.0));
1588 }
1589
1590 #[test]
1591 fn queries_update_lazy_and_reject_stale_manual() {
1592 let mut scene = Scene::new(SceneOptions::default());
1593 let lazy = scene
1594 .add_mesh(
1595 wall(5.0, 1.0),
1596 MeshOptions {
1597 mode: UpdateMode::Lazy,
1598 ..MeshOptions::default()
1599 },
1600 )
1601 .unwrap();
1602 let manual = scene
1603 .add_mesh(
1604 wall(9.0, 1.0),
1605 MeshOptions {
1606 mode: UpdateMode::Manual,
1607 ..MeshOptions::default()
1608 },
1609 )
1610 .unwrap();
1611 let o = [0.0, 0.0, 0.0];
1612 let x = [1.0, 0.0, 0.0];
1613 let ray = SceneRayOptions::default();
1614 let near = SceneClosestOptions::default();
1615 scene
1617 .edit(lazy, |tx| {
1618 let ids: Vec<u32> = (0..4).collect();
1619 let pos: Vec<[f32; 3]> = ids
1620 .iter()
1621 .map(|&v| {
1622 let p = tx.mesh().position(v).unwrap();
1623 [4.0, p[1], p[2]]
1624 })
1625 .collect();
1626 tx.set_positions(&ids, &pos)
1627 })
1628 .unwrap();
1629 assert_eq!(scene.is_current(lazy), Ok(false));
1630 let hit = scene.raycast(o, x, &ray).unwrap().unwrap();
1631 assert_eq!((hit.mesh, hit.t), (lazy, 4.0));
1632 assert_eq!(scene.is_current(lazy), Ok(true));
1633 scene
1636 .edit(manual, |tx| tx.set_positions(&[0], &[[9.0, -1.0, -2.0]]))
1637 .unwrap();
1638 scene
1639 .edit(lazy, |tx| tx.set_positions(&[0], &[[4.0, -1.0, -2.0]]))
1640 .unwrap();
1641 let stale = Some(SceneError::StaleBvh(manual));
1642 assert_eq!(scene.raycast(o, x, &ray).err(), stale);
1643 assert_eq!(scene.is_current(lazy), Ok(true));
1645 assert_eq!(scene.closest_point(o, &near).err(), stale);
1646 assert_eq!(scene.closest_point_on(manual, o, &near).err(), stale);
1647 assert_eq!(scene.intersects(lazy, manual).err(), stale);
1648 assert_eq!(scene.mesh_distance(manual, lazy, 1.0).err(), stale);
1649 let (mut a, mut b) = (Vec::new(), Vec::new());
1650 assert_eq!(
1651 scene
1652 .triangles_near(lazy, manual, 1.0, &mut a, &mut b)
1653 .err(),
1654 stale
1655 );
1656 assert!(scene.closest_point_on(lazy, o, &near).unwrap().is_some());
1658 assert_eq!(scene.intersects(lazy, lazy), Ok(true));
1659 scene.update(manual).unwrap();
1661 assert!(scene.raycast(o, x, &ray).unwrap().is_some());
1662 scene.entries.get_mut(manual).unwrap().rebuild = true;
1665 assert!(
1666 scene
1667 .entries
1668 .get(manual)
1669 .unwrap()
1670 .bvh
1671 .is_current(scene.mesh(manual).unwrap())
1672 );
1673 assert_eq!(scene.raycast(o, x, &ray).err(), stale);
1674 assert_eq!(scene.closest_point_on(manual, o, &near).err(), stale);
1675 scene.update(manual).unwrap();
1676 assert_eq!(
1677 scene
1678 .closest_point_on(manual, o, &near)
1679 .unwrap()
1680 .unwrap()
1681 .mesh,
1682 manual
1683 );
1684 scene.remove_mesh(manual).unwrap();
1686 let unknown = Some(SceneError::UnknownMesh);
1687 assert_eq!(scene.closest_point_on(manual, o, &near).err(), unknown);
1688 assert_eq!(scene.intersects(lazy, manual).err(), unknown);
1689 assert_eq!(scene.mesh_distance(manual, lazy, 1.0).err(), unknown);
1690 assert_eq!(
1691 scene
1692 .triangles_near(manual, lazy, 1.0, &mut a, &mut b)
1693 .err(),
1694 unknown
1695 );
1696 }
1697
1698 #[test]
1699 fn bounds_round_outward() {
1700 let tiny = 1e-12;
1703 let mut scene = Scene::new(SceneOptions::default());
1704 let identity = Placement::IDENTITY.rotation;
1705 let a = scene
1706 .add_mesh(wall(1.0, 1.0), place(identity, [2.0, 0.0, 0.0]))
1707 .unwrap();
1708 let o = [0.0, 0.0, 0.0];
1709 let x = [1.0, 0.0, 0.0];
1710 let ray = |min_t: f64, max_t: f64| SceneRayOptions {
1711 min_t,
1712 max_t,
1713 cull: Cull::None,
1714 };
1715 let t = |scene: &mut Scene, opts| scene.raycast(o, x, &opts).unwrap().map(|h| h.t);
1716 assert_eq!(t(&mut scene, ray(0.0, 3.0)), Some(3.0));
1718 assert_eq!(t(&mut scene, ray(0.0, 3.0 - tiny)), None);
1719 assert_eq!(t(&mut scene, ray(0.0, 3.0 + tiny)), Some(3.0));
1720 assert_eq!(t(&mut scene, ray(3.0, 3.0)), Some(3.0));
1721 assert_eq!(t(&mut scene, ray(3.0 - tiny, 4.0)), Some(3.0));
1722 assert_eq!(t(&mut scene, ray(3.0 + tiny, 10.0)), None);
1723 let b = scene
1725 .add_mesh(wall(4.0, 1.0), MeshOptions::default())
1726 .unwrap();
1727 assert_eq!(t(&mut scene, ray(3.0 + tiny, 10.0)), Some(4.0));
1728 scene.remove_mesh(b).unwrap();
1729 let two = {
1731 let w = [
1732 3.0, -1.0, -1.0, 3.0, 1.0, -1.0, 3.0, -1.0, 1.0, 4.0, -1.0, -1.0, 4.0, 1.0, -1.0,
1733 4.0, -1.0, 1.0,
1734 ];
1735 Mesh::from_buffers(&w, &[0, 1, 2, 3, 4, 5]).unwrap()
1736 };
1737 scene.remove_mesh(a).unwrap();
1738 scene.add_mesh(two, MeshOptions::default()).unwrap();
1739 assert_eq!(t(&mut scene, ray(3.0 + tiny, 10.0)), Some(4.0));
1740 assert_eq!(t(&mut scene, ray(3.0 - tiny, 10.0)), Some(3.0));
1741
1742 let mut scene = Scene::new(SceneOptions::default());
1744 let w = scene
1745 .add_mesh(wall(1.0, 1.0), place(identity, [2.0, 0.0, 0.0]))
1746 .unwrap();
1747 let near = |max_distance| SceneClosestOptions { max_distance };
1748 let d =
1749 |scene: &mut Scene, opts| scene.closest_point(o, &opts).unwrap().map(|h| h.distance);
1750 assert_eq!(d(&mut scene, near(3.0)), Some(3.0));
1751 assert_eq!(d(&mut scene, near(3.0 - tiny)), None);
1752 assert_eq!(d(&mut scene, near(3.0 + tiny)), Some(3.0));
1753 let on = |scene: &mut Scene, opts| {
1754 scene
1755 .closest_point_on(w, o, &opts)
1756 .unwrap()
1757 .map(|h| h.distance)
1758 };
1759 assert_eq!(on(&mut scene, near(3.0)), Some(3.0));
1760 assert_eq!(on(&mut scene, near(3.0 - tiny)), None);
1761
1762 let v = scene
1764 .add_mesh(wall(4.0, 1.0), place(identity, [2.0, 0.0, 0.0]))
1765 .unwrap();
1766 let md = |scene: &mut Scene, m| scene.mesh_distance(w, v, m).unwrap().map(|r| r.distance);
1767 assert_eq!(md(&mut scene, 3.0), Some(3.0));
1768 assert_eq!(md(&mut scene, 3.0 - tiny), None);
1769 assert_eq!(md(&mut scene, 3.0 + tiny), Some(3.0));
1770 }
1771
1772 #[test]
1773 fn invalid_inputs_are_rejected() {
1774 let (mut scene, ids) = three_cubes();
1775 let o = [0.0, 0.0, 0.0];
1776 let ray = SceneRayOptions::default();
1777 let invalid =
1778 |r: Result<Option<SceneRayHit>>| matches!(r, Err(SceneError::InvalidInput(_)));
1779 assert!(invalid(scene.raycast(o, [0.0; 3], &ray)));
1780 assert!(invalid(scene.raycast(o, [f64::NAN, 1.0, 0.0], &ray)));
1781 assert!(invalid(scene.raycast(o, [f64::INFINITY, 0.0, 0.0], &ray)));
1782 assert!(invalid(scene.raycast(
1783 [f64::NAN, 0.0, 0.0],
1784 [1.0, 0.0, 0.0],
1785 &ray
1786 )));
1787 let bad = SceneRayOptions {
1788 min_t: 2.0,
1789 max_t: 1.0,
1790 cull: Cull::None,
1791 };
1792 assert!(invalid(scene.raycast(o, [1.0, 0.0, 0.0], &bad)));
1793 let bad = SceneRayOptions {
1794 max_t: f64::NAN,
1795 ..SceneRayOptions::default()
1796 };
1797 assert!(invalid(scene.raycast(o, [1.0, 0.0, 0.0], &bad)));
1798 let big = [f64::MAX, f64::MAX, 0.0];
1800 assert!(scene.raycast(o, big, &ray).is_ok());
1801 for max_distance in [-1.0, f64::NAN] {
1802 let opts = SceneClosestOptions { max_distance };
1803 assert!(matches!(
1804 scene.closest_point(o, &opts),
1805 Err(SceneError::InvalidInput(_))
1806 ));
1807 assert!(matches!(
1808 scene.closest_point_on(ids[0], o, &opts),
1809 Err(SceneError::InvalidInput(_))
1810 ));
1811 assert!(matches!(
1812 scene.mesh_distance(ids[0], ids[1], max_distance),
1813 Err(SceneError::InvalidInput(_))
1814 ));
1815 let (mut a, mut b) = (Vec::new(), Vec::new());
1816 assert!(matches!(
1817 scene.triangles_near(ids[0], ids[1], max_distance, &mut a, &mut b),
1818 Err(SceneError::InvalidInput(_))
1819 ));
1820 }
1821 assert!(matches!(
1822 scene.closest_point([0.0, f64::INFINITY, 0.0], &SceneClosestOptions::default()),
1823 Err(SceneError::InvalidInput(_))
1824 ));
1825 }
1826
1827 #[test]
1828 fn world_box_holds_every_placed_vertex() {
1829 let (scene, ids) = three_cubes();
1830 for id in ids {
1831 let e = scene.entries.get(id).unwrap();
1832 let b = world_box(e);
1833 assert!(!b.is_empty());
1834 for &p in e.mesh.positions() {
1835 let w = to_world(&e.placement, e.mesh.origin(), p);
1836 for k in 0..3 {
1837 assert!(b.min[k] <= w[k] && w[k] <= b.max[k], "{w:?} outside {b:?}");
1838 }
1839 }
1840 }
1841 let mut scene = Scene::new(SceneOptions::default());
1842 let empty = scene.add_mesh(Mesh::new(), MeshOptions::default()).unwrap();
1843 assert!(world_box(scene.entries.get(empty).unwrap()).is_empty());
1844 let o = [0.0, 0.0, 0.0];
1845 assert_eq!(
1846 scene.raycast(o, [1.0, 0.0, 0.0], &SceneRayOptions::default()),
1847 Ok(None)
1848 );
1849 assert_eq!(
1850 scene.closest_point(o, &SceneClosestOptions::default()),
1851 Ok(None)
1852 );
1853 }
1854
1855 #[test]
1856 fn queries_skip_empty_meshes_before_mapping_the_point() {
1857 let mut scene = Scene::new(SceneOptions::default());
1858 let wall_id = scene
1859 .add_mesh(wall(5.0, 1.0), MeshOptions::default())
1860 .unwrap();
1861 let far = place(Placement::IDENTITY.rotation, [1e39, 0.0, 0.0]);
1865 scene.add_mesh(Mesh::new(), far).unwrap();
1866 let o = [0.0, 0.0, 0.0];
1867 let hit = scene
1868 .raycast(o, [1.0, 0.0, 0.0], &SceneRayOptions::default())
1869 .unwrap()
1870 .unwrap();
1871 assert_eq!((hit.mesh, hit.t), (wall_id, 5.0));
1872 let near = scene
1873 .closest_point(o, &SceneClosestOptions::default())
1874 .unwrap()
1875 .unwrap();
1876 assert_eq!((near.mesh, near.distance), (wall_id, 5.0));
1877 }
1878
1879 #[test]
1882 fn world_boxes_are_cached_and_invalidated() {
1883 let mut scene = Scene::new(SceneOptions::default());
1884 let id = scene
1885 .add_mesh(wall(5.0, 1.0), MeshOptions::default())
1886 .unwrap();
1887 let cached = |scene: &Scene| scene.entries.get(id).unwrap().wbox;
1888 let fresh = |scene: &Scene| {
1889 let e = scene.entries.get(id).unwrap();
1890 (world_box(e), local_mag(e))
1891 };
1892 assert_eq!(cached(&scene), None);
1893 let o = [0.0, 0.0, 0.0];
1894 scene
1895 .raycast(o, [1.0, 0.0, 0.0], &SceneRayOptions::default())
1896 .unwrap();
1897 let first = cached(&scene).expect("cached by the query");
1898 assert_eq!((first.bounds, first.mag), fresh(&scene));
1899 scene
1901 .set_placement(
1902 id,
1903 Placement {
1904 rotation: Placement::IDENTITY.rotation,
1905 translation: [10.0, 0.0, 0.0],
1906 ..Placement::IDENTITY
1907 },
1908 )
1909 .unwrap();
1910 scene
1911 .raycast(o, [1.0, 0.0, 0.0], &SceneRayOptions::default())
1912 .unwrap();
1913 let moved_box = cached(&scene).expect("cached again");
1914 assert_ne!(moved_box.bounds, first.bounds);
1915 assert_eq!((moved_box.bounds, moved_box.mag), fresh(&scene));
1916 scene.edit(id, |tx| tx.remove_triangles(&[0, 1])).unwrap();
1918 assert_eq!(
1919 scene.raycast(o, [1.0, 0.0, 0.0], &SceneRayOptions::default()),
1920 Ok(None)
1921 );
1922 let emptied = cached(&scene).expect("cached again");
1923 assert!(emptied.bounds.is_empty());
1924 assert_eq!((emptied.bounds, emptied.mag), fresh(&scene));
1925 }
1926
1927 #[test]
1928 fn bvh_accessor_rejects_stale() {
1929 let mut scene = Scene::new(SceneOptions::default());
1930 let manual = MeshOptions {
1931 mode: UpdateMode::Manual,
1932 ..MeshOptions::default()
1933 };
1934 let id = scene.add_mesh(cube([0.0; 3], 1.0), manual).unwrap();
1935 assert_eq!(scene.bvh(id).unwrap().generation(), {
1936 scene.entries.get(id).unwrap().bvh.generation()
1937 });
1938 scene
1939 .edit(id, |tx| tx.set_positions(&[0], &[[-1.0, -1.0, -1.0]]))
1940 .unwrap();
1941 assert_eq!(scene.bvh(id).err(), Some(SceneError::StaleBvh(id)));
1942 scene.update(id).unwrap();
1943 let bvh = scene.bvh(id).unwrap();
1944 assert!(bvh.is_current(scene.mesh(id).unwrap()));
1945 scene.remove_mesh(id).unwrap();
1946 assert_eq!(scene.bvh(id).err(), Some(SceneError::UnknownMesh));
1947 }
1948
1949 #[test]
1950 fn relative_transform_matches_pair_queries() {
1951 let mut scene = Scene::new(SceneOptions::default());
1952 let a = scene
1953 .add_mesh(
1954 far_cube([1000.5, -20.0, 3.0], 2.0),
1955 place(RZ90, [5.0, 1.0, 0.0]),
1956 )
1957 .unwrap();
1958 let b = scene
1959 .add_mesh(cube([0.0; 3], 1.0), place(RX90, [7.5, 0.5, 0.5]))
1960 .unwrap();
1961 assert_eq!(scene.relative_transform(a, b), Ok(hand_rigid(&scene, a, b)));
1962 assert_eq!(scene.relative_transform(b, a), Ok(hand_rigid(&scene, b, a)));
1963 let xf = scene.relative_transform(a, b).unwrap();
1965 let (ea, eb) = (scene.entries.get(a).unwrap(), scene.entries.get(b).unwrap());
1966 let want = fast_mesh_query::mesh_distance(&ea.mesh, &ea.bvh, &eb.mesh, &eb.bvh, &xf, 100.0)
1967 .unwrap()
1968 .map(|d| f64::from(d.distance));
1969 let got = scene
1970 .mesh_distance(a, b, 100.0)
1971 .unwrap()
1972 .map(|d| d.distance);
1973 assert_eq!(got, want);
1974
1975 let pa = scene.placement(a).unwrap();
1977 let oa = scene.mesh(a).unwrap().origin();
1978 assert_ne!(oa, [0.0; 3]);
1979 let q = [3.0, -2.0, 7.0];
1980 let l = scene.world_to_local(a, q).unwrap();
1981 assert_eq!(l, to_local(&pa, oa, q));
1982 let back = scene.local_to_world(a, l).unwrap();
1983 for k in 0..3 {
1984 assert!((back[k] - q[k]).abs() < 1e-9, "{back:?} {q:?}");
1985 }
1986 let lb = [0.25, 0.5, 0.75];
1988 let via_world = scene
1989 .world_to_local(a, scene.local_to_world(b, lb).unwrap())
1990 .unwrap();
1991 let direct = xf.apply(lb.map(|c| c as f32));
1992 for k in 0..3 {
1993 assert!((via_world[k] - direct[k]).abs() < 1e-9);
1994 }
1995 scene.remove_mesh(b).unwrap();
1996 assert_eq!(scene.relative_transform(a, b), Err(SceneError::UnknownMesh));
1997 assert_eq!(scene.world_to_local(b, q), Err(SceneError::UnknownMesh));
1998 assert_eq!(scene.local_to_world(b, q), Err(SceneError::UnknownMesh));
1999 }
2000
2001 fn scaled_opts(rotation: [[f64; 3]; 3], translation: [f64; 3], scale: [f64; 3]) -> MeshOptions {
2002 MeshOptions {
2003 placement: Placement {
2004 rotation,
2005 translation,
2006 scale,
2007 },
2008 ..MeshOptions::default()
2009 }
2010 }
2011
2012 #[test]
2013 fn scaled_world_boxes_cover_the_scaled_mesh() {
2014 let mut scene = Scene::new(SceneOptions::default());
2015 let rigid = scene
2017 .add_mesh(cube([0.0; 3], 2.0), MeshOptions::default())
2018 .unwrap();
2019 let e = scene.entries.get(rigid).unwrap();
2020 let b = world_box(e);
2021 for k in 0..3 {
2022 assert!((b.min[k] - 0.0).abs() < 1e-9, "{b:?}");
2023 assert!((b.max[k] - 2.0).abs() < 1e-9, "{b:?}");
2024 }
2025 let uniform = scene
2028 .add_mesh(
2029 cube([0.0; 3], 2.0),
2030 scaled_opts(Placement::IDENTITY.rotation, [0.0; 3], [2.0; 3]),
2031 )
2032 .unwrap();
2033 let e = scene.entries.get(uniform).unwrap();
2034 let b = world_box(e);
2035 for k in 0..3 {
2036 assert!((b.min[k] - 0.0).abs() < 1e-9, "{b:?}");
2037 assert!((b.max[k] - 4.0).abs() < 1e-9, "{b:?}");
2038 }
2039 let stretched = scene
2042 .add_mesh(
2043 cube([0.0; 3], 2.0),
2044 scaled_opts(
2045 Placement::IDENTITY.rotation,
2046 [10.0, 1.0, -1.0],
2047 [2.0, 0.5, 0.5],
2048 ),
2049 )
2050 .unwrap();
2051 let e = scene.entries.get(stretched).unwrap();
2052 let b = world_box(e);
2053 for (k, (lo, hi)) in [(10.0, 14.0), (1.0, 2.0), (-1.0, 0.0)].iter().enumerate() {
2054 assert!((b.min[k] - lo).abs() < 1e-9, "{b:?}");
2055 assert!((b.max[k] - hi).abs() < 1e-9, "{b:?}");
2056 }
2057 }
2058
2059 #[test]
2060 fn scaled_raycast_and_closest_point() {
2061 let mut scene = Scene::new(SceneOptions::default());
2062 let a = scene
2065 .add_mesh(cube([0.0; 3], 2.0), MeshOptions::default())
2066 .unwrap();
2067 let b = scene
2068 .add_mesh(
2069 cube([0.0; 3], 2.0),
2070 scaled_opts(
2071 Placement::IDENTITY.rotation,
2072 [10.0, 0.0, 0.0],
2073 [2.0, 1.0, 1.0],
2074 ),
2075 )
2076 .unwrap();
2077 let hit = scene
2080 .raycast(
2081 [3.0, 1.0, 1.0],
2082 [1.0, 0.0, 0.0],
2083 &SceneRayOptions::default(),
2084 )
2085 .unwrap()
2086 .unwrap();
2087 assert_eq!(hit.mesh, b);
2088 assert!((hit.t - 7.0).abs() < 1e-9, "{hit:?}");
2089 assert!((hit.point[0] - 10.0).abs() < 1e-6, "{hit:?}");
2090 let hit = scene
2094 .closest_point([5.0, 1.0, 1.0], &SceneClosestOptions::default())
2095 .unwrap()
2096 .unwrap();
2097 assert_eq!((hit.mesh, hit.distance), (a, 3.0));
2098 let hit = scene
2100 .closest_point([8.0, 1.0, 1.0], &SceneClosestOptions::default())
2101 .unwrap()
2102 .unwrap();
2103 assert_eq!(hit.mesh, b);
2104 assert!((hit.distance - 2.0).abs() < 1e-9, "{hit:?}");
2105 assert!((hit.point[0] - 10.0).abs() < 1e-6, "{hit:?}");
2106 assert!((f64::from(hit.hit.point[0]) - 0.0).abs() < 1e-6, "{hit:?}");
2109 let twin_mesh = {
2112 let pos: Vec<f32> = (0..8u32)
2113 .flat_map(|i| {
2114 let x = if i & 1 == 0 { 10.0 } else { 14.0 };
2115 let y = if i & 2 == 0 { 0.0 } else { 2.0 };
2116 let z = if i & 4 == 0 { 0.0 } else { 2.0 };
2117 [x, y, z]
2118 })
2119 .collect();
2120 Mesh::from_buffers(
2121 &pos,
2122 &[
2123 0, 2, 1, 1, 2, 3, 4, 5, 6, 5, 7, 6, 0, 1, 4, 1, 5, 4, 2, 6, 3, 3, 6, 7, 0, 4,
2124 2, 2, 4, 6, 1, 3, 5, 3, 7, 5,
2125 ],
2126 )
2127 .unwrap()
2128 };
2129 let twin = scene.add_mesh(twin_mesh, MeshOptions::default()).unwrap();
2130 for p in [
2131 [8.0, 1.0, 1.0],
2132 [5.0, 5.0, 5.0],
2133 [12.0, 1.0, 9.0],
2134 [11.0, 1.0, 1.0],
2135 ] {
2136 let opts = SceneClosestOptions::default();
2137 let scaled_hit = scene.closest_point_on(b, p, &opts).unwrap();
2138 let twin_hit = scene.closest_point_on(twin, p, &opts).unwrap();
2139 match (scaled_hit, twin_hit) {
2140 (None, None) => {}
2141 (Some(h), Some(w)) => {
2142 assert!(
2143 (h.distance - w.distance).abs() < 1e-6,
2144 "{p:?}: {h:?} vs {w:?}"
2145 );
2146 for k in 0..3 {
2147 assert!(
2148 (h.point[k] - w.point[k]).abs() < 1e-5,
2149 "{p:?}: {h:?} vs {w:?}"
2150 );
2151 }
2152 let back = scene.world_to_local(b, h.point).unwrap();
2154 for k in 0..3 {
2155 assert!(
2156 (back[k] - f64::from(h.hit.point[k])).abs() < 1e-6,
2157 "{h:?} {back:?}"
2158 );
2159 }
2160 }
2161 (h, w) => panic!("{p:?}: {h:?} vs {w:?}"),
2162 }
2163 }
2164 let opts = SceneClosestOptions { max_distance: 1.9 };
2166 assert_eq!(scene.closest_point_on(b, [8.0, 1.0, 1.0], &opts), Ok(None));
2167 let opts = SceneClosestOptions { max_distance: 2.1 };
2168 let hit = scene
2169 .closest_point_on(b, [8.0, 1.0, 1.0], &opts)
2170 .unwrap()
2171 .unwrap();
2172 assert!((hit.distance - 2.0).abs() < 1e-9);
2173 }
2174
2175 #[test]
2176 fn scaled_pair_queries_match_rigid_twins() {
2177 let mut scene = Scene::new(SceneOptions::default());
2178 let a = scene
2180 .add_mesh(cube([0.0; 3], 2.0), MeshOptions::default())
2181 .unwrap();
2182 let b = scene
2185 .add_mesh(
2186 cube([0.0; 3], 2.0),
2187 scaled_opts(
2188 Placement::IDENTITY.rotation,
2189 [1.0, 0.0, 0.0],
2190 [2.0, 1.0, 1.0],
2191 ),
2192 )
2193 .unwrap();
2194 assert_eq!(scene.intersects(a, b), Ok(true));
2195 let d = scene.mesh_distance(a, b, 10.0).unwrap().unwrap();
2196 assert_eq!(d.distance, 0.0);
2197 scene
2199 .set_placement(
2200 b,
2201 Placement {
2202 rotation: Placement::IDENTITY.rotation,
2203 translation: [3.0, 0.0, 0.0],
2204 scale: [2.0, 1.0, 1.0],
2205 },
2206 )
2207 .unwrap();
2208 assert_eq!(scene.intersects(a, b), Ok(false));
2209 let d = scene.mesh_distance(a, b, 10.0).unwrap().unwrap();
2210 assert!((d.distance - 1.0).abs() < 1e-6, "{d:?}");
2211 assert!((d.point_a[0] - 2.0).abs() < 1e-6, "{d:?}");
2212 assert!((d.point_b[0] - 3.0).abs() < 1e-6, "{d:?}");
2213 assert_eq!(scene.mesh_distance(a, b, 0.5), Ok(None));
2214 let (mut ga, mut gb) = (Vec::new(), Vec::new());
2215 scene.triangles_near(a, b, 1.0, &mut ga, &mut gb).unwrap();
2216 assert!(!ga.is_empty() && !gb.is_empty());
2217 let (mut ha, mut hb) = (Vec::new(), Vec::new());
2218 scene.triangles_near(a, b, 0.5, &mut ha, &mut hb).unwrap();
2219 assert!(ha.is_empty() && hb.is_empty());
2220 scene
2223 .set_placement(
2224 b,
2225 Placement {
2226 rotation: Placement::IDENTITY.rotation,
2227 translation: [4.0, 0.0, 0.0],
2228 scale: [2.0, 1.0, 0.5],
2229 },
2230 )
2231 .unwrap();
2232 assert_eq!(scene.intersects(a, b), Ok(false));
2233 let d = scene.mesh_distance(a, b, 10.0).unwrap().unwrap();
2234 assert!((d.distance - 2.0).abs() < 1e-6, "{d:?}");
2236 assert!((d.point_a[0] - 2.0).abs() < 1e-5, "{d:?}");
2237 assert!((d.point_b[0] - 4.0).abs() < 1e-5, "{d:?}");
2238 assert!(matches!(
2240 scene.relative_transform(a, b),
2241 Err(SceneError::InvalidInput(_))
2242 ));
2243 assert_eq!(scene.intersects(b, b), Ok(true));
2245 let d = scene.mesh_distance(b, b, 10.0).unwrap().unwrap();
2246 assert_eq!(d.distance, 0.0);
2247 let (mut sa, mut sb) = (Vec::new(), Vec::new());
2248 scene.triangles_near(b, b, 0.0, &mut sa, &mut sb).unwrap();
2249 let n = scene.mesh(b).unwrap().triangle_count();
2250 assert_eq!((sa.len(), sb.len()), (n, n));
2251 }
2252}