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;