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;