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