ifc_lite_wasm/
api.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//! JavaScript API for IFC-Lite
6//!
7//! Modern async/await API for parsing IFC files.
8
9use wasm_bindgen::prelude::*;
10use wasm_bindgen_futures::spawn_local;
11use js_sys::{Function, Promise};
12use ifc_lite_core::{EntityScanner, ParseEvent, StreamConfig, GeoReference, RtcOffset};
13use crate::zero_copy::{ZeroCopyMesh, MeshDataJs, MeshCollection, InstancedMeshCollection, InstancedGeometry, InstanceData};
14
15/// Georeferencing information exposed to JavaScript
16#[wasm_bindgen]
17#[derive(Debug, Clone)]
18pub struct GeoReferenceJs {
19    /// CRS name (e.g., "EPSG:32632")
20    #[wasm_bindgen(skip)]
21    pub crs_name: Option<String>,
22    /// Eastings (X offset)
23    pub eastings: f64,
24    /// Northings (Y offset)
25    pub northings: f64,
26    /// Orthogonal height (Z offset)
27    pub orthogonal_height: f64,
28    /// X-axis abscissa (cos of rotation)
29    pub x_axis_abscissa: f64,
30    /// X-axis ordinate (sin of rotation)
31    pub x_axis_ordinate: f64,
32    /// Scale factor
33    pub scale: f64,
34}
35
36#[wasm_bindgen]
37impl GeoReferenceJs {
38    /// Get CRS name
39    #[wasm_bindgen(getter, js_name = crsName)]
40    pub fn crs_name(&self) -> Option<String> {
41        self.crs_name.clone()
42    }
43
44    /// Get rotation angle in radians
45    #[wasm_bindgen(getter)]
46    pub fn rotation(&self) -> f64 {
47        self.x_axis_ordinate.atan2(self.x_axis_abscissa)
48    }
49
50    /// Transform local coordinates to map coordinates
51    #[wasm_bindgen(js_name = localToMap)]
52    pub fn local_to_map(&self, x: f64, y: f64, z: f64) -> Vec<f64> {
53        let cos_r = self.x_axis_abscissa;
54        let sin_r = self.x_axis_ordinate;
55        let s = self.scale;
56
57        let e = s * (cos_r * x - sin_r * y) + self.eastings;
58        let n = s * (sin_r * x + cos_r * y) + self.northings;
59        let h = z + self.orthogonal_height;
60
61        vec![e, n, h]
62    }
63
64    /// Transform map coordinates to local coordinates
65    #[wasm_bindgen(js_name = mapToLocal)]
66    pub fn map_to_local(&self, e: f64, n: f64, h: f64) -> Vec<f64> {
67        let cos_r = self.x_axis_abscissa;
68        let sin_r = self.x_axis_ordinate;
69        let inv_scale = if self.scale.abs() < f64::EPSILON {
70            1.0
71        } else {
72            1.0 / self.scale
73        };
74
75        let dx = e - self.eastings;
76        let dy = n - self.northings;
77
78        let x = inv_scale * (cos_r * dx + sin_r * dy);
79        let y = inv_scale * (-sin_r * dx + cos_r * dy);
80        let z = h - self.orthogonal_height;
81
82        vec![x, y, z]
83    }
84
85    /// Get 4x4 transformation matrix (column-major for WebGL)
86    #[wasm_bindgen(js_name = toMatrix)]
87    pub fn to_matrix(&self) -> Vec<f64> {
88        let cos_r = self.x_axis_abscissa;
89        let sin_r = self.x_axis_ordinate;
90        let s = self.scale;
91
92        vec![
93            s * cos_r,  s * sin_r,  0.0, 0.0,
94            -s * sin_r, s * cos_r,  0.0, 0.0,
95            0.0,        0.0,        1.0, 0.0,
96            self.eastings, self.northings, self.orthogonal_height, 1.0,
97        ]
98    }
99}
100
101impl From<GeoReference> for GeoReferenceJs {
102    fn from(geo: GeoReference) -> Self {
103        Self {
104            crs_name: geo.crs_name,
105            eastings: geo.eastings,
106            northings: geo.northings,
107            orthogonal_height: geo.orthogonal_height,
108            x_axis_abscissa: geo.x_axis_abscissa,
109            x_axis_ordinate: geo.x_axis_ordinate,
110            scale: geo.scale,
111        }
112    }
113}
114
115/// RTC offset information exposed to JavaScript
116#[wasm_bindgen]
117#[derive(Debug, Clone, Default)]
118pub struct RtcOffsetJs {
119    /// X offset (subtracted from positions)
120    pub x: f64,
121    /// Y offset
122    pub y: f64,
123    /// Z offset
124    pub z: f64,
125}
126
127#[wasm_bindgen]
128impl RtcOffsetJs {
129    /// Check if offset is significant (>10km)
130    #[wasm_bindgen(js_name = isSignificant)]
131    pub fn is_significant(&self) -> bool {
132        const THRESHOLD: f64 = 10000.0;
133        self.x.abs() > THRESHOLD || self.y.abs() > THRESHOLD || self.z.abs() > THRESHOLD
134    }
135
136    /// Convert local coordinates to world coordinates
137    #[wasm_bindgen(js_name = toWorld)]
138    pub fn to_world(&self, x: f64, y: f64, z: f64) -> Vec<f64> {
139        vec![x + self.x, y + self.y, z + self.z]
140    }
141}
142
143impl From<RtcOffset> for RtcOffsetJs {
144    fn from(offset: RtcOffset) -> Self {
145        Self {
146            x: offset.x,
147            y: offset.y,
148            z: offset.z,
149        }
150    }
151}
152
153/// Mesh collection with RTC offset for large coordinates
154#[wasm_bindgen]
155pub struct MeshCollectionWithRtc {
156    meshes: MeshCollection,
157    rtc_offset: RtcOffsetJs,
158}
159
160#[wasm_bindgen]
161impl MeshCollectionWithRtc {
162    /// Get the mesh collection
163    #[wasm_bindgen(getter)]
164    pub fn meshes(&self) -> MeshCollection {
165        self.meshes.clone()
166    }
167
168    /// Get the RTC offset
169    #[wasm_bindgen(getter, js_name = rtcOffset)]
170    pub fn rtc_offset(&self) -> RtcOffsetJs {
171        self.rtc_offset.clone()
172    }
173
174    /// Get number of meshes
175    #[wasm_bindgen(getter)]
176    pub fn length(&self) -> usize {
177        self.meshes.len()
178    }
179
180    /// Get mesh at index
181    pub fn get(&self, index: usize) -> Option<MeshDataJs> {
182        self.meshes.get(index)
183    }
184}
185
186/// Main IFC-Lite API
187#[wasm_bindgen]
188pub struct IfcAPI {
189    initialized: bool,
190}
191
192#[wasm_bindgen]
193impl IfcAPI {
194    /// Create and initialize the IFC API
195    #[wasm_bindgen(constructor)]
196    pub fn new() -> Self {
197        #[cfg(feature = "console_error_panic_hook")]
198        console_error_panic_hook::set_once();
199
200        Self {
201            initialized: true,
202        }
203    }
204
205    /// Check if API is initialized
206    #[wasm_bindgen(getter)]
207    pub fn is_ready(&self) -> bool {
208        self.initialized
209    }
210
211    /// Parse IFC file with streaming events
212    /// Calls the callback function for each parse event
213    ///
214    /// Example:
215    /// ```javascript
216    /// const api = new IfcAPI();
217    /// await api.parseStreaming(ifcData, (event) => {
218    ///   console.log('Event:', event);
219    /// });
220    /// ```
221    #[wasm_bindgen(js_name = parseStreaming)]
222    pub fn parse_streaming(&self, content: String, callback: Function) -> Promise {
223        use futures_util::StreamExt;
224
225        let promise = Promise::new(&mut |resolve, _reject| {
226            let content = content.clone();
227            let callback = callback.clone();
228            spawn_local(async move {
229                let config = StreamConfig::default();
230                let mut stream = ifc_lite_core::parse_stream(&content, config);
231
232                while let Some(event) = stream.next().await {
233                    // Convert event to JsValue and call callback
234                    let event_obj = parse_event_to_js(&event);
235                    let _ = callback.call1(&JsValue::NULL, &event_obj);
236
237                    // Check if this is the completion event
238                    if matches!(event, ParseEvent::Completed { .. }) {
239                        resolve.call0(&JsValue::NULL).unwrap();
240                        return;
241                    }
242                }
243
244                resolve.call0(&JsValue::NULL).unwrap();
245            });
246        });
247
248        promise
249    }
250
251    /// Parse IFC file (traditional - waits for completion)
252    ///
253    /// Example:
254    /// ```javascript
255    /// const api = new IfcAPI();
256    /// const result = await api.parse(ifcData);
257    /// console.log('Entities:', result.entityCount);
258    /// ```
259    #[wasm_bindgen]
260    pub fn parse(&self, content: String) -> Promise {
261        let promise = Promise::new(&mut |resolve, _reject| {
262            let content = content.clone();
263            spawn_local(async move {
264                // Quick scan to get entity count
265                let mut scanner = EntityScanner::new(&content);
266                let counts = scanner.count_by_type();
267
268                let total_entities: usize = counts.values().sum();
269
270                // Create result object
271                let result = js_sys::Object::new();
272                js_sys::Reflect::set(
273                    &result,
274                    &"entityCount".into(),
275                    &JsValue::from_f64(total_entities as f64),
276                )
277                .unwrap();
278
279                js_sys::Reflect::set(&result, &"entityTypes".into(), &counts_to_js(&counts))
280                    .unwrap();
281
282                resolve.call1(&JsValue::NULL, &result).unwrap();
283            });
284        });
285
286        promise
287    }
288
289    /// Parse IFC file with zero-copy mesh data
290    /// Maximum performance - returns mesh with direct memory access
291    ///
292    /// Example:
293    /// ```javascript
294    /// const api = new IfcAPI();
295    /// const mesh = await api.parseZeroCopy(ifcData);
296    ///
297    /// // Create TypedArray views (NO COPYING!)
298    /// const memory = await api.getMemory();
299    /// const positions = new Float32Array(
300    ///   memory.buffer,
301    ///   mesh.positions_ptr,
302    ///   mesh.positions_len
303    /// );
304    ///
305    /// // Upload directly to GPU
306    /// gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW);
307    /// ```
308    #[wasm_bindgen(js_name = parseZeroCopy)]
309    pub fn parse_zero_copy(&self, content: String) -> ZeroCopyMesh {
310        // Parse IFC file and generate geometry with optimized processing
311        use ifc_lite_core::{EntityScanner, EntityDecoder, build_entity_index};
312        use ifc_lite_geometry::{GeometryRouter, Mesh, calculate_normals};
313
314        // Build entity index once upfront for O(1) lookups
315        let entity_index = build_entity_index(&content);
316
317        // Create scanner and decoder with pre-built index
318        let mut scanner = EntityScanner::new(&content);
319        let mut decoder = EntityDecoder::with_index(&content, entity_index);
320
321        // Create geometry router (reuses processor instances)
322        let router = GeometryRouter::new();
323
324        // Collect all meshes first (better for batch merge)
325        let mut meshes: Vec<Mesh> = Vec::with_capacity(2000);
326
327        // Process all building elements
328        while let Some((_id, type_name, start, end)) = scanner.next_entity() {
329            // Check if this is a building element type
330            if !ifc_lite_core::has_geometry_by_name(type_name) {
331                continue;
332            }
333
334            // Decode and process the entity
335            if let Ok(entity) = decoder.decode_at(start, end) {
336                if let Ok(mesh) = router.process_element(&entity, &mut decoder) {
337                    if !mesh.is_empty() {
338                        meshes.push(mesh);
339                    }
340                }
341            }
342        }
343
344        // Batch merge all meshes at once (more efficient)
345        let mut combined_mesh = Mesh::new();
346        combined_mesh.merge_all(&meshes);
347
348        // Calculate normals if not present
349        if combined_mesh.normals.is_empty() && !combined_mesh.positions.is_empty() {
350            calculate_normals(&mut combined_mesh);
351        }
352
353        ZeroCopyMesh::from(combined_mesh)
354    }
355
356    /// Parse IFC file and return individual meshes with express IDs and colors
357    /// This matches the MeshData[] format expected by the viewer
358    ///
359    /// Example:
360    /// ```javascript
361    /// const api = new IfcAPI();
362    /// const collection = api.parseMeshes(ifcData);
363    /// for (let i = 0; i < collection.length; i++) {
364    ///   const mesh = collection.get(i);
365    ///   console.log('Express ID:', mesh.expressId);
366    ///   console.log('Positions:', mesh.positions);
367    ///   console.log('Color:', mesh.color);
368    /// }
369    /// ```
370    #[wasm_bindgen(js_name = parseMeshes)]
371    pub fn parse_meshes(&self, content: String) -> MeshCollection {
372        use ifc_lite_core::{EntityScanner, EntityDecoder, build_entity_index};
373        use ifc_lite_geometry::{GeometryRouter, calculate_normals};
374
375        // Build entity index once upfront for O(1) lookups
376        let entity_index = build_entity_index(&content);
377
378        // Create decoder with pre-built index
379        let mut decoder = EntityDecoder::with_index(&content, entity_index.clone());
380
381        // Build style index: first map geometry IDs to colors, then map element IDs to colors
382        let geometry_styles = build_geometry_style_index(&content, &mut decoder);
383        let style_index = build_element_style_index(&content, &geometry_styles, &mut decoder);
384
385        // OPTIMIZATION: Collect all FacetedBrep IDs for batch processing
386        let mut scanner = EntityScanner::new(&content);
387        let mut faceted_brep_ids: Vec<u32> = Vec::new();
388        while let Some((id, type_name, _, _)) = scanner.next_entity() {
389            if type_name == "IFCFACETEDBREP" {
390                faceted_brep_ids.push(id);
391            }
392        }
393
394        // Create geometry router (reuses processor instances)
395        let router = GeometryRouter::new();
396
397        // Batch preprocess FacetedBrep entities for maximum parallelism
398        // This triangulates ALL faces from ALL BREPs in one parallel batch
399        if !faceted_brep_ids.is_empty() {
400            router.preprocess_faceted_breps(&faceted_brep_ids, &mut decoder);
401        }
402
403        // Reset scanner for main processing pass
404        scanner = EntityScanner::new(&content);
405
406        // Estimate capacity: typical IFC files have ~5-10% building elements
407        let estimated_elements = content.len() / 500;
408        let mut mesh_collection = MeshCollection::with_capacity(estimated_elements);
409
410        // Process all building elements
411        while let Some((id, type_name, start, end)) = scanner.next_entity() {
412            // Check if this is a building element type
413            if !ifc_lite_core::has_geometry_by_name(type_name) {
414                continue;
415            }
416
417            // Decode and process the entity
418            if let Ok(entity) = decoder.decode_at(start, end) {
419                if let Ok(mut mesh) = router.process_element(&entity, &mut decoder) {
420                    if !mesh.is_empty() {
421                        // Calculate normals if not present
422                        if mesh.normals.is_empty() {
423                            calculate_normals(&mut mesh);
424                        }
425
426                        // Try to get color from style index, otherwise use default
427                        let color = style_index.get(&id)
428                            .copied()
429                            .unwrap_or_else(|| get_default_color_for_type(&entity.ifc_type));
430
431                        // Create mesh data with express ID and color
432                        let mesh_data = MeshDataJs::new(id, mesh, color);
433                        mesh_collection.add(mesh_data);
434                    }
435                }
436            }
437        }
438
439        mesh_collection
440    }
441
442    /// Parse IFC file and return instanced geometry grouped by geometry hash
443    /// This reduces draw calls by grouping identical geometries with different transforms
444    ///
445    /// Example:
446    /// ```javascript
447    /// const api = new IfcAPI();
448    /// const collection = api.parseMeshesInstanced(ifcData);
449    /// for (let i = 0; i < collection.length; i++) {
450    ///   const geometry = collection.get(i);
451    ///   console.log('Geometry ID:', geometry.geometryId);
452    ///   console.log('Instances:', geometry.instanceCount);
453    ///   for (let j = 0; j < geometry.instanceCount; j++) {
454    ///     const inst = geometry.getInstance(j);
455    ///     console.log('  Express ID:', inst.expressId);
456    ///     console.log('  Transform:', inst.transform);
457    ///   }
458    /// }
459    /// ```
460    #[wasm_bindgen(js_name = parseMeshesInstanced)]
461    pub fn parse_meshes_instanced(&self, content: String) -> InstancedMeshCollection {
462        use ifc_lite_core::{EntityScanner, EntityDecoder, build_entity_index};
463        use ifc_lite_geometry::{GeometryRouter, calculate_normals, Mesh};
464        use rustc_hash::FxHashMap;
465        use std::hash::{Hash, Hasher};
466        use rustc_hash::FxHasher;
467
468        // Build entity index once upfront for O(1) lookups
469        let entity_index = build_entity_index(&content);
470
471        // Create decoder with pre-built index
472        let mut decoder = EntityDecoder::with_index(&content, entity_index.clone());
473
474        // Build style index: first map geometry IDs to colors, then map element IDs to colors
475        let geometry_styles = build_geometry_style_index(&content, &mut decoder);
476        let style_index = build_element_style_index(&content, &geometry_styles, &mut decoder);
477
478        // OPTIMIZATION: Collect all FacetedBrep IDs for batch processing
479        let mut scanner = EntityScanner::new(&content);
480        let mut faceted_brep_ids: Vec<u32> = Vec::new();
481        while let Some((id, type_name, _, _)) = scanner.next_entity() {
482            if type_name == "IFCFACETEDBREP" {
483                faceted_brep_ids.push(id);
484            }
485        }
486
487        // Create geometry router (reuses processor instances)
488        let router = GeometryRouter::new();
489
490        // Batch preprocess FacetedBrep entities for maximum parallelism
491        if !faceted_brep_ids.is_empty() {
492            router.preprocess_faceted_breps(&faceted_brep_ids, &mut decoder);
493        }
494
495        // Reset scanner for main processing pass
496        scanner = EntityScanner::new(&content);
497
498        // Group meshes by geometry hash
499        // Key: geometry hash, Value: (base mesh, Vec<(express_id, transform, color)>)
500        // Note: transform is returned as Matrix4<f64> from process_element_with_transform
501        let mut geometry_groups: FxHashMap<u64, (Mesh, Vec<(u32, [f64; 16], [f32; 4])>)> = FxHashMap::default();
502
503        // Process all building elements
504        while let Some((id, type_name, start, end)) = scanner.next_entity() {
505            // Check if this is a building element type
506            if !ifc_lite_core::has_geometry_by_name(type_name) {
507                continue;
508            }
509
510            // Decode and process the entity
511            if let Ok(entity) = decoder.decode_at(start, end) {
512                if let Ok((mut mesh, transform)) = router.process_element_with_transform(&entity, &mut decoder) {
513                    if !mesh.is_empty() {
514                        // Calculate normals if not present
515                        if mesh.normals.is_empty() {
516                            calculate_normals(&mut mesh);
517                        }
518
519                        // Compute geometry hash (same as router does)
520                        let mut hasher = FxHasher::default();
521                        mesh.positions.len().hash(&mut hasher);
522                        mesh.indices.len().hash(&mut hasher);
523                        for pos in &mesh.positions {
524                            pos.to_bits().hash(&mut hasher);
525                        }
526                        for idx in &mesh.indices {
527                            idx.hash(&mut hasher);
528                        }
529                        let geometry_hash = hasher.finish();
530
531                        // Try to get color from style index, otherwise use default
532                        let color = style_index.get(&id)
533                            .copied()
534                            .unwrap_or_else(|| get_default_color_for_type(&entity.ifc_type));
535
536                        // Convert Matrix4<f64> to [f64; 16] array (column-major for WebGPU)
537                        let mut transform_array = [0.0; 16];
538                        for col in 0..4 {
539                            for row in 0..4 {
540                                transform_array[col * 4 + row] = transform[(row, col)];
541                            }
542                        }
543
544                        // Add to group - only store mesh once per hash
545                        let entry = geometry_groups.entry(geometry_hash);
546                        match entry {
547                            std::collections::hash_map::Entry::Occupied(mut o) => {
548                                // Geometry already exists, just add instance
549                                o.get_mut().1.push((id, transform_array, color));
550                            }
551                            std::collections::hash_map::Entry::Vacant(v) => {
552                                // First instance of this geometry
553                                v.insert((mesh, vec![(id, transform_array, color)]));
554                            }
555                        }
556                    }
557                }
558            }
559        }
560
561        // Convert groups to InstancedGeometry
562        let mut collection = InstancedMeshCollection::new();
563        for (geometry_id, (mesh, instances)) in geometry_groups {
564            let mut instanced_geom = InstancedGeometry::new(
565                geometry_id,
566                mesh.positions,
567                mesh.normals,
568                mesh.indices,
569            );
570
571            // Convert transforms from [f64; 16] to Vec<f32>
572            for (express_id, transform_array, color) in instances {
573                let mut transform_f32 = Vec::with_capacity(16);
574                for val in transform_array.iter() {
575                    transform_f32.push(*val as f32);
576                }
577                instanced_geom.add_instance(InstanceData::new(express_id, transform_f32, color));
578            }
579
580            collection.add(instanced_geom);
581        }
582
583        collection
584    }
585
586    /// Parse IFC file with streaming instanced geometry batches for progressive rendering
587    /// Groups identical geometries and yields batches of InstancedGeometry
588    /// Uses fast-first-frame streaming: simple geometry (walls, slabs) first
589    ///
590    /// Example:
591    /// ```javascript
592    /// const api = new IfcAPI();
593    /// await api.parseMeshesInstancedAsync(ifcData, {
594    ///   batchSize: 25,  // Number of unique geometries per batch
595    ///   onBatch: (geometries, progress) => {
596    ///     for (const geom of geometries) {
597    ///       renderer.addInstancedGeometry(geom);
598    ///     }
599    ///   },
600    ///   onComplete: (stats) => {
601    ///     console.log(`Done! ${stats.totalGeometries} unique geometries, ${stats.totalInstances} instances`);
602    ///   }
603    /// });
604    /// ```
605    #[wasm_bindgen(js_name = parseMeshesInstancedAsync)]
606    pub fn parse_meshes_instanced_async(&self, content: String, options: JsValue) -> Promise {
607        use ifc_lite_core::{EntityScanner, EntityDecoder, build_entity_index};
608        use ifc_lite_geometry::{GeometryRouter, calculate_normals, Mesh};
609        use rustc_hash::{FxHashMap, FxHasher};
610        use std::hash::{Hash, Hasher};
611
612        let promise = Promise::new(&mut |resolve, _reject| {
613            let content = content.clone();
614            let options = options.clone();
615
616            spawn_local(async move {
617                // Parse options
618                let batch_size: usize = js_sys::Reflect::get(&options, &"batchSize".into())
619                    .ok()
620                    .and_then(|v| v.as_f64())
621                    .map(|v| v as usize)
622                    .unwrap_or(25);  // Batch size = number of unique geometries per batch
623
624                let on_batch = js_sys::Reflect::get(&options, &"onBatch".into())
625                    .ok()
626                    .and_then(|v| v.dyn_into::<Function>().ok());
627
628                let on_complete = js_sys::Reflect::get(&options, &"onComplete".into())
629                    .ok()
630                    .and_then(|v| v.dyn_into::<Function>().ok());
631
632                // Build entity index once upfront for O(1) lookups
633                let entity_index = build_entity_index(&content);
634                let mut decoder = EntityDecoder::with_index(&content, entity_index.clone());
635
636                // Build style index
637                let geometry_styles = build_geometry_style_index(&content, &mut decoder);
638                let style_index = build_element_style_index(&content, &geometry_styles, &mut decoder);
639
640                // Collect FacetedBrep IDs for batch preprocessing
641                let mut scanner = EntityScanner::new(&content);
642                let mut faceted_brep_ids: Vec<u32> = Vec::new();
643                while let Some((id, type_name, _, _)) = scanner.next_entity() {
644                    if type_name == "IFCFACETEDBREP" {
645                        faceted_brep_ids.push(id);
646                    }
647                }
648
649                // Create geometry router
650                let router = GeometryRouter::new();
651
652                // Batch preprocess FacetedBreps
653                if !faceted_brep_ids.is_empty() {
654                    router.preprocess_faceted_breps(&faceted_brep_ids, &mut decoder);
655                }
656
657                // Reset scanner for main processing
658                scanner = EntityScanner::new(&content);
659
660                // Group meshes by geometry hash (accumulated across batches)
661                // Key: geometry hash, Value: (base mesh, Vec<(express_id, transform, color)>)
662                let mut geometry_groups: FxHashMap<u64, (Mesh, Vec<(u32, [f64; 16], [f32; 4])>)> = FxHashMap::default();
663                let mut processed = 0;
664                let mut total_geometries = 0;
665                let mut total_instances = 0;
666                let mut deferred_complex: Vec<(u32, usize, usize, ifc_lite_core::IfcType)> = Vec::new();
667
668                // First pass - process simple geometry immediately
669                while let Some((id, type_name, start, end)) = scanner.next_entity() {
670                    if !ifc_lite_core::has_geometry_by_name(type_name) {
671                        continue;
672                    }
673
674                    let ifc_type = ifc_lite_core::IfcType::from_str(type_name);
675
676                    // Simple geometry: process immediately
677                    if matches!(type_name, "IFCWALL" | "IFCWALLSTANDARDCASE" | "IFCSLAB" |
678                               "IFCBEAM" | "IFCCOLUMN" | "IFCPLATE" | "IFCROOF" |
679                               "IFCCOVERING" | "IFCFOOTING" | "IFCRAILING" | "IFCSTAIR" |
680                               "IFCSTAIRFLIGHT" | "IFCRAMP" | "IFCRAMPFLIGHT") {
681                        if let Ok(entity) = decoder.decode_at(start, end) {
682                            if let Ok((mut mesh, transform)) = router.process_element_with_transform(&entity, &mut decoder) {
683                                if !mesh.is_empty() {
684                                    if mesh.normals.is_empty() {
685                                        calculate_normals(&mut mesh);
686                                    }
687
688                                    // Compute geometry hash (before transformation)
689                                    let mut hasher = FxHasher::default();
690                                    mesh.positions.len().hash(&mut hasher);
691                                    mesh.indices.len().hash(&mut hasher);
692                                    for pos in &mesh.positions {
693                                        pos.to_bits().hash(&mut hasher);
694                                    }
695                                    for idx in &mesh.indices {
696                                        idx.hash(&mut hasher);
697                                    }
698                                    let geometry_hash = hasher.finish();
699
700                                    // Get color
701                                    let color = style_index.get(&id)
702                                        .copied()
703                                        .unwrap_or_else(|| get_default_color_for_type(&ifc_type));
704
705                                    // Convert Matrix4<f64> to [f64; 16] array (column-major for WebGPU)
706                                    let mut transform_array = [0.0; 16];
707                                    for col in 0..4 {
708                                        for row in 0..4 {
709                                            transform_array[col * 4 + row] = transform[(row, col)];
710                                        }
711                                    }
712
713                                    // Add to group
714                                    let entry = geometry_groups.entry(geometry_hash);
715                                    match entry {
716                                        std::collections::hash_map::Entry::Occupied(mut o) => {
717                                            o.get_mut().1.push((id, transform_array, color));
718                                            total_instances += 1;
719                                        }
720                                        std::collections::hash_map::Entry::Vacant(v) => {
721                                            v.insert((mesh, vec![(id, transform_array, color)]));
722                                            total_geometries += 1;
723                                            total_instances += 1;
724                                        }
725                                    }
726                                    processed += 1;
727                                }
728                            }
729                        }
730
731                        // Yield batch when we have enough unique geometries
732                        if geometry_groups.len() >= batch_size {
733                            let mut batch_geometries = Vec::new();
734                            let mut geometries_to_remove = Vec::new();
735
736                            // Convert groups to InstancedGeometry
737                            for (geometry_id, (mesh, instances)) in geometry_groups.iter() {
738                                let mut instanced_geom = InstancedGeometry::new(
739                                    *geometry_id,
740                                    mesh.positions.clone(),
741                                    mesh.normals.clone(),
742                                    mesh.indices.clone(),
743                                );
744
745                                for (express_id, transform_array, color) in instances.iter() {
746                                    let mut transform_f32 = Vec::with_capacity(16);
747                                    for val in transform_array.iter() {
748                                        transform_f32.push(*val as f32);
749                                    }
750                                    instanced_geom.add_instance(InstanceData::new(*express_id, transform_f32, *color));
751                                }
752
753                                batch_geometries.push(instanced_geom);
754                                geometries_to_remove.push(*geometry_id);
755                            }
756
757                            // Remove processed geometries from map
758                            for geometry_id in geometries_to_remove {
759                                geometry_groups.remove(&geometry_id);
760                            }
761
762                            // Yield batch
763                            if let Some(ref callback) = on_batch {
764                                let js_geometries = js_sys::Array::new();
765                                for geom in batch_geometries {
766                                    js_geometries.push(&geom.into());
767                                }
768
769                                let progress = js_sys::Object::new();
770                                js_sys::Reflect::set(&progress, &"percent".into(), &0u32.into()).unwrap();
771                                js_sys::Reflect::set(&progress, &"processed".into(), &(processed as f64).into()).unwrap();
772                                js_sys::Reflect::set(&progress, &"phase".into(), &"simple".into()).unwrap();
773
774                                let _ = callback.call2(&JsValue::NULL, &js_geometries, &progress);
775                            }
776
777                            // Yield to browser
778                            gloo_timers::future::TimeoutFuture::new(0).await;
779                        }
780                    } else {
781                        // Defer complex geometry
782                        deferred_complex.push((id, start, end, ifc_type));
783                    }
784                }
785
786                // Flush remaining simple geometries
787                if !geometry_groups.is_empty() {
788                    let mut batch_geometries = Vec::new();
789                    for (geometry_id, (mesh, instances)) in geometry_groups.drain() {
790                        let mut instanced_geom = InstancedGeometry::new(
791                            geometry_id,
792                            mesh.positions,
793                            mesh.normals,
794                            mesh.indices,
795                        );
796
797                        for (express_id, transform_array, color) in instances {
798                            let mut transform_f32 = Vec::with_capacity(16);
799                            for val in transform_array.iter() {
800                                transform_f32.push(*val as f32);
801                            }
802                            instanced_geom.add_instance(InstanceData::new(express_id, transform_f32, color));
803                        }
804
805                        batch_geometries.push(instanced_geom);
806                    }
807
808                    if let Some(ref callback) = on_batch {
809                        let js_geometries = js_sys::Array::new();
810                        for geom in batch_geometries {
811                            js_geometries.push(&geom.into());
812                        }
813
814                        let progress = js_sys::Object::new();
815                        js_sys::Reflect::set(&progress, &"phase".into(), &"simple_complete".into()).unwrap();
816
817                        let _ = callback.call2(&JsValue::NULL, &js_geometries, &progress);
818                    }
819
820                    gloo_timers::future::TimeoutFuture::new(0).await;
821                }
822
823                // Process deferred complex geometry
824                let total_elements = processed + deferred_complex.len();
825                for (id, start, end, ifc_type) in deferred_complex {
826                    if let Ok(entity) = decoder.decode_at(start, end) {
827                        if let Ok((mut mesh, transform)) = router.process_element_with_transform(&entity, &mut decoder) {
828                            if !mesh.is_empty() {
829                                if mesh.normals.is_empty() {
830                                    calculate_normals(&mut mesh);
831                                }
832
833                                // Compute geometry hash
834                                let mut hasher = FxHasher::default();
835                                mesh.positions.len().hash(&mut hasher);
836                                mesh.indices.len().hash(&mut hasher);
837                                for pos in &mesh.positions {
838                                    pos.to_bits().hash(&mut hasher);
839                                }
840                                for idx in &mesh.indices {
841                                    idx.hash(&mut hasher);
842                                }
843                                let geometry_hash = hasher.finish();
844
845                                // Get color
846                                let color = style_index.get(&id)
847                                    .copied()
848                                    .unwrap_or_else(|| get_default_color_for_type(&ifc_type));
849
850                                // Convert transform (column-major for WebGPU)
851                                let mut transform_array = [0.0; 16];
852                                for col in 0..4 {
853                                    for row in 0..4 {
854                                        transform_array[col * 4 + row] = transform[(row, col)];
855                                    }
856                                }
857
858                                // Add to group
859                                let entry = geometry_groups.entry(geometry_hash);
860                                match entry {
861                                    std::collections::hash_map::Entry::Occupied(mut o) => {
862                                        o.get_mut().1.push((id, transform_array, color));
863                                        total_instances += 1;
864                                    }
865                                    std::collections::hash_map::Entry::Vacant(v) => {
866                                        v.insert((mesh, vec![(id, transform_array, color)]));
867                                        total_geometries += 1;
868                                        total_instances += 1;
869                                    }
870                                }
871                                processed += 1;
872                            }
873                        }
874                    }
875
876                    // Yield batch when we have enough unique geometries
877                    if geometry_groups.len() >= batch_size {
878                        let mut batch_geometries = Vec::new();
879                        let mut geometries_to_remove = Vec::new();
880
881                        for (geometry_id, (mesh, instances)) in geometry_groups.iter() {
882                            let mut instanced_geom = InstancedGeometry::new(
883                                *geometry_id,
884                                mesh.positions.clone(),
885                                mesh.normals.clone(),
886                                mesh.indices.clone(),
887                            );
888
889                            for (express_id, transform_array, color) in instances.iter() {
890                                let mut transform_f32 = Vec::with_capacity(16);
891                                for val in transform_array.iter() {
892                                    transform_f32.push(*val as f32);
893                                }
894                                instanced_geom.add_instance(InstanceData::new(*express_id, transform_f32, *color));
895                            }
896
897                            batch_geometries.push(instanced_geom);
898                            geometries_to_remove.push(*geometry_id);
899                        }
900
901                        for geometry_id in geometries_to_remove {
902                            geometry_groups.remove(&geometry_id);
903                        }
904
905                        if let Some(ref callback) = on_batch {
906                            let js_geometries = js_sys::Array::new();
907                            for geom in batch_geometries {
908                                js_geometries.push(&geom.into());
909                            }
910
911                            let progress = js_sys::Object::new();
912                            let percent = (processed as f64 / total_elements as f64 * 100.0) as u32;
913                            js_sys::Reflect::set(&progress, &"percent".into(), &percent.into()).unwrap();
914                            js_sys::Reflect::set(&progress, &"processed".into(), &(processed as f64).into()).unwrap();
915                            js_sys::Reflect::set(&progress, &"total".into(), &(total_elements as f64).into()).unwrap();
916                            js_sys::Reflect::set(&progress, &"phase".into(), &"complex".into()).unwrap();
917
918                            let _ = callback.call2(&JsValue::NULL, &js_geometries, &progress);
919                        }
920
921                        gloo_timers::future::TimeoutFuture::new(0).await;
922                    }
923                }
924
925                // Final flush
926                if !geometry_groups.is_empty() {
927                    let mut batch_geometries = Vec::new();
928                    for (geometry_id, (mesh, instances)) in geometry_groups.drain() {
929                        let mut instanced_geom = InstancedGeometry::new(
930                            geometry_id,
931                            mesh.positions,
932                            mesh.normals,
933                            mesh.indices,
934                        );
935
936                        for (express_id, transform_array, color) in instances {
937                            let mut transform_f32 = Vec::with_capacity(16);
938                            for val in transform_array.iter() {
939                                transform_f32.push(*val as f32);
940                            }
941                            instanced_geom.add_instance(InstanceData::new(express_id, transform_f32, color));
942                        }
943
944                        batch_geometries.push(instanced_geom);
945                    }
946
947                    if let Some(ref callback) = on_batch {
948                        let js_geometries = js_sys::Array::new();
949                        for geom in batch_geometries {
950                            js_geometries.push(&geom.into());
951                        }
952
953                        let progress = js_sys::Object::new();
954                        js_sys::Reflect::set(&progress, &"percent".into(), &100u32.into()).unwrap();
955                        js_sys::Reflect::set(&progress, &"phase".into(), &"complete".into()).unwrap();
956
957                        let _ = callback.call2(&JsValue::NULL, &js_geometries, &progress);
958                    }
959                }
960
961                // Call completion callback
962                if let Some(ref callback) = on_complete {
963                    let stats = js_sys::Object::new();
964                    js_sys::Reflect::set(&stats, &"totalGeometries".into(), &(total_geometries as f64).into()).unwrap();
965                    js_sys::Reflect::set(&stats, &"totalInstances".into(), &(total_instances as f64).into()).unwrap();
966                    let _ = callback.call1(&JsValue::NULL, &stats);
967                }
968
969                resolve.call0(&JsValue::NULL).unwrap();
970            });
971        });
972
973        promise
974    }
975
976    /// Parse IFC file with streaming mesh batches for progressive rendering
977    /// Calls the callback with batches of meshes, yielding to browser between batches
978    ///
979    /// Example:
980    /// ```javascript
981    /// const api = new IfcAPI();
982    /// await api.parseMeshesAsync(ifcData, {
983    ///   batchSize: 100,
984    ///   onBatch: (meshes, progress) => {
985    ///     // Add meshes to scene
986    ///     for (const mesh of meshes) {
987    ///       scene.add(createThreeMesh(mesh));
988    ///     }
989    ///     console.log(`Progress: ${progress.percent}%`);
990    ///   },
991    ///   onComplete: (stats) => {
992    ///     console.log(`Done! ${stats.totalMeshes} meshes`);
993    ///   }
994    /// });
995    /// ```
996    #[wasm_bindgen(js_name = parseMeshesAsync)]
997    pub fn parse_meshes_async(&self, content: String, options: JsValue) -> Promise {
998        use ifc_lite_core::{EntityScanner, EntityDecoder, build_entity_index};
999        use ifc_lite_geometry::{GeometryRouter, calculate_normals};
1000
1001        let promise = Promise::new(&mut |resolve, _reject| {
1002            let content = content.clone();
1003            let options = options.clone();
1004
1005            spawn_local(async move {
1006                // Parse options - smaller default batch size for faster first frame
1007                let batch_size: usize = js_sys::Reflect::get(&options, &"batchSize".into())
1008                    .ok()
1009                    .and_then(|v| v.as_f64())
1010                    .map(|v| v as usize)
1011                    .unwrap_or(25);  // Reduced from 50 for faster first frame
1012
1013                let on_batch = js_sys::Reflect::get(&options, &"onBatch".into())
1014                    .ok()
1015                    .and_then(|v| v.dyn_into::<Function>().ok());
1016
1017                let on_complete = js_sys::Reflect::get(&options, &"onComplete".into())
1018                    .ok()
1019                    .and_then(|v| v.dyn_into::<Function>().ok());
1020
1021                // LAZY INDEXING: Don't build full index upfront
1022                // Index will be built on first reference resolution
1023                let mut decoder = EntityDecoder::new(&content);
1024
1025                // Create geometry router
1026                let router = GeometryRouter::new();
1027
1028                // Process counters
1029                let mut processed = 0;
1030                let mut total_meshes = 0;
1031                let mut total_vertices = 0;
1032                let mut total_triangles = 0;
1033                let mut batch_meshes: Vec<MeshDataJs> = Vec::with_capacity(batch_size);
1034                let mut elements_since_yield = 0;
1035
1036                // SINGLE PASS: Process elements as we find them
1037                let mut scanner = EntityScanner::new(&content);
1038                let mut deferred_complex: Vec<(u32, usize, usize, ifc_lite_core::IfcType)> = Vec::new();
1039                let mut faceted_brep_ids: Vec<u32> = Vec::new();  // Collect for batch preprocessing
1040
1041                // First pass - process simple geometry immediately, defer complex
1042                while let Some((id, type_name, start, end)) = scanner.next_entity() {
1043                    // Track FacetedBrep IDs for batch preprocessing
1044                    if type_name == "IFCFACETEDBREP" {
1045                        faceted_brep_ids.push(id);
1046                    }
1047
1048                    if !ifc_lite_core::has_geometry_by_name(type_name) {
1049                        continue;
1050                    }
1051
1052                    let ifc_type = ifc_lite_core::IfcType::from_str(type_name);
1053
1054                    // Simple geometry: process immediately
1055                    if matches!(type_name, "IFCWALL" | "IFCWALLSTANDARDCASE" | "IFCSLAB" |
1056                               "IFCBEAM" | "IFCCOLUMN" | "IFCPLATE" | "IFCROOF" |
1057                               "IFCCOVERING" | "IFCFOOTING" | "IFCRAILING" | "IFCSTAIR" |
1058                               "IFCSTAIRFLIGHT" | "IFCRAMP" | "IFCRAMPFLIGHT") {
1059                        if let Ok(entity) = decoder.decode_at(start, end) {
1060                            if let Ok(mut mesh) = router.process_element(&entity, &mut decoder) {
1061                                if !mesh.is_empty() {
1062                                    if mesh.normals.is_empty() {
1063                                        calculate_normals(&mut mesh);
1064                                    }
1065
1066                                    let color = get_default_color_for_type(&ifc_type);
1067                                    total_vertices += mesh.positions.len() / 3;
1068                                    total_triangles += mesh.indices.len() / 3;
1069
1070                                    let mesh_data = MeshDataJs::new(id, mesh, color);
1071                                    batch_meshes.push(mesh_data);
1072                                    processed += 1;
1073                                }
1074                            }
1075                        }
1076
1077                        elements_since_yield += 1;
1078
1079                        // Yield batch frequently for responsive UI
1080                        if batch_meshes.len() >= batch_size {
1081                            if let Some(ref callback) = on_batch {
1082                                let js_meshes = js_sys::Array::new();
1083                                for mesh in batch_meshes.drain(..) {
1084                                    js_meshes.push(&mesh.into());
1085                                }
1086
1087                                let progress = js_sys::Object::new();
1088                                js_sys::Reflect::set(&progress, &"percent".into(), &0u32.into()).unwrap();
1089                                js_sys::Reflect::set(&progress, &"processed".into(), &(processed as f64).into()).unwrap();
1090                                js_sys::Reflect::set(&progress, &"phase".into(), &"simple".into()).unwrap();
1091
1092                                let _ = callback.call2(&JsValue::NULL, &js_meshes, &progress);
1093                                total_meshes += js_meshes.length() as usize;
1094                            }
1095
1096                            // Yield to browser
1097                            gloo_timers::future::TimeoutFuture::new(0).await;
1098                            elements_since_yield = 0;
1099                        }
1100                    } else {
1101                        // Defer complex geometry
1102                        deferred_complex.push((id, start, end, ifc_type));
1103                    }
1104                }
1105
1106                // Flush remaining simple elements
1107                if !batch_meshes.is_empty() {
1108                    if let Some(ref callback) = on_batch {
1109                        let js_meshes = js_sys::Array::new();
1110                        for mesh in batch_meshes.drain(..) {
1111                            js_meshes.push(&mesh.into());
1112                        }
1113
1114                        let progress = js_sys::Object::new();
1115                        js_sys::Reflect::set(&progress, &"phase".into(), &"simple_complete".into()).unwrap();
1116
1117                        let _ = callback.call2(&JsValue::NULL, &js_meshes, &progress);
1118                        total_meshes += js_meshes.length() as usize;
1119                    }
1120
1121                    gloo_timers::future::TimeoutFuture::new(0).await;
1122                }
1123
1124                let total_elements = processed + deferred_complex.len();
1125
1126                // CRITICAL: Batch preprocess FacetedBreps BEFORE complex phase
1127                // This triangulates ALL faces in parallel - massive speedup for repeated geometry
1128                if !faceted_brep_ids.is_empty() {
1129                    router.preprocess_faceted_breps(&faceted_brep_ids, &mut decoder);
1130                }
1131
1132                // Process deferred complex geometry
1133                // Build style index now (deferred from start)
1134                let geometry_styles = build_geometry_style_index(&content, &mut decoder);
1135                let style_index = build_element_style_index(&content, &geometry_styles, &mut decoder);
1136
1137                for (id, start, end, ifc_type) in deferred_complex {
1138                    if let Ok(entity) = decoder.decode_at(start, end) {
1139                        if let Ok(mut mesh) = router.process_element(&entity, &mut decoder) {
1140                            if !mesh.is_empty() {
1141                                if mesh.normals.is_empty() {
1142                                    calculate_normals(&mut mesh);
1143                                }
1144
1145                                let color = style_index.get(&id)
1146                                    .copied()
1147                                    .unwrap_or_else(|| get_default_color_for_type(&ifc_type));
1148
1149                                total_vertices += mesh.positions.len() / 3;
1150                                total_triangles += mesh.indices.len() / 3;
1151
1152                                let mesh_data = MeshDataJs::new(id, mesh, color);
1153                                batch_meshes.push(mesh_data);
1154                            }
1155                        }
1156                    }
1157
1158                    processed += 1;
1159
1160                    // Yield batch
1161                    if batch_meshes.len() >= batch_size {
1162                        if let Some(ref callback) = on_batch {
1163                            let js_meshes = js_sys::Array::new();
1164                            for mesh in batch_meshes.drain(..) {
1165                                js_meshes.push(&mesh.into());
1166                            }
1167
1168                            let progress = js_sys::Object::new();
1169                            let percent = (processed as f64 / total_elements as f64 * 100.0) as u32;
1170                            js_sys::Reflect::set(&progress, &"percent".into(), &percent.into()).unwrap();
1171                            js_sys::Reflect::set(&progress, &"processed".into(), &(processed as f64).into()).unwrap();
1172                            js_sys::Reflect::set(&progress, &"total".into(), &(total_elements as f64).into()).unwrap();
1173                            js_sys::Reflect::set(&progress, &"phase".into(), &"complex".into()).unwrap();
1174
1175                            let _ = callback.call2(&JsValue::NULL, &js_meshes, &progress);
1176                            total_meshes += js_meshes.length() as usize;
1177                        }
1178
1179                        gloo_timers::future::TimeoutFuture::new(0).await;
1180                    }
1181                }
1182
1183                // Final flush
1184                if !batch_meshes.is_empty() {
1185                    if let Some(ref callback) = on_batch {
1186                        let js_meshes = js_sys::Array::new();
1187                        for mesh in batch_meshes.drain(..) {
1188                            js_meshes.push(&mesh.into());
1189                        }
1190
1191                        let progress = js_sys::Object::new();
1192                        js_sys::Reflect::set(&progress, &"percent".into(), &100u32.into()).unwrap();
1193                        js_sys::Reflect::set(&progress, &"phase".into(), &"complete".into()).unwrap();
1194
1195                        let _ = callback.call2(&JsValue::NULL, &js_meshes, &progress);
1196                        total_meshes += js_meshes.length() as usize;
1197                    }
1198                }
1199
1200                // Call completion callback
1201                if let Some(ref callback) = on_complete {
1202                    let stats = js_sys::Object::new();
1203                    js_sys::Reflect::set(&stats, &"totalMeshes".into(), &(total_meshes as f64).into()).unwrap();
1204                    js_sys::Reflect::set(&stats, &"totalVertices".into(), &(total_vertices as f64).into()).unwrap();
1205                    js_sys::Reflect::set(&stats, &"totalTriangles".into(), &(total_triangles as f64).into()).unwrap();
1206                    let _ = callback.call1(&JsValue::NULL, &stats);
1207                }
1208
1209                resolve.call0(&JsValue::NULL).unwrap();
1210            });
1211        });
1212
1213        promise
1214    }
1215
1216    /// Get WASM memory for zero-copy access
1217    #[wasm_bindgen(js_name = getMemory)]
1218    pub fn get_memory(&self) -> JsValue {
1219        crate::zero_copy::get_memory()
1220    }
1221
1222    /// Get version string
1223    #[wasm_bindgen(getter)]
1224    pub fn version(&self) -> String {
1225        env!("CARGO_PKG_VERSION").to_string()
1226    }
1227
1228    /// Extract georeferencing information from IFC content
1229    /// Returns null if no georeferencing is present
1230    ///
1231    /// Example:
1232    /// ```javascript
1233    /// const api = new IfcAPI();
1234    /// const georef = api.getGeoReference(ifcData);
1235    /// if (georef) {
1236    ///   console.log('CRS:', georef.crsName);
1237    ///   const [e, n, h] = georef.localToMap(10, 20, 5);
1238    /// }
1239    /// ```
1240    #[wasm_bindgen(js_name = getGeoReference)]
1241    pub fn get_geo_reference(&self, content: String) -> Option<GeoReferenceJs> {
1242        use ifc_lite_core::{EntityScanner, EntityDecoder, build_entity_index, IfcType, GeoRefExtractor};
1243
1244        // Build entity index and decoder
1245        let entity_index = build_entity_index(&content);
1246        let mut decoder = EntityDecoder::with_index(&content, entity_index);
1247
1248        // Collect entity types
1249        let mut scanner = EntityScanner::new(&content);
1250        let mut entity_types: Vec<(u32, IfcType)> = Vec::new();
1251
1252        while let Some((id, type_name, _, _)) = scanner.next_entity() {
1253            let ifc_type = IfcType::from_str(type_name);
1254            entity_types.push((id, ifc_type));
1255        }
1256
1257        // Extract georeferencing
1258        match GeoRefExtractor::extract(&mut decoder, &entity_types) {
1259            Ok(Some(georef)) => Some(GeoReferenceJs::from(georef)),
1260            _ => None,
1261        }
1262    }
1263
1264    /// Parse IFC file and return mesh with RTC offset for large coordinates
1265    /// This handles georeferenced models by shifting to centroid
1266    ///
1267    /// Example:
1268    /// ```javascript
1269    /// const api = new IfcAPI();
1270    /// const result = api.parseMeshesWithRtc(ifcData);
1271    /// const rtcOffset = result.rtcOffset;
1272    /// const meshes = result.meshes;
1273    ///
1274    /// // Convert local coords back to world:
1275    /// if (rtcOffset.isSignificant()) {
1276    ///   const [wx, wy, wz] = rtcOffset.toWorld(localX, localY, localZ);
1277    /// }
1278    /// ```
1279    #[wasm_bindgen(js_name = parseMeshesWithRtc)]
1280    pub fn parse_meshes_with_rtc(&self, content: String) -> MeshCollectionWithRtc {
1281        use ifc_lite_core::{EntityScanner, EntityDecoder, build_entity_index, RtcOffset};
1282        use ifc_lite_geometry::{GeometryRouter, calculate_normals};
1283
1284        // Build entity index once upfront
1285        let entity_index = build_entity_index(&content);
1286        let mut decoder = EntityDecoder::with_index(&content, entity_index.clone());
1287
1288        // Build style indices
1289        let geometry_styles = build_geometry_style_index(&content, &mut decoder);
1290        let style_index = build_element_style_index(&content, &geometry_styles, &mut decoder);
1291
1292        // OPTIMIZATION: Collect all FacetedBrep IDs for batch processing
1293        let mut scanner = EntityScanner::new(&content);
1294        let mut faceted_brep_ids: Vec<u32> = Vec::new();
1295        while let Some((id, type_name, _, _)) = scanner.next_entity() {
1296            if type_name == "IFCFACETEDBREP" {
1297                faceted_brep_ids.push(id);
1298            }
1299        }
1300
1301        let router = GeometryRouter::new();
1302
1303        // Batch preprocess FacetedBrep entities for maximum parallelism
1304        if !faceted_brep_ids.is_empty() {
1305            router.preprocess_faceted_breps(&faceted_brep_ids, &mut decoder);
1306        }
1307
1308        // Reset scanner for main processing pass
1309        scanner = EntityScanner::new(&content);
1310
1311        let estimated_elements = content.len() / 500;
1312        let mut mesh_collection = MeshCollection::with_capacity(estimated_elements);
1313
1314        // Collect all positions to calculate RTC offset
1315        let mut all_positions: Vec<f32> = Vec::with_capacity(100000);
1316
1317        // Process all building elements
1318        while let Some((id, type_name, start, end)) = scanner.next_entity() {
1319            if !ifc_lite_core::has_geometry_by_name(type_name) {
1320                continue;
1321            }
1322
1323            if let Ok(entity) = decoder.decode_at(start, end) {
1324                if let Ok(mut mesh) = router.process_element(&entity, &mut decoder) {
1325                    if !mesh.is_empty() {
1326                        if mesh.normals.is_empty() {
1327                            calculate_normals(&mut mesh);
1328                        }
1329
1330                        // Collect positions for RTC calculation
1331                        all_positions.extend_from_slice(&mesh.positions);
1332
1333                        let color = style_index.get(&id)
1334                            .copied()
1335                            .unwrap_or_else(|| get_default_color_for_type(&entity.ifc_type));
1336
1337                        let mesh_data = MeshDataJs::new(id, mesh, color);
1338                        mesh_collection.add(mesh_data);
1339                    }
1340                }
1341            }
1342        }
1343
1344        // Calculate RTC offset from all positions
1345        let rtc_offset = RtcOffset::from_positions(&all_positions);
1346        let rtc_offset_js = RtcOffsetJs::from(rtc_offset.clone());
1347
1348        // Apply RTC offset if significant
1349        if rtc_offset.is_significant() {
1350            mesh_collection.apply_rtc_offset(rtc_offset.x, rtc_offset.y, rtc_offset.z);
1351        }
1352
1353        MeshCollectionWithRtc {
1354            meshes: mesh_collection,
1355            rtc_offset: rtc_offset_js,
1356        }
1357    }
1358
1359    /// Debug: Test processing entity #953 (FacetedBrep wall)
1360    #[wasm_bindgen(js_name = debugProcessEntity953)]
1361    pub fn debug_process_entity_953(&self, content: String) -> String {
1362        use ifc_lite_core::{EntityScanner, EntityDecoder};
1363        use ifc_lite_geometry::GeometryRouter;
1364
1365        let router = GeometryRouter::new();
1366        let mut scanner = EntityScanner::new(&content);
1367        let mut decoder = EntityDecoder::new(&content);
1368
1369        // Find entity 953
1370        while let Some((id, type_name, start, end)) = scanner.next_entity() {
1371            if id == 953 {
1372                match decoder.decode_at(start, end) {
1373                    Ok(entity) => {
1374                        match router.process_element(&entity, &mut decoder) {
1375                            Ok(mesh) => {
1376                                return format!(
1377                                    "SUCCESS! Entity #953: {} vertices, {} triangles, empty={}",
1378                                    mesh.vertex_count(), mesh.triangle_count(), mesh.is_empty()
1379                                );
1380                            }
1381                            Err(e) => {
1382                                return format!("ERROR processing entity #953: {}", e);
1383                            }
1384                        }
1385                    }
1386                    Err(e) => {
1387                        return format!("ERROR decoding entity #953: {}", e);
1388                    }
1389                }
1390            }
1391        }
1392        "Entity #953 not found".to_string()
1393    }
1394
1395    /// Debug: Test processing a single wall
1396    #[wasm_bindgen(js_name = debugProcessFirstWall)]
1397    pub fn debug_process_first_wall(&self, content: String) -> String {
1398        use ifc_lite_core::{EntityScanner, EntityDecoder};
1399        use ifc_lite_geometry::GeometryRouter;
1400
1401        let router = GeometryRouter::new();
1402        let mut scanner = EntityScanner::new(&content);
1403        let mut decoder = EntityDecoder::new(&content);
1404
1405        // Find first wall
1406        while let Some((id, type_name, start, end)) = scanner.next_entity() {
1407            if type_name.contains("WALL") {
1408                let ifc_type = ifc_lite_core::IfcType::from_str(type_name);
1409                if router.schema().has_geometry(&ifc_type) {
1410                    // Try to decode and process
1411                    match decoder.decode_at(start, end) {
1412                        Ok(entity) => {
1413                            match router.process_element(&entity, &mut decoder) {
1414                                Ok(mesh) => {
1415                                    return format!(
1416                                        "SUCCESS! Wall #{}: {} vertices, {} triangles",
1417                                        id, mesh.vertex_count(), mesh.triangle_count()
1418                                    );
1419                                }
1420                                Err(e) => {
1421                                    return format!(
1422                                        "ERROR processing wall #{} ({}): {}",
1423                                        id, type_name, e
1424                                    );
1425                                }
1426                            }
1427                        }
1428                        Err(e) => {
1429                            return format!("ERROR decoding wall #{}: {}", id, e);
1430                        }
1431                    }
1432                }
1433            }
1434        }
1435
1436        "No walls found".to_string()
1437    }
1438
1439}
1440
1441impl Default for IfcAPI {
1442    fn default() -> Self {
1443        Self::new()
1444    }
1445}
1446
1447/// Convert ParseEvent to JavaScript object
1448fn parse_event_to_js(event: &ParseEvent) -> JsValue {
1449    let obj = js_sys::Object::new();
1450
1451    match event {
1452        ParseEvent::Started {
1453            file_size,
1454            timestamp,
1455        } => {
1456            js_sys::Reflect::set(&obj, &"type".into(), &"started".into()).unwrap();
1457            js_sys::Reflect::set(&obj, &"fileSize".into(), &(*file_size as f64).into()).unwrap();
1458            js_sys::Reflect::set(&obj, &"timestamp".into(), &(*timestamp).into()).unwrap();
1459        }
1460        ParseEvent::EntityScanned {
1461            id,
1462            ifc_type,
1463            position,
1464        } => {
1465            js_sys::Reflect::set(&obj, &"type".into(), &"entityScanned".into()).unwrap();
1466            js_sys::Reflect::set(&obj, &"id".into(), &(*id as f64).into()).unwrap();
1467            js_sys::Reflect::set(&obj, &"ifcType".into(), &ifc_type.as_str().into()).unwrap();
1468            js_sys::Reflect::set(&obj, &"position".into(), &(*position as f64).into()).unwrap();
1469        }
1470        ParseEvent::GeometryReady {
1471            id,
1472            vertex_count,
1473            triangle_count,
1474        } => {
1475            js_sys::Reflect::set(&obj, &"type".into(), &"geometryReady".into()).unwrap();
1476            js_sys::Reflect::set(&obj, &"id".into(), &(*id as f64).into()).unwrap();
1477            js_sys::Reflect::set(&obj, &"vertexCount".into(), &(*vertex_count as f64).into())
1478                .unwrap();
1479            js_sys::Reflect::set(&obj, &"triangleCount".into(), &(*triangle_count as f64).into())
1480                .unwrap();
1481        }
1482        ParseEvent::Progress {
1483            phase,
1484            percent,
1485            entities_processed,
1486            total_entities,
1487        } => {
1488            js_sys::Reflect::set(&obj, &"type".into(), &"progress".into()).unwrap();
1489            js_sys::Reflect::set(&obj, &"phase".into(), &phase.as_str().into()).unwrap();
1490            js_sys::Reflect::set(&obj, &"percent".into(), &(*percent as f64).into()).unwrap();
1491            js_sys::Reflect::set(
1492                &obj,
1493                &"entitiesProcessed".into(),
1494                &(*entities_processed as f64).into(),
1495            )
1496            .unwrap();
1497            js_sys::Reflect::set(
1498                &obj,
1499                &"totalEntities".into(),
1500                &(*total_entities as f64).into(),
1501            )
1502            .unwrap();
1503        }
1504        ParseEvent::Completed {
1505            duration_ms,
1506            entity_count,
1507            triangle_count,
1508        } => {
1509            js_sys::Reflect::set(&obj, &"type".into(), &"completed".into()).unwrap();
1510            js_sys::Reflect::set(&obj, &"durationMs".into(), &(*duration_ms).into()).unwrap();
1511            js_sys::Reflect::set(&obj, &"entityCount".into(), &(*entity_count as f64).into())
1512                .unwrap();
1513            js_sys::Reflect::set(&obj, &"triangleCount".into(), &(*triangle_count as f64).into())
1514                .unwrap();
1515        }
1516        ParseEvent::Error { message, position } => {
1517            js_sys::Reflect::set(&obj, &"type".into(), &"error".into()).unwrap();
1518            js_sys::Reflect::set(&obj, &"message".into(), &message.as_str().into()).unwrap();
1519            if let Some(pos) = position {
1520                js_sys::Reflect::set(&obj, &"position".into(), &(*pos as f64).into()).unwrap();
1521            }
1522        }
1523    }
1524
1525    obj.into()
1526}
1527
1528/// Build style index: maps geometry express IDs to RGBA colors
1529/// Follows the chain: IfcStyledItem → IfcSurfaceStyle → IfcSurfaceStyleRendering → IfcColourRgb
1530fn build_geometry_style_index(
1531    content: &str,
1532    decoder: &mut ifc_lite_core::EntityDecoder,
1533) -> rustc_hash::FxHashMap<u32, [f32; 4]> {
1534    use ifc_lite_core::EntityScanner;
1535    use rustc_hash::FxHashMap;
1536
1537    let mut style_index: FxHashMap<u32, [f32; 4]> = FxHashMap::default();
1538    let mut scanner = EntityScanner::new(content);
1539
1540    // First pass: find all IfcStyledItem entities
1541    while let Some((_id, type_name, start, end)) = scanner.next_entity() {
1542        if type_name != "IFCSTYLEDITEM" {
1543            continue;
1544        }
1545
1546        // Decode the IfcStyledItem
1547        let styled_item = match decoder.decode_at(start, end) {
1548            Ok(entity) => entity,
1549            Err(_) => continue,
1550        };
1551
1552        // IfcStyledItem: Item (ref to geometry), Styles (list of style refs), Name
1553        // Attribute 0: Item (geometry reference)
1554        let geometry_id = match styled_item.get_ref(0) {
1555            Some(id) => id,
1556            None => continue,
1557        };
1558
1559        // Skip if we already have a color for this geometry
1560        if style_index.contains_key(&geometry_id) {
1561            continue;
1562        }
1563
1564        // Attribute 1: Styles (list of style assignment refs)
1565        let styles_attr = match styled_item.get(1) {
1566            Some(attr) => attr,
1567            None => continue,
1568        };
1569
1570        // Extract color from styles list
1571        if let Some(color) = extract_color_from_styles(styles_attr, decoder) {
1572            style_index.insert(geometry_id, color);
1573        }
1574    }
1575
1576    style_index
1577}
1578
1579/// Build element style index: maps building element IDs to RGBA colors
1580/// Follows: Element → IfcProductDefinitionShape → IfcShapeRepresentation → geometry items
1581fn build_element_style_index(
1582    content: &str,
1583    geometry_styles: &rustc_hash::FxHashMap<u32, [f32; 4]>,
1584    decoder: &mut ifc_lite_core::EntityDecoder,
1585) -> rustc_hash::FxHashMap<u32, [f32; 4]> {
1586    use ifc_lite_core::EntityScanner;
1587    use rustc_hash::FxHashMap;
1588
1589    let mut element_styles: FxHashMap<u32, [f32; 4]> = FxHashMap::default();
1590    let mut scanner = EntityScanner::new(content);
1591
1592    // Scan all building elements
1593    while let Some((element_id, type_name, start, end)) = scanner.next_entity() {
1594        // Check if this is a building element type
1595        if !ifc_lite_core::has_geometry_by_name(type_name) {
1596            continue;
1597        }
1598
1599        // Decode the element
1600        let element = match decoder.decode_at(start, end) {
1601            Ok(entity) => entity,
1602            Err(_) => continue,
1603        };
1604
1605        // Building elements have Representation attribute at index 6
1606        // IfcProduct: GlobalId, OwnerHistory, Name, Description, ObjectType, ObjectPlacement, Representation
1607        let repr_id = match element.get_ref(6) {
1608            Some(id) => id,
1609            None => continue,
1610        };
1611
1612        // Decode IfcProductDefinitionShape
1613        let product_shape = match decoder.decode_by_id(repr_id) {
1614            Ok(entity) => entity,
1615            Err(_) => continue,
1616        };
1617
1618        // IfcProductDefinitionShape: Name, Description, Representations (list)
1619        // Attribute 2: Representations
1620        let reprs_attr = match product_shape.get(2) {
1621            Some(attr) => attr,
1622            None => continue,
1623        };
1624
1625        let reprs_list = match reprs_attr.as_list() {
1626            Some(list) => list,
1627            None => continue,
1628        };
1629
1630        // Look through representations for geometry with styles
1631        for repr_item in reprs_list {
1632            let shape_repr_id = match repr_item.as_entity_ref() {
1633                Some(id) => id,
1634                None => continue,
1635            };
1636
1637            // Decode IfcShapeRepresentation
1638            let shape_repr = match decoder.decode_by_id(shape_repr_id) {
1639                Ok(entity) => entity,
1640                Err(_) => continue,
1641            };
1642
1643            // IfcShapeRepresentation: ContextOfItems, RepresentationIdentifier, RepresentationType, Items
1644            // Attribute 3: Items (list of geometry items)
1645            let items_attr = match shape_repr.get(3) {
1646                Some(attr) => attr,
1647                None => continue,
1648            };
1649
1650            let items_list = match items_attr.as_list() {
1651                Some(list) => list,
1652                None => continue,
1653            };
1654
1655            // Check each geometry item for a style
1656            for geom_item in items_list {
1657                let geom_id = match geom_item.as_entity_ref() {
1658                    Some(id) => id,
1659                    None => continue,
1660                };
1661
1662                // Check if this geometry has a style
1663                if let Some(&color) = geometry_styles.get(&geom_id) {
1664                    element_styles.insert(element_id, color);
1665                    break; // Found a color for this element
1666                }
1667            }
1668
1669            // If we found a color, stop looking at more representations
1670            if element_styles.contains_key(&element_id) {
1671                break;
1672            }
1673        }
1674    }
1675
1676    element_styles
1677}
1678
1679/// Extract RGBA color from IfcStyledItem.Styles attribute
1680fn extract_color_from_styles(
1681    styles_attr: &ifc_lite_core::AttributeValue,
1682    decoder: &mut ifc_lite_core::EntityDecoder,
1683) -> Option<[f32; 4]> {
1684    use ifc_lite_core::IfcType;
1685
1686    // Styles can be a list or a single reference
1687    if let Some(list) = styles_attr.as_list() {
1688        for item in list {
1689            if let Some(style_id) = item.as_entity_ref() {
1690                if let Some(color) = extract_color_from_style_assignment(style_id, decoder) {
1691                    return Some(color);
1692                }
1693            }
1694        }
1695    } else if let Some(style_id) = styles_attr.as_entity_ref() {
1696        return extract_color_from_style_assignment(style_id, decoder);
1697    }
1698
1699    None
1700}
1701
1702/// Extract color from IfcPresentationStyleAssignment or IfcSurfaceStyle
1703fn extract_color_from_style_assignment(
1704    style_id: u32,
1705    decoder: &mut ifc_lite_core::EntityDecoder,
1706) -> Option<[f32; 4]> {
1707    use ifc_lite_core::IfcType;
1708
1709    let style = decoder.decode_by_id(style_id).ok()?;
1710
1711    match style.ifc_type {
1712        IfcType::IfcPresentationStyleAssignment => {
1713            // IfcPresentationStyleAssignment: Styles (list)
1714            let styles_attr = style.get(0)?;
1715            if let Some(list) = styles_attr.as_list() {
1716                for item in list {
1717                    if let Some(inner_id) = item.as_entity_ref() {
1718                        if let Some(color) = extract_color_from_surface_style(inner_id, decoder) {
1719                            return Some(color);
1720                        }
1721                    }
1722                }
1723            }
1724        }
1725        IfcType::IfcSurfaceStyle => {
1726            return extract_color_from_surface_style(style_id, decoder);
1727        }
1728        _ => {}
1729    }
1730
1731    None
1732}
1733
1734/// Extract color from IfcSurfaceStyle
1735fn extract_color_from_surface_style(
1736    style_id: u32,
1737    decoder: &mut ifc_lite_core::EntityDecoder,
1738) -> Option<[f32; 4]> {
1739    use ifc_lite_core::IfcType;
1740
1741    let style = decoder.decode_by_id(style_id).ok()?;
1742
1743    if style.ifc_type != IfcType::IfcSurfaceStyle {
1744        return None;
1745    }
1746
1747    // IfcSurfaceStyle: Name, Side, Styles (list of surface style elements)
1748    // Attribute 2: Styles
1749    let styles_attr = style.get(2)?;
1750
1751    if let Some(list) = styles_attr.as_list() {
1752        for item in list {
1753            if let Some(element_id) = item.as_entity_ref() {
1754                if let Some(color) = extract_color_from_rendering(element_id, decoder) {
1755                    return Some(color);
1756                }
1757            }
1758        }
1759    }
1760
1761    None
1762}
1763
1764/// Extract color from IfcSurfaceStyleRendering or IfcSurfaceStyleShading
1765fn extract_color_from_rendering(
1766    rendering_id: u32,
1767    decoder: &mut ifc_lite_core::EntityDecoder,
1768) -> Option<[f32; 4]> {
1769    use ifc_lite_core::IfcType;
1770
1771    let rendering = decoder.decode_by_id(rendering_id).ok()?;
1772
1773    match rendering.ifc_type {
1774        IfcType::IfcSurfaceStyleRendering | IfcType::IfcSurfaceStyleShading => {
1775            // Both have SurfaceColour as attribute 0
1776            let color_ref = rendering.get_ref(0)?;
1777            return extract_color_rgb(color_ref, decoder);
1778        }
1779        _ => {}
1780    }
1781
1782    None
1783}
1784
1785/// Extract RGB color from IfcColourRgb
1786fn extract_color_rgb(
1787    color_id: u32,
1788    decoder: &mut ifc_lite_core::EntityDecoder,
1789) -> Option<[f32; 4]> {
1790    use ifc_lite_core::IfcType;
1791
1792    let color = decoder.decode_by_id(color_id).ok()?;
1793
1794    if color.ifc_type != IfcType::IfcColourRgb {
1795        return None;
1796    }
1797
1798    // IfcColourRgb: Name, Red, Green, Blue
1799    // Note: In IFC2x3, attributes are at indices 1, 2, 3 (0 is Name)
1800    // In IFC4, attributes are also at 1, 2, 3
1801    let red = color.get_float(1).unwrap_or(0.8);
1802    let green = color.get_float(2).unwrap_or(0.8);
1803    let blue = color.get_float(3).unwrap_or(0.8);
1804
1805    Some([red as f32, green as f32, blue as f32, 1.0])
1806}
1807
1808/// Get default color for IFC type (matches default-materials.ts)
1809fn get_default_color_for_type(ifc_type: &ifc_lite_core::IfcType) -> [f32; 4] {
1810    use ifc_lite_core::IfcType;
1811
1812    match ifc_type {
1813        // Walls - light gray
1814        IfcType::IfcWall | IfcType::IfcWallStandardCase => [0.85, 0.85, 0.85, 1.0],
1815
1816        // Slabs - darker gray
1817        IfcType::IfcSlab => [0.7, 0.7, 0.7, 1.0],
1818
1819        // Roofs - brown-ish
1820        IfcType::IfcRoof => [0.6, 0.5, 0.4, 1.0],
1821
1822        // Columns/Beams - steel gray
1823        IfcType::IfcColumn | IfcType::IfcBeam | IfcType::IfcMember => [0.6, 0.65, 0.7, 1.0],
1824
1825        // Windows - light blue transparent
1826        IfcType::IfcWindow => [0.6, 0.8, 1.0, 0.4],
1827
1828        // Doors - wood brown
1829        IfcType::IfcDoor => [0.6, 0.45, 0.3, 1.0],
1830
1831        // Stairs
1832        IfcType::IfcStair => [0.75, 0.75, 0.75, 1.0],
1833
1834        // Railings
1835        IfcType::IfcRailing => [0.4, 0.4, 0.45, 1.0],
1836
1837        // Plates/Coverings
1838        IfcType::IfcPlate | IfcType::IfcCovering => [0.8, 0.8, 0.8, 1.0],
1839
1840        // Curtain walls - glass blue
1841        IfcType::IfcCurtainWall => [0.5, 0.7, 0.9, 0.5],
1842
1843        // Furniture - wood
1844        IfcType::IfcFurnishingElement => [0.7, 0.55, 0.4, 1.0],
1845
1846        // Default gray
1847        _ => [0.8, 0.8, 0.8, 1.0],
1848    }
1849}
1850
1851/// Convert entity counts map to JavaScript object
1852fn counts_to_js(counts: &rustc_hash::FxHashMap<String, usize>) -> JsValue {
1853    let obj = js_sys::Object::new();
1854
1855    for (type_name, count) in counts {
1856        let key = JsValue::from_str(type_name.as_str());
1857        let value = JsValue::from_f64(*count as f64);
1858        js_sys::Reflect::set(&obj, &key, &value).unwrap();
1859    }
1860
1861    obj.into()
1862}