Skip to main content

ifc_lite_wasm/zero_copy/
mesh_fingerprint.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 per-entity geometry-fingerprint surface of [`MeshCollection`]: five
6//! index-parallel arrays (id, hash, world AABB, volume, closure flags) and the
7//! one call that appends to all of them together.
8//!
9//! A CHILD module of `mesh`, so it reads that module's private
10//! `MeshCollection` fields without widening them. Split out because the
11//! contract these arrays carry — what a `NaN` means, why a volume is usually
12//! absent, what each closure bit diagnoses — is most of their weight, and
13//! `mesh.rs` is the mesh buffer's home, not the diff engine's.
14//!
15//! THE INVARIANT, once: every array has one entry (or one fixed-width span) per
16//! `geometry_hash_ids` entry, always. An absent value is written as `NaN`, never
17//! skipped — a short array would mis-attribute every entry past the gap, and the
18//! consumer reads purely by index.
19
20use super::MeshCollection;
21use crate::zero_copy::frame_swap::swap_zup_to_yup_aabb;
22use wasm_bindgen::prelude::*;
23
24/// Everything one hashing pass learned about one entity, pushed as a unit.
25///
26/// A struct rather than five positional arguments precisely BECAUSE of the
27/// index-parallel invariant: there is one push site per entity, and a
28/// positional call cannot be given three of five values in the wrong order or
29/// silently drop one.
30pub struct GeometryFingerprint {
31    pub express_id: u32,
32    /// The #924 winding-invariant shape fingerprint.
33    pub hash: u64,
34    /// World AABB `[minx, miny, minz, maxx, maxy, maxz]`, or `None` → six NaNs.
35    /// Stays in the producer's IFC Z-up frame; the getter converts it to the
36    /// viewer's Y-up (see [`MeshCollection::geometry_aabb_values`]).
37    pub aabb: Option<[f64; 6]>,
38    /// Enclosed volume in m³, `None` (→ NaN) unless the geometry was provably a
39    /// single closed orientable solid. See `ifc_lite_geometry::GeometryHasher::volume`.
40    pub volume: Option<f64>,
41    /// Packed `ifc_lite_geometry::GeometryClosure` verdict.
42    pub closure_bits: u8,
43}
44
45#[wasm_bindgen]
46impl MeshCollection {
47    /// Express ids for the per-entity geometry fingerprints, parallel to
48    /// [`Self::geometry_hash_values`]. Empty unless geometry hashing was
49    /// enabled via `IfcAPI.setComputeGeometryHashes`.
50    #[wasm_bindgen(getter, js_name = geometryHashIds)]
51    pub fn geometry_hash_ids(&self) -> js_sys::Uint32Array {
52        js_sys::Uint32Array::from(&self.geometry_hash_ids[..])
53    }
54
55    /// Per-entity geometry fingerprints as a `BigUint64Array`, parallel to
56    /// [`Self::geometry_hash_ids`]. `u64` is exposed (not hex strings) so JS
57    /// can compare with `===` and key maps without allocation. Empty unless
58    /// geometry hashing was enabled.
59    #[wasm_bindgen(getter, js_name = geometryHashValues)]
60    pub fn geometry_hash_values(&self) -> js_sys::BigUint64Array {
61        js_sys::BigUint64Array::from(&self.geometry_hash_values[..])
62    }
63
64    /// Per-entity world-space AABBs as a `Float64Array`, SIX values per entry
65    /// (`minx, miny, minz, maxx, maxy, maxz`), in the same order as
66    /// [`Self::geometry_hash_ids`] — entry `i` spans `[6*i, 6*i+6)`. Empty
67    /// unless geometry hashing was enabled; the same
68    /// `IfcAPI.setComputeGeometryHashes` switch gates both, so nothing is
69    /// computed when the diff feature is off.
70    ///
71    /// Unquantized world `f64` (the file's RTC folded back in), so two
72    /// revisions that chose different RTC offsets report the same box. This is
73    /// what lets a consumer say "MOVED" honestly instead of inferring it from a
74    /// changed hash, which also fires on reshape and on retriangulation.
75    ///
76    /// **Frame: WebGL Y-up**, like every other box, position, origin and
77    /// placement that crosses this boundary (see `MeshDataJs::local_bounds`).
78    /// The hasher accumulates in the producer's IFC Z-up frame, so the swap
79    /// `(x,y,z) -> (x,z,-y)` is applied here, on the way out. Unconverted, the
80    /// boxes would not enclose the very meshes `processGeometryBatch` returns
81    /// alongside them. Positions are RTC-relative and this box is absolute, so
82    /// a consumer comparing the two folds `rtcOffset*` in — itself Y-up-swapped.
83    ///
84    /// Present for every hashed entity. Its companion
85    /// [`Self::geometry_volume_values`] is not — see there.
86    #[wasm_bindgen(getter, js_name = geometryAabbValues)]
87    pub fn geometry_aabb_values(&self) -> js_sys::Float64Array {
88        // `push_geometry_hash` is the only writer and always extends by exactly
89        // six, so `chunks_exact` drops nothing. NaN placeholders (hash without a
90        // box) survive the swap as NaN, since `-NaN` is NaN.
91        let y_up: Vec<f64> = self
92            .geometry_aabb_values
93            .chunks_exact(6)
94            .flat_map(|b| swap_zup_to_yup_aabb([b[0], b[1], b[2], b[3], b[4], b[5]]))
95            .collect();
96        js_sys::Float64Array::from(&y_up[..])
97    }
98
99    /// Per-entity enclosed volume in CUBIC METRES as a `Float64Array`, one
100    /// value per entry in [`Self::geometry_hash_ids`] order. `NaN` means NO
101    /// TRUSTWORTHY VOLUME — the same absent convention as
102    /// [`Self::geometry_aabb_values`] — and it is `NaN` for roughly a third of
103    /// entities by design, not by failure.
104    ///
105    /// A value is emitted only when that entity's produced geometry was
106    /// PROVABLY a single closed, orientable, single-component solid, as decided
107    /// by the mesher's own orientation pass. Read
108    /// `ifc_lite_geometry::GeometryHasher::volume` before treating a `NaN` as a
109    /// bug: an open `SurfaceModel`, a material-layered wall (whose slices are
110    /// open bands by construction), and any element assembled from more than one
111    /// representation item all correctly report nothing rather than a plausible
112    /// wrong number. [`Self::geometry_closure_flags`] says which.
113    ///
114    /// This is NOT a substitute for an IFC `BaseQuantities` `GrossVolume`: it is
115    /// the volume of the geometry that was actually meshed, after opening cuts,
116    /// and it says nothing about whether a CSG degradation left the host uncut
117    /// (see the `diagnostics` getter).
118    #[wasm_bindgen(getter, js_name = geometryVolumeValues)]
119    pub fn geometry_volume_values(&self) -> js_sys::Float64Array {
120        js_sys::Float64Array::from(&self.geometry_volume_values[..])
121    }
122
123    /// Per-entity topology verdict as a `Uint8Array`, one packed byte per entry
124    /// in [`Self::geometry_hash_ids`] order:
125    ///
126    /// * bit 0 (`1`) — every segment closed (no boundary / non-manifold edge)
127    /// * bit 1 (`2`) — every segment orientable
128    /// * bit 2 (`4`) — every segment a single connected component
129    /// * bit 3 (`8`) — the entity produced exactly one segment
130    ///
131    /// `0x0F` is exactly the set that carries a volume in
132    /// [`Self::geometry_volume_values`]. The individual bits are the diagnosis:
133    /// a model checker can distinguish "this wall is an open shell" (bit 0
134    /// clear) from "this door is a multi-item assembly whose parts may overlap"
135    /// (bit 3 clear), which are different findings with different fixes.
136    #[wasm_bindgen(getter, js_name = geometryClosureFlags)]
137    pub fn geometry_closure_flags(&self) -> js_sys::Uint8Array {
138        js_sys::Uint8Array::from(&self.geometry_closure_flags[..])
139    }
140}
141
142impl MeshCollection {
143    /// Record one entity's geometry fingerprint and everything the SAME hashing
144    /// pass measured about it. Taken as a struct rather than five positional
145    /// arguments precisely because the five arrays must stay index-parallel:
146    /// there is one push site per entity, and a caller cannot supply three of
147    /// the five and silently misalign every later entry.
148    ///
149    /// Absent values are written in place, never skipped — `None` becomes a
150    /// six-`NaN` box / a `NaN` volume — because shortening an array would
151    /// mis-attribute every entry past it.
152    #[inline]
153    pub fn push_geometry_hash(&mut self, fp: GeometryFingerprint) {
154        self.geometry_hash_ids.push(fp.express_id);
155        self.geometry_hash_values.push(fp.hash);
156        self.geometry_aabb_values
157            .extend_from_slice(&fp.aabb.unwrap_or([f64::NAN; 6]));
158        self.geometry_volume_values.push(fp.volume.unwrap_or(f64::NAN));
159        self.geometry_closure_flags.push(fp.closure_bits);
160    }
161}