ifc_lite_processing/processor/site_local.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 crate::mesh_frame::MeshCoordinateSpace;
6use crate::types::mesh::MeshData;
7/// The rotation-removal condition, spelled once beside the frame it belongs
8/// to. Re-exported here because this module's converter and `element.rs`'s
9/// guard are its other two readers.
10pub(super) use crate::mesh_frame::rotation_is_identity;
11
12/// #1474: `element_mesh_build.rs` captures a mesh's local-bounds and
13/// local-to-world transform BEFORE [`convert_mesh_to_site_local`] runs, because
14/// those describe the mesh in its OWN frame and `convert_mesh_to_site_local`
15/// re-expresses positions/origin in the site-local frame — a captured transform
16/// would go stale if that re-expression actually rotated anything.
17///
18/// It doesn't, for a pure-translation site placement: `rotation_is_identity`
19/// is exactly the condition under which `apply_inverse_rotation_in_place`
20/// already no-ops on positions/normals/origin. So only an ACTUALLY rotating
21/// site placement invalidates the captured transforms — `site_local_rotation`
22/// being `Some` is not enough on its own, since it is `Some` for every
23/// `site_local`-tier model (translation-only included).
24///
25/// This no longer gates INSTANCING metadata (#4118 part B). That consumer got a
26/// way to express the same staleness as a frame rather than lose the data:
27/// [`native_to_baked`] hands the collator the basis the bake happened in, and
28/// the collator conjugates both its reconstruction check and its emitted
29/// relative transform by it. The local-bounds/local-to-world consumers (the
30/// zero-copy mesh getters and the demesher) have no such basis argument, so
31/// they still drop.
32#[inline]
33pub(crate) fn site_local_rotation_invalidates_captured_transforms(
34 site_local_rotation: Option<&Vec<f64>>,
35) -> bool {
36 site_local_rotation.is_some_and(|m| !rotation_is_identity(m))
37}
38
39/// The row-major 4x4 mapping a NATIVE-frame point (IFC Z-up, pre-RTC — the
40/// frame `InstanceMeta.transform` and every captured `local_to_world` describe)
41/// onto the BAKED point this pipeline actually writes into `MeshData`, for the
42/// given coordinate-space tier.
43///
44/// One function, next to the converter that creates the divergence, so a
45/// consumer of baked vertices never re-derives the tier taxonomy by hand.
46/// What it encodes:
47///
48/// * the router subtracts the model RTC offset while baking, so every tier
49/// carries `T(-origin_shift)`;
50/// * `site_local` ALSO runs [`convert_mesh_to_site_local`], which applies the
51/// site placement's inverse rotation `Rᵀ` to positions, normals and the f64
52/// origin — and in that tier the RTC offset IS the site translation (the
53/// three-tier selection in `processor::mod` takes it straight off the site
54/// matrix), so a baked point is exactly `Rᵀ · (P − t_site)`;
55/// * `model_rtc` and `raw_ifc` remove no rotation, so they are
56/// `T(-origin_shift)` alone. `raw_ifc` selects `origin_shift = (0,0,0)`,
57/// which makes that the identity that tier is documented to be. The
58/// translation is still applied unconditionally, so a caller handing in a
59/// non-zero shift under some other tier string gets the frame its vertices
60/// are actually in rather than a silent identity.
61///
62/// `Rᵀ` is applied under exactly [`rotation_is_identity`]'s condition, because
63/// that is the condition [`apply_inverse_rotation_in_place`] itself no-ops
64/// under: the two must agree, or this basis describes a bake that did not
65/// happen.
66///
67/// Row-major, to match the glTF exporter's matrix module, which composes its
68/// Z-up→Y-up swap on top of this.
69pub fn native_to_baked(
70 mesh_coordinate_space: MeshCoordinateSpace,
71 site_transform: Option<&[f64]>,
72 origin_shift: [f64; 3],
73) -> [f64; 16] {
74 // Linear part: `Rᵀ` in the site_local tier — the ROWS of the row-major
75 // result are the COLUMNS of the column-major site matrix — identity
76 // everywhere else.
77 let rot = site_transform
78 .filter(|_| mesh_coordinate_space == MeshCoordinateSpace::SiteLocal)
79 .filter(|m| m.len() >= 16 && !rotation_is_identity(m))
80 .map(|m| [m[0], m[1], m[2], m[4], m[5], m[6], m[8], m[9], m[10]])
81 .unwrap_or([1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0]);
82 let t = [-origin_shift[0], -origin_shift[1], -origin_shift[2]];
83 // `Rᵀ · T(-rtc)`: linear block `Rᵀ`, translation `Rᵀ · (-rtc)`.
84 #[rustfmt::skip]
85 let m = [
86 rot[0], rot[1], rot[2], rot[0] * t[0] + rot[1] * t[1] + rot[2] * t[2],
87 rot[3], rot[4], rot[5], rot[3] * t[0] + rot[4] * t[1] + rot[5] * t[2],
88 rot[6], rot[7], rot[8], rot[6] * t[0] + rot[7] * t[1] + rot[8] * t[2],
89 0.0, 0.0, 0.0, 1.0,
90 ];
91 m
92}
93
94/// Hand a streaming caller the basis its batches are baked in (#5407), from
95/// the SAME frame the router was just given, so the two cannot disagree.
96pub(super) fn publish_baked_basis(
97 out: Option<&std::sync::OnceLock<[f64; 16]>>,
98 frame: crate::mesh_frame::MeshFrame,
99 site_transform: Option<&[f64]>,
100) {
101 if let Some(out) = out {
102 let (x, y, z) = frame.rtc_offset();
103 // `set` fails only if already set: one pass publishes one frame.
104 let _ = out.set(native_to_baked(frame.coordinate_space(), site_transform, [x, y, z]));
105 }
106}
107
108/// Apply the inverse of the site placement's 3×3 rotation to in-place `f32`
109/// triplets (positions or normals). Translation is handled separately via the
110/// router's `rtc_offset`; this only rotates vertices into the site-local axis
111/// frame when that frame is non-identity.
112fn apply_inverse_rotation_in_place(values: &mut [f32], column_major_matrix: &[f64]) {
113 if values.len() < 3 || column_major_matrix.len() < 16 {
114 return;
115 }
116 if rotation_is_identity(column_major_matrix) {
117 return;
118 }
119
120 let r00 = column_major_matrix[0];
121 let r10 = column_major_matrix[1];
122 let r20 = column_major_matrix[2];
123 let r01 = column_major_matrix[4];
124 let r11 = column_major_matrix[5];
125 let r21 = column_major_matrix[6];
126 let r02 = column_major_matrix[8];
127 let r12 = column_major_matrix[9];
128 let r22 = column_major_matrix[10];
129
130 for chunk in values.chunks_exact_mut(3) {
131 let x = chunk[0] as f64;
132 let y = chunk[1] as f64;
133 let z = chunk[2] as f64;
134 chunk[0] = (r00 * x + r10 * y + r20 * z) as f32;
135 chunk[1] = (r01 * x + r11 * y + r21 * z) as f32;
136 chunk[2] = (r02 * x + r12 * y + r22 * z) as f32;
137 }
138}
139
140/// Rotate a mesh into the site-local axis frame. Only runs for the
141/// `site_local` coordinate-space tier; translation alignment happens upstream
142/// via the router's RTC subtraction.
143///
144/// `pub` only as public API surface: the claim that the streaming server calls
145/// it to rotate meshes produced outside this crate's parallel loop was stale —
146/// a repo-wide search finds no caller outside this crate and its own integration
147/// tests. Kept `pub` because removing a re-exported item is a breaking change
148/// (#4192), not because anything downstream is known to need it.
149pub fn convert_mesh_to_site_local(mesh: &mut MeshData, site_transform: Option<&Vec<f64>>) {
150 let Some(site_transform) = site_transform else {
151 return;
152 };
153
154 apply_inverse_rotation_in_place(&mut mesh.positions, site_transform);
155 apply_inverse_rotation_in_place(&mut mesh.normals, site_transform);
156 // Positions are stored RELATIVE to `mesh.origin`, so the world point is
157 // `origin + position`. The site-local inverse rotation acts on the world
158 // point, so the origin must be rotated by the SAME inverse rotation (in f64)
159 // — otherwise the element would be rotated about the wrong centre.
160 apply_inverse_rotation_point_f64(&mut mesh.origin, site_transform);
161}
162
163/// Inverse-rotate a single f64 point in place by `column_major_matrix` (the same
164/// Rᵀ used by `apply_inverse_rotation_in_place`). Used for the per-mesh origin.
165fn apply_inverse_rotation_point_f64(p: &mut [f64; 3], column_major_matrix: &[f64]) {
166 if column_major_matrix.len() < 16
167 || rotation_is_identity(column_major_matrix)
168 || (p[0] == 0.0 && p[1] == 0.0 && p[2] == 0.0)
169 {
170 return;
171 }
172 let (r00, r10, r20) = (
173 column_major_matrix[0],
174 column_major_matrix[1],
175 column_major_matrix[2],
176 );
177 let (r01, r11, r21) = (
178 column_major_matrix[4],
179 column_major_matrix[5],
180 column_major_matrix[6],
181 );
182 let (r02, r12, r22) = (
183 column_major_matrix[8],
184 column_major_matrix[9],
185 column_major_matrix[10],
186 );
187 let (x, y, z) = (p[0], p[1], p[2]);
188 p[0] = r00 * x + r10 * y + r20 * z;
189 p[1] = r01 * x + r11 * y + r21 * z;
190 p[2] = r02 * x + r12 * y + r22 * z;
191}
192
193#[cfg(test)]
194#[path = "site_local_tests.rs"]
195mod tests;