ifc_lite_geometry/
router.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//! Geometry Router - Dynamic dispatch to geometry processors
6//!
7//! Routes IFC representation entities to appropriate processors based on type.
8
9use crate::{Mesh, Point3, Vector3, Result, Error};
10use crate::processors::{ExtrudedAreaSolidProcessor, TriangulatedFaceSetProcessor, MappedItemProcessor, FacetedBrepProcessor, BooleanClippingProcessor, SweptDiskSolidProcessor, RevolvedAreaSolidProcessor, AdvancedBrepProcessor};
11use ifc_lite_core::{
12    DecodedEntity, EntityDecoder, GeometryCategory, IfcSchema, IfcType, ProfileCategory,
13};
14use nalgebra::{Matrix4, Rotation3};
15use rustc_hash::FxHashMap;
16use std::cell::RefCell;
17use std::collections::HashMap;
18use std::hash::{Hash, Hasher};
19use std::sync::Arc;
20
21/// Geometry processor trait
22/// Each processor handles one type of IFC representation
23pub trait GeometryProcessor {
24    /// Process entity into mesh
25    fn process(
26        &self,
27        entity: &DecodedEntity,
28        decoder: &mut EntityDecoder,
29        schema: &IfcSchema,
30    ) -> Result<Mesh>;
31
32    /// Get supported IFC types
33    fn supported_types(&self) -> Vec<IfcType>;
34}
35
36/// Geometry router - routes entities to processors
37pub struct GeometryRouter {
38    schema: IfcSchema,
39    processors: HashMap<IfcType, Arc<dyn GeometryProcessor>>,
40    /// Cache for IfcRepresentationMap source geometry (MappedItem instancing)
41    /// Key: RepresentationMap entity ID, Value: Processed mesh
42    mapped_item_cache: RefCell<FxHashMap<u32, Arc<Mesh>>>,
43    /// Cache for FacetedBrep geometry (batch processed)
44    /// Key: FacetedBrep entity ID, Value: Processed mesh
45    /// Uses Box to avoid copying large meshes, entries are taken (removed) when used
46    faceted_brep_cache: RefCell<FxHashMap<u32, Mesh>>,
47    /// Cache for geometry deduplication by content hash
48    /// Buildings with repeated floors have 99% identical geometry
49    /// Key: Hash of mesh content, Value: Processed mesh
50    geometry_hash_cache: RefCell<FxHashMap<u64, Arc<Mesh>>>,
51}
52
53impl GeometryRouter {
54    /// Create new router with default processors
55    pub fn new() -> Self {
56        let schema = IfcSchema::new();
57        let schema_clone = schema.clone();
58        let mut router = Self {
59            schema,
60            processors: HashMap::new(),
61            mapped_item_cache: RefCell::new(FxHashMap::default()),
62            faceted_brep_cache: RefCell::new(FxHashMap::default()),
63            geometry_hash_cache: RefCell::new(FxHashMap::default()),
64        };
65
66        // Register default P0 processors
67        router.register(Box::new(ExtrudedAreaSolidProcessor::new(schema_clone.clone())));
68        router.register(Box::new(TriangulatedFaceSetProcessor::new()));
69        router.register(Box::new(MappedItemProcessor::new()));
70        router.register(Box::new(FacetedBrepProcessor::new()));
71        router.register(Box::new(BooleanClippingProcessor::new()));
72        router.register(Box::new(SweptDiskSolidProcessor::new(schema_clone.clone())));
73        router.register(Box::new(RevolvedAreaSolidProcessor::new(schema_clone.clone())));
74        router.register(Box::new(AdvancedBrepProcessor::new()));
75
76        router
77    }
78
79    /// Register a geometry processor
80    pub fn register(&mut self, processor: Box<dyn GeometryProcessor>) {
81        let processor_arc: Arc<dyn GeometryProcessor> = Arc::from(processor);
82        for ifc_type in processor_arc.supported_types() {
83            self.processors.insert(ifc_type, Arc::clone(&processor_arc));
84        }
85    }
86
87    /// Batch preprocess FacetedBrep entities for maximum parallelism
88    /// Call this before processing elements to enable batch triangulation
89    /// across all FacetedBrep entities instead of per-entity parallelism
90    pub fn preprocess_faceted_breps(&self, brep_ids: &[u32], decoder: &mut EntityDecoder) {
91        if brep_ids.is_empty() {
92            return;
93        }
94
95        // Use batch processing for parallel triangulation
96        let processor = FacetedBrepProcessor::new();
97        let results = processor.process_batch(brep_ids, decoder);
98
99        // Store results in cache (preallocate to avoid rehashing)
100        let mut cache = self.faceted_brep_cache.borrow_mut();
101        cache.reserve(results.len());
102        for (brep_idx, mesh) in results {
103            let brep_id = brep_ids[brep_idx];
104            cache.insert(brep_id, mesh);
105        }
106    }
107
108    /// Take FacetedBrep from cache (removes entry since each BREP is only used once)
109    /// Returns owned Mesh directly - no cloning needed
110    #[inline]
111    pub fn take_cached_faceted_brep(&self, brep_id: u32) -> Option<Mesh> {
112        self.faceted_brep_cache.borrow_mut().remove(&brep_id)
113    }
114
115    /// Compute hash of mesh geometry for deduplication
116    /// Uses FxHasher for speed - we don't need cryptographic hashing
117    #[inline]
118    fn compute_mesh_hash(mesh: &Mesh) -> u64 {
119        use rustc_hash::FxHasher;
120        let mut hasher = FxHasher::default();
121
122        // Hash vertex count and index count first for fast rejection
123        mesh.positions.len().hash(&mut hasher);
124        mesh.indices.len().hash(&mut hasher);
125
126        // Hash position data (the main differentiator)
127        // Convert f32 to bits for reliable hashing
128        for pos in &mesh.positions {
129            pos.to_bits().hash(&mut hasher);
130        }
131
132        // Hash indices
133        for idx in &mesh.indices {
134            idx.hash(&mut hasher);
135        }
136
137        hasher.finish()
138    }
139
140    /// Try to get cached mesh by hash, or cache the provided mesh
141    /// Returns Arc<Mesh> - either from cache or newly cached
142    /// 
143    /// Note: Uses hash-only lookup without full equality check for performance.
144    /// FxHasher's 64-bit output makes collisions extremely rare (~1 in 2^64).
145    #[inline]
146    fn get_or_cache_by_hash(&self, mesh: Mesh) -> Arc<Mesh> {
147        let hash = Self::compute_mesh_hash(&mesh);
148
149        // Check cache first
150        {
151            let cache = self.geometry_hash_cache.borrow();
152            if let Some(cached) = cache.get(&hash) {
153                return Arc::clone(cached);
154            }
155        }
156
157        // Cache miss - store and return
158        let arc_mesh = Arc::new(mesh);
159        {
160            let mut cache = self.geometry_hash_cache.borrow_mut();
161            cache.insert(hash, Arc::clone(&arc_mesh));
162        }
163        arc_mesh
164    }
165
166    /// Process building element (IfcWall, IfcBeam, etc.) into mesh
167    /// Follows the representation chain:
168    /// Element → Representation → ShapeRepresentation → Items
169    #[inline]
170    pub fn process_element(
171        &self,
172        element: &DecodedEntity,
173        decoder: &mut EntityDecoder,
174    ) -> Result<Mesh> {
175        // Get representation (attribute 6 for most building elements)
176        // IfcProduct: GlobalId, OwnerHistory, Name, Description, ObjectType, ObjectPlacement, Representation, Tag
177        let representation_attr = element.get(6).ok_or_else(|| {
178            Error::geometry(format!(
179                "Element #{} has no representation attribute",
180                element.id
181            ))
182        })?;
183
184        if representation_attr.is_null() {
185            return Ok(Mesh::new()); // No geometry
186        }
187
188        let representation = decoder
189            .resolve_ref(representation_attr)?
190            .ok_or_else(|| Error::geometry("Failed to resolve representation".to_string()))?;
191
192        // IfcProductDefinitionShape has Representations attribute (list of IfcRepresentation)
193        if representation.ifc_type != IfcType::IfcProductDefinitionShape {
194            return Err(Error::geometry(format!(
195                "Expected IfcProductDefinitionShape, got {}",
196                representation.ifc_type
197            )));
198        }
199
200        // Get representations list (attribute 2)
201        let representations_attr = representation.get(2).ok_or_else(|| {
202            Error::geometry("IfcProductDefinitionShape missing Representations".to_string())
203        })?;
204
205        let representations = decoder.resolve_ref_list(representations_attr)?;
206
207        // Process all representations and merge meshes
208        let mut combined_mesh = Mesh::new();
209
210        // First pass: check if we have any direct geometry representations
211        // This prevents duplication when both direct and MappedRepresentation exist
212        let has_direct_geometry = representations.iter().any(|rep| {
213            if rep.ifc_type != IfcType::IfcShapeRepresentation {
214                return false;
215            }
216            if let Some(rep_type_attr) = rep.get(2) {
217                if let Some(rep_type) = rep_type_attr.as_string() {
218                    matches!(
219                        rep_type,
220                        "Body" | "SweptSolid" | "Brep" | "CSG" | "Clipping" | "SurfaceModel" | "Tessellation" | "AdvancedSweptSolid" | "AdvancedBrep"
221                    )
222                } else {
223                    false
224                }
225            } else {
226                false
227            }
228        });
229
230        for shape_rep in representations {
231            if shape_rep.ifc_type != IfcType::IfcShapeRepresentation {
232                continue;
233            }
234
235            // Check RepresentationType (attribute 2) - only process geometric representations
236            // Skip 'Axis', 'Curve2D', 'FootPrint', etc. - only process 'Body', 'SweptSolid', 'Brep', etc.
237            if let Some(rep_type_attr) = shape_rep.get(2) {
238                if let Some(rep_type) = rep_type_attr.as_string() {
239                    // Skip MappedRepresentation if we already have direct geometry
240                    // This prevents duplication when an element has both direct and mapped representations
241                    if rep_type == "MappedRepresentation" && has_direct_geometry {
242                        continue;
243                    }
244
245                    // Only process solid geometry representations
246                    if !matches!(
247                        rep_type,
248                        "Body" | "SweptSolid" | "Brep" | "CSG" | "Clipping" | "SurfaceModel" | "Tessellation" | "MappedRepresentation" | "AdvancedSweptSolid" | "AdvancedBrep"
249                    ) {
250                        continue; // Skip non-solid representations like 'Axis', 'Curve2D', etc.
251                    }
252                }
253            }
254
255            // Get items list (attribute 3)
256            let items_attr = shape_rep.get(3).ok_or_else(|| {
257                Error::geometry("IfcShapeRepresentation missing Items".to_string())
258            })?;
259
260            let items = decoder.resolve_ref_list(items_attr)?;
261
262            // Process each representation item
263            for item in items {
264                let mesh = self.process_representation_item(&item, decoder)?;
265                combined_mesh.merge(&mesh);
266            }
267        }
268
269        // Apply placement transformation
270        self.apply_placement(element, decoder, &mut combined_mesh)?;
271
272        Ok(combined_mesh)
273    }
274
275    /// Process building element and return geometry + transform separately
276    /// Used for instanced rendering - geometry is returned untransformed, transform is separate
277    #[inline]
278    pub fn process_element_with_transform(
279        &self,
280        element: &DecodedEntity,
281        decoder: &mut EntityDecoder,
282    ) -> Result<(Mesh, Matrix4<f64>)> {
283        // Get representation (attribute 6 for most building elements)
284        let representation_attr = element.get(6).ok_or_else(|| {
285            Error::geometry(format!(
286                "Element #{} has no representation attribute",
287                element.id
288            ))
289        })?;
290
291        if representation_attr.is_null() {
292            return Ok((Mesh::new(), Matrix4::identity())); // No geometry
293        }
294
295        let representation = decoder
296            .resolve_ref(representation_attr)?
297            .ok_or_else(|| Error::geometry("Failed to resolve representation".to_string()))?;
298
299        if representation.ifc_type != IfcType::IfcProductDefinitionShape {
300            return Err(Error::geometry(format!(
301                "Expected IfcProductDefinitionShape, got {}",
302                representation.ifc_type
303            )));
304        }
305
306        // Get representations list (attribute 2)
307        let representations_attr = representation.get(2).ok_or_else(|| {
308            Error::geometry("IfcProductDefinitionShape missing Representations".to_string())
309        })?;
310
311        let representations = decoder.resolve_ref_list(representations_attr)?;
312
313        // Process all representations and merge meshes
314        let mut combined_mesh = Mesh::new();
315
316        // Check for direct geometry
317        let has_direct_geometry = representations.iter().any(|rep| {
318            if rep.ifc_type != IfcType::IfcShapeRepresentation {
319                return false;
320            }
321            if let Some(rep_type_attr) = rep.get(2) {
322                if let Some(rep_type) = rep_type_attr.as_string() {
323                    matches!(
324                        rep_type,
325                        "Body" | "SweptSolid" | "Brep" | "CSG" | "Clipping" | "SurfaceModel" | "Tessellation" | "AdvancedSweptSolid" | "AdvancedBrep"
326                    )
327                } else {
328                    false
329                }
330            } else {
331                false
332            }
333        });
334
335        for shape_rep in representations {
336            if shape_rep.ifc_type != IfcType::IfcShapeRepresentation {
337                continue;
338            }
339
340            if let Some(rep_type_attr) = shape_rep.get(2) {
341                if let Some(rep_type) = rep_type_attr.as_string() {
342                    if rep_type == "MappedRepresentation" && has_direct_geometry {
343                        continue;
344                    }
345
346                    if !matches!(
347                        rep_type,
348                        "Body" | "SweptSolid" | "Brep" | "CSG" | "Clipping" | "SurfaceModel" | "Tessellation" | "MappedRepresentation" | "AdvancedSweptSolid" | "AdvancedBrep"
349                    ) {
350                        continue;
351                    }
352                }
353            }
354
355            let items_attr = shape_rep.get(3).ok_or_else(|| {
356                Error::geometry("IfcShapeRepresentation missing Items".to_string())
357            })?;
358
359            let items = decoder.resolve_ref_list(items_attr)?;
360
361            for item in items {
362                let mesh = self.process_representation_item(&item, decoder)?;
363                combined_mesh.merge(&mesh);
364            }
365        }
366
367        // Get placement transform WITHOUT applying it
368        let transform = self.get_placement_transform_from_element(element, decoder)?;
369
370        Ok((combined_mesh, transform))
371    }
372
373    /// Get placement transform from element without applying it
374    fn get_placement_transform_from_element(
375        &self,
376        element: &DecodedEntity,
377        decoder: &mut EntityDecoder,
378    ) -> Result<Matrix4<f64>> {
379        // Get ObjectPlacement (attribute 5)
380        let placement_attr = match element.get(5) {
381            Some(attr) if !attr.is_null() => attr,
382            _ => return Ok(Matrix4::identity()), // No placement
383        };
384
385        let placement = match decoder.resolve_ref(placement_attr)? {
386            Some(p) => p,
387            None => return Ok(Matrix4::identity()),
388        };
389
390        // Recursively get combined transform from placement hierarchy
391        self.get_placement_transform(&placement, decoder)
392    }
393
394    /// Process a single representation item (IfcExtrudedAreaSolid, etc.)
395    /// Uses hash-based caching for geometry deduplication across repeated floors
396    #[inline]
397    pub fn process_representation_item(
398        &self,
399        item: &DecodedEntity,
400        decoder: &mut EntityDecoder,
401    ) -> Result<Mesh> {
402        // Special handling for MappedItem with caching
403        if item.ifc_type == IfcType::IfcMappedItem {
404            return self.process_mapped_item_cached(item, decoder);
405        }
406
407        // Check FacetedBrep cache first (from batch preprocessing)
408        if item.ifc_type == IfcType::IfcFacetedBrep {
409            if let Some(mesh) = self.take_cached_faceted_brep(item.id) {
410                // FacetedBrep meshes are already processed, deduplicate by hash
411                let cached = self.get_or_cache_by_hash(mesh);
412                return Ok((*cached).clone());
413            }
414        }
415
416        // Check if we have a processor for this type
417        if let Some(processor) = self.processors.get(&item.ifc_type) {
418            let mesh = processor.process(item, decoder, &self.schema)?;
419            // Deduplicate by hash - buildings with repeated floors have identical geometry
420            if !mesh.positions.is_empty() {
421                let cached = self.get_or_cache_by_hash(mesh);
422                return Ok((*cached).clone());
423            }
424            return Ok(mesh);
425        }
426
427        // Check category for fallback handling
428        match self.schema.geometry_category(&item.ifc_type) {
429            Some(GeometryCategory::SweptSolid) => {
430                // For now, return empty mesh - processors will handle this
431                Ok(Mesh::new())
432            }
433            Some(GeometryCategory::ExplicitMesh) => {
434                // For now, return empty mesh - processors will handle this
435                Ok(Mesh::new())
436            }
437            Some(GeometryCategory::Boolean) => {
438                // For now, return empty mesh - processors will handle this
439                Ok(Mesh::new())
440            }
441            Some(GeometryCategory::MappedItem) => {
442                // For now, return empty mesh - processors will handle this
443                Ok(Mesh::new())
444            }
445            _ => Err(Error::geometry(format!(
446                "Unsupported representation type: {}",
447                item.ifc_type
448            ))),
449        }
450    }
451
452    /// Process MappedItem with caching for repeated geometry
453    #[inline]
454    fn process_mapped_item_cached(
455        &self,
456        item: &DecodedEntity,
457        decoder: &mut EntityDecoder,
458    ) -> Result<Mesh> {
459        // IfcMappedItem attributes:
460        // 0: MappingSource (IfcRepresentationMap)
461        // 1: MappingTarget (IfcCartesianTransformationOperator)
462
463        // Get mapping source (RepresentationMap)
464        let source_attr = item
465            .get(0)
466            .ok_or_else(|| Error::geometry("MappedItem missing MappingSource".to_string()))?;
467
468        let source_entity = decoder
469            .resolve_ref(source_attr)?
470            .ok_or_else(|| Error::geometry("Failed to resolve MappingSource".to_string()))?;
471
472        let source_id = source_entity.id;
473
474        // Get MappingTarget transformation (attribute 1: CartesianTransformationOperator)
475        let mapping_transform = if let Some(target_attr) = item.get(1) {
476            if !target_attr.is_null() {
477                if let Some(target_entity) = decoder.resolve_ref(target_attr)? {
478                    Some(self.parse_cartesian_transformation_operator(&target_entity, decoder)?)
479                } else {
480                    None
481                }
482            } else {
483                None
484            }
485        } else {
486            None
487        };
488
489        // Check cache first
490        {
491            let cache = self.mapped_item_cache.borrow();
492            if let Some(cached_mesh) = cache.get(&source_id) {
493                // Clone the cached mesh and apply MappingTarget transformation
494                let mut mesh = cached_mesh.as_ref().clone();
495                if let Some(transform) = &mapping_transform {
496                    self.transform_mesh(&mut mesh, transform);
497                }
498                return Ok(mesh);
499            }
500        }
501
502        // Cache miss - process the geometry
503        // IfcRepresentationMap has:
504        // 0: MappingOrigin (IfcAxis2Placement)
505        // 1: MappedRepresentation (IfcRepresentation)
506
507        let mapped_rep_attr = source_entity
508            .get(1)
509            .ok_or_else(|| {
510                Error::geometry("RepresentationMap missing MappedRepresentation".to_string())
511            })?;
512
513        let mapped_rep = decoder
514            .resolve_ref(mapped_rep_attr)?
515            .ok_or_else(|| Error::geometry("Failed to resolve MappedRepresentation".to_string()))?;
516
517        // Get representation items
518        let items_attr = mapped_rep
519            .get(3)
520            .ok_or_else(|| Error::geometry("Representation missing Items".to_string()))?;
521
522        let items = decoder.resolve_ref_list(items_attr)?;
523
524        // Process all items and merge (without recursing into MappedItem to avoid infinite loop)
525        let mut mesh = Mesh::new();
526        for sub_item in items {
527            if sub_item.ifc_type == IfcType::IfcMappedItem {
528                continue; // Skip nested MappedItems to avoid recursion
529            }
530            if let Some(processor) = self.processors.get(&sub_item.ifc_type) {
531                if let Ok(sub_mesh) = processor.process(&sub_item, decoder, &self.schema) {
532                    mesh.merge(&sub_mesh);
533                }
534            }
535        }
536
537        // Store in cache (before transformation, so cached mesh is in source coordinates)
538        {
539            let mut cache = self.mapped_item_cache.borrow_mut();
540            cache.insert(source_id, Arc::new(mesh.clone()));
541        }
542
543        // Apply MappingTarget transformation to this instance
544        if let Some(transform) = &mapping_transform {
545            self.transform_mesh(&mut mesh, transform);
546        }
547
548        Ok(mesh)
549    }
550
551    /// Apply local placement transformation to mesh
552    fn apply_placement(
553        &self,
554        element: &DecodedEntity,
555        decoder: &mut EntityDecoder,
556        mesh: &mut Mesh,
557    ) -> Result<()> {
558        // Get ObjectPlacement (attribute 5)
559        let placement_attr = match element.get(5) {
560            Some(attr) if !attr.is_null() => attr,
561            _ => return Ok(()), // No placement
562        };
563
564        let placement = match decoder.resolve_ref(placement_attr)? {
565            Some(p) => p,
566            None => return Ok(()),
567        };
568
569        // Recursively get combined transform from placement hierarchy
570        let transform = self.get_placement_transform(&placement, decoder)?;
571        self.transform_mesh(mesh, &transform);
572        Ok(())
573    }
574
575    /// Recursively resolve placement hierarchy
576    fn get_placement_transform(
577        &self,
578        placement: &DecodedEntity,
579        decoder: &mut EntityDecoder,
580    ) -> Result<Matrix4<f64>> {
581        if placement.ifc_type != IfcType::IfcLocalPlacement {
582            return Ok(Matrix4::identity());
583        }
584
585        // Get parent transform first (attribute 0: PlacementRelTo)
586        let parent_transform = if let Some(parent_attr) = placement.get(0) {
587            if !parent_attr.is_null() {
588                if let Some(parent) = decoder.resolve_ref(parent_attr)? {
589                    self.get_placement_transform(&parent, decoder)?
590                } else {
591                    Matrix4::identity()
592                }
593            } else {
594                Matrix4::identity()
595            }
596        } else {
597            Matrix4::identity()
598        };
599
600        // Get local transform (attribute 1: RelativePlacement)
601        let local_transform = if let Some(rel_attr) = placement.get(1) {
602            if !rel_attr.is_null() {
603                if let Some(rel) = decoder.resolve_ref(rel_attr)? {
604                    if rel.ifc_type == IfcType::IfcAxis2Placement3D {
605                        self.parse_axis2_placement_3d(&rel, decoder)?
606                    } else {
607                        Matrix4::identity()
608                    }
609                } else {
610                    Matrix4::identity()
611                }
612            } else {
613                Matrix4::identity()
614            }
615        } else {
616            Matrix4::identity()
617        };
618
619        // Compose: parent * local
620        Ok(parent_transform * local_transform)
621    }
622
623    /// Parse IfcAxis2Placement3D into transformation matrix
624    fn parse_axis2_placement_3d(
625        &self,
626        placement: &DecodedEntity,
627        decoder: &mut EntityDecoder,
628    ) -> Result<Matrix4<f64>> {
629        // IfcAxis2Placement3D: Location, Axis, RefDirection
630        let location = self.parse_cartesian_point(placement, decoder, 0)?;
631
632        // Default axes if not specified
633        let z_axis = if let Some(axis_attr) = placement.get(1) {
634            if !axis_attr.is_null() {
635                if let Some(axis_entity) = decoder.resolve_ref(axis_attr)? {
636                    self.parse_direction(&axis_entity)?
637                } else {
638                    Vector3::new(0.0, 0.0, 1.0)
639                }
640            } else {
641                Vector3::new(0.0, 0.0, 1.0)
642            }
643        } else {
644            Vector3::new(0.0, 0.0, 1.0)
645        };
646
647        let x_axis = if let Some(ref_dir_attr) = placement.get(2) {
648            if !ref_dir_attr.is_null() {
649                if let Some(ref_dir_entity) = decoder.resolve_ref(ref_dir_attr)? {
650                    self.parse_direction(&ref_dir_entity)?
651                } else {
652                    Vector3::new(1.0, 0.0, 0.0)
653                }
654            } else {
655                Vector3::new(1.0, 0.0, 0.0)
656            }
657        } else {
658            Vector3::new(1.0, 0.0, 0.0)
659        };
660
661        // Y axis is cross product of Z and X
662        let y_axis = z_axis.cross(&x_axis).normalize();
663        let x_axis = y_axis.cross(&z_axis).normalize();
664        let z_axis = z_axis.normalize();
665
666        // Build transformation matrix
667        let mut transform = Matrix4::identity();
668        transform[(0, 0)] = x_axis.x;
669        transform[(1, 0)] = x_axis.y;
670        transform[(2, 0)] = x_axis.z;
671        transform[(0, 1)] = y_axis.x;
672        transform[(1, 1)] = y_axis.y;
673        transform[(2, 1)] = y_axis.z;
674        transform[(0, 2)] = z_axis.x;
675        transform[(1, 2)] = z_axis.y;
676        transform[(2, 2)] = z_axis.z;
677        transform[(0, 3)] = location.x;
678        transform[(1, 3)] = location.y;
679        transform[(2, 3)] = location.z;
680
681        Ok(transform)
682    }
683
684    /// Parse IfcCartesianPoint
685    #[inline]
686    fn parse_cartesian_point(
687        &self,
688        parent: &DecodedEntity,
689        decoder: &mut EntityDecoder,
690        attr_index: usize,
691    ) -> Result<Point3<f64>> {
692        let point_attr = parent
693            .get(attr_index)
694            .ok_or_else(|| Error::geometry("Missing cartesian point".to_string()))?;
695
696        let point_entity = decoder
697            .resolve_ref(point_attr)?
698            .ok_or_else(|| Error::geometry("Failed to resolve cartesian point".to_string()))?;
699
700        if point_entity.ifc_type != IfcType::IfcCartesianPoint {
701            return Err(Error::geometry(format!(
702                "Expected IfcCartesianPoint, got {}",
703                point_entity.ifc_type
704            )));
705        }
706
707        // Get coordinates list (attribute 0)
708        let coords_attr = point_entity
709            .get(0)
710            .ok_or_else(|| Error::geometry("IfcCartesianPoint missing coordinates".to_string()))?;
711
712        let coords = coords_attr
713            .as_list()
714            .ok_or_else(|| Error::geometry("Expected coordinate list".to_string()))?;
715
716        let x = coords
717            .get(0)
718            .and_then(|v| v.as_float())
719            .unwrap_or(0.0);
720        let y = coords
721            .get(1)
722            .and_then(|v| v.as_float())
723            .unwrap_or(0.0);
724        let z = coords
725            .get(2)
726            .and_then(|v| v.as_float())
727            .unwrap_or(0.0);
728
729        Ok(Point3::new(x, y, z))
730    }
731
732    /// Parse IfcDirection
733    #[inline]
734    fn parse_direction(&self, direction_entity: &DecodedEntity) -> Result<Vector3<f64>> {
735        if direction_entity.ifc_type != IfcType::IfcDirection {
736            return Err(Error::geometry(format!(
737                "Expected IfcDirection, got {}",
738                direction_entity.ifc_type
739            )));
740        }
741
742        // Get direction ratios (attribute 0)
743        let ratios_attr = direction_entity
744            .get(0)
745            .ok_or_else(|| Error::geometry("IfcDirection missing ratios".to_string()))?;
746
747        let ratios = ratios_attr
748            .as_list()
749            .ok_or_else(|| Error::geometry("Expected ratio list".to_string()))?;
750
751        let x = ratios.get(0).and_then(|v| v.as_float()).unwrap_or(0.0);
752        let y = ratios.get(1).and_then(|v| v.as_float()).unwrap_or(0.0);
753        let z = ratios.get(2).and_then(|v| v.as_float()).unwrap_or(0.0);
754
755        Ok(Vector3::new(x, y, z))
756    }
757
758    /// Parse IfcCartesianTransformationOperator (2D or 3D)
759    /// Used for MappedItem MappingTarget transformation
760    #[inline]
761    fn parse_cartesian_transformation_operator(
762        &self,
763        entity: &DecodedEntity,
764        decoder: &mut EntityDecoder,
765    ) -> Result<Matrix4<f64>> {
766        // IfcCartesianTransformationOperator3D has:
767        // 0: Axis1 (IfcDirection) - X axis direction (optional)
768        // 1: Axis2 (IfcDirection) - Y axis direction (optional)
769        // 2: LocalOrigin (IfcCartesianPoint) - translation
770        // 3: Scale (IfcReal) - uniform scale (optional, defaults to 1.0)
771        // 4: Axis3 (IfcDirection) - Z axis direction (optional, for 3D only)
772
773        // Get LocalOrigin (attribute 2)
774        let origin = if let Some(origin_attr) = entity.get(2) {
775            if !origin_attr.is_null() {
776                if let Some(origin_entity) = decoder.resolve_ref(origin_attr)? {
777                    if origin_entity.ifc_type == IfcType::IfcCartesianPoint {
778                        let coords_attr = origin_entity.get(0);
779                        if let Some(coords) = coords_attr.and_then(|a| a.as_list()) {
780                            Point3::new(
781                                coords.get(0).and_then(|v| v.as_float()).unwrap_or(0.0),
782                                coords.get(1).and_then(|v| v.as_float()).unwrap_or(0.0),
783                                coords.get(2).and_then(|v| v.as_float()).unwrap_or(0.0),
784                            )
785                        } else {
786                            Point3::origin()
787                        }
788                    } else {
789                        Point3::origin()
790                    }
791                } else {
792                    Point3::origin()
793                }
794            } else {
795                Point3::origin()
796            }
797        } else {
798            Point3::origin()
799        };
800
801        // Get Scale (attribute 3)
802        let scale = entity.get_float(3).unwrap_or(1.0);
803
804        // Get Axis1 (X axis, attribute 0)
805        let x_axis = if let Some(axis1_attr) = entity.get(0) {
806            if !axis1_attr.is_null() {
807                if let Some(axis1_entity) = decoder.resolve_ref(axis1_attr)? {
808                    self.parse_direction(&axis1_entity)?.normalize()
809                } else {
810                    Vector3::new(1.0, 0.0, 0.0)
811                }
812            } else {
813                Vector3::new(1.0, 0.0, 0.0)
814            }
815        } else {
816            Vector3::new(1.0, 0.0, 0.0)
817        };
818
819        // Get Axis3 (Z axis, attribute 4 for 3D)
820        let z_axis = if let Some(axis3_attr) = entity.get(4) {
821            if !axis3_attr.is_null() {
822                if let Some(axis3_entity) = decoder.resolve_ref(axis3_attr)? {
823                    self.parse_direction(&axis3_entity)?.normalize()
824                } else {
825                    Vector3::new(0.0, 0.0, 1.0)
826                }
827            } else {
828                Vector3::new(0.0, 0.0, 1.0)
829            }
830        } else {
831            Vector3::new(0.0, 0.0, 1.0)
832        };
833
834        // Derive Y axis from Z and X (right-hand coordinate system)
835        let y_axis = z_axis.cross(&x_axis).normalize();
836        let x_axis = y_axis.cross(&z_axis).normalize();
837
838        // Build transformation matrix with scale
839        let mut transform = Matrix4::identity();
840        transform[(0, 0)] = x_axis.x * scale;
841        transform[(1, 0)] = x_axis.y * scale;
842        transform[(2, 0)] = x_axis.z * scale;
843        transform[(0, 1)] = y_axis.x * scale;
844        transform[(1, 1)] = y_axis.y * scale;
845        transform[(2, 1)] = y_axis.z * scale;
846        transform[(0, 2)] = z_axis.x * scale;
847        transform[(1, 2)] = z_axis.y * scale;
848        transform[(2, 2)] = z_axis.z * scale;
849        transform[(0, 3)] = origin.x;
850        transform[(1, 3)] = origin.y;
851        transform[(2, 3)] = origin.z;
852
853        Ok(transform)
854    }
855
856    /// Transform mesh by matrix - optimized with chunk-based iteration
857    #[inline]
858    fn transform_mesh(&self, mesh: &mut Mesh, transform: &Matrix4<f64>) {
859        // Use chunks for better cache locality and less indexing overhead
860        mesh.positions.chunks_exact_mut(3).for_each(|chunk| {
861            let point = Point3::new(
862                chunk[0] as f64,
863                chunk[1] as f64,
864                chunk[2] as f64,
865            );
866            let transformed = transform.transform_point(&point);
867            chunk[0] = transformed.x as f32;
868            chunk[1] = transformed.y as f32;
869            chunk[2] = transformed.z as f32;
870        });
871
872        // Transform normals (without translation) - optimized chunk iteration
873        let rotation = transform.fixed_view::<3, 3>(0, 0);
874        mesh.normals.chunks_exact_mut(3).for_each(|chunk| {
875            let normal = Vector3::new(
876                chunk[0] as f64,
877                chunk[1] as f64,
878                chunk[2] as f64,
879            );
880            let transformed = (rotation * normal).normalize();
881            chunk[0] = transformed.x as f32;
882            chunk[1] = transformed.y as f32;
883            chunk[2] = transformed.z as f32;
884        });
885    }
886
887    /// Get schema reference
888    pub fn schema(&self) -> &IfcSchema {
889        &self.schema
890    }
891}
892
893impl Default for GeometryRouter {
894    fn default() -> Self {
895        Self::new()
896    }
897}
898
899#[cfg(test)]
900mod tests {
901    use super::*;
902
903    #[test]
904    fn test_router_creation() {
905        let router = GeometryRouter::new();
906        // Router registers default processors on creation
907        assert!(!router.processors.is_empty());
908    }
909
910    #[test]
911    fn test_parse_cartesian_point() {
912        let content = r#"
913#1=IFCCARTESIANPOINT((100.0,200.0,300.0));
914#2=IFCWALL('guid',$,$,$,$,$,#1,$);
915"#;
916
917        let mut decoder = EntityDecoder::new(content);
918        let router = GeometryRouter::new();
919
920        let wall = decoder.decode_by_id(2).unwrap();
921        let point = router.parse_cartesian_point(&wall, &mut decoder, 6).unwrap();
922
923        assert_eq!(point.x, 100.0);
924        assert_eq!(point.y, 200.0);
925        assert_eq!(point.z, 300.0);
926    }
927
928    #[test]
929    fn test_parse_direction() {
930        let content = r#"
931#1=IFCDIRECTION((1.0,0.0,0.0));
932"#;
933
934        let mut decoder = EntityDecoder::new(content);
935        let router = GeometryRouter::new();
936
937        let direction = decoder.decode_by_id(1).unwrap();
938        let vec = router.parse_direction(&direction).unwrap();
939
940        assert_eq!(vec.x, 1.0);
941        assert_eq!(vec.y, 0.0);
942        assert_eq!(vec.z, 0.0);
943    }
944}