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/// Apply the inverse of the site placement's 3×3 rotation to in-place `f32`
95/// triplets (positions or normals). Translation is handled separately via the
96/// router's `rtc_offset`; this only rotates vertices into the site-local axis
97/// frame when that frame is non-identity.
98fn apply_inverse_rotation_in_place(values: &mut [f32], column_major_matrix: &[f64]) {
99 if values.len() < 3 || column_major_matrix.len() < 16 {
100 return;
101 }
102 if rotation_is_identity(column_major_matrix) {
103 return;
104 }
105
106 let r00 = column_major_matrix[0];
107 let r10 = column_major_matrix[1];
108 let r20 = column_major_matrix[2];
109 let r01 = column_major_matrix[4];
110 let r11 = column_major_matrix[5];
111 let r21 = column_major_matrix[6];
112 let r02 = column_major_matrix[8];
113 let r12 = column_major_matrix[9];
114 let r22 = column_major_matrix[10];
115
116 for chunk in values.chunks_exact_mut(3) {
117 let x = chunk[0] as f64;
118 let y = chunk[1] as f64;
119 let z = chunk[2] as f64;
120 chunk[0] = (r00 * x + r10 * y + r20 * z) as f32;
121 chunk[1] = (r01 * x + r11 * y + r21 * z) as f32;
122 chunk[2] = (r02 * x + r12 * y + r22 * z) as f32;
123 }
124}
125
126/// Rotate a mesh into the site-local axis frame. Only runs for the
127/// `site_local` coordinate-space tier; translation alignment happens upstream
128/// via the router's RTC subtraction.
129///
130/// `pub` only as public API surface: the claim that the streaming server calls
131/// it to rotate meshes produced outside this crate's parallel loop was stale —
132/// a repo-wide search finds no caller outside this crate and its own integration
133/// tests. Kept `pub` because removing a re-exported item is a breaking change
134/// (#4192), not because anything downstream is known to need it.
135pub fn convert_mesh_to_site_local(mesh: &mut MeshData, site_transform: Option<&Vec<f64>>) {
136 let Some(site_transform) = site_transform else {
137 return;
138 };
139
140 apply_inverse_rotation_in_place(&mut mesh.positions, site_transform);
141 apply_inverse_rotation_in_place(&mut mesh.normals, site_transform);
142 // Positions are stored RELATIVE to `mesh.origin`, so the world point is
143 // `origin + position`. The site-local inverse rotation acts on the world
144 // point, so the origin must be rotated by the SAME inverse rotation (in f64)
145 // — otherwise the element would be rotated about the wrong centre.
146 apply_inverse_rotation_point_f64(&mut mesh.origin, site_transform);
147}
148
149/// Inverse-rotate a single f64 point in place by `column_major_matrix` (the same
150/// Rᵀ used by `apply_inverse_rotation_in_place`). Used for the per-mesh origin.
151fn apply_inverse_rotation_point_f64(p: &mut [f64; 3], column_major_matrix: &[f64]) {
152 if column_major_matrix.len() < 16
153 || rotation_is_identity(column_major_matrix)
154 || (p[0] == 0.0 && p[1] == 0.0 && p[2] == 0.0)
155 {
156 return;
157 }
158 let (r00, r10, r20) = (
159 column_major_matrix[0],
160 column_major_matrix[1],
161 column_major_matrix[2],
162 );
163 let (r01, r11, r21) = (
164 column_major_matrix[4],
165 column_major_matrix[5],
166 column_major_matrix[6],
167 );
168 let (r02, r12, r22) = (
169 column_major_matrix[8],
170 column_major_matrix[9],
171 column_major_matrix[10],
172 );
173 let (x, y, z) = (p[0], p[1], p[2]);
174 p[0] = r00 * x + r10 * y + r20 * z;
175 p[1] = r01 * x + r11 * y + r21 * z;
176 p[2] = r02 * x + r12 * y + r22 * z;
177}
178
179#[cfg(test)]
180#[path = "site_local_tests.rs"]
181mod tests;