Skip to main content

ifc_lite_geometry/router/
mod.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
9mod caching;
10mod clipping;
11mod processing;
12mod transforms;
13mod voids;
14mod voids_2d;
15
16#[cfg(test)]
17mod tests;
18
19use crate::processors::{
20    AdvancedBrepProcessor, BooleanClippingProcessor, ExtrudedAreaSolidProcessor,
21    FaceBasedSurfaceModelProcessor, FacetedBrepProcessor, MappedItemProcessor,
22    PolygonalFaceSetProcessor, RevolvedAreaSolidProcessor, ShellBasedSurfaceModelProcessor,
23    SweptDiskSolidProcessor, TriangulatedFaceSetProcessor,
24};
25use crate::{Mesh, Result};
26use ifc_lite_core::{DecodedEntity, EntityDecoder, IfcSchema, IfcType};
27use nalgebra::Matrix4;
28use rustc_hash::FxHashMap;
29use std::cell::RefCell;
30use std::collections::HashMap;
31use std::sync::Arc;
32
33/// Geometry processor trait
34/// Each processor handles one type of IFC representation
35pub trait GeometryProcessor {
36    /// Process entity into mesh
37    fn process(
38        &self,
39        entity: &DecodedEntity,
40        decoder: &mut EntityDecoder,
41        schema: &IfcSchema,
42    ) -> Result<Mesh>;
43
44    /// Get supported IFC types
45    fn supported_types(&self) -> Vec<IfcType>;
46}
47
48/// Geometry router - routes entities to processors
49pub struct GeometryRouter {
50    schema: IfcSchema,
51    processors: HashMap<IfcType, Arc<dyn GeometryProcessor>>,
52    /// Cache for IfcRepresentationMap source geometry (MappedItem instancing)
53    /// Key: RepresentationMap entity ID, Value: Processed mesh
54    mapped_item_cache: RefCell<FxHashMap<u32, Arc<Mesh>>>,
55    /// Cache for FacetedBrep geometry (batch processed)
56    /// Key: FacetedBrep entity ID, Value: Processed mesh
57    /// Uses Box to avoid copying large meshes, entries are taken (removed) when used
58    faceted_brep_cache: RefCell<FxHashMap<u32, Mesh>>,
59    /// Cache for geometry deduplication by content hash
60    /// Buildings with repeated floors have 99% identical geometry
61    /// Key: Hash of mesh content, Value: Processed mesh
62    geometry_hash_cache: RefCell<FxHashMap<u64, Arc<Mesh>>>,
63    /// Unit scale factor (e.g., 0.001 for millimeters -> meters)
64    /// Applied to all mesh positions after processing
65    unit_scale: f64,
66    /// RTC (Relative-to-Center) offset for handling large coordinates
67    /// Subtracted from all world positions in f64 before converting to f32
68    /// This preserves precision for georeferenced models (e.g., Swiss UTM)
69    rtc_offset: (f64, f64, f64),
70}
71
72impl GeometryRouter {
73    /// Create new router with default processors
74    pub fn new() -> Self {
75        let schema = IfcSchema::new();
76        let schema_clone = schema.clone();
77        let mut router = Self {
78            schema,
79            processors: HashMap::new(),
80            mapped_item_cache: RefCell::new(FxHashMap::default()),
81            faceted_brep_cache: RefCell::new(FxHashMap::default()),
82            geometry_hash_cache: RefCell::new(FxHashMap::default()),
83            unit_scale: 1.0, // Default to base meters
84            rtc_offset: (0.0, 0.0, 0.0), // Default to no offset
85        };
86
87        // Register default P0 processors
88        router.register(Box::new(ExtrudedAreaSolidProcessor::new(
89            schema_clone.clone(),
90        )));
91        router.register(Box::new(TriangulatedFaceSetProcessor::new()));
92        router.register(Box::new(PolygonalFaceSetProcessor::new()));
93        router.register(Box::new(MappedItemProcessor::new()));
94        router.register(Box::new(FacetedBrepProcessor::new()));
95        router.register(Box::new(BooleanClippingProcessor::new()));
96        router.register(Box::new(SweptDiskSolidProcessor::new(schema_clone.clone())));
97        router.register(Box::new(RevolvedAreaSolidProcessor::new(
98            schema_clone.clone(),
99        )));
100        router.register(Box::new(AdvancedBrepProcessor::new()));
101        router.register(Box::new(ShellBasedSurfaceModelProcessor::new()));
102        router.register(Box::new(FaceBasedSurfaceModelProcessor::new()));
103
104        router
105    }
106
107    /// Create router and extract unit scale from IFC file
108    /// Automatically finds IFCPROJECT and extracts length unit conversion
109    pub fn with_units(content: &str, decoder: &mut EntityDecoder) -> Self {
110        let mut scanner = ifc_lite_core::EntityScanner::new(content);
111        let mut scale = 1.0;
112
113        // Scan through file to find IFCPROJECT
114        while let Some((id, type_name, _, _)) = scanner.next_entity() {
115            if type_name == "IFCPROJECT" {
116                if let Ok(s) = ifc_lite_core::extract_length_unit_scale(decoder, id) {
117                    scale = s;
118                }
119                break;
120            }
121        }
122
123        Self::with_scale(scale)
124    }
125
126    /// Create router with unit scale extracted from IFC file AND RTC offset for large coordinates
127    /// This is the recommended method for georeferenced models (Swiss UTM, etc.)
128    ///
129    /// # Arguments
130    /// * `content` - IFC file content
131    /// * `decoder` - Entity decoder
132    /// * `rtc_offset` - RTC offset to subtract from world coordinates (typically model centroid)
133    pub fn with_units_and_rtc(
134        content: &str,
135        decoder: &mut ifc_lite_core::EntityDecoder,
136        rtc_offset: (f64, f64, f64),
137    ) -> Self {
138        let mut scanner = ifc_lite_core::EntityScanner::new(content);
139        let mut scale = 1.0;
140
141        // Scan through file to find IFCPROJECT
142        while let Some((id, type_name, _, _)) = scanner.next_entity() {
143            if type_name == "IFCPROJECT" {
144                if let Ok(s) = ifc_lite_core::extract_length_unit_scale(decoder, id) {
145                    scale = s;
146                }
147                break;
148            }
149        }
150
151        Self::with_scale_and_rtc(scale, rtc_offset)
152    }
153
154    /// Create router with pre-calculated unit scale
155    pub fn with_scale(unit_scale: f64) -> Self {
156        let mut router = Self::new();
157        router.unit_scale = unit_scale;
158        router
159    }
160
161    /// Create router with RTC offset for large coordinate handling
162    /// Use this for georeferenced models (e.g., Swiss UTM coordinates)
163    pub fn with_rtc(rtc_offset: (f64, f64, f64)) -> Self {
164        let mut router = Self::new();
165        router.rtc_offset = rtc_offset;
166        router
167    }
168
169    /// Create router with both unit scale and RTC offset
170    pub fn with_scale_and_rtc(unit_scale: f64, rtc_offset: (f64, f64, f64)) -> Self {
171        let mut router = Self::new();
172        router.unit_scale = unit_scale;
173        router.rtc_offset = rtc_offset;
174        router
175    }
176
177    /// Set the RTC offset for large coordinate handling
178    pub fn set_rtc_offset(&mut self, offset: (f64, f64, f64)) {
179        self.rtc_offset = offset;
180    }
181
182    /// Get the current RTC offset
183    pub fn rtc_offset(&self) -> (f64, f64, f64) {
184        self.rtc_offset
185    }
186
187    /// Check if RTC offset is active (non-zero)
188    #[inline]
189    pub fn has_rtc_offset(&self) -> bool {
190        self.rtc_offset.0 != 0.0 || self.rtc_offset.1 != 0.0 || self.rtc_offset.2 != 0.0
191    }
192
193    /// Get the current unit scale factor
194    pub fn unit_scale(&self) -> f64 {
195        self.unit_scale
196    }
197
198    /// Scale mesh positions from file units to meters
199    /// Only applies scaling if unit_scale != 1.0
200    #[inline]
201    fn scale_mesh(&self, mesh: &mut Mesh) {
202        if self.unit_scale != 1.0 {
203            let scale = self.unit_scale as f32;
204            for pos in mesh.positions.iter_mut() {
205                *pos *= scale;
206            }
207        }
208    }
209
210    /// Scale the translation component of a transform matrix from file units to meters
211    /// The rotation/scale part stays unchanged, only translation (column 3) is scaled
212    #[inline]
213    fn scale_transform(&self, transform: &mut Matrix4<f64>) {
214        if self.unit_scale != 1.0 {
215            transform[(0, 3)] *= self.unit_scale;
216            transform[(1, 3)] *= self.unit_scale;
217            transform[(2, 3)] *= self.unit_scale;
218        }
219    }
220
221    /// Register a geometry processor
222    pub fn register(&mut self, processor: Box<dyn GeometryProcessor>) {
223        let processor_arc: Arc<dyn GeometryProcessor> = Arc::from(processor);
224        for ifc_type in processor_arc.supported_types() {
225            self.processors.insert(ifc_type, Arc::clone(&processor_arc));
226        }
227    }
228
229    /// Batch preprocess FacetedBrep entities for maximum parallelism
230    /// Call this before processing elements to enable batch triangulation
231    /// across all FacetedBrep entities instead of per-entity parallelism
232    pub fn preprocess_faceted_breps(&self, brep_ids: &[u32], decoder: &mut EntityDecoder) {
233        if brep_ids.is_empty() {
234            return;
235        }
236
237        // Use batch processing for parallel triangulation
238        let processor = FacetedBrepProcessor::new();
239        let results = processor.process_batch(brep_ids, decoder);
240
241        // Store results in cache (preallocate to avoid rehashing)
242        let mut cache = self.faceted_brep_cache.borrow_mut();
243        cache.reserve(results.len());
244        for (brep_idx, mesh) in results {
245            let brep_id = brep_ids[brep_idx];
246            cache.insert(brep_id, mesh);
247        }
248    }
249
250    /// Take FacetedBrep from cache (removes entry since each BREP is only used once)
251    /// Returns owned Mesh directly - no cloning needed
252    #[inline]
253    pub fn take_cached_faceted_brep(&self, brep_id: u32) -> Option<Mesh> {
254        self.faceted_brep_cache.borrow_mut().remove(&brep_id)
255    }
256
257    /// Get schema reference
258    pub fn schema(&self) -> &IfcSchema {
259        &self.schema
260    }
261}
262
263impl Default for GeometryRouter {
264    fn default() -> Self {
265        Self::new()
266    }
267}