Skip to main content

ifc_lite_geometry/
geom_accumulate.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//! Folding one mesh segment's triangles into the hasher's private
6//! accumulators (vertex set, per-plane area, world bounds, volume, closure).
7//!
8//! Split out of the parent `geom_hash` module (whose child it is, so it
9//! reaches [`GeometryHasher`]'s private fields directly) because HOW a
10//! segment's triangles get folded in — the world reconstruction, the
11//! quantization, the per-triangle degenerate/volume/bounds bookkeeping — is a
12//! separate subject from what the surface channels mean ([`super::surface`])
13//! or what the struct/gate around them expose.
14
15use super::surface::{self, plane_of, vertex_hash};
16use super::{quantize, GeometryHasher};
17use crate::kernel::signed_volume::tetra_volume6;
18use crate::mesh_orient::OrientVerdict;
19
20impl GeometryHasher {
21    /// Reconstruct the full `f64` WORLD coordinate of one vertex. `origin` is
22    /// the per-mesh local-frame origin (`world = origin + position`); pass
23    /// `[0.0; 3]` for absolute-coordinate positions.
24    #[inline]
25    fn world(&self, positions: &[f32], vi: usize, origin: &[f64; 3]) -> [f64; 3] {
26        let base = vi * 3;
27        [
28            positions[base] as f64 + origin[0] + self.rtc[0],
29            positions[base + 1] as f64 + origin[1] + self.rtc[1],
30            positions[base + 2] as f64 + origin[2] + self.rtc[2],
31        ]
32    }
33
34    /// Snap a reconstructed world corner to the quantization grid.
35    #[inline]
36    fn quantize_corner(&self, world: &[f64; 3]) -> [i64; 3] {
37        [
38            quantize(world[0], self.inv_tol),
39            quantize(world[1], self.inv_tol),
40            quantize(world[2], self.inv_tol),
41        ]
42    }
43
44    /// Add one mesh segment (a flat `[x,y,z, ...]` position buffer and a
45    /// triangle index buffer). Indices that run past the position buffer or
46    /// trailing non-triangle remainder are skipped defensively.
47    pub fn add_mesh(&mut self, positions: &[f32], indices: &[u32]) {
48        self.add_mesh_with_origin(positions, indices, [0.0; 3]);
49    }
50
51    /// Like [`Self::add_mesh`] but for positions stored in a per-element LOCAL
52    /// frame: `origin` (the per-mesh AABB-centre origin) is folded back so the
53    /// hash is over absolute world coordinates. This keeps the fingerprint
54    /// identical whether the producer emitted absolute positions (native) or
55    /// local + origin (the wasm local-frame path), and still detects element
56    /// MOVES.
57    ///
58    /// The segment carries no topology verdict, so it counts as NOT a closed
59    /// solid and permanently disarms [`Self::volume`]. Producers that ran
60    /// [`crate::orient_mesh_outward_verdict`] on this exact buffer should call
61    /// [`Self::add_oriented_mesh`] instead.
62    pub fn add_mesh_with_origin(&mut self, positions: &[f32], indices: &[u32], origin: [f64; 3]) {
63        self.add_oriented_mesh(positions, indices, origin, OrientVerdict::INDETERMINATE);
64    }
65
66    /// [`Self::add_mesh_with_origin`] for a segment the producer just ran the
67    /// outward-orienter over, passing that pass's [`OrientVerdict`] along.
68    ///
69    /// `verdict` MUST describe this exact position/index buffer — the volume
70    /// below is only as honest as the closedness claim behind it. Anything
71    /// short of a single closed orientable component disarms the element's
72    /// volume permanently; see [`Self::volume`].
73    pub fn add_oriented_mesh(
74        &mut self,
75        positions: &[f32],
76        indices: &[u32],
77        origin: [f64; 3],
78        verdict: OrientVerdict,
79    ) {
80        // Σ 6·V for THIS segment, referenced to its own first in-range corner
81        // (`vol_ref`). Any reference gives the same total on a closed surface,
82        // but referencing a point ON the surface keeps every operand bounded by
83        // the segment's own diameter — a georeferenced model at 1e5 m would
84        // otherwise multiply three ~1e5 coordinates and cancel a ~1 m³ answer
85        // out of ~1e15, losing every significant digit.
86        let mut seg_volume6 = 0.0f64;
87        let mut vol_ref: Option<[f64; 3]> = None;
88        let vertex_limit = positions.len() / 3;
89        let triangle_end = indices.len() - (indices.len() % 3);
90        let mut i = 0;
91        while i < triangle_end {
92            let i0 = indices[i] as usize;
93            let i1 = indices[i + 1] as usize;
94            let i2 = indices[i + 2] as usize;
95            i += 3;
96            if i0 >= vertex_limit || i1 >= vertex_limit || i2 >= vertex_limit {
97                continue;
98            }
99
100            let world = [
101                self.world(positions, i0, &origin),
102                self.world(positions, i1, &origin),
103                self.world(positions, i2, &origin),
104            ];
105
106            // Bounds take EVERY in-range corner, including those of triangles
107            // the hash rejects as post-quantization degenerate below. A sliver
108            // or zero-area face carries no shape signal for the fingerprint,
109            // but its corners are real geometry and do contribute extent —
110            // dropping them would under-report the element's box.
111            for corner in &world {
112                self.extend_bounds(corner);
113            }
114
115            // Volume accumulates HERE, from `world`, whose corners are still in
116            // the buffer's authored order. The quantized copy `tri` below is
117            // SORTED (that is what makes the fingerprint winding-invariant), so
118            // anything downstream of that sort has no winding left to integrate.
119            //
120            // Every in-range triangle counts, including the ones the hash drops
121            // as post-quantization degenerate: a sub-millimetre sliver carries
122            // no shape signal for a fingerprint, but it is part of the closed
123            // surface, and its (near-zero) flux belongs in the sum.
124            let o = *vol_ref.get_or_insert(world[0]);
125            seg_volume6 += tetra_volume6(&world[0], &world[1], &world[2], &o);
126
127            // Sort the three quantized corners so triangle winding and the
128            // starting vertex don't affect the hash — only the (multiset of)
129            // positions and their adjacency as a triangle.
130            let mut tri = [
131                self.quantize_corner(&world[0]),
132                self.quantize_corner(&world[1]),
133                self.quantize_corner(&world[2]),
134            ];
135            tri.sort_unstable();
136
137            // Skip degenerate (zero-area) triangles. After quantization,
138            // coincident or colinear corners carry no shape signal, and
139            // counting them lets triangulation noise (sliver/zero-area faces)
140            // flip the fingerprint even when the rendered geometry is
141            // unchanged. `edge_cross` returns `None` for exactly those.
142            let Some(cross) = surface::edge_cross(&tri) else {
143                continue;
144            };
145
146            // Channel 1 — the vertex SET: every corner of every surviving
147            // triangle, deduplicated. A retriangulation reconnects the same
148            // corners, so this is exactly what it cannot move.
149            for corner in tri {
150                if self.vertices.insert(corner) {
151                    self.vertex_accum = self.vertex_accum.wrapping_add(vertex_hash(&corner));
152                }
153            }
154
155            // Channel 2 — area per supporting plane. The vertex set alone
156            // cannot see a face deleted from between corners other faces still
157            // use; the area can, and a retriangulation leaves it untouched.
158            let plane = plane_of(cross, &tri[0]);
159            self.plane_area_accum = self
160                .plane_area_accum
161                .wrapping_add(plane.key.wrapping_mul(plane.weight as u64));
162
163            self.triangle_count = self.triangle_count.wrapping_add(1);
164        }
165
166        // A call that contributed no in-range triangle is not a segment: it has
167        // no geometry, so its verdict says nothing about the element.
168        if vol_ref.is_none() {
169            return;
170        }
171        self.closure.fold_segment(&verdict);
172        self.volume6 += seg_volume6;
173    }
174}