Skip to main content

ifc_lite_geometry/
geom_bounds.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5//! The #1891 follow-on world AABB: the unquantized `f64` box the fingerprinting
6//! pass accumulates alongside the hash, and the well-formedness rule that
7//! decides when it may be published.
8//!
9//! Split out of the parent `geom_hash` module (whose child it is, so it reaches
10//! [`GeometryHasher`]'s private `min`/`max` accumulators directly) because
11//! bounds are a separate subject from the fingerprint: the hash answers "is
12//! this the same shape", the box answers "where is it and how big". The box
13//! also carries a long well-formedness rationale — the case where a NaN
14//! coordinate leaves ONE axis at its sentinel while its neighbours hold real
15//! bounds — which is about malformed position buffers, not about hashing, and
16//! reads as noise in the hashing file.
17
18use super::GeometryHasher;
19
20impl GeometryHasher {
21    /// Grow the world AABB by one reconstructed corner.
22    ///
23    /// `f64::min`/`f64::max` return the non-NaN operand, so a NaN coordinate
24    /// (a malformed position buffer) is skipped rather than poisoning the box.
25    ///
26    /// `pub(super)` — the parent's `add_oriented_mesh` is the only caller; the
27    /// accumulators are not open for writing from outside this pair of modules.
28    #[inline]
29    pub(super) fn extend_bounds(&mut self, world: &[f64; 3]) {
30        for k in 0..3 {
31            self.min[k] = self.min[k].min(world[k]);
32            self.max[k] = self.max[k].max(world[k]);
33        }
34    }
35
36    /// The entity's world-space AABB as `[minx, miny, minz, maxx, maxy, maxz]`,
37    /// or `None` if the box is not well-formed on all three axes — which
38    /// covers both "no in-range triangle corner was ever seen" and the
39    /// partial-accumulation case below.
40    ///
41    /// ### Why all three axes are tested, not just one
42    ///
43    /// The axes look like they must accumulate together — `extend_bounds` runs
44    /// the same loop over all three for every corner — but they do not, because
45    /// that loop is `f64::min`/`f64::max`, which DROP a NaN operand. A position
46    /// buffer carrying NaN on one axis and finite values on the others leaves
47    /// that axis at its `INFINITY..NEG_INFINITY` sentinel while its neighbours
48    /// hold real bounds. Testing only axis 0 then returns
49    /// `Some([x0, inf, z0, x1, -inf, z1])` — an inverted, infinite axis
50    /// presented as a measured box, which downstream differences to NaN.
51    /// Requiring every axis to be finite and ordered turns that into `None`,
52    /// which the wire format already reserves NaN slots for
53    /// (`MeshCollection::push_geometry_hash`).
54    ///
55    /// The hash makes no such promise: NaN quantizes to 0, so a NaN-carrying
56    /// entity still produces a fingerprint. `Some(hash)` with `None` box is
57    /// therefore a REACHABLE pair, not a structural impossibility, and
58    /// `produce_element_meshes` keeps the hash when it happens rather than
59    /// discarding both. The fingerprint is the diff engine's primary signal and
60    /// is well-defined here; dropping it would remove the element from the
61    /// comparison in exchange for nothing, and it cannot desynchronize the
62    /// parallel FFI arrays because `push_geometry_hash` writes six NaNs for a
63    /// missing box instead of shortening the array (pinned by
64    /// `a_missing_box_reserves_its_slots_instead_of_shifting_the_array`).
65    ///
66    /// The converse stays impossible: a `Some` box needs an accumulated corner,
67    /// which needs a triangle, which `is_empty()` already gates on.
68    ///
69    /// UNQUANTIZED `f64` world coordinates, not grid indices: the box is meant
70    /// to be read as a length, so snapping it to the hash tolerance would put a
71    /// millimetre of noise on every face for no benefit. It is RTC-invariant
72    /// for the same reason the hash is — both are built from the reconstructed
73    /// world coordinate.
74    ///
75    /// This is the diff engine's "did it MOVE?" signal. The hash answers only
76    /// "is it different"; comparing two boxes separates a translation (same
77    /// extent, shifted centre) from a reshape (different extent) from pure
78    /// re-tessellation (identical box, different hash).
79    ///
80    /// The companion [`Self::volume`] exists but is far narrower — it is
81    /// `None` for a large minority of entities, by design.
82    pub fn world_aabb(&self) -> Option<[f64; 6]> {
83        let well_formed = (0..3).all(|k| {
84            self.min[k].is_finite() && self.max[k].is_finite() && self.min[k] <= self.max[k]
85        });
86        if !well_formed {
87            return None;
88        }
89        Some([
90            self.min[0], self.min[1], self.min[2], self.max[0], self.max[1], self.max[2],
91        ])
92    }
93}