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}