Skip to main content

bimifc_parser/
model.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//! ParsedModel - Main IFC model implementation
6
7use crate::properties::PropertyReaderImpl;
8use crate::resolver::ResolverImpl;
9use crate::scanner::{parse_header, EntityScanner};
10use crate::spatial::SpatialQueryImpl;
11use crate::units::extract_unit_scale;
12
13use bimifc_model::{
14    EntityId, EntityResolver, IfcModel, IfcType, ModelMetadata, ProgressCallback, PropertyReader,
15    Result, SpatialQuery,
16};
17use rustc_hash::FxHashMap;
18use std::sync::Arc;
19
20/// Parsed IFC model implementing the `IfcModel` trait
21///
22/// This is the main entry point for accessing IFC data. It provides access
23/// to entities, properties, and spatial structure through trait objects.
24pub struct ParsedModel {
25    /// Entity resolver for lookups
26    resolver: Arc<ResolverImpl>,
27    /// Property reader
28    properties: Arc<PropertyReaderImpl>,
29    /// Spatial query
30    spatial: Arc<SpatialQueryImpl>,
31    /// Unit scale (file units to meters)
32    unit_scale: f64,
33    /// File metadata
34    metadata: ModelMetadata,
35}
36
37impl ParsedModel {
38    /// Parse IFC content and create a model
39    pub fn parse(content: &str, build_spatial: bool, _extract_properties: bool) -> Result<Self> {
40        // Build entity index
41        let index = EntityScanner::build_index(content);
42
43        // Build type index during parse
44        let mut type_index: FxHashMap<IfcType, Vec<EntityId>> = FxHashMap::default();
45        let mut scanner = EntityScanner::new(content);
46        while let Some((id, type_name, _, _)) = scanner.next_entity() {
47            let ifc_type = IfcType::parse(type_name);
48            type_index.entry(ifc_type).or_default().push(EntityId(id));
49        }
50
51        // Create resolver
52        let resolver = Arc::new(ResolverImpl::with_type_index(
53            content.to_string(),
54            index,
55            type_index,
56        ));
57
58        // Extract unit scale
59        let unit_scale = extract_unit_scale(resolver.as_ref());
60
61        // Create property reader
62        let properties = Arc::new(PropertyReaderImpl::new(resolver.clone()));
63
64        // Build spatial structure
65        let spatial = if build_spatial {
66            Arc::new(SpatialQueryImpl::build(resolver.as_ref()))
67        } else {
68            Arc::new(SpatialQueryImpl::empty())
69        };
70
71        // Parse header metadata
72        let header = parse_header(content);
73        let metadata = ModelMetadata {
74            schema_version: header.schema_version,
75            originating_system: header.originating_system,
76            preprocessor_version: header.preprocessor_version,
77            file_name: header.file_name,
78            file_description: None, // TODO: extract from FILE_DESCRIPTION
79            author: header.author,
80            organization: header.organization,
81            timestamp: header.timestamp,
82        };
83
84        Ok(Self {
85            resolver,
86            properties,
87            spatial,
88            unit_scale,
89            metadata,
90        })
91    }
92
93    /// Parse with progress reporting
94    pub fn parse_with_progress(
95        content: &str,
96        build_spatial: bool,
97        _extract_properties: bool,
98        on_progress: ProgressCallback,
99    ) -> Result<Self> {
100        on_progress("Scanning entities", 0.0);
101
102        // Build entity index
103        let index = EntityScanner::build_index(content);
104        on_progress("Building index", 20.0);
105
106        // Build type index during parse
107        let mut type_index: FxHashMap<IfcType, Vec<EntityId>> = FxHashMap::default();
108        let mut scanner = EntityScanner::new(content);
109        while let Some((id, type_name, _, _)) = scanner.next_entity() {
110            let ifc_type = IfcType::parse(type_name);
111            type_index.entry(ifc_type).or_default().push(EntityId(id));
112        }
113        on_progress("Indexing types", 40.0);
114
115        // Create resolver
116        let resolver = Arc::new(ResolverImpl::with_type_index(
117            content.to_string(),
118            index,
119            type_index,
120        ));
121
122        // Extract unit scale
123        let unit_scale = extract_unit_scale(resolver.as_ref());
124        on_progress("Extracting units", 50.0);
125
126        // Create property reader
127        let properties = Arc::new(PropertyReaderImpl::new(resolver.clone()));
128        on_progress("Building property index", 60.0);
129
130        // Build spatial structure
131        let spatial = if build_spatial {
132            on_progress("Building spatial structure", 70.0);
133            Arc::new(SpatialQueryImpl::build(resolver.as_ref()))
134        } else {
135            Arc::new(SpatialQueryImpl::empty())
136        };
137        on_progress("Processing metadata", 90.0);
138
139        // Parse header metadata
140        let header = parse_header(content);
141        let metadata = ModelMetadata {
142            schema_version: header.schema_version,
143            originating_system: header.originating_system,
144            preprocessor_version: header.preprocessor_version,
145            file_name: header.file_name,
146            file_description: None, // TODO: extract from FILE_DESCRIPTION
147            author: header.author,
148            organization: header.organization,
149            timestamp: header.timestamp,
150        };
151
152        on_progress("Complete", 100.0);
153
154        Ok(Self {
155            resolver,
156            properties,
157            spatial,
158            unit_scale,
159            metadata,
160        })
161    }
162
163    /// Get the resolver (for geometry processing, etc.)
164    pub fn resolver_arc(&self) -> Arc<ResolverImpl> {
165        self.resolver.clone()
166    }
167}
168
169impl IfcModel for ParsedModel {
170    fn resolver(&self) -> &dyn EntityResolver {
171        self.resolver.as_ref()
172    }
173
174    fn properties(&self) -> &dyn PropertyReader {
175        self.properties.as_ref()
176    }
177
178    fn spatial(&self) -> &dyn SpatialQuery {
179        self.spatial.as_ref()
180    }
181
182    fn unit_scale(&self) -> f64 {
183        self.unit_scale
184    }
185
186    fn metadata(&self) -> &ModelMetadata {
187        &self.metadata
188    }
189}
190
191#[cfg(test)]
192mod tests {
193    use super::*;
194
195    const TEST_IFC: &str = r#"ISO-10303-21;
196HEADER;
197FILE_DESCRIPTION(('ViewDefinition [CoordinationView]'),'2;1');
198FILE_NAME('test.ifc','2024-01-01T00:00:00',('Author'),('Org'),'Preprocessor','App','');
199FILE_SCHEMA(('IFC2X3'));
200ENDSEC;
201DATA;
202#1=IFCPROJECT('guid',$,'Test Project',$,$,$,$,$,#2);
203#2=IFCUNITASSIGNMENT((#3));
204#3=IFCSIUNIT(*,.LENGTHUNIT.,.MILLI.,.METRE.);
205#4=IFCSITE('guid2',$,'Site',$,$,$,$,$,$,$,$,$,$,$);
206#5=IFCRELAGGREGATES('guid3',$,$,$,#1,(#4));
207#6=IFCBUILDING('guid4',$,'Building',$,$,$,$,$,$,$,$,$);
208#7=IFCRELAGGREGATES('guid5',$,$,$,#4,(#6));
209#8=IFCBUILDINGSTOREY('guid6',$,'Ground Floor',$,$,$,$,$,.ELEMENT.,0.0);
210#9=IFCRELAGGREGATES('guid7',$,$,$,#6,(#8));
211#10=IFCWALL('guid8',$,'Wall 1',$,$,$,$,$);
212#11=IFCRELCONTAINEDINSPATIALSTRUCTURE('guid9',$,$,$,(#10),#8);
213ENDSEC;
214END-ISO-10303-21;
215"#;
216
217    #[test]
218    fn test_parse_model() {
219        let model = ParsedModel::parse(TEST_IFC, true, true).unwrap();
220
221        // Check metadata
222        assert_eq!(model.metadata().schema_version, "IFC2X3");
223        assert_eq!(model.metadata().file_name, Some("test.ifc".to_string()));
224
225        // Check unit scale (millimeters -> meters = 0.001)
226        assert!((model.unit_scale() - 0.001).abs() < 1e-10);
227
228        // Check resolver works
229        let walls = model.resolver().find_by_type_name("IFCWALL");
230        assert_eq!(walls.len(), 1);
231
232        // Check spatial structure
233        let tree = model.spatial().spatial_tree();
234        assert!(tree.is_some());
235        let project = tree.unwrap();
236        assert_eq!(project.name, "Test Project");
237
238        // Check storeys
239        let storeys = model.spatial().storeys();
240        assert_eq!(storeys.len(), 1);
241        assert_eq!(storeys[0].name, "Ground Floor");
242    }
243
244    #[test]
245    fn test_search() {
246        let model = ParsedModel::parse(TEST_IFC, true, true).unwrap();
247
248        // Search by name
249        let results = model.spatial().search("wall");
250        assert!(!results.is_empty());
251
252        // Search by type
253        let results = model.spatial().search("IFCWALL");
254        assert!(!results.is_empty());
255    }
256
257    #[test]
258    fn test_elements_in_storey() {
259        let model = ParsedModel::parse(TEST_IFC, true, true).unwrap();
260
261        let storeys = model.spatial().storeys();
262        assert!(!storeys.is_empty());
263
264        let elements = model.spatial().elements_in_storey(storeys[0].id);
265        assert!(!elements.is_empty());
266    }
267}