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}