ifc_lite_geometry/kernel/
mesh_volume.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//! Public, documented home for reading a [`Mesh`]'s volume.
6
7use super::mesh_bridge::mesh_to_tris;
8use super::signed_volume::signed_volume_of;
9use crate::mesh::Mesh;
10
11/// Signed volume of a `Mesh`, via the divergence theorem.
12///
13/// Positive for a closed, outward-wound mesh; negative if inward-wound;
14/// meaningless if the mesh is not closed — the divergence theorem this sum
15/// implements requires a closed surface, and an open one has no true volume
16/// to read regardless of where it sits. That reading is translation-stable
17/// only up to a bounded QUANTIZATION noise floor, not exactly
18/// translation-invariant: [`mesh_to_tris`] rounds every coordinate to
19/// `kernel::mesh_bridge::SNAP_GRID` (1/65536 m, ~15.26 µm) before this
20/// function ever sees it, and a non-grid-aligned translation moves each
21/// vertex's rounding independently, so the sum drifts by roughly
22/// `surface_area * SNAP_GRID` (plus, at far-from-origin offsets, the
23/// coarser f32-ulp term the `Mesh` positions already carried on the way in).
24/// Because it delegates to `signed_volume6`, which sums about the
25/// operand's own AABB centre rather than a fixed point, that drift is the
26/// ONLY thing that moves the reading — a world-origin implementation would
27/// additionally vary with the reference point itself, wrong by orders of
28/// magnitude more (see the
29/// `mesh_volume_is_stable_far_from_the_world_origin_for_an_open_mesh` test
30/// below, which pins the AABB-centred reading's noise floor and would fail
31/// hard against that class of regression). It is still not the mesh's actual
32/// volume. Callers that need a trustworthy reading (e.g. reporting a split
33/// zone piece's volume) must establish closedness first, same requirement
34/// `signed_volume6` itself carries.
35///
36/// Delegates to `signed_volume_of` - itself one line over `signed_volume6`,
37/// which returns SIX times the volume - rather than dividing here as well.
38/// #2579 landed that helper an hour before this file did, with a doc that says
39/// exactly why the divide belongs in one place: "a caller that wants the VOLUME
40/// divides once, here, rather than growing a second hand-rolled sum in another
41/// module, which is how two producers of the same number start to disagree."
42///
43/// The reference point is what makes the shared implementation matter:
44/// `signed_volume6` deliberately sums about the OPERAND'S OWN AABB CENTRE
45/// rather than the world origin. That
46/// choice is not cosmetic — see `signed_volume::signed_volume6`'s doc for the
47/// #198779 incident where a world-origin reference turned a crack sliver on a
48/// far-from-origin operand into a wildly wrong, sign-flipping volume. A split
49/// zone piece is exactly the shape of operand that provokes this: it can sit
50/// anywhere in a building/national-grid-scale model and can carry the same
51/// kind of boundary sliver from the cut that produced it, so this function
52/// inherits the AABB-centred reference rather than exposing a second,
53/// world-origin-referenced volume primitive alongside it.
54///
55/// This is a raw, UNGATED kernel primitive — it reads whatever `Mesh` it is
56/// handed, closed or not, and (via [`mesh_to_tris`]) silently drops any
57/// out-of-range-index or non-finite triangle. It is the low-level counterpart
58/// to `geom_closure::GeometryHasher::volume`, which is the crate's
59/// closedness-GATED, per-entity volume: that one returns `None` unless the
60/// hasher's accumulated `GeometryClosure` proves the entity is a single
61/// closed orientable solid, but it requires that accumulated per-entity
62/// state — it has nothing to gate a bare, freshly-produced `Mesh` (e.g. a
63/// zone-split piece) that never went through the hasher. A caller of THIS
64/// function that needs the same trustworthiness guarantee must establish
65/// closedness itself first — `router::voids::geom::mesh_is_closed_exact` is
66/// the crate's existing closed-surface check but is `pub(super)`-scoped to
67/// that module today, so a caller outside it needs either that visibility
68/// widened or an equivalent check of its own — same requirement
69/// `GeometryHasher::volume`'s own gate establishes upstream.
70///
71/// `router::voids::geom::mesh_signed_volume` folds the same sum over the f32
72/// positions unsnapped and panics on a bad index; this one reads the kernel
73/// grid through `mesh_to_tris`.
74pub fn mesh_volume(mesh: &Mesh) -> f64 {
75    signed_volume_of(&mesh_to_tris(mesh))
76}
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81    use crate::kernel::arrangement::cube_mesh;
82    use crate::kernel::mesh_bridge::tris_to_mesh;
83
84    #[test]
85    fn mesh_volume_of_a_closed_cube_is_correct() {
86        let cube = tris_to_mesh(&cube_mesh(0.0, 2.0)); // side 2 → vol 8
87        let v = mesh_volume(&cube);
88        assert!((v - 8.0).abs() < 1.0e-9, "expected 8.0, got {v}");
89    }
90
91    /// The property that makes this the right function to expose (see the
92    /// fn's own doc, and `signed_volume::signed_volume6`'s doc for the
93    /// #198779 incident this reproduces the shape of): for an OPEN surface —
94    /// a mesh with an unresolved boundary, e.g. a zone piece carrying a
95    /// boundary sliver from the cut that produced it — the divergence sum
96    /// referenced to a FIXED point is translation-variant, so a world-origin
97    /// implementation reads a different (and for a far-from-origin operand,
98    /// wildly different) value depending only on where the model sits. The
99    /// AABB-centred reference this function actually uses co-moves with the
100    /// operand, so the reading must hold up to a bounded quantization noise
101    /// floor regardless of that translation. A world-origin implementation,
102    /// tested the same way, would NOT hold — this is the reproduction shape
103    /// for that class of implementation bug.
104    ///
105    /// Deliberately NOT grid-aligned: an earlier version of this test used
106    /// integer cube corners and an offset that was an exact multiple of
107    /// `SNAP_GRID` (1/65536 m), which makes BOTH the f32 store and the
108    /// reconciliation snap in [`mesh_to_tris`] exact no-ops — so it could
109    /// only ever discriminate the AABB-centred-vs-world-origin reference
110    /// choice, never the quantization noise those two steps genuinely
111    /// introduce for a real-world placement (a caught house review finding:
112    /// "a test that can't fail" against the property it was named for). This
113    /// version uses off-grid corners and a non-multiple offset, so the snap
114    /// noise is real and the tolerance below is sized to it rather than to
115    /// bit-exactness — see the `..._is_not_bit_exact_across_a_non_grid_translation`
116    /// test for the proof that this specific loosening was necessary.
117    #[test]
118    fn mesh_volume_is_stable_far_from_the_world_origin_for_an_open_mesh() {
119        let mut tris = cube_mesh(0.1, 2.3); // off-grid corners, side 2.2
120        tris.pop(); // drop one triangle → open boundary (one missing face)
121        let near = tris_to_mesh(&tris);
122        let mut far = near.clone();
123        for c in far.positions.as_chunks_mut::<3>().0 {
124            c[0] += 10_000.1; // off-grid, far-from-origin offset
125            c[1] += 10_000.1;
126            c[2] += 10_000.1;
127        }
128        let v_near = mesh_volume(&near);
129        let v_far = mesh_volume(&far);
130        let drift = (v_near - v_far).abs();
131        // Bound sized to the documented noise floor (surface_area * SNAP_GRID
132        // plus f32 ulp at a 10 km offset), NOT to bit-exactness — see the doc
133        // comment above and `mesh_volume`'s own doc. A world-origin
134        // regression drifts by ~6.7e3 on this exact fixture shape (measured
135        // separately, see PR #2260 review thread), thousands of times past
136        // this bound, so the test still catches that class of bug.
137        assert!(
138            drift < 1.0e-2,
139            "near={v_near} far={v_far} drift={drift} exceeds the documented \
140             quantization noise floor for an AABB-centred reading"
141        );
142    }
143
144    /// Proves the loosened tolerance above is load-bearing, not decorative:
145    /// re-run the exact same off-grid fixture at the OLD 1e-6 bit-exactness
146    /// bound the previous (grid-exact) test used, and watch it fail. This is
147    /// the RED half of the RED/GREEN pair — without the off-grid corners and
148    /// offset, this assertion would spuriously pass, which is exactly the
149    /// "test that can't fail" defect being fixed.
150    #[test]
151    fn mesh_volume_is_not_bit_exact_across_a_non_grid_translation() {
152        let mut tris = cube_mesh(0.1, 2.3);
153        tris.pop();
154        let near = tris_to_mesh(&tris);
155        let mut far = near.clone();
156        for c in far.positions.as_chunks_mut::<3>().0 {
157            c[0] += 10_000.1;
158            c[1] += 10_000.1;
159            c[2] += 10_000.1;
160        }
161        let drift = (mesh_volume(&near) - mesh_volume(&far)).abs();
162        assert!(
163            drift > 1.0e-6,
164            "expected the off-grid translation to show measurable snap/f32 \
165             drift (drift={drift}); if this now passes, mesh_to_tris's \
166             quantization changed and the doc/tolerance above need revisiting"
167        );
168    }
169}