ifc_lite_wasm/
zero_copy.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//! Zero-copy mesh data structures for WASM
6//!
7//! Enables direct access to WASM memory from JavaScript without copying.
8
9use ifc_lite_geometry::Mesh;
10use wasm_bindgen::prelude::*;
11
12/// Individual mesh data with express ID and color (matches MeshData interface)
13#[wasm_bindgen]
14pub struct MeshDataJs {
15    express_id: u32,
16    ifc_type: String, // IFC type name (e.g., "IfcWall", "IfcSpace")
17    positions: Vec<f32>,
18    normals: Vec<f32>,
19    indices: Vec<u32>,
20    color: [f32; 4], // RGBA
21}
22
23#[wasm_bindgen]
24impl MeshDataJs {
25    /// Get express ID
26    #[wasm_bindgen(getter, js_name = expressId)]
27    pub fn express_id(&self) -> u32 {
28        self.express_id
29    }
30
31    /// Get IFC type name (e.g., "IfcWall", "IfcSpace")
32    #[wasm_bindgen(getter, js_name = ifcType)]
33    pub fn ifc_type(&self) -> String {
34        self.ifc_type.clone()
35    }
36
37    /// Get positions as Float32Array (copy to JS)
38    #[wasm_bindgen(getter)]
39    pub fn positions(&self) -> js_sys::Float32Array {
40        js_sys::Float32Array::from(&self.positions[..])
41    }
42
43    /// Get normals as Float32Array (copy to JS)
44    #[wasm_bindgen(getter)]
45    pub fn normals(&self) -> js_sys::Float32Array {
46        js_sys::Float32Array::from(&self.normals[..])
47    }
48
49    /// Get indices as Uint32Array (copy to JS)
50    #[wasm_bindgen(getter)]
51    pub fn indices(&self) -> js_sys::Uint32Array {
52        js_sys::Uint32Array::from(&self.indices[..])
53    }
54
55    /// Get color as [r, g, b, a] array
56    #[wasm_bindgen(getter)]
57    pub fn color(&self) -> Vec<f32> {
58        self.color.to_vec()
59    }
60
61    /// Get vertex count
62    #[wasm_bindgen(getter, js_name = vertexCount)]
63    pub fn vertex_count(&self) -> usize {
64        self.positions.len() / 3
65    }
66
67    /// Get triangle count
68    #[wasm_bindgen(getter, js_name = triangleCount)]
69    pub fn triangle_count(&self) -> usize {
70        self.indices.len() / 3
71    }
72}
73
74impl MeshDataJs {
75    /// Create new mesh data
76    pub fn new(express_id: u32, ifc_type: String, mesh: Mesh, color: [f32; 4]) -> Self {
77        Self {
78            express_id,
79            ifc_type,
80            positions: mesh.positions,
81            normals: mesh.normals,
82            indices: mesh.indices,
83            color,
84        }
85    }
86}
87
88/// Collection of mesh data for returning multiple meshes
89#[wasm_bindgen]
90pub struct MeshCollection {
91    meshes: Vec<MeshDataJs>,
92}
93
94#[wasm_bindgen]
95impl MeshCollection {
96    /// Get number of meshes
97    #[wasm_bindgen(getter)]
98    pub fn length(&self) -> usize {
99        self.meshes.len()
100    }
101
102    /// Get mesh at index
103    #[wasm_bindgen]
104    pub fn get(&self, index: usize) -> Option<MeshDataJs> {
105        self.meshes.get(index).map(|m| MeshDataJs {
106            express_id: m.express_id,
107            ifc_type: m.ifc_type.clone(),
108            positions: m.positions.clone(),
109            normals: m.normals.clone(),
110            indices: m.indices.clone(),
111            color: m.color,
112        })
113    }
114
115    /// Get total vertex count across all meshes
116    #[wasm_bindgen(getter, js_name = totalVertices)]
117    pub fn total_vertices(&self) -> usize {
118        self.meshes.iter().map(|m| m.positions.len() / 3).sum()
119    }
120
121    /// Get total triangle count across all meshes
122    #[wasm_bindgen(getter, js_name = totalTriangles)]
123    pub fn total_triangles(&self) -> usize {
124        self.meshes.iter().map(|m| m.indices.len() / 3).sum()
125    }
126}
127
128impl MeshCollection {
129    /// Create new empty collection
130    pub fn new() -> Self {
131        Self { meshes: Vec::new() }
132    }
133
134    /// Create new collection with capacity hint
135    pub fn with_capacity(capacity: usize) -> Self {
136        Self {
137            meshes: Vec::with_capacity(capacity),
138        }
139    }
140
141    /// Add a mesh to the collection
142    #[inline]
143    pub fn add(&mut self, mesh: MeshDataJs) {
144        self.meshes.push(mesh);
145    }
146
147    /// Create from vec of meshes
148    pub fn from_vec(meshes: Vec<MeshDataJs>) -> Self {
149        Self { meshes }
150    }
151
152    /// Get number of meshes (internal)
153    pub fn len(&self) -> usize {
154        self.meshes.len()
155    }
156
157    /// Check if collection is empty
158    pub fn is_empty(&self) -> bool {
159        self.meshes.is_empty()
160    }
161
162    /// Apply RTC offset to all meshes (shift coordinates)
163    pub fn apply_rtc_offset(&mut self, x: f64, y: f64, z: f64) {
164        for mesh in &mut self.meshes {
165            for chunk in mesh.positions.chunks_exact_mut(3) {
166                chunk[0] = (chunk[0] as f64 - x) as f32;
167                chunk[1] = (chunk[1] as f64 - y) as f32;
168                chunk[2] = (chunk[2] as f64 - z) as f32;
169            }
170        }
171    }
172}
173
174impl Clone for MeshCollection {
175    fn clone(&self) -> Self {
176        Self {
177            meshes: self
178                .meshes
179                .iter()
180                .map(|m| MeshDataJs {
181                    express_id: m.express_id,
182                    ifc_type: m.ifc_type.clone(),
183                    positions: m.positions.clone(),
184                    normals: m.normals.clone(),
185                    indices: m.indices.clone(),
186                    color: m.color,
187                })
188                .collect(),
189        }
190    }
191}
192
193impl Default for MeshCollection {
194    fn default() -> Self {
195        Self::new()
196    }
197}
198
199/// Zero-copy mesh that exposes pointers to WASM memory
200#[wasm_bindgen]
201pub struct ZeroCopyMesh {
202    mesh: Mesh,
203}
204
205#[wasm_bindgen]
206impl ZeroCopyMesh {
207    /// Create a new zero-copy mesh from a Mesh
208    #[wasm_bindgen(constructor)]
209    pub fn new() -> Self {
210        Self { mesh: Mesh::new() }
211    }
212
213    /// Get pointer to positions array
214    /// JavaScript can create Float32Array view: new Float32Array(memory.buffer, ptr, length)
215    #[wasm_bindgen(getter)]
216    pub fn positions_ptr(&self) -> *const f32 {
217        self.mesh.positions.as_ptr()
218    }
219
220    /// Get length of positions array (in f32 elements, not bytes)
221    #[wasm_bindgen(getter)]
222    pub fn positions_len(&self) -> usize {
223        self.mesh.positions.len()
224    }
225
226    /// Get pointer to normals array
227    #[wasm_bindgen(getter)]
228    pub fn normals_ptr(&self) -> *const f32 {
229        self.mesh.normals.as_ptr()
230    }
231
232    /// Get length of normals array
233    #[wasm_bindgen(getter)]
234    pub fn normals_len(&self) -> usize {
235        self.mesh.normals.len()
236    }
237
238    /// Get pointer to indices array
239    #[wasm_bindgen(getter)]
240    pub fn indices_ptr(&self) -> *const u32 {
241        self.mesh.indices.as_ptr()
242    }
243
244    /// Get length of indices array
245    #[wasm_bindgen(getter)]
246    pub fn indices_len(&self) -> usize {
247        self.mesh.indices.len()
248    }
249
250    /// Get vertex count
251    #[wasm_bindgen(getter)]
252    pub fn vertex_count(&self) -> usize {
253        self.mesh.vertex_count()
254    }
255
256    /// Get triangle count
257    #[wasm_bindgen(getter)]
258    pub fn triangle_count(&self) -> usize {
259        self.mesh.triangle_count()
260    }
261
262    /// Check if mesh is empty
263    #[wasm_bindgen(getter)]
264    pub fn is_empty(&self) -> bool {
265        self.mesh.is_empty()
266    }
267
268    /// Get bounding box minimum point
269    #[wasm_bindgen]
270    pub fn bounds_min(&self) -> Vec<f32> {
271        let (min, _) = self.mesh.bounds();
272        vec![min.x, min.y, min.z]
273    }
274
275    /// Get bounding box maximum point
276    #[wasm_bindgen]
277    pub fn bounds_max(&self) -> Vec<f32> {
278        let (_, max) = self.mesh.bounds();
279        vec![max.x, max.y, max.z]
280    }
281}
282
283impl From<Mesh> for ZeroCopyMesh {
284    fn from(mesh: Mesh) -> Self {
285        Self { mesh }
286    }
287}
288
289impl Default for ZeroCopyMesh {
290    fn default() -> Self {
291        Self::new()
292    }
293}
294
295/// Instance data for instanced rendering
296#[wasm_bindgen]
297pub struct InstanceData {
298    express_id: u32,
299    transform: Vec<f32>, // 16 floats (4x4 matrix)
300    color: [f32; 4],     // RGBA
301}
302
303#[wasm_bindgen]
304impl InstanceData {
305    #[wasm_bindgen(getter, js_name = expressId)]
306    pub fn express_id(&self) -> u32 {
307        self.express_id
308    }
309
310    #[wasm_bindgen(getter)]
311    pub fn transform(&self) -> js_sys::Float32Array {
312        js_sys::Float32Array::from(&self.transform[..])
313    }
314
315    #[wasm_bindgen(getter)]
316    pub fn color(&self) -> Vec<f32> {
317        self.color.to_vec()
318    }
319}
320
321impl InstanceData {
322    pub fn new(express_id: u32, transform: Vec<f32>, color: [f32; 4]) -> Self {
323        Self {
324            express_id,
325            transform,
326            color,
327        }
328    }
329}
330
331/// Instanced geometry - one geometry definition with multiple instances
332#[wasm_bindgen]
333pub struct InstancedGeometry {
334    geometry_id: u64,
335    positions: Vec<f32>,
336    normals: Vec<f32>,
337    indices: Vec<u32>,
338    instances: Vec<InstanceData>,
339}
340
341#[wasm_bindgen]
342impl InstancedGeometry {
343    #[wasm_bindgen(getter, js_name = geometryId)]
344    pub fn geometry_id(&self) -> u64 {
345        self.geometry_id
346    }
347
348    #[wasm_bindgen(getter)]
349    pub fn positions(&self) -> js_sys::Float32Array {
350        js_sys::Float32Array::from(&self.positions[..])
351    }
352
353    #[wasm_bindgen(getter)]
354    pub fn normals(&self) -> js_sys::Float32Array {
355        js_sys::Float32Array::from(&self.normals[..])
356    }
357
358    #[wasm_bindgen(getter)]
359    pub fn indices(&self) -> js_sys::Uint32Array {
360        js_sys::Uint32Array::from(&self.indices[..])
361    }
362
363    #[wasm_bindgen(getter)]
364    pub fn instance_count(&self) -> usize {
365        self.instances.len()
366    }
367
368    #[wasm_bindgen]
369    pub fn get_instance(&self, index: usize) -> Option<InstanceData> {
370        self.instances.get(index).map(|inst| InstanceData {
371            express_id: inst.express_id,
372            transform: inst.transform.clone(),
373            color: inst.color,
374        })
375    }
376}
377
378impl InstancedGeometry {
379    pub fn new(
380        geometry_id: u64,
381        positions: Vec<f32>,
382        normals: Vec<f32>,
383        indices: Vec<u32>,
384    ) -> Self {
385        Self {
386            geometry_id,
387            positions,
388            normals,
389            indices,
390            instances: Vec::new(),
391        }
392    }
393
394    pub fn add_instance(&mut self, instance: InstanceData) {
395        self.instances.push(instance);
396    }
397}
398
399/// Collection of instanced geometries
400#[wasm_bindgen]
401pub struct InstancedMeshCollection {
402    geometries: Vec<InstancedGeometry>,
403}
404
405#[wasm_bindgen]
406impl InstancedMeshCollection {
407    #[wasm_bindgen(getter)]
408    pub fn length(&self) -> usize {
409        self.geometries.len()
410    }
411
412    #[wasm_bindgen]
413    pub fn get(&self, index: usize) -> Option<InstancedGeometry> {
414        self.geometries.get(index).map(|g| InstancedGeometry {
415            geometry_id: g.geometry_id,
416            positions: g.positions.clone(),
417            normals: g.normals.clone(),
418            indices: g.indices.clone(),
419            instances: g
420                .instances
421                .iter()
422                .map(|inst| InstanceData {
423                    express_id: inst.express_id,
424                    transform: inst.transform.clone(),
425                    color: inst.color,
426                })
427                .collect(),
428        })
429    }
430
431    #[wasm_bindgen(getter, js_name = totalGeometries)]
432    pub fn total_geometries(&self) -> usize {
433        self.geometries.len()
434    }
435
436    #[wasm_bindgen(getter, js_name = totalInstances)]
437    pub fn total_instances(&self) -> usize {
438        self.geometries.iter().map(|g| g.instances.len()).sum()
439    }
440}
441
442impl InstancedMeshCollection {
443    pub fn new() -> Self {
444        Self {
445            geometries: Vec::new(),
446        }
447    }
448
449    pub fn add(&mut self, geometry: InstancedGeometry) {
450        self.geometries.push(geometry);
451    }
452}
453
454impl Default for InstancedMeshCollection {
455    fn default() -> Self {
456        Self::new()
457    }
458}
459
460/// Get WASM memory to allow JavaScript to create TypedArray views
461#[wasm_bindgen]
462pub fn get_memory() -> JsValue {
463    wasm_bindgen::memory()
464}
465
466#[cfg(test)]
467mod tests {
468    use super::*;
469
470    #[test]
471    fn test_zero_copy_mesh_creation() {
472        let mesh = ZeroCopyMesh::new();
473        assert!(mesh.is_empty());
474        assert_eq!(mesh.vertex_count(), 0);
475        assert_eq!(mesh.triangle_count(), 0);
476    }
477
478    #[test]
479    fn test_zero_copy_mesh_pointers() {
480        let mesh = ZeroCopyMesh::new();
481
482        // Pointers should be valid even for empty mesh
483        assert!(!mesh.positions_ptr().is_null());
484        assert!(!mesh.normals_ptr().is_null());
485        assert!(!mesh.indices_ptr().is_null());
486    }
487}