Skip to main content

ifc_lite_wasm/
gpu_geometry.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//! GPU-ready geometry data structures for zero-copy WebGPU upload
6//!
7//! This module provides geometry data structures that are pre-processed for direct
8//! GPU upload without intermediate copies. Data is:
9//! - Interleaved (position + normal per vertex)
10//! - Coordinate-converted (Z-up to Y-up)
11//! - Stored contiguously for efficient memory access
12//!
13//! # Zero-Copy Pattern
14//!
15//! ```javascript
16//! // Get GPU-ready geometry from WASM
17//! const gpuGeom = api.parseToGpuGeometry(ifcData);
18//!
19//! // Get WASM memory buffer
20//! const memory = api.getMemory();
21//!
22//! // Create views directly into WASM memory (NO COPY!)
23//! const vertexView = new Float32Array(
24//!   memory.buffer,
25//!   gpuGeom.vertexDataPtr,
26//!   gpuGeom.vertexDataLen
27//! );
28//!
29//! // Upload directly to GPU (single copy: WASM → GPU)
30//! device.queue.writeBuffer(gpuBuffer, 0, vertexView);
31//!
32//! // IMPORTANT: Free the geometry when done (allows WASM to reuse memory)
33//! gpuGeom.free();
34//! ```
35
36use wasm_bindgen::prelude::*;
37
38/// Metadata for a single mesh within the GPU geometry buffer
39#[wasm_bindgen]
40#[derive(Debug, Clone)]
41pub struct GpuMeshMetadata {
42    /// Express ID of the IFC entity
43    express_id: u32,
44    /// Index into the IFC type string table
45    ifc_type_idx: u16,
46    /// Offset in vertex_data (in floats, not bytes)
47    vertex_offset: u32,
48    /// Number of vertices
49    vertex_count: u32,
50    /// Offset in indices array
51    index_offset: u32,
52    /// Number of indices
53    index_count: u32,
54    /// RGBA color
55    color: [f32; 4],
56}
57
58#[wasm_bindgen]
59impl GpuMeshMetadata {
60    #[wasm_bindgen(getter, js_name = expressId)]
61    pub fn express_id(&self) -> u32 {
62        self.express_id
63    }
64
65    #[wasm_bindgen(getter, js_name = ifcTypeIdx)]
66    pub fn ifc_type_idx(&self) -> u16 {
67        self.ifc_type_idx
68    }
69
70    #[wasm_bindgen(getter, js_name = vertexOffset)]
71    pub fn vertex_offset(&self) -> u32 {
72        self.vertex_offset
73    }
74
75    #[wasm_bindgen(getter, js_name = vertexCount)]
76    pub fn vertex_count(&self) -> u32 {
77        self.vertex_count
78    }
79
80    #[wasm_bindgen(getter, js_name = indexOffset)]
81    pub fn index_offset(&self) -> u32 {
82        self.index_offset
83    }
84
85    #[wasm_bindgen(getter, js_name = indexCount)]
86    pub fn index_count(&self) -> u32 {
87        self.index_count
88    }
89
90    #[wasm_bindgen(getter)]
91    pub fn color(&self) -> Vec<f32> {
92        self.color.to_vec()
93    }
94}
95
96/// GPU-ready geometry stored in WASM linear memory
97///
98/// Data layout:
99/// - vertex_data: Interleaved [px, py, pz, nx, ny, nz, ...] (6 floats per vertex)
100/// - indices: Triangle indices [i0, i1, i2, ...]
101/// - mesh_metadata: Per-mesh metadata for draw calls
102///
103/// All coordinates are pre-converted from IFC Z-up to WebGL Y-up
104#[wasm_bindgen]
105pub struct GpuGeometry {
106    /// Interleaved vertex data: [px, py, pz, nx, ny, nz, ...]
107    /// Already converted from Z-up to Y-up
108    vertex_data: Vec<f32>,
109
110    /// Triangle indices
111    indices: Vec<u32>,
112
113    /// Metadata per mesh (for selection, draw call ranges, etc.)
114    mesh_metadata: Vec<GpuMeshMetadata>,
115
116    /// IFC type names (deduplicated)
117    ifc_type_names: Vec<String>,
118
119    /// RTC (Relative To Center) offset applied to coordinates
120    /// Used for models with large world coordinates (>10km from origin)
121    rtc_offset_x: f64,
122    rtc_offset_y: f64,
123    rtc_offset_z: f64,
124}
125
126#[wasm_bindgen]
127impl GpuGeometry {
128    /// Create a new empty GPU geometry container
129    #[wasm_bindgen(constructor)]
130    pub fn new() -> Self {
131        Self {
132            vertex_data: Vec::new(),
133            indices: Vec::new(),
134            mesh_metadata: Vec::new(),
135            ifc_type_names: Vec::new(),
136            rtc_offset_x: 0.0,
137            rtc_offset_y: 0.0,
138            rtc_offset_z: 0.0,
139        }
140    }
141
142    /// Set the RTC (Relative To Center) offset applied to coordinates
143    pub fn set_rtc_offset(&mut self, x: f64, y: f64, z: f64) {
144        self.rtc_offset_x = x;
145        self.rtc_offset_y = y;
146        self.rtc_offset_z = z;
147    }
148
149    /// Get X component of RTC offset
150    #[wasm_bindgen(getter, js_name = rtcOffsetX)]
151    pub fn rtc_offset_x(&self) -> f64 {
152        self.rtc_offset_x
153    }
154
155    /// Get Y component of RTC offset
156    #[wasm_bindgen(getter, js_name = rtcOffsetY)]
157    pub fn rtc_offset_y(&self) -> f64 {
158        self.rtc_offset_y
159    }
160
161    /// Get Z component of RTC offset
162    #[wasm_bindgen(getter, js_name = rtcOffsetZ)]
163    pub fn rtc_offset_z(&self) -> f64 {
164        self.rtc_offset_z
165    }
166
167    /// Check if RTC offset is active (non-zero)
168    #[wasm_bindgen(getter, js_name = hasRtcOffset)]
169    pub fn has_rtc_offset(&self) -> bool {
170        self.rtc_offset_x != 0.0 || self.rtc_offset_y != 0.0 || self.rtc_offset_z != 0.0
171    }
172
173    /// Get pointer to vertex data for zero-copy view
174    ///
175    /// SAFETY: View is only valid until next WASM allocation!
176    /// Create view, upload to GPU, then discard view immediately.
177    #[wasm_bindgen(getter, js_name = vertexDataPtr)]
178    pub fn vertex_data_ptr(&self) -> *const f32 {
179        self.vertex_data.as_ptr()
180    }
181
182    /// Get length of vertex data array (in f32 elements, not bytes)
183    #[wasm_bindgen(getter, js_name = vertexDataLen)]
184    pub fn vertex_data_len(&self) -> usize {
185        self.vertex_data.len()
186    }
187
188    /// Get byte length of vertex data (for GPU buffer creation)
189    #[wasm_bindgen(getter, js_name = vertexDataByteLength)]
190    pub fn vertex_data_byte_length(&self) -> usize {
191        self.vertex_data.len() * 4 // f32 = 4 bytes
192    }
193
194    /// Get pointer to indices array for zero-copy view
195    #[wasm_bindgen(getter, js_name = indicesPtr)]
196    pub fn indices_ptr(&self) -> *const u32 {
197        self.indices.as_ptr()
198    }
199
200    /// Get length of indices array (in u32 elements)
201    #[wasm_bindgen(getter, js_name = indicesLen)]
202    pub fn indices_len(&self) -> usize {
203        self.indices.len()
204    }
205
206    /// Get byte length of indices (for GPU buffer creation)
207    #[wasm_bindgen(getter, js_name = indicesByteLength)]
208    pub fn indices_byte_length(&self) -> usize {
209        self.indices.len() * 4 // u32 = 4 bytes
210    }
211
212    /// Get number of meshes in this geometry batch
213    #[wasm_bindgen(getter, js_name = meshCount)]
214    pub fn mesh_count(&self) -> usize {
215        self.mesh_metadata.len()
216    }
217
218    /// Get total vertex count
219    #[wasm_bindgen(getter, js_name = totalVertexCount)]
220    pub fn total_vertex_count(&self) -> usize {
221        self.vertex_data.len() / 6 // 6 floats per vertex (pos + normal)
222    }
223
224    /// Get total triangle count
225    #[wasm_bindgen(getter, js_name = totalTriangleCount)]
226    pub fn total_triangle_count(&self) -> usize {
227        self.indices.len() / 3
228    }
229
230    /// Get metadata for a specific mesh
231    #[wasm_bindgen(js_name = getMeshMetadata)]
232    pub fn get_mesh_metadata(&self, index: usize) -> Option<GpuMeshMetadata> {
233        self.mesh_metadata.get(index).cloned()
234    }
235
236    /// Get IFC type name by index
237    #[wasm_bindgen(js_name = getIfcTypeName)]
238    pub fn get_ifc_type_name(&self, index: u16) -> Option<String> {
239        self.ifc_type_names.get(index as usize).cloned()
240    }
241
242    /// Check if geometry is empty
243    #[wasm_bindgen(getter, js_name = isEmpty)]
244    pub fn is_empty(&self) -> bool {
245        self.vertex_data.is_empty()
246    }
247}
248
249impl GpuGeometry {
250    /// Create with pre-allocated capacity
251    pub fn with_capacity(vertex_capacity: usize, index_capacity: usize) -> Self {
252        Self {
253            vertex_data: Vec::with_capacity(vertex_capacity),
254            indices: Vec::with_capacity(index_capacity),
255            mesh_metadata: Vec::with_capacity(256),
256            ifc_type_names: Vec::with_capacity(64),
257            rtc_offset_x: 0.0,
258            rtc_offset_y: 0.0,
259            rtc_offset_z: 0.0,
260        }
261    }
262
263    /// Add a mesh with positions and normals, interleaving and converting coordinates
264    pub fn add_mesh(
265        &mut self,
266        express_id: u32,
267        ifc_type: &str,
268        positions: &[f32],
269        normals: &[f32],
270        indices: &[u32],
271        color: [f32; 4],
272    ) {
273        let vertex_count = positions.len() / 3;
274        if vertex_count == 0 {
275            return;
276        }
277
278        // Get or add IFC type name
279        let ifc_type_idx = self.get_or_add_ifc_type(ifc_type);
280
281        // Record current offsets
282        let vertex_offset = (self.vertex_data.len() / 6) as u32;
283        let index_offset = self.indices.len() as u32;
284
285        // Interleave positions and normals with coordinate conversion
286        // Layout: [px, py, pz, nx, ny, nz] per vertex
287        self.vertex_data.reserve(vertex_count * 6);
288
289        for i in 0..vertex_count {
290            let pi = i * 3;
291
292            // Position (convert Z-up to Y-up)
293            let px = positions[pi];
294            let py = positions[pi + 2]; // New Y = old Z
295            let pz = -positions[pi + 1]; // New Z = -old Y
296
297            // Normal (convert Z-up to Y-up)
298            let nx = normals[pi];
299            let ny = normals[pi + 2]; // New Y = old Z
300            let nz = -normals[pi + 1]; // New Z = -old Y
301
302            self.vertex_data.push(px);
303            self.vertex_data.push(py);
304            self.vertex_data.push(pz);
305            self.vertex_data.push(nx);
306            self.vertex_data.push(ny);
307            self.vertex_data.push(nz);
308        }
309
310        // Add indices (offset by current vertex count)
311        self.indices.reserve(indices.len());
312        for &idx in indices {
313            self.indices.push(idx + vertex_offset);
314        }
315
316        // Add metadata
317        self.mesh_metadata.push(GpuMeshMetadata {
318            express_id,
319            ifc_type_idx,
320            vertex_offset,
321            vertex_count: vertex_count as u32,
322            index_offset,
323            index_count: indices.len() as u32,
324            color,
325        });
326    }
327
328    /// Get or add an IFC type name to the string table
329    fn get_or_add_ifc_type(&mut self, ifc_type: &str) -> u16 {
330        // Check if already exists
331        for (i, name) in self.ifc_type_names.iter().enumerate() {
332            if name == ifc_type {
333                return i as u16;
334            }
335        }
336
337        // Add new
338        let idx = self.ifc_type_names.len() as u16;
339        self.ifc_type_names.push(ifc_type.to_string());
340        idx
341    }
342
343    /// Clear all data (for reuse)
344    pub fn clear(&mut self) {
345        self.vertex_data.clear();
346        self.indices.clear();
347        self.mesh_metadata.clear();
348        // Keep ifc_type_names for reuse
349    }
350}
351
352impl Default for GpuGeometry {
353    fn default() -> Self {
354        Self::new()
355    }
356}
357
358/// GPU-ready instanced geometry for efficient rendering of repeated shapes
359///
360/// Data layout:
361/// - vertex_data: Interleaved [px, py, pz, nx, ny, nz, ...] (shared geometry)
362/// - indices: Triangle indices (shared geometry)
363/// - instance_data: [transform (16 floats) + color (4 floats)] per instance = 20 floats
364#[wasm_bindgen]
365pub struct GpuInstancedGeometry {
366    /// Geometry ID (hash of the geometry for deduplication)
367    geometry_id: u64,
368
369    /// Interleaved vertex data for shared geometry
370    vertex_data: Vec<f32>,
371
372    /// Triangle indices for shared geometry
373    indices: Vec<u32>,
374
375    /// Instance data: [transform (16 floats) + color (4 floats)] per instance
376    instance_data: Vec<f32>,
377
378    /// Express IDs for each instance (for selection)
379    instance_express_ids: Vec<u32>,
380}
381
382#[wasm_bindgen]
383impl GpuInstancedGeometry {
384    /// Create new instanced geometry
385    #[wasm_bindgen(constructor)]
386    pub fn new(geometry_id: u64) -> Self {
387        Self {
388            geometry_id,
389            vertex_data: Vec::new(),
390            indices: Vec::new(),
391            instance_data: Vec::new(),
392            instance_express_ids: Vec::new(),
393        }
394    }
395
396    #[wasm_bindgen(getter, js_name = geometryId)]
397    pub fn geometry_id(&self) -> u64 {
398        self.geometry_id
399    }
400
401    // Vertex data pointers
402    #[wasm_bindgen(getter, js_name = vertexDataPtr)]
403    pub fn vertex_data_ptr(&self) -> *const f32 {
404        self.vertex_data.as_ptr()
405    }
406
407    #[wasm_bindgen(getter, js_name = vertexDataLen)]
408    pub fn vertex_data_len(&self) -> usize {
409        self.vertex_data.len()
410    }
411
412    #[wasm_bindgen(getter, js_name = vertexDataByteLength)]
413    pub fn vertex_data_byte_length(&self) -> usize {
414        self.vertex_data.len() * 4
415    }
416
417    // Indices pointers
418    #[wasm_bindgen(getter, js_name = indicesPtr)]
419    pub fn indices_ptr(&self) -> *const u32 {
420        self.indices.as_ptr()
421    }
422
423    #[wasm_bindgen(getter, js_name = indicesLen)]
424    pub fn indices_len(&self) -> usize {
425        self.indices.len()
426    }
427
428    #[wasm_bindgen(getter, js_name = indicesByteLength)]
429    pub fn indices_byte_length(&self) -> usize {
430        self.indices.len() * 4
431    }
432
433    // Instance data pointers
434    #[wasm_bindgen(getter, js_name = instanceDataPtr)]
435    pub fn instance_data_ptr(&self) -> *const f32 {
436        self.instance_data.as_ptr()
437    }
438
439    #[wasm_bindgen(getter, js_name = instanceDataLen)]
440    pub fn instance_data_len(&self) -> usize {
441        self.instance_data.len()
442    }
443
444    #[wasm_bindgen(getter, js_name = instanceDataByteLength)]
445    pub fn instance_data_byte_length(&self) -> usize {
446        self.instance_data.len() * 4
447    }
448
449    // Instance express IDs pointer
450    #[wasm_bindgen(getter, js_name = instanceExpressIdsPtr)]
451    pub fn instance_express_ids_ptr(&self) -> *const u32 {
452        self.instance_express_ids.as_ptr()
453    }
454
455    #[wasm_bindgen(getter, js_name = instanceCount)]
456    pub fn instance_count(&self) -> usize {
457        self.instance_express_ids.len()
458    }
459
460    #[wasm_bindgen(getter, js_name = vertexCount)]
461    pub fn vertex_count(&self) -> usize {
462        self.vertex_data.len() / 6
463    }
464
465    #[wasm_bindgen(getter, js_name = triangleCount)]
466    pub fn triangle_count(&self) -> usize {
467        self.indices.len() / 3
468    }
469}
470
471impl GpuInstancedGeometry {
472    /// Set shared geometry with interleaving and coordinate conversion
473    pub fn set_geometry(&mut self, positions: &[f32], normals: &[f32], indices: &[u32]) {
474        let vertex_count = positions.len() / 3;
475
476        // Clear and reserve
477        self.vertex_data.clear();
478        self.vertex_data.reserve(vertex_count * 6);
479        self.indices.clear();
480        self.indices.reserve(indices.len());
481
482        // Interleave with Z-up to Y-up conversion
483        for i in 0..vertex_count {
484            let pi = i * 3;
485
486            // Position (convert Z-up to Y-up)
487            self.vertex_data.push(positions[pi]);
488            self.vertex_data.push(positions[pi + 2]); // New Y = old Z
489            self.vertex_data.push(-positions[pi + 1]); // New Z = -old Y
490
491            // Normal (convert Z-up to Y-up)
492            self.vertex_data.push(normals[pi]);
493            self.vertex_data.push(normals[pi + 2]); // New Y = old Z
494            self.vertex_data.push(-normals[pi + 1]); // New Z = -old Y
495        }
496
497        // Copy indices directly
498        self.indices.extend_from_slice(indices);
499    }
500
501    /// Add an instance with transform and color
502    pub fn add_instance(&mut self, express_id: u32, transform: &[f32; 16], color: [f32; 4]) {
503        // Add transform (16 floats)
504        self.instance_data.extend_from_slice(transform);
505
506        // Add color (4 floats)
507        self.instance_data.extend_from_slice(&color);
508
509        // Track express ID
510        self.instance_express_ids.push(express_id);
511    }
512}
513
514/// Collection of GPU-ready instanced geometries
515#[wasm_bindgen]
516pub struct GpuInstancedGeometryCollection {
517    geometries: Vec<GpuInstancedGeometry>,
518}
519
520#[wasm_bindgen]
521impl GpuInstancedGeometryCollection {
522    #[wasm_bindgen(constructor)]
523    pub fn new() -> Self {
524        Self {
525            geometries: Vec::new(),
526        }
527    }
528
529    #[wasm_bindgen(getter)]
530    pub fn length(&self) -> usize {
531        self.geometries.len()
532    }
533
534    #[wasm_bindgen]
535    pub fn get(&self, index: usize) -> Option<GpuInstancedGeometry> {
536        self.geometries.get(index).map(|g| GpuInstancedGeometry {
537            geometry_id: g.geometry_id,
538            vertex_data: g.vertex_data.clone(),
539            indices: g.indices.clone(),
540            instance_data: g.instance_data.clone(),
541            instance_express_ids: g.instance_express_ids.clone(),
542        })
543    }
544
545    /// Get geometry by index with zero-copy access
546    /// Returns a reference that provides pointer access
547    #[wasm_bindgen(js_name = getRef)]
548    pub fn get_ref(&self, index: usize) -> Option<GpuInstancedGeometryRef> {
549        if index < self.geometries.len() {
550            Some(GpuInstancedGeometryRef {
551                collection_ptr: self as *const GpuInstancedGeometryCollection,
552                index,
553            })
554        } else {
555            None
556        }
557    }
558}
559
560impl GpuInstancedGeometryCollection {
561    pub fn add(&mut self, geometry: GpuInstancedGeometry) {
562        self.geometries.push(geometry);
563    }
564
565    pub fn get_mut(&mut self, index: usize) -> Option<&mut GpuInstancedGeometry> {
566        self.geometries.get_mut(index)
567    }
568}
569
570impl Default for GpuInstancedGeometryCollection {
571    fn default() -> Self {
572        Self::new()
573    }
574}
575
576/// Reference to geometry in collection for zero-copy access
577/// This avoids cloning when accessing geometry data
578#[wasm_bindgen]
579pub struct GpuInstancedGeometryRef {
580    collection_ptr: *const GpuInstancedGeometryCollection,
581    index: usize,
582}
583
584#[wasm_bindgen]
585impl GpuInstancedGeometryRef {
586    fn get_geometry(&self) -> Option<&GpuInstancedGeometry> {
587        unsafe {
588            let collection = &*self.collection_ptr;
589            collection.geometries.get(self.index)
590        }
591    }
592
593    #[wasm_bindgen(getter, js_name = geometryId)]
594    pub fn geometry_id(&self) -> u64 {
595        self.get_geometry().map(|g| g.geometry_id).unwrap_or(0)
596    }
597
598    #[wasm_bindgen(getter, js_name = vertexDataPtr)]
599    pub fn vertex_data_ptr(&self) -> *const f32 {
600        self.get_geometry()
601            .map(|g| g.vertex_data.as_ptr())
602            .unwrap_or(std::ptr::null())
603    }
604
605    #[wasm_bindgen(getter, js_name = vertexDataLen)]
606    pub fn vertex_data_len(&self) -> usize {
607        self.get_geometry().map(|g| g.vertex_data.len()).unwrap_or(0)
608    }
609
610    #[wasm_bindgen(getter, js_name = vertexDataByteLength)]
611    pub fn vertex_data_byte_length(&self) -> usize {
612        self.vertex_data_len() * 4
613    }
614
615    #[wasm_bindgen(getter, js_name = indicesPtr)]
616    pub fn indices_ptr(&self) -> *const u32 {
617        self.get_geometry()
618            .map(|g| g.indices.as_ptr())
619            .unwrap_or(std::ptr::null())
620    }
621
622    #[wasm_bindgen(getter, js_name = indicesLen)]
623    pub fn indices_len(&self) -> usize {
624        self.get_geometry().map(|g| g.indices.len()).unwrap_or(0)
625    }
626
627    #[wasm_bindgen(getter, js_name = indicesByteLength)]
628    pub fn indices_byte_length(&self) -> usize {
629        self.indices_len() * 4
630    }
631
632    #[wasm_bindgen(getter, js_name = instanceDataPtr)]
633    pub fn instance_data_ptr(&self) -> *const f32 {
634        self.get_geometry()
635            .map(|g| g.instance_data.as_ptr())
636            .unwrap_or(std::ptr::null())
637    }
638
639    #[wasm_bindgen(getter, js_name = instanceDataLen)]
640    pub fn instance_data_len(&self) -> usize {
641        self.get_geometry()
642            .map(|g| g.instance_data.len())
643            .unwrap_or(0)
644    }
645
646    #[wasm_bindgen(getter, js_name = instanceDataByteLength)]
647    pub fn instance_data_byte_length(&self) -> usize {
648        self.instance_data_len() * 4
649    }
650
651    #[wasm_bindgen(getter, js_name = instanceExpressIdsPtr)]
652    pub fn instance_express_ids_ptr(&self) -> *const u32 {
653        self.get_geometry()
654            .map(|g| g.instance_express_ids.as_ptr())
655            .unwrap_or(std::ptr::null())
656    }
657
658    #[wasm_bindgen(getter, js_name = instanceCount)]
659    pub fn instance_count(&self) -> usize {
660        self.get_geometry()
661            .map(|g| g.instance_express_ids.len())
662            .unwrap_or(0)
663    }
664}
665
666#[cfg(test)]
667mod tests {
668    use super::*;
669
670    #[test]
671    fn test_gpu_geometry_creation() {
672        let geom = GpuGeometry::new();
673        assert!(geom.is_empty());
674        assert_eq!(geom.mesh_count(), 0);
675    }
676
677    #[test]
678    fn test_gpu_geometry_add_mesh() {
679        let mut geom = GpuGeometry::new();
680
681        // Simple triangle
682        let positions = vec![0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.5, 0.0, 1.0];
683        let normals = vec![0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0];
684        let indices = vec![0, 1, 2];
685        let color = [1.0, 0.0, 0.0, 1.0];
686
687        geom.add_mesh(123, "IfcWall", &positions, &normals, &indices, color);
688
689        assert!(!geom.is_empty());
690        assert_eq!(geom.mesh_count(), 1);
691        assert_eq!(geom.total_vertex_count(), 3);
692        assert_eq!(geom.total_triangle_count(), 1);
693
694        // Check metadata
695        let meta = geom.get_mesh_metadata(0).unwrap();
696        assert_eq!(meta.express_id, 123);
697        assert_eq!(meta.vertex_count, 3);
698        assert_eq!(meta.index_count, 3);
699    }
700
701    #[test]
702    fn test_coordinate_conversion() {
703        let mut geom = GpuGeometry::new();
704
705        // Point at (1, 2, 3) in Z-up should become (1, 3, -2) in Y-up
706        let positions = vec![1.0, 2.0, 3.0];
707        let normals = vec![0.0, 0.0, 1.0]; // Normal pointing up in Z-up
708        let indices = vec![0];
709        let color = [1.0, 1.0, 1.0, 1.0];
710
711        geom.add_mesh(1, "Test", &positions, &normals, &indices, color);
712
713        // Vertex data is interleaved: [px, py, pz, nx, ny, nz]
714        assert_eq!(geom.vertex_data[0], 1.0); // px unchanged
715        assert_eq!(geom.vertex_data[1], 3.0); // py = old z
716        assert_eq!(geom.vertex_data[2], -2.0); // pz = -old y
717
718        assert_eq!(geom.vertex_data[3], 0.0); // nx unchanged
719        assert_eq!(geom.vertex_data[4], 1.0); // ny = old nz (normal pointing up)
720        assert_eq!(geom.vertex_data[5], 0.0); // nz = -old ny
721    }
722
723    #[test]
724    fn test_instanced_geometry() {
725        let mut geom = GpuInstancedGeometry::new(12345);
726
727        let positions = vec![0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.5, 0.0, 1.0];
728        let normals = vec![0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0];
729        let indices = vec![0, 1, 2];
730
731        geom.set_geometry(&positions, &normals, &indices);
732
733        // Identity transform
734        let transform = [
735            1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0,
736        ];
737        let color = [1.0, 0.0, 0.0, 1.0];
738
739        geom.add_instance(100, &transform, color);
740        geom.add_instance(101, &transform, color);
741
742        assert_eq!(geom.instance_count(), 2);
743        assert_eq!(geom.vertex_count(), 3);
744        assert_eq!(geom.triangle_count(), 1);
745    }
746}