ifc_lite_geometry/router/transforms/mod.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//! Placement and transformation: axis placement parsing, coordinate transforms, RTC offset.
6
7mod grid;
8mod grid_frame;
9mod linear;
10pub(crate) mod mapped;
11mod mesh_world;
12pub(crate) use mesh_world::LARGE_MAPPED_ORIGIN_M;
13pub(crate) mod operator;
14
15mod parsers;
16mod walk;
17
18#[cfg(test)]
19#[path = "placement_depth_tests.rs"]
20mod placement_depth_tests;
21
22use super::GeometryRouter;
23use crate::{Mesh, Result, SubMeshCollection};
24use ifc_lite_core::{DecodedEntity, EntityDecoder};
25use nalgebra::Matrix4;
26
27static LOCAL_FRAME_OVERRIDE: std::sync::atomic::AtomicI8 = std::sync::atomic::AtomicI8::new(-1);
28
29/// Test/harness-only: force the target/env fallback on/off, or `None` for the
30/// target default. A router constructed with an explicit frame policy does not
31/// consult this process-global hook. Mirrors `rect_fast::param_set_enabled_override`. The
32/// mesh-output determinism manifest uses it to run native and wasm with the
33/// SAME flag state (wasm defaults ON, native defaults OFF below), so the two
34/// targets' outputs are comparable byte-for-byte.
35pub fn local_frame_set_enabled_override(v: Option<bool>) {
36 LOCAL_FRAME_OVERRIDE.store(
37 match v {
38 None => -1,
39 Some(false) => 0,
40 Some(true) => 1,
41 },
42 std::sync::atomic::Ordering::Relaxed,
43 );
44}
45
46/// Whether per-element local-frame vertex precision is enabled.
47///
48/// When ON, `transform_mesh_world` stores positions relative to a per-element
49/// f64 `origin` (so f32 coords stay element-small and never collapse to
50/// degenerate fans at building/georef scale), and the void CSG runs in that same
51/// local frame. Consumers reconstruct world = `MeshData.origin` + position.
52/// Default is ON for wasm (the precision-critical viewer path, whose renderer
53/// consumes `origin`) and OFF for native, where `IFC_LITE_LOCAL_FRAME=1` opts
54/// in. Env/cfg default read once and cached. An explicit per-router policy takes
55/// precedence; otherwise [`local_frame_set_enabled_override`] selects this fallback.
56pub(crate) fn local_frame_enabled() -> bool {
57 match LOCAL_FRAME_OVERRIDE.load(std::sync::atomic::Ordering::Relaxed) {
58 0 => return false,
59 1 => return true,
60 _ => {}
61 }
62 static ENABLED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
63 *ENABLED.get_or_init(|| {
64 // The viewer (wasm) is the precision-critical target: building-scale f32
65 // vertex storage collapses near-edges into fans, fixed by storing each
66 // element relative to its AABB-centre origin (the renderer reconstructs
67 // world = origin + position). Default ON for wasm. Native stays opt-in
68 // (env) so server output + the cross-arch determinism snapshots remain
69 // absolute-coord byte-identical; native consumers reconstruct from
70 // MeshData.origin when they want the local frame.
71 if cfg!(target_arch = "wasm32") {
72 true
73 } else {
74 std::env::var("IFC_LITE_LOCAL_FRAME").is_ok()
75 }
76 })
77}
78
79/// GPU-instancing capture is ALWAYS ON (no flag). The pipeline attaches
80/// [`crate::mesh::InstanceMeta`] (rep-identity + per-occurrence world transform)
81/// to every instanceable mesh so the collator can group occurrences into unique
82/// templates + per-instance transforms. This adds only metadata + an O(verts)
83/// content hash — the flat geometry output (positions/normals/indices) is
84/// unchanged, so determinism snapshots (which hash geometry, not `instance_meta`)
85/// stay byte-identical, and the instancing renderer path is data-driven, not
86/// toggled. (The old env flag never fired in wasm — `std::env` is empty there —
87/// which is exactly the browser path that needs it.)
88#[inline]
89pub(crate) fn instancing_enabled() -> bool {
90 true
91}
92
93/// Flatten a nalgebra `Matrix4<f64>` into a **column-major** `[f64; 16]` for the
94/// [`EntityDecoder`] placement-transform memo. `Matrix4::as_slice` is already
95/// column-major length 16, and [`Matrix4::from_column_slice`] reconstructs it
96/// bit-for-bit (an f64 round-trip is exact), so the memo is byte-identical to
97/// recomputing the transform. Distinct from [`mat4_to_row_major`], which is the
98/// row-major GPU-instancing convention.
99#[inline]
100fn mat4_to_col_array(m: &Matrix4<f64>) -> [f64; 16] {
101 *m.as_slice()
102 .first_chunk::<16>()
103 .expect("Matrix4<f64> as_slice is exactly 16 elements")
104}
105
106/// Flatten a column-major nalgebra `Matrix4<f64>` into a row-major `[f64; 16]`
107/// (the [`crate::mesh::InstanceMeta`] convention; matches a GPU mat4 fed row-by-row).
108pub(crate) fn mat4_to_row_major(m: &Matrix4<f64>) -> [f64; 16] {
109 [
110 m[(0, 0)], m[(0, 1)], m[(0, 2)], m[(0, 3)],
111 m[(1, 0)], m[(1, 1)], m[(1, 2)], m[(1, 3)],
112 m[(2, 0)], m[(2, 1)], m[(2, 2)], m[(2, 3)],
113 m[(3, 0)], m[(3, 1)], m[(3, 2)], m[(3, 3)],
114 ]
115}
116
117impl GeometryRouter {
118 pub(super) fn local_frame_enabled(&self) -> bool {
119 self.local_frame_enabled.unwrap_or_else(local_frame_enabled)
120 }
121
122 /// Apply local placement transformation to mesh
123 ///
124 /// Welds the source vertices FIRST (#4103). This is the last point at which
125 /// the vertices are still in the object frame, and the weld keys on raw f32
126 /// bits, so welding after the bake makes the merge depend on where the
127 /// element sits and leaves two occurrences of one representation with
128 /// different buffers. See `crate::mesh_weld`'s module doc.
129 pub(super) fn apply_placement(
130 &self,
131 element: &DecodedEntity,
132 decoder: &mut EntityDecoder,
133 mesh: &mut Mesh,
134 ) -> Result<()> {
135 crate::mesh_weld::weld_mesh(mesh);
136 let placement_attr = match element.get(5) {
137 Some(attr) if !attr.is_null() => attr,
138 _ => {
139 // No ObjectPlacement: the world frame IS the object frame, so
140 // run the ordinary world step with an identity placement. This
141 // both records the #1474 frame capture AND applies the model
142 // RTC shift / local-frame relativization exactly like every
143 // placed mesh — consumers (renderer, demesher session)
144 // reconstruct `true_world = origin + position + rtc` uniformly
145 // and must not special-case placement-less geometry.
146 self.transform_mesh_world(mesh, &Matrix4::identity());
147 return Ok(());
148 }
149 };
150
151 let placement = match decoder.resolve_ref(placement_attr)? {
152 Some(p) => p,
153 None => {
154 self.transform_mesh_world(mesh, &Matrix4::identity());
155 return Ok(());
156 }
157 };
158
159 let mut transform = self.get_placement_transform(&placement, decoder)?;
160 self.scale_transform(&mut transform);
161 // Instancing: record the full (scaled) world placement on the mesh's
162 // instance metadata BEFORE it is baked + RTC-folded by transform_mesh_world.
163 // Only fires when processing already marked this mesh instanceable (so the
164 // metadata exists); a no-op otherwise, keeping the flat path untouched.
165 if let Some(im) = mesh.instance_meta.as_mut() {
166 im.transform = mat4_to_row_major(&transform);
167 }
168 self.transform_mesh_world(mesh, &transform);
169 Ok(())
170 }
171
172 /// Apply the element's `ObjectPlacement` (scaled to metres) to every sub-mesh.
173 /// Placement is a rigid per-instance transform, kept OUT of the dedup cache so
174 /// instances of one shared geometry land at their own positions.
175 /// (Moved here from `processing.rs` — placement logic lives with the other
176 /// placement appliers, and `processing.rs` sits at its ratchet budget.)
177 ///
178 /// Welds each sub-mesh's source vertices first, for the reason
179 /// [`Self::apply_placement`] gives. Every per-style channel (plain, layered,
180 /// textured) converges here, so one loop covers them all, and the sub-mesh
181 /// weld carries `SubMesh::uvs` through the same remap.
182 pub(super) fn apply_submesh_placement(
183 &self,
184 sub_meshes: &mut SubMeshCollection,
185 element: &DecodedEntity,
186 decoder: &mut EntityDecoder,
187 ) -> Result<()> {
188 for sub in &mut sub_meshes.sub_meshes {
189 crate::mesh_weld::weld_sub_mesh(sub);
190 }
191 // ObjectPlacement translation is in file units (e.g. mm) but geometry is
192 // scaled to metres, so the transform MUST be scaled to match.
193 if let Some(placement_attr) = element.get(5) {
194 if !placement_attr.is_null() {
195 if let Some(placement) = decoder.resolve_ref(placement_attr)? {
196 let mut transform = self.get_placement_transform(&placement, decoder)?;
197 self.scale_transform(&mut transform);
198 // Instancing: record the per-element world placement on EACH sub-mesh's
199 // instance metadata BEFORE it is baked into the vertices, mirroring
200 // `apply_placement` for the single-mesh path. Without this, the sub-mesh
201 // path (every multi-item element — all the Tekla steel: beams, plates,
202 // assemblies) leaves `instance_meta.transform` at the IDENTITY placeholder,
203 // so `collate_refs` computes rel_k = m_k · m_ref⁻¹ = identity for every
204 // occurrence and they all stack on the first one. The flat path was
205 // always correct (placement IS baked into the vertices below), so the
206 // dedup made it look like repeated geometry was "missing".
207 if instancing_enabled() {
208 let row_major = mat4_to_row_major(&transform);
209 for sub in &mut sub_meshes.sub_meshes {
210 if let Some(im) = sub.mesh.instance_meta.as_mut() {
211 im.transform = row_major;
212 }
213 }
214 }
215 for sub in &mut sub_meshes.sub_meshes {
216 self.transform_mesh_world(&mut sub.mesh, &transform);
217 }
218 return Ok(());
219 }
220 }
221 }
222 // No resolvable ObjectPlacement: run the ordinary world step with an
223 // identity placement — records the #1474 frame capture AND applies
224 // the model RTC shift / relativization, mirroring the same branch in
225 // `apply_placement` (single-mesh path).
226 for sub in &mut sub_meshes.sub_meshes {
227 self.transform_mesh_world(&mut sub.mesh, &Matrix4::identity());
228 }
229 Ok(())
230 }
231
232 /// Get placement transform from element without applying it
233 pub(super) fn get_placement_transform_from_element(
234 &self,
235 element: &DecodedEntity,
236 decoder: &mut EntityDecoder,
237 ) -> Result<Matrix4<f64>> {
238 Ok(self.get_placement_transform_from_element_with_status(element, decoder)?.0)
239 }
240
241 /// Return the placement and whether the shared depth guard truncated its
242 /// parent chain. Exact geometry descriptions must not report the partial
243 /// transform as a complete world placement.
244 pub(super) fn get_placement_transform_from_element_with_status(
245 &self,
246 element: &DecodedEntity,
247 decoder: &mut EntityDecoder,
248 ) -> Result<(Matrix4<f64>, bool)> {
249 // Get ObjectPlacement (attribute 5)
250 let placement_attr = match element.get(5) {
251 Some(attr) if !attr.is_null() => attr,
252 _ => return Ok((Matrix4::identity(), false)), // No placement
253 };
254
255 let placement = match decoder.resolve_ref(placement_attr)? {
256 Some(p) => p,
257 None => return Ok((Matrix4::identity(), false)),
258 };
259
260 let walk = self.get_placement_transform_with_depth(&placement, decoder, 0)?;
261 Ok((walk.transform, walk.truncated))
262 }
263
264 /// Exact counterpart to `resolve_scaled_placement`: refuses a world
265 /// placement when the shared reference-walk guard truncated its chain.
266 pub fn resolve_scaled_placement_strict(
267 &self,
268 element: &DecodedEntity,
269 decoder: &mut EntityDecoder,
270 ) -> Result<[f64; 16]> {
271 if let Some(attr) = element.get(5).filter(|attr| !attr.is_null()) {
272 if decoder.resolve_ref(attr)?.is_none() {
273 return Err(crate::Error::geometry(format!(
274 "ObjectPlacement for #{} cannot be resolved",
275 element.id
276 )));
277 }
278 }
279 let (mut transform, truncated) =
280 self.get_placement_transform_from_element_with_status(element, decoder)?;
281 if truncated {
282 return Err(crate::Error::geometry(format!(
283 "placement chain for #{} exceeded maximum depth",
284 element.id
285 )));
286 }
287 self.scale_transform(&mut transform);
288 let mut result = [0.0; 16];
289 result.copy_from_slice(transform.as_slice());
290 Ok(result)
291 }
292
293}