Skip to main content

fast_mesh_scene/
query.rs

1//! World-space queries: raycasts, closest points and mesh-vs-mesh queries.
2//!
3//! Every query maps its world `f64` input into each mesh's local space (in
4//! `f64`, then rounded to `f32`), runs the Phase 3 query there and maps the
5//! result back. World bounds are rounded outward to `f32` and the results
6//! filtered again against the exact `f64` bound (spec §15.9).
7//!
8//! Queries over all meshes cull meshes by their world boxes ([`world_box`]).
9//! The culling test uses the exact world input, while the Phase 3 query sees
10//! it rounded to `f32` and mapped with a rotation that is orthonormal only
11//! within `1e-6`. So each test grows the box by a pad that covers both (see
12//! [`query_pad`]): culling never drops a mesh the local query would find
13//! something in.
14
15use 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
24/// Relative slack for a placement's rotation being orthonormal only within
25/// `1e-6` per entry, and for the re-orthonormalised relative rotation of
26/// mesh-vs-mesh queries (§15.2), with room to spare.
27pub(crate) const PLACEMENT_SLACK: f64 = 2e-5;
28
29/// Relative rounding slack of the `f32` arithmetic inside Phase 3 queries.
30const F32_SLACK: f64 = 16.0 * f32::EPSILON as f64;
31
32/// The smallest `f32` not below `x` (`x` not NaN).
33pub(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
38/// The largest `f32` not above `x` (`x` not NaN).
39pub(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
52/// The largest absolute coordinate of an `f32` point.
53fn max_abs(v: [f32; 3]) -> f64 {
54    v.iter().fold(0.0, |m, c| m.max(f64::from(c.abs())))
55}
56
57/// `S⁻¹ Rᵀ d`: a world direction in the mesh's local frame (see
58/// [`Placement::inverse_direction`](crate::Placement::inverse_direction)).
59fn 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
67/// An empty vector with room for `n` elements.
68pub(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
82/// Checks a distance-like bound: zero or greater, infinity allowed.
83pub(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
91/// `d` scaled to unit length.
92fn 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    // Scale first when squaring would overflow or underflow.
99    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
108/// The mesh's local box: its BVH root box, `None` when it has no triangle.
109fn local_box(entry: &Entry) -> Option<Aabb> {
110    entry.bvh.root_bounds().filter(|b| !b.is_empty())
111}
112
113/// The largest absolute local coordinate of the mesh's box (0 when empty).
114pub(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
118/// A world point in a mesh's local `f32` space.
119struct LocalPoint {
120    p: [f32; 3],
121    /// How far rounding to `f32` moved it.
122    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
137/// How much to grow the world box `wbox` of a mesh (placed by `placement`,
138/// with the local magnitude `mesh_mag`) so that a culling test with the
139/// exact world point `q` keeps every mesh in which the local query (from
140/// the `f32` point `local`, `err` from the exact local point, and for a ray
141/// an `f32` direction `err_dir` from the exact one) can find something.
142///
143/// Anything found lies in the local box, so within `far` of `q`. Mapped back
144/// to the world, the local query's point or ray strays from the exact one by
145/// at most its rounding (`err`, plus `err_dir` per unit of `t`), plus the
146/// rotation's distance from orthonormal times `|q - t|` and `t`, plus the
147/// `f32` rounding inside the query. A scale of `s` maps a local error to at
148/// most `s` times itself in the world, so every term grows by the largest
149/// scale component (1 for a rigid placement, which reads exactly as before).
150fn 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
167/// How much to grow the distance test between the world boxes `wa`, `wb` of
168/// `a`, `b` so that it keeps every pair that Phase 3b, run in A's space with
169/// the re-orthonormalised relative transform, finds within the margin. Local
170/// magnitudes cross into the world scaled (see [`query_pad`]), so each grows
171/// by its mesh's largest scale component.
172pub(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
186/// The world point of a mesh-local `f32` point.
187fn 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/// Options for [`Scene::raycast`], in world units.
201#[derive(Debug, Clone, Copy, PartialEq)]
202pub struct SceneRayOptions {
203    /// Smallest world distance that counts as a hit (inclusive). Default `0.0`.
204    pub min_t: f64,
205    /// Largest world distance that counts as a hit (inclusive). Default
206    /// `f64::INFINITY`.
207    pub max_t: f64,
208    /// Which faces to ignore. Default [`Cull::None`].
209    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/// Options for [`Scene::closest_point`] and [`Scene::closest_point_on`], in
223/// world units.
224#[derive(Debug, Clone, Copy, PartialEq)]
225pub struct SceneClosestOptions {
226    /// Largest world distance that counts as found (inclusive). Default
227    /// `f64::INFINITY`.
228    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
239/// One broadphase candidate of [`Scene::raycast`]: the padded world-box
240/// entry distance, the mesh and the ray in its local space.
241pub(crate) type RayCandidate = (f64, MeshId, Ray);
242
243/// One broadphase candidate of [`Scene::closest_point`]: the padded lower
244/// bound, the mesh and the query point in its local space.
245pub(crate) type ClosestCandidate = (f64, MeshId, [f32; 3]);
246
247/// A scaled pair query's frame (see [`Scene::bake_pair_inner`]): A's mesh
248/// in the frame (`None`: the real A), B's baked mesh with its BVH, and the
249/// frame-to-world distance factor.
250type BakedPair = (Option<(Mesh, Bvh)>, (Mesh, Bvh), f64);
251
252/// Where a world ray hit a mesh of the scene.
253#[derive(Debug, Clone, Copy, PartialEq)]
254pub struct SceneRayHit {
255    /// The mesh that was hit.
256    pub mesh: MeshId,
257    /// The hit as the Phase 3 raycast reported it, in the mesh's local space.
258    pub hit: RayHit,
259    /// The hit point in the world: `hit.point` placed by the mesh's origin
260    /// and placement.
261    pub point: [f64; 3],
262    /// The world distance from the ray's origin: `hit.t`, which is a world
263    /// distance because the mapped local ray keeps the parameter (the
264    /// placement is affine, so `apply(origin_l + t * dir_l)` is the world
265    /// point at `t`).
266    pub t: f64,
267}
268
269/// The point of the scene nearest to a world point.
270#[derive(Debug, Clone, Copy, PartialEq)]
271pub struct SceneClosestHit {
272    /// The mesh the point lies on.
273    pub mesh: MeshId,
274    /// The hit as the Phase 3 query reported it, in the mesh's local space.
275    pub hit: ClosestHit,
276    /// The nearest point in the world: `hit.point` placed by the mesh's
277    /// origin and placement.
278    pub point: [f64; 3],
279    /// The world distance from the query point. With a rigid placement it is
280    /// the local distance; with a uniform scale it is the local distance
281    /// times the scale; with a non-uniform scale the local query runs on a
282    /// world-baked copy, so it is that query's distance.
283    pub distance: f64,
284}
285
286/// The closest pair of points between two meshes, in the world.
287#[derive(Debug, Clone, Copy, PartialEq)]
288pub struct SceneMeshDistance {
289    /// The distance between the two surfaces (0 when they touch or
290    /// intersect), as Phase 3's `mesh_distance` reports it.
291    pub distance: f64,
292    /// The closest point on A's triangle `triangle_a`, in the world.
293    pub point_a: [f64; 3],
294    /// The closest point on B's triangle `triangle_b`, in the world.
295    pub point_b: [f64; 3],
296    /// The triangle of A that `point_a` lies on.
297    pub triangle_a: TriangleId,
298    /// The triangle of B that `point_b` lies on.
299    pub triangle_b: TriangleId,
300}
301
302impl Scene {
303    /// The nearest hit of a world ray over every mesh, with
304    /// `opts.min_t <= t <= opts.max_t` in world distance. Equal `t` goes to
305    /// the lower [`MeshId`]; within a mesh, Phase 3's rule (smallest
306    /// triangle id) applies.
307    ///
308    /// `dir` is normalised in `f64`, so `t` is a world distance. For each
309    /// mesh not culled by its world box, the ray is mapped into the mesh's
310    /// local space (the origin by the placement inverse, the direction by
311    /// its linear inverse, in `f64`, then rounded to `f32`) and Phase 3's
312    /// [`fast_mesh_query::raycast`] runs there, with the bounds rounded
313    /// outward to `f32`. The affine map keeps the ray parameter, so the
314    /// local `t` is the world `t` up to that rounding; hits are kept only
315    /// when it lies within the exact `f64` bounds, and a hit dropped just
316    /// below `min_t` does not hide one beyond it.
317    ///
318    /// Every Lazy BVH is brought up to date first (§15.4).
319    ///
320    /// # Errors
321    /// [`SceneError::InvalidInput`] for a non-finite origin, a zero or
322    /// non-finite direction, NaN bounds or `min_t > max_t`; then a BVH or
323    /// memory error from a Lazy update, or [`SceneError::StaleBvh`] for the
324    /// lowest stale Manual mesh; then [`SceneError::InvalidInput`] for an
325    /// origin out of `f32` range in some mesh's local space, or a memory
326    /// error.
327    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        // The order buffer is reused across calls: taken out here and put
343        // back cleared below, so queries after the first do not allocate.
344        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    /// [`Scene::raycast`] past its checks, over the reused `order` buffer.
352    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        // Broadphase: the meshes whose padded world box the ray enters within
360        // the bounds, nearest entry first.
361        for (id, e) in self.entries.iter_mut() {
362            // Empty meshes hold nothing: skip them before mapping the
363            // point, which may be out of f32 range in their local space.
364            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            // Nothing beyond the best hit so far can win.
393            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                // Only `min32` itself can fall below `min_t`: look past it.
406                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    /// The point of the scene nearest to `p`, within `opts.max_distance`.
429    /// Equal distances go to the lower [`MeshId`]; within a mesh, Phase 3's
430    /// rule (smallest triangle id) applies.
431    ///
432    /// Meshes are visited by the distance from `p` to their world box and
433    /// pruned by the best world distance so far. In each mesh not culled,
434    /// `p` is mapped into local space (in `f64`, then rounded to `f32`) and
435    /// Phase 3's [`fast_mesh_query::closest_point`] runs there: with a rigid
436    /// placement the local distance is the world distance; with a uniform
437    /// scale the local bound is the world bound divided by the scale and the
438    /// local distance is multiplied back; with a non-uniform scale the query
439    /// runs on a world-baked copy of the mesh (see [`baked_mesh`]). The
440    /// world distance must then be within the exact bound.
441    ///
442    /// Every Lazy BVH is brought up to date first (§15.4).
443    ///
444    /// # Errors
445    /// [`SceneError::InvalidInput`] for a non-finite `p` or a negative or
446    /// NaN `max_distance`; then a BVH or memory error from a Lazy update, or
447    /// [`SceneError::StaleBvh`] for the lowest stale Manual mesh; then
448    /// [`SceneError::InvalidInput`] for a `p` out of `f32` range in some
449    /// mesh's local space (or in the world, for a non-uniformly scaled
450    /// mesh), or a memory error.
451    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        // The order buffer is reused across calls: taken out here and put
461        // back cleared below, so queries after the first do not allocate.
462        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    /// [`Scene::closest_point`] past its checks, over the reused `order`
470    /// buffer.
471    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            // Empty meshes hold nothing: skip them before mapping the
479            // point, which may be out of f32 range in their local space.
480            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    /// The nearest point on mesh `id` to the world point `p` (`q`: `p` in
514    /// the mesh's local `f32` space), within `bound` (a world distance, at
515    /// most the query's `max_distance` from pruning): the local Phase 3
516    /// query for rigid and uniformly scaled meshes, a world-baked copy for
517    /// non-uniformly scaled ones (see [`baked_mesh`]). The hit stays in the
518    /// mesh's local space; the point and the distance are in the world.
519    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            // A uniform scale multiplies every distance: the local query
538            // runs with the world bound divided by it.
539            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        // A non-uniform scale distorts distances, so the query runs on a
553        // copy baked into the world, where frame distances are world
554        // distances. Triangle ids, barycentric coordinates and features
555        // carry over (affine maps preserve them); the hit's local point
556        // and distance are mapped back.
557        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    /// [`Scene::closest_point`] on mesh `id` alone. Only that mesh's BVH is
600    /// brought up to date (§15.4).
601    ///
602    /// # Errors
603    /// [`SceneError::UnknownMesh`] for a stale id; then as for
604    /// [`Scene::closest_point`], with [`SceneError::StaleBvh`] only when this
605    /// mesh is a stale Manual one.
606    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    /// Checks both ids, then brings both BVHs up to date (§15.4) and returns
623    /// the entries with the transform from B's local space to A's (§5,
624    /// re-orthonormalised per §15.2). Only for rigid pairs; scaled pairs run
625    /// on baked copies (see [`Scene::bake_pair`]).
626    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    /// Whether the pair's relative map is rigid (both placements unscaled).
639    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    /// The frame a scaled pair query runs in, with B baked into it: A's
645    /// local space when A is rigid or uniformly scaled (frame distances
646    /// times A's scale are world distances), else the world with A baked
647    /// too. Slot ids match the real meshes, so triangle ids carry over; the
648    /// map is a positive-determinant affine map, so intersections carry
649    /// over too. The caller brings both BVHs up to date first.
650    ///
651    /// Returns A's mesh in the frame (`None`: the real A), B's baked mesh
652    /// with its BVH, and the frame-to-world distance factor.
653    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    /// [`Scene::bake_pair_inner`] after bringing both BVHs up to date
706    /// (§15.4).
707    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    /// Maps a frame point of a scaled pair query back to the world: through
713    /// A's placement when A is real (rigid or uniform), directly when the
714    /// frame is the world.
715    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    /// The mesh's BVH, in mesh-local `f32` space, for running Phase 3
725    /// queries directly. Never updates it.
726    ///
727    /// # Errors
728    /// [`SceneError::UnknownMesh`] for a stale id, or
729    /// [`SceneError::StaleBvh`] when the BVH is not current with the mesh
730    /// (see [`Scene::is_current`]); bring it up to date with
731    /// [`Scene::update`] or [`Scene::flush`] first.
732    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    /// The transform from mesh `b`'s local space to mesh `a`'s local space,
741    /// origins included: the one the pair queries ([`Scene::intersects`],
742    /// [`Scene::mesh_distance`], [`Scene::triangles_near`]) pass to Phase 3,
743    /// in the convention of [`Rigid`]. Its rotation is re-orthonormalised,
744    /// so it always passes `Rigid::validate`. Only for rigid pairs: with a
745    /// scaled placement on either side the relative map is affine, not
746    /// rigid (pair queries then run on baked copies instead).
747    ///
748    /// # Errors
749    /// [`SceneError::UnknownMesh`] for a stale id;
750    /// [`SceneError::InvalidInput`] when either placement is scaled.
751    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    /// A world point in mesh `id`'s local coordinates, origin included:
767    /// `S⁻¹ Rᵀ (q - t) - o`, in `f64`.
768    ///
769    /// # Errors
770    /// [`SceneError::UnknownMesh`] for a stale id.
771    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    /// A point in mesh `id`'s local coordinates in the world, origin
777    /// included: `R (S (o + p)) + t`, in `f64`.
778    ///
779    /// # Errors
780    /// [`SceneError::UnknownMesh`] for a stale id.
781    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    /// Whether any triangle of mesh `a` intersects or touches any triangle of
793    /// mesh `b`, as placed in the world: Phase 3's
794    /// [`fast_mesh_query::intersects`] with the relative transform of B's
795    /// local space into A's. Passing the same id twice means two coincident
796    /// copies, as in Phase 3. Only these two BVHs are brought up to date.
797    ///
798    /// With a scaled placement on either side the query runs on a copy
799    /// baked into A's local space (or into the world when A is
800    /// non-uniformly scaled; see [`Scene::bake_pair_inner`]): same result,
801    /// up to the copy's `f32` rounding.
802    ///
803    /// # Errors
804    /// [`SceneError::UnknownMesh`]; then a BVH or memory error from a Lazy
805    /// update, or [`SceneError::StaleBvh`] when either is a stale Manual
806    /// mesh (the lower id first).
807    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            // Two coincident copies of one mesh.
818            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    /// The distance between the surfaces of meshes `a` and `b` as placed in
843    /// the world, with a closest pair of points in the world, or `None` when
844    /// it is greater than `max_distance` (inclusive, may be infinite) or a
845    /// mesh has no triangle.
846    ///
847    /// Phase 3's [`fast_mesh_query::mesh_distance`] runs in A's space with
848    /// `max_distance` rounded up to `f32`; the result is kept only when its
849    /// distance is within the exact bound. Distances carry over unchanged
850    /// with rigid placements, times A's uniform scale otherwise; with a
851    /// non-uniformly scaled A the query runs on copies baked into the world
852    /// (see [`Scene::bake_pair_inner`]). Only these two BVHs are brought up
853    /// to date.
854    ///
855    /// # Errors
856    /// [`SceneError::UnknownMesh`]; [`SceneError::InvalidInput`] for a
857    /// negative or NaN `max_distance`; then as for [`Scene::intersects`].
858    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                    // Both points are in A's space.
882                    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            // Two coincident copies: distance 0 wherever both have a
890            // triangle, with a closest pair from the mesh itself.
891            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                // Both points are in A's frame.
929                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    /// Appends to `out_a` the triangles of mesh `a` within `margin` of mesh
939    /// `b`, and to `out_b` those of `b` within `margin` of `a`, as placed in
940    /// the world: Phase 3's [`fast_mesh_query::triangles_near`] in A's space.
941    /// Elements already in the vectors stay in front; each appended part is
942    /// ascending without duplicates. Only these two BVHs are brought up to
943    /// date. With a scaled placement on either side the query runs on a copy
944    /// baked into A's local space (or into the world when A is
945    /// non-uniformly scaled); passing the same id twice lists every alive
946    /// triangle of the mesh in both vectors.
947    ///
948    /// `margin` is rounded up to `f32`. The lists cannot be filtered again
949    /// per triangle, so when `margin` is not an `f32` value they may hold
950    /// triangles up to one `f32` step (relative `6e-8`) beyond it.
951    ///
952    /// # Errors
953    /// [`SceneError::UnknownMesh`]; [`SceneError::InvalidInput`] for a
954    /// negative or NaN `margin`; then as for [`Scene::intersects`], or
955    /// [`SceneError::Query`] with `OutOfMemory` when a vector cannot grow. On
956    /// any error both vectors keep their lengths.
957    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            // Every triangle is within `margin` of itself.
983            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/// An axis-aligned box in world `f64` coordinates.
1022#[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    /// The box holding nothing.
1030    pub(crate) const EMPTY: Aabb64 = Aabb64 {
1031        min: [f64::INFINITY; 3],
1032        max: [f64::NEG_INFINITY; 3],
1033    };
1034
1035    /// True when the box holds no point.
1036    pub(crate) fn is_empty(&self) -> bool {
1037        (0..3).any(|k| self.min[k] > self.max[k])
1038    }
1039
1040    /// The centre and half extents (for a non-empty box).
1041    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    /// Distance from `p` to the box; 0 inside.
1048    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    /// Distance between two boxes; 0 when they overlap.
1055    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    /// The smallest `t` in `[t0, t1]` at which the ray `o + t d` is inside
1064    /// the box grown by `pad`, or `None`.
1065    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            // `max`/`min` skip a NaN (0 / tiny overflow): no constraint.
1077            lo = lo.max(a.min(b));
1078            hi = hi.min(a.max(b));
1079        }
1080        (lo <= hi).then_some(lo)
1081    }
1082}
1083
1084/// A world box cached on an [`Entry`](crate::scene::Entry), with the key
1085/// that validates it: the mesh revision (equal revisions mean equal
1086/// content), the origin and the placement. Valid only while the BVH is
1087/// current; queries bring it up to date first.
1088#[derive(Debug, Clone, Copy, PartialEq)]
1089pub(crate) struct CachedBox {
1090    rev: u64,
1091    origin: [f64; 3],
1092    placement: Placement,
1093    bounds: Aabb64,
1094    /// The mesh's local magnitude (see [`local_mag`]), from the same root
1095    /// box, for [`query_pad`] without another lookup.
1096    mag: f64,
1097}
1098
1099/// The entry's world box and local magnitude, cached across queries. A hit
1100/// needs only the revision, the origin and the placement to match: equal
1101/// revisions mean equal content, and the root box is the bounds of that
1102/// content whatever the tree looks like. Queries bring the BVH up to date
1103/// first, and [`Entry::flag_unknown`] clears the cache with the rebuild
1104/// flag, so a hit never reads a stale tree.
1105pub(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
1128/// A world box holding every triangle of the mesh: its BVH root box (local
1129/// `f32`, plus the origin) placed by the transform, as centre ± `|R S|`
1130/// half extents, grown by an `f64` rounding bound so it stays conservative.
1131/// Empty for a mesh with no triangle.
1132pub(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        // Every rounding above is relative to at most this size.
1149        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    /// A rotation by `deg` degrees about the axis `(1, 2, 2) / 3`.
1171    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    /// The closed box `[min, min + size]`, 12 triangles.
1194    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    /// A unit square in the plane `x = x0` spanning `y, z` in `[-s, s]`.
1208    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    /// A cube built from `f64` positions far from zero, so its origin is not
1214    /// zero.
1215    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    /// `Rᵀ (q - t) - o`, written out independently of the crate's helpers.
1227    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    /// A scene of three placed cubes: integer, quarter-turn and tilted.
1250    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    /// The nearest hit by calling Phase 3 on every mesh with hand-computed
1273    /// local rays (default bounds).
1274    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        // A direction of any length gives the same world distances.
1353        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    /// The relative transform from B's local space to A's, by hand, for
1390    /// quarter-turn placements (where Gram-Schmidt changes nothing).
1391    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            // Overlaps the first.
1419            scene
1420                .add_mesh(cube([0.0; 3], 1.0), place(RZ90, [7.5, 0.5, 0.5]))
1421                .unwrap(),
1422            // 0.75 beyond the first along +y.
1423            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        // `low` (index 0) and `high` (index 1) both have a wall at x = 5, but
1467        // `high`'s box starts at x = 1, so it is visited first.
1468        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        // A nearer mesh wins whatever its id: a wall turned about x (which
1488        // keeps it in its plane) and moved to x = 3.
1489        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        // Two walls at x = 2, one unplaced and one turned and moved there:
1498        // the lower id wins the tie.
1499        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        // Bounds pick among meshes too: `turned` (at 2) is skipped from
1527        // 2.5 on, and `flat` (now at 2.5) from 2.75 on.
1528        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        // Two walls at distance 2 from the origin: `low` (index 0) at x = 2,
1547        // `high` (index 1) moved to x = -2, whose larger pad sorts it first.
1548        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        // Two coincident walls tie exactly too.
1558        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        // A nearer mesh wins whatever its id.
1564        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        // Move the Lazy wall to x = 4: the next query sees the move.
1616        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        // A stale Manual mesh fails every query over all meshes, and the
1634        // named queries that need it.
1635        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        // The Lazy mesh was still updated.
1644        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        // Queries naming only other meshes still run.
1657        assert!(scene.closest_point_on(lazy, o, &near).unwrap().is_some());
1658        assert_eq!(scene.intersects(lazy, lazy), Ok(true));
1659        // Once updated, the Manual mesh takes part again.
1660        scene.update(manual).unwrap();
1661        assert!(scene.raycast(o, x, &ray).unwrap().is_some());
1662        // A Manual mesh flagged for a rebuild is stale even though its BVH
1663        // matches the mesh's revision.
1664        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        // Unknown ids.
1685        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        // 1e-12 is far below an f32 ulp at 3, so each bound below rounds to
1701        // 3.0 in f32 one way or the other.
1702        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        // The hit is at exactly t = 3.
1717        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        // A second wall at 4: a hit dropped below `min_t` does not hide it.
1724        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        // Also within one mesh: both walls in the same mesh.
1730        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        // Closest point: the wall is at distance exactly 3 from the origin.
1743        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        // Mesh distance: a second wall at x = 6, 3 away.
1763        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        // A huge but finite direction is fine.
1799        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        // An empty mesh far beyond f32 range: mapping the query point into
1862        // its local space would fail, but it holds nothing and must be
1863        // skipped first.
1864        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    /// World boxes are cached across queries and recomputed when the mesh
1880    /// or its placement changes.
1881    #[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        // A placement change recomputes the box on the next query.
1900        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        // So does an edit: without triangles the cached box is empty.
1917        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        // The transform is the one the pair queries run with.
1964        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        // Local and world points round-trip through the placement and origin.
1976        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        // B-local to A-local agrees with going through the world.
1987        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        // `cube([0, 0, 0], 2)` is [0, 2]^3 with origin [1, 1, 1].
2016        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        // Uniform scale 2 about the origin of the stored coordinates: the
2026        // world box is [0, 4]^3.
2027        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        // Non-uniform scale plus translation: x in [10, 14], y in [1, 2],
2040        // z in [-1, 0].
2041        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        // A's cube stays [0, 2]^3; B's is scaled by [2, 1, 1] and moved to
2063        // x in [10, 14].
2064        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        // A ray down +x from x = 3 misses A (which ends at 2) and hits B's
2078        // x = 10 face at t = 7.
2079        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        // The closest point to [5, 1, 1] is B's x = 10 face... no: A ends
2091        // at 2 (distance 3) and B starts at 10 (distance 5), so A wins with
2092        // distance 3.
2093        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        // From [8, 1, 1] B wins: its face at distance 2.
2099        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        // Its local point is on the cube's x = 0 face (stored space; the
2107        // f32-built cube's origin is zero).
2108        assert!((f64::from(hit.hit.point[0]) - 0.0).abs() < 1e-6, "{hit:?}");
2109        // A rigid twin with the same world shape ([10, 14] x [0, 2]^2,
2110        // built directly): scaled queries agree with rigid ones on it.
2111        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                    // The reported world point maps back to the local hit.
2153                    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        // closest_point_on with a bound: beyond the scaled distance it is None.
2165        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        // A's cube stays [0, 2]^3.
2179        let a = scene
2180            .add_mesh(cube([0.0; 3], 2.0), MeshOptions::default())
2181            .unwrap();
2182        // B's cube scaled by [2, 1, 1] with translation [1, 0, 0]: the world
2183        // shape [1, 5] x [0, 2]^2, overlapping A in x in [1, 2].
2184        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        // Moved to [3, 7] x [0, 2]^2: a gap of 1 along x.
2198        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        // Non-uniform scales on both sides, measured in the world: B scaled
2221        // by [2, 1, 0.5] moved to x in [4, 8], z in [0, 1].
2222        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        // A ends at x = 2, B starts at x = 4: the gap is 2.
2235        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        // A scaled pair is not rigid.
2239        assert!(matches!(
2240            scene.relative_transform(a, b),
2241            Err(SceneError::InvalidInput(_))
2242        ));
2243        // The same mesh twice is coincident whatever its scale.
2244        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}