ifc_lite_server/services/
parquet_mesh_tables.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//! The flat transport's three Arrow tables, packed from a SHAPE PLAN
6//! (`parquet_shape_plan.rs`) that decides which mesh row draws which block of
7//! vertex/index rows.
8//!
9//! Split out of `parquet.rs`, which keeps the wire framing and the writers.
10//! The seam is the plan: above it decides what to emit, below it packs
11//! columns. The default layout is the case where that decision is the identity
12//! (row `i` owns block `i`); `shared-shapes` (#3888) adds the case where
13//! several rows share one block and a per-row rotation places each of them,
14//! and the cross-batch stream (#5407) the case where that block was written by
15//! an earlier batch.
16
17use crate::services::parquet::ParquetError;
18use crate::services::parquet_layout::ParquetLayout;
19use crate::services::parquet_shape_plan::{ShapePlan, ShapeRef};
20use crate::services::parquet_vertex_columns::{shape_vertices, VertexColumns};
21use crate::services::parquet_schema::{
22    index_schema, mesh_schema, vertex_schema, MeshRow, RowPlacement, ABSENT_SOURCE_ID,
23};
24use crate::types::MeshData;
25use arrow::array::{Float32Array, Float64Array, StringArray, UInt8Array, UInt32Array};
26use arrow::record_batch::RecordBatch;
27use rayon::prelude::*;
28use std::sync::Arc;
29
30/// Build the three Arrow tables (mesh metadata / vertices / indices) for a
31/// slice of meshes under `plan`. `base_vertex_offset` / `base_index_offset`
32/// seed the mesh-table `vertex_start` / `index_start` columns so an
33/// incremental caller (the streaming cache writer) emits GLOBAL whole-model
34/// offsets while the per-batch client blobs keep batch-local ones (bases 0/0).
35/// The Z-up to Y-up transform lives here, in one place, for both paths.
36pub(super) fn build_mesh_tables(
37    meshes: &[MeshData],
38    plan: &ShapePlan,
39    layout: ParquetLayout,
40    base_vertex_offset: u32,
41    base_index_offset: u32,
42) -> Result<(RecordBatch, RecordBatch, RecordBatch), ParquetError> {
43    // A `Shared` plan indexes the exact slice it was planned from, and nothing
44    // in the signature ties the two together: a plan applied to a different
45    // slice reads as plausible geometry, not as a failure.
46    debug_assert!(
47        plan.row_count().is_none_or(|n| n == meshes.len()),
48        "shape plan was built from a different mesh slice"
49    );
50    let shape_meshes = plan.shape_meshes(meshes);
51    let total_vertices: usize = shape_meshes.iter().map(|m| m.positions.len() / 3).sum();
52    let total_triangles: usize = shape_meshes.iter().map(|m| m.indices.len() / 3).sum();
53    let mesh_count = meshes.len();
54
55    // Phase 1: cumulative offsets over the EMITTED shapes (must be sequential).
56    let mut shape_vertex_start = Vec::with_capacity(shape_meshes.len());
57    let mut shape_index_start = Vec::with_capacity(shape_meshes.len());
58    let mut vertex_offset: u32 = base_vertex_offset;
59    let mut index_offset: u32 = base_index_offset;
60    for shape in &shape_meshes {
61        shape_vertex_start.push(vertex_offset);
62        shape_index_start.push(index_offset);
63        vertex_offset += (shape.positions.len() / 3) as u32;
64        index_offset += shape.indices.len() as u32;
65    }
66
67    // Phase 2: extract mesh metadata in parallel.
68    let metadata: Vec<MeshRow<'_>> = (0..mesh_count)
69        .into_par_iter()
70        .map(|i| {
71            let (shape, origin, rotation) = plan.row(meshes, i);
72            let placement = match shape {
73                ShapeRef::Slot(slot) => RowPlacement {
74                    v_start: shape_vertex_start[slot],
75                    vert_count: (shape_meshes[slot].positions.len() / 3) as u32,
76                    i_start: shape_index_start[slot],
77                    index_count: shape_meshes[slot].indices.len() as u32,
78                    origin,
79                    rotation,
80                },
81                // Written by an earlier stream batch (#5407): its range is
82                // already whole-stream, so it is carried through untouched.
83                ShapeRef::Emitted(emitted) => RowPlacement {
84                    v_start: emitted.vertex_start,
85                    vert_count: emitted.vertex_count,
86                    i_start: emitted.index_start,
87                    index_count: emitted.index_count,
88                    origin,
89                    rotation,
90                },
91            };
92            MeshRow::new(&meshes[i], placement)
93        })
94        .collect();
95
96    let mut express_ids = Vec::with_capacity(mesh_count);
97    let mut ifc_types: Vec<&str> = Vec::with_capacity(mesh_count);
98    let mut vertex_starts = Vec::with_capacity(mesh_count);
99    let mut vertex_counts = Vec::with_capacity(mesh_count);
100    let mut index_starts = Vec::with_capacity(mesh_count);
101    let mut index_counts = Vec::with_capacity(mesh_count);
102    let mut color_r = Vec::with_capacity(mesh_count);
103    let mut color_g = Vec::with_capacity(mesh_count);
104    let mut color_b = Vec::with_capacity(mesh_count);
105    let mut color_a = Vec::with_capacity(mesh_count);
106    let mut origin_x = Vec::with_capacity(mesh_count);
107    let mut origin_y = Vec::with_capacity(mesh_count);
108    let mut origin_z = Vec::with_capacity(mesh_count);
109    let mut geometry_class = Vec::with_capacity(mesh_count);
110    let mut geometry_item_ids: Vec<u32> = Vec::with_capacity(mesh_count);
111    let mut material_ids: Vec<u32> = Vec::with_capacity(mesh_count);
112    let mut metallic: Vec<f32> = Vec::with_capacity(mesh_count);
113    let mut roughness: Vec<f32> = Vec::with_capacity(mesh_count);
114    let mut rotation: [Vec<f32>; 9] = std::array::from_fn(|_| Vec::with_capacity(mesh_count));
115
116    for m in metadata {
117        express_ids.push(m.express_id);
118        ifc_types.push(m.ifc_type);
119        vertex_starts.push(m.v_start);
120        vertex_counts.push(m.vert_count);
121        index_starts.push(m.i_start);
122        index_counts.push(m.index_count);
123        color_r.push(m.color[0]);
124        color_g.push(m.color[1]);
125        color_b.push(m.color[2]);
126        color_a.push(m.color[3]);
127        origin_x.push(m.origin[0]);
128        origin_y.push(m.origin[1]);
129        origin_z.push(m.origin[2]);
130        geometry_class.push(m.geometry_class);
131        geometry_item_ids.push(m.geometry_item_id.unwrap_or(ABSENT_SOURCE_ID));
132        material_ids.push(m.material_id.unwrap_or(ABSENT_SOURCE_ID));
133        metallic.push(m.finish[0]);
134        roughness.push(m.finish[1]);
135        for (column, value) in rotation.iter_mut().zip(m.rotation.iter()) {
136            column.push(*value);
137        }
138    }
139
140    // Phase 3: vertex and index data, in parallel over the EMITTED shapes.
141    // The Z-up to Y-up transform is applied server-side so no client repeats
142    // it per vertex (IFC is Z-up, WebGL is Y-up: new Y = old Z, new Z = -old Y).
143    let vertex_data: Vec<VertexColumns> =
144        shape_meshes.par_iter().map(|mesh| shape_vertices(mesh)).collect();
145
146    let mut pos_x = Vec::with_capacity(total_vertices);
147    let mut pos_y = Vec::with_capacity(total_vertices);
148    let mut pos_z = Vec::with_capacity(total_vertices);
149    let mut norm_x = Vec::with_capacity(total_vertices);
150    let mut norm_y = Vec::with_capacity(total_vertices);
151    let mut norm_z = Vec::with_capacity(total_vertices);
152    for (px, py, pz, nx, ny, nz) in vertex_data {
153        pos_x.extend(px);
154        pos_y.extend(py);
155        pos_z.extend(pz);
156        norm_x.extend(nx);
157        norm_y.extend(ny);
158        norm_z.extend(nz);
159    }
160
161    let index_data: Vec<(Vec<u32>, Vec<u32>, Vec<u32>)> = shape_meshes
162        .par_iter()
163        .map(|mesh| {
164            let tri_count = mesh.indices.len() / 3;
165            let mut i0 = Vec::with_capacity(tri_count);
166            let mut i1 = Vec::with_capacity(tri_count);
167            let mut i2 = Vec::with_capacity(tri_count);
168            for i in 0..tri_count {
169                i0.push(mesh.indices[i * 3]);
170                i1.push(mesh.indices[i * 3 + 1]);
171                i2.push(mesh.indices[i * 3 + 2]);
172            }
173            (i0, i1, i2)
174        })
175        .collect();
176
177    let mut idx_0 = Vec::with_capacity(total_triangles);
178    let mut idx_1 = Vec::with_capacity(total_triangles);
179    let mut idx_2 = Vec::with_capacity(total_triangles);
180    for (i0, i1, i2) in index_data {
181        idx_0.extend(i0);
182        idx_1.extend(i1);
183        idx_2.extend(i2);
184    }
185
186    let mut mesh_columns: Vec<arrow::array::ArrayRef> = vec![
187        Arc::new(UInt32Array::from(express_ids)),
188        Arc::new(StringArray::from(ifc_types)),
189        Arc::new(UInt32Array::from(vertex_starts)),
190        Arc::new(UInt32Array::from(vertex_counts)),
191        Arc::new(UInt32Array::from(index_starts)),
192        Arc::new(UInt32Array::from(index_counts)),
193        Arc::new(Float32Array::from(color_r)),
194        Arc::new(Float32Array::from(color_g)),
195        Arc::new(Float32Array::from(color_b)),
196        Arc::new(Float32Array::from(color_a)),
197        Arc::new(Float64Array::from(origin_x)),
198        Arc::new(Float64Array::from(origin_y)),
199        Arc::new(Float64Array::from(origin_z)),
200        Arc::new(UInt8Array::from(geometry_class)),
201        Arc::new(UInt32Array::from(geometry_item_ids)),
202        Arc::new(UInt32Array::from(material_ids)),
203        Arc::new(Float32Array::from(metallic)),
204        Arc::new(Float32Array::from(roughness)),
205    ];
206    if layout.has_rotation() {
207        for column in rotation {
208            mesh_columns.push(Arc::new(Float32Array::from(column)));
209        }
210    }
211    let mesh_batch = RecordBatch::try_new(mesh_schema(layout.has_rotation()), mesh_columns)?;
212
213    let vertex_batch = RecordBatch::try_new(
214        vertex_schema(),
215        vec![
216            Arc::new(Float32Array::from(pos_x)),
217            Arc::new(Float32Array::from(pos_y)),
218            Arc::new(Float32Array::from(pos_z)),
219            Arc::new(Float32Array::from(norm_x)),
220            Arc::new(Float32Array::from(norm_y)),
221            Arc::new(Float32Array::from(norm_z)),
222        ],
223    )?;
224
225    let index_batch = RecordBatch::try_new(
226        index_schema(),
227        vec![
228            Arc::new(UInt32Array::from(idx_0)),
229            Arc::new(UInt32Array::from(idx_1)),
230            Arc::new(UInt32Array::from(idx_2)),
231        ],
232    )?;
233
234    Ok((mesh_batch, vertex_batch, index_batch))
235}
236
237// The unit tests live in the ratchet-exempt sibling file
238// `parquet_mesh_tables_tests.rs`.
239#[cfg(test)]
240#[path = "parquet_mesh_tables_tests.rs"]
241mod parquet_mesh_tables_tests;