ifc_lite_geometry/instancing/
collate.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
5use super::group::collate_refs;
6use crate::mesh::{InstanceMeta, Mesh};
7use nalgebra::Matrix4;
8
9const IDENTITY16: [f64; 16] = [
10    1.0, 0.0, 0.0, 0.0, //
11    0.0, 1.0, 0.0, 0.0, //
12    0.0, 0.0, 1.0, 0.0, //
13    0.0, 0.0, 0.0, 1.0, //
14];
15
16/// Full world transform `transform · local_transform` for an occurrence.
17pub(super) fn compose_world(meta: &InstanceMeta) -> Matrix4<f64> {
18    let t = Matrix4::from_row_slice(&meta.transform);
19    let l = Matrix4::from_row_slice(meta.local_transform.as_ref().unwrap_or(&IDENTITY16));
20    // Rigid tier: canonical->local transform, composed innermost. For occurrences
21    // grouped by congruence (not bit-identity) this carries the recovered rotation
22    // so the shared template reproduces this occurrence's baked geometry.
23    let c = Matrix4::from_row_slice(meta.canonical_transform.as_ref().unwrap_or(&IDENTITY16));
24    t * l * c
25}
26
27/// Flatten a column-major nalgebra matrix into a row-major `[f32; 16]`.
28pub(super) fn mat4_to_row_major_f32(m: &Matrix4<f64>) -> [f32; 16] {
29    let mut out = [0.0f32; 16];
30    for r in 0..4 {
31        for c in 0..4 {
32            out[r * 4 + c] = m[(r, c)] as f32;
33        }
34    }
35    out
36}
37
38/// One occurrence of a template geometry.
39#[derive(Debug, Clone)]
40pub struct InstanceOccurrence {
41    /// Index of the original mesh in the input slice (carries entity id / colour).
42    pub mesh_index: usize,
43    /// Row-major mat4 mapping the template's baked world geometry onto this
44    /// occurrence. The template occurrence's transform is identity.
45    ///
46    /// Expressed in the CALLER'S basis frame — whatever `baked_basis` was
47    /// handed to `collate_refs_in_basis` — and in the native post-RTC frame
48    /// when that basis was `None` (which is what `collate_refs` passes). A
49    /// consumer that reads this field directly rather than recomputing from
50    /// `InstanceMeta` is therefore reading the frame its own baked vertices are
51    /// in, not a native-frame matrix it has to conjugate itself.
52    pub transform: [f32; 16],
53}
54
55/// A unique geometry shared by two or more occurrences.
56#[derive(Debug, Clone)]
57pub struct InstanceTemplate {
58    /// Representation-identity key (RepresentationMap id for mapped items).
59    pub rep_identity: u128,
60    /// Index of the mesh whose geometry is the template to upload.
61    pub template_index: usize,
62    /// Every occurrence (including the template itself, with identity transform).
63    pub occurrences: Vec<InstanceOccurrence>,
64}
65
66/// Result of collation: instanced templates + the meshes left to render flat.
67#[derive(Debug, Clone, Default)]
68pub struct Collated {
69    /// Unique geometries with their per-instance transforms.
70    pub templates: Vec<InstanceTemplate>,
71    /// Indices of input meshes rendered without instancing (non-instanceable,
72    /// singleton groups, or groups that failed the geometry-shape guard).
73    pub flat_indices: Vec<usize>,
74    /// Pose-only (#1623 don't-bake) occurrences that appear in NEITHER
75    /// `templates` nor `flat_indices`: they carry no geometry of their own, so
76    /// the flat path is not somewhere they can go, and their group had no
77    /// invertible template placement to instance them against. Nothing else in
78    /// this result records that they existed, which is exactly why the count is
79    /// here — an occurrence going missing must not look like an occurrence that
80    /// was never fed in. Zero on every ordinary model.
81    pub dropped_placeholders: usize,
82    /// Groups the #3666 reconstruction (or same-shape) check refused, so their
83    /// materialized members sit in `flat_indices` instead of sharing a template.
84    ///
85    /// The refusal is not free where the caller has no flat path of its own:
86    /// `encode_refs` emits every `flat_indices` entry as a ONE-INSTANCE template,
87    /// so a refused group of N becomes N singleton templates in an IFNS shard —
88    /// O(unique-geometry) per-frame draws, which is the orbit-FPS regression the
89    /// WASM viewer's occurrence-count gate exists to prevent. A caller that HAS a
90    /// flat path should route these members to it (the WASM batch partition
91    /// does), and this count is how the case is observable at all.
92    pub verification_rejections: usize,
93}
94
95impl Collated {
96    /// Total number of unique geometries that would be uploaded (templates +
97    /// flat meshes) — the figure that bounds browser ingestion.
98    pub fn unique_geometry_count(&self) -> usize {
99        self.templates.len() + self.flat_indices.len()
100    }
101
102    /// Total occurrences represented across all templates (excludes flat meshes).
103    pub fn instanced_occurrence_count(&self) -> usize {
104        self.templates.iter().map(|t| t.occurrences.len()).sum()
105    }
106}
107
108/// A borrowed view of a mesh for collation/encoding — lets callers feed geometry
109/// from any owner (geometry's `Mesh`, processing's `MeshData`) WITHOUT cloning the
110/// vertex data (cloning 219k meshes' geometry risks the build-container OOM).
111pub struct InstanceMeshRef<'a> {
112    pub positions: &'a [f32],
113    pub normals: &'a [f32],
114    pub indices: &'a [u32],
115    pub origin: [f64; 3],
116    pub instance_meta: Option<&'a InstanceMeta>,
117    /// Per-occurrence entity id (used only by the encoder).
118    pub entity_id: u32,
119    /// Per-occurrence RGBA (used only by the encoder).
120    pub color: [f32; 4],
121    /// The `IfcRepresentationItem` this occurrence's geometry was tessellated
122    /// from (`MeshData::geometry_item_id`) — the host's drill-to-source link,
123    /// used only by the encoder. `Option` like every other link in the chain
124    /// (`RawInstanceOccurrence`, `InstanceRecord`, `DecodedInstance::item_id`), so
125    /// no caller can pass a meaningful-looking `0`; the encoder collapses `None`
126    /// to the wire's `0` once, where that sentinel is a wire fact.
127    pub item_id: Option<u32>,
128}
129
130impl<'a> InstanceMeshRef<'a> {
131    /// Build a view over a geometry `Mesh` (encoder id/colour default to 0).
132    pub fn from_mesh(m: &'a Mesh) -> Self {
133        InstanceMeshRef {
134            positions: &m.positions,
135            normals: &m.normals,
136            indices: &m.indices,
137            origin: m.origin,
138            instance_meta: m.instance_meta.as_ref(),
139            entity_id: 0,
140            color: [0.0; 4],
141            item_id: None,
142        }
143    }
144}
145
146/// Subtract the model RTC offset from a composed (pre-RTC) world transform's
147/// translation column, giving the post-RTC placement that matches the post-RTC
148/// per-mesh `origin` the renderer applies the relative transform to.
149pub(super) fn to_post_rtc(mut m: Matrix4<f64>, rtc: [f64; 3]) -> Matrix4<f64> {
150    m[(0, 3)] -= rtc[0];
151    m[(1, 3)] -= rtc[1];
152    m[(2, 3)] -= rtc[2];
153    m
154}
155
156
157// The #1623 Phase 2 don't-bake finalize helpers (`compose_instance_world_row_major`,
158// `instance_rel_row_major_f32`, `bake_source_at_world`) moved to `super::dont_bake`
159// (kept re-exported below) — the module-size ratchet budget pushed them out once
160// the per-member rigidity fix and its diagnostics landed here.
161pub use super::dont_bake::{
162    bake_source_at_world, compose_instance_world_row_major, instance_rel_row_major_f32,
163};
164
165/// `collate_refs` over geometry `Mesh` values (thin wrapper, no geometry clone).
166pub fn collate_instances(meshes: &[Mesh], min_group: usize, rtc: [f64; 3]) -> Collated {
167    let refs: Vec<InstanceMeshRef> = meshes.iter().map(InstanceMeshRef::from_mesh).collect();
168    collate_refs(&refs, min_group, rtc)
169}
170
171// verify_recomposition moved to super::verify (kept as a re-export below) —
172// the module-size ratchet budget pushed it out once #3666's inline pairing
173// check landed here; it belongs next to that check's shared math anyway.
174pub use super::verify::verify_recomposition;