Skip to main content

ifc_lite_core/
schema_gen.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//! IFC Schema - Dynamic type system
6//!
7//! Generated from IFC4 EXPRESS schema for maintainability.
8//! All types are handled generically through enum dispatch.
9
10use crate::generated::IfcType;
11use crate::parser::Token;
12use std::collections::HashMap;
13
14/// Geometry representation categories (internal use only)
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
16pub enum GeometryCategory {
17    SweptSolid,
18    Boolean,
19    ExplicitMesh,
20    MappedItem,
21    Surface,
22    Curve,
23    Other,
24}
25
26/// Profile definition categories (internal use only)
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
28pub enum ProfileCategory {
29    Parametric,
30    Arbitrary,
31    Composite,
32}
33
34/// IFC entity attribute value
35#[derive(Debug, Clone)]
36pub enum AttributeValue {
37    /// Entity reference
38    EntityRef(u32),
39    /// String value
40    String(String),
41    /// Integer value
42    Integer(i64),
43    /// Float value
44    Float(f64),
45    /// Enum value
46    Enum(String),
47    /// List of values
48    List(Vec<AttributeValue>),
49    /// Null/undefined
50    Null,
51    /// Derived value (*)
52    Derived,
53}
54
55impl AttributeValue {
56    /// Convert from Token
57    pub fn from_token(token: &Token) -> Self {
58        match token {
59            Token::EntityRef(id) => AttributeValue::EntityRef(*id),
60            Token::String(s) => AttributeValue::String(String::from_utf8_lossy(s).into_owned()),
61            Token::Integer(i) => AttributeValue::Integer(*i),
62            Token::Float(f) => AttributeValue::Float(*f),
63            Token::Enum(e) => AttributeValue::Enum(String::from_utf8_lossy(e).into_owned()),
64            Token::List(items) => {
65                AttributeValue::List(items.iter().map(Self::from_token).collect())
66            }
67            Token::TypedValue(type_name, args) => {
68                // For typed values like IFCPARAMETERVALUE(0.), extract the inner value
69                // Store as a list with the type name first, followed by args
70                let mut values = vec![AttributeValue::String(
71                    String::from_utf8_lossy(type_name).into_owned(),
72                )];
73                values.extend(args.iter().map(Self::from_token));
74                AttributeValue::List(values)
75            }
76            Token::Null => AttributeValue::Null,
77            Token::Derived => AttributeValue::Derived,
78        }
79    }
80
81    /// Get as entity reference
82    #[inline]
83    pub fn as_entity_ref(&self) -> Option<u32> {
84        match self {
85            AttributeValue::EntityRef(id) => Some(*id),
86            _ => None,
87        }
88    }
89
90    /// Get as string
91    #[inline]
92    pub fn as_string(&self) -> Option<&str> {
93        match self {
94            AttributeValue::String(s) => Some(s),
95            _ => None,
96        }
97    }
98
99    /// Get as enum value (strips the dots from .ENUM.)
100    #[inline]
101    pub fn as_enum(&self) -> Option<&str> {
102        match self {
103            AttributeValue::Enum(s) => Some(s),
104            _ => None,
105        }
106    }
107
108    /// Get as float
109    /// Also handles TypedValue wrappers like IFCNORMALISEDRATIOMEASURE(0.5)
110    /// which are stored as List([String("typename"), Float(value)])
111    #[inline]
112    pub fn as_float(&self) -> Option<f64> {
113        match self {
114            AttributeValue::Float(f) => Some(*f),
115            AttributeValue::Integer(i) => Some(*i as f64),
116            // Handle TypedValue wrappers (stored as List with type name + value)
117            AttributeValue::List(items) if items.len() >= 2 => {
118                // Check if first item is a string (type name) and second is numeric
119                if matches!(items.first(), Some(AttributeValue::String(_))) {
120                    // Try to get the numeric value from the second element
121                    match items.get(1) {
122                        Some(AttributeValue::Float(f)) => Some(*f),
123                        Some(AttributeValue::Integer(i)) => Some(*i as f64),
124                        _ => None,
125                    }
126                } else {
127                    None
128                }
129            }
130            _ => None,
131        }
132    }
133
134    /// Get as integer (more efficient than as_float for indices)
135    #[inline]
136    pub fn as_int(&self) -> Option<i64> {
137        match self {
138            AttributeValue::Integer(i) => Some(*i),
139            AttributeValue::Float(f) => Some(*f as i64),
140            _ => None,
141        }
142    }
143
144    /// Get as list
145    #[inline]
146    pub fn as_list(&self) -> Option<&[AttributeValue]> {
147        match self {
148            AttributeValue::List(items) => Some(items),
149            _ => None,
150        }
151    }
152
153    /// Check if null/derived
154    #[inline]
155    pub fn is_null(&self) -> bool {
156        matches!(self, AttributeValue::Null | AttributeValue::Derived)
157    }
158
159    /// Batch parse 3D coordinates from a list of coordinate triples
160    /// Returns flattened f32 array: [x0, y0, z0, x1, y1, z1, ...]
161    /// Optimized for large coordinate lists
162    #[inline]
163    pub fn parse_coordinate_list_3d(coord_list: &[AttributeValue]) -> Vec<f32> {
164        let mut result = Vec::with_capacity(coord_list.len() * 3);
165
166        for coord_attr in coord_list {
167            if let Some(coord) = coord_attr.as_list() {
168                // Fast path: extract x, y, z directly
169                let x = coord.first().and_then(|v| v.as_float()).unwrap_or(0.0) as f32;
170                let y = coord.get(1).and_then(|v| v.as_float()).unwrap_or(0.0) as f32;
171                let z = coord.get(2).and_then(|v| v.as_float()).unwrap_or(0.0) as f32;
172
173                result.push(x);
174                result.push(y);
175                result.push(z);
176            }
177        }
178
179        result
180    }
181
182    /// Batch parse 2D coordinates from a list of coordinate pairs
183    /// Returns flattened f32 array: [x0, y0, x1, y1, ...]
184    #[inline]
185    pub fn parse_coordinate_list_2d(coord_list: &[AttributeValue]) -> Vec<f32> {
186        let mut result = Vec::with_capacity(coord_list.len() * 2);
187
188        for coord_attr in coord_list {
189            if let Some(coord) = coord_attr.as_list() {
190                let x = coord.first().and_then(|v| v.as_float()).unwrap_or(0.0) as f32;
191                let y = coord.get(1).and_then(|v| v.as_float()).unwrap_or(0.0) as f32;
192
193                result.push(x);
194                result.push(y);
195            }
196        }
197
198        result
199    }
200
201    /// Batch parse triangle indices from a list of index triples
202    /// Converts from 1-based IFC indices to 0-based indices
203    /// Returns flattened u32 array: [i0, i1, i2, ...]
204    #[inline]
205    pub fn parse_index_list(face_list: &[AttributeValue]) -> Vec<u32> {
206        let mut result = Vec::with_capacity(face_list.len() * 3);
207
208        for face_attr in face_list {
209            if let Some(face) = face_attr.as_list() {
210                // Use as_int for faster parsing, convert from 1-based to 0-based
211                let i0 = (face.first().and_then(|v| v.as_int()).unwrap_or(1) - 1) as u32;
212                let i1 = (face.get(1).and_then(|v| v.as_int()).unwrap_or(1) - 1) as u32;
213                let i2 = (face.get(2).and_then(|v| v.as_int()).unwrap_or(1) - 1) as u32;
214
215                result.push(i0);
216                result.push(i1);
217                result.push(i2);
218            }
219        }
220
221        result
222    }
223
224    /// Batch parse coordinate list with f64 precision
225    /// Returns Vec of (x, y, z) tuples
226    #[inline]
227    pub fn parse_coordinate_list_3d_f64(coord_list: &[AttributeValue]) -> Vec<(f64, f64, f64)> {
228        coord_list
229            .iter()
230            .filter_map(|coord_attr| {
231                let coord = coord_attr.as_list()?;
232                let x = coord.first().and_then(|v| v.as_float()).unwrap_or(0.0);
233                let y = coord.get(1).and_then(|v| v.as_float()).unwrap_or(0.0);
234                let z = coord.get(2).and_then(|v| v.as_float()).unwrap_or(0.0);
235                Some((x, y, z))
236            })
237            .collect()
238    }
239}
240
241/// Decoded IFC entity with attributes
242#[derive(Debug, Clone)]
243pub struct DecodedEntity {
244    pub id: u32,
245    pub ifc_type: IfcType,
246    pub attributes: Vec<AttributeValue>,
247}
248
249impl DecodedEntity {
250    /// Create new decoded entity
251    pub fn new(id: u32, ifc_type: IfcType, attributes: Vec<AttributeValue>) -> Self {
252        Self {
253            id,
254            ifc_type,
255            attributes,
256        }
257    }
258
259    /// Get attribute by index
260    pub fn get(&self, index: usize) -> Option<&AttributeValue> {
261        self.attributes.get(index)
262    }
263
264    /// Get entity reference attribute
265    pub fn get_ref(&self, index: usize) -> Option<u32> {
266        self.get(index).and_then(|v| v.as_entity_ref())
267    }
268
269    /// Get string attribute
270    pub fn get_string(&self, index: usize) -> Option<&str> {
271        self.get(index).and_then(|v| v.as_string())
272    }
273
274    /// Get float attribute
275    pub fn get_float(&self, index: usize) -> Option<f64> {
276        self.get(index).and_then(|v| v.as_float())
277    }
278
279    /// Get list attribute
280    pub fn get_list(&self, index: usize) -> Option<&[AttributeValue]> {
281        self.get(index).and_then(|v| v.as_list())
282    }
283}
284
285/// IFC schema metadata for dynamic processing
286#[derive(Clone)]
287pub struct IfcSchema {
288    /// Geometry representation types (for routing)
289    pub geometry_types: HashMap<IfcType, GeometryCategory>,
290    /// Profile types
291    pub profile_types: HashMap<IfcType, ProfileCategory>,
292}
293
294impl IfcSchema {
295    /// Create schema with geometry type mappings
296    pub fn new() -> Self {
297        let mut geometry_types = HashMap::new();
298        let mut profile_types = HashMap::new();
299
300        // Swept solids (P0)
301        geometry_types.insert(IfcType::IfcExtrudedAreaSolid, GeometryCategory::SweptSolid);
302        geometry_types.insert(IfcType::IfcRevolvedAreaSolid, GeometryCategory::SweptSolid);
303
304        // Boolean operations (P0)
305        geometry_types.insert(IfcType::IfcBooleanResult, GeometryCategory::Boolean);
306        geometry_types.insert(IfcType::IfcBooleanClippingResult, GeometryCategory::Boolean);
307
308        // Explicit meshes (P0)
309        geometry_types.insert(IfcType::IfcFacetedBrep, GeometryCategory::ExplicitMesh);
310        geometry_types.insert(
311            IfcType::IfcTriangulatedFaceSet,
312            GeometryCategory::ExplicitMesh,
313        );
314        geometry_types.insert(IfcType::IfcPolygonalFaceSet, GeometryCategory::ExplicitMesh);
315        geometry_types.insert(IfcType::IfcFaceBasedSurfaceModel, GeometryCategory::Surface);
316        geometry_types.insert(
317            IfcType::IfcSurfaceOfLinearExtrusion,
318            GeometryCategory::Surface,
319        );
320        geometry_types.insert(
321            IfcType::IfcShellBasedSurfaceModel,
322            GeometryCategory::Surface,
323        );
324
325        // Instancing (P0)
326        geometry_types.insert(IfcType::IfcMappedItem, GeometryCategory::MappedItem);
327
328        // Profile types - Parametric
329        profile_types.insert(IfcType::IfcRectangleProfileDef, ProfileCategory::Parametric);
330        profile_types.insert(
331            IfcType::IfcRoundedRectangleProfileDef,
332            ProfileCategory::Parametric,
333        );
334        profile_types.insert(IfcType::IfcCircleProfileDef, ProfileCategory::Parametric);
335        profile_types.insert(
336            IfcType::IfcCircleHollowProfileDef,
337            ProfileCategory::Parametric,
338        );
339        profile_types.insert(
340            IfcType::IfcRectangleHollowProfileDef,
341            ProfileCategory::Parametric,
342        );
343        profile_types.insert(IfcType::IfcIShapeProfileDef, ProfileCategory::Parametric);
344        profile_types.insert(
345            IfcType::IfcAsymmetricIShapeProfileDef,
346            ProfileCategory::Parametric,
347        );
348        profile_types.insert(IfcType::IfcLShapeProfileDef, ProfileCategory::Parametric);
349        profile_types.insert(IfcType::IfcUShapeProfileDef, ProfileCategory::Parametric);
350        profile_types.insert(IfcType::IfcTShapeProfileDef, ProfileCategory::Parametric);
351        profile_types.insert(IfcType::IfcCShapeProfileDef, ProfileCategory::Parametric);
352        profile_types.insert(IfcType::IfcZShapeProfileDef, ProfileCategory::Parametric);
353
354        // Profile types - Arbitrary
355        profile_types.insert(
356            IfcType::IfcArbitraryClosedProfileDef,
357            ProfileCategory::Arbitrary,
358        );
359        profile_types.insert(
360            IfcType::IfcArbitraryProfileDefWithVoids,
361            ProfileCategory::Arbitrary,
362        );
363
364        // Profile types - Composite
365        profile_types.insert(IfcType::IfcCompositeProfileDef, ProfileCategory::Composite);
366
367        Self {
368            geometry_types,
369            profile_types,
370        }
371    }
372
373    /// Get geometry category for a type
374    pub fn geometry_category(&self, ifc_type: &IfcType) -> Option<GeometryCategory> {
375        self.geometry_types.get(ifc_type).copied()
376    }
377
378    /// Get profile category for a type
379    pub fn profile_category(&self, ifc_type: &IfcType) -> Option<ProfileCategory> {
380        self.profile_types.get(ifc_type).copied()
381    }
382
383    /// Check if type is a geometry representation
384    pub fn is_geometry_type(&self, ifc_type: &IfcType) -> bool {
385        self.geometry_types.contains_key(ifc_type)
386    }
387
388    /// Check if type is a profile
389    pub fn is_profile_type(&self, ifc_type: &IfcType) -> bool {
390        self.profile_types.contains_key(ifc_type)
391    }
392
393    /// Check if type has geometry
394    pub fn has_geometry(&self, ifc_type: &IfcType) -> bool {
395        // Building elements, furnishing, etc.
396        let name = ifc_type.name();
397        (matches!(
398            ifc_type,
399            IfcType::IfcWall
400                | IfcType::IfcWallStandardCase
401                | IfcType::IfcSlab
402                | IfcType::IfcBeam
403                | IfcType::IfcColumn
404                | IfcType::IfcRoof
405                | IfcType::IfcStair
406                | IfcType::IfcRamp
407                | IfcType::IfcRailing
408                | IfcType::IfcPlate
409                | IfcType::IfcMember
410                | IfcType::IfcFooting
411                | IfcType::IfcPile
412                | IfcType::IfcCovering
413                | IfcType::IfcCurtainWall
414                | IfcType::IfcDoor
415                | IfcType::IfcWindow
416                | IfcType::IfcChimney
417                | IfcType::IfcShadingDevice
418                | IfcType::IfcBuildingElementProxy
419                | IfcType::IfcBuildingElementPart
420        ) || name.contains("Reinforc"))
421            || matches!(
422                ifc_type,
423                IfcType::IfcFurnishingElement
424                | IfcType::IfcFurniture
425                | IfcType::IfcDuctSegment
426                | IfcType::IfcPipeSegment
427                | IfcType::IfcCableSegment
428                | IfcType::IfcProduct // Base type for all products
429                | IfcType::IfcDistributionElement
430                | IfcType::IfcFlowSegment
431                | IfcType::IfcFlowFitting
432                | IfcType::IfcFlowTerminal
433            )
434            // Spatial elements with geometry (for visibility toggling)
435            || matches!(
436                ifc_type,
437                IfcType::IfcSpace
438                | IfcType::IfcOpeningElement
439                | IfcType::IfcSite
440            )
441    }
442}
443
444impl Default for IfcSchema {
445    fn default() -> Self {
446        Self::new()
447    }
448}
449
450// Note: IFC types are now defined as proper enum variants in schema.rs
451// This avoids the issue where from_str() would return Unknown(hash) instead of matching the constant.
452
453#[cfg(test)]
454mod tests {
455    use super::*;
456
457    #[test]
458    fn test_schema_geometry_categories() {
459        let schema = IfcSchema::new();
460
461        assert_eq!(
462            schema.geometry_category(&IfcType::IfcExtrudedAreaSolid),
463            Some(GeometryCategory::SweptSolid)
464        );
465
466        assert_eq!(
467            schema.geometry_category(&IfcType::IfcBooleanResult),
468            Some(GeometryCategory::Boolean)
469        );
470
471        assert_eq!(
472            schema.geometry_category(&IfcType::IfcTriangulatedFaceSet),
473            Some(GeometryCategory::ExplicitMesh)
474        );
475
476        assert_eq!(
477            schema.profile_category(&IfcType::IfcRoundedRectangleProfileDef),
478            Some(ProfileCategory::Parametric)
479        );
480    }
481
482    #[test]
483    fn test_attribute_value_conversion() {
484        let token = Token::EntityRef(123);
485        let attr = AttributeValue::from_token(&token);
486        assert_eq!(attr.as_entity_ref(), Some(123));
487
488        let token = Token::String(b"test");
489        let attr = AttributeValue::from_token(&token);
490        assert_eq!(attr.as_string(), Some("test"));
491    }
492
493    #[test]
494    fn test_decoded_entity() {
495        let entity = DecodedEntity::new(
496            1,
497            IfcType::IfcWall,
498            vec![
499                AttributeValue::EntityRef(2),
500                AttributeValue::String("Wall-001".to_string()),
501                AttributeValue::Float(3.5),
502            ],
503        );
504
505        assert_eq!(entity.get_ref(0), Some(2));
506        assert_eq!(entity.get_string(1), Some("Wall-001"));
507        assert_eq!(entity.get_float(2), Some(3.5));
508    }
509
510    #[test]
511    fn test_as_float_with_typed_value() {
512        // Test plain float
513        let plain_float = AttributeValue::Float(0.5);
514        assert_eq!(plain_float.as_float(), Some(0.5));
515
516        // Test integer to float conversion
517        let integer = AttributeValue::Integer(42);
518        assert_eq!(integer.as_float(), Some(42.0));
519
520        // Test TypedValue wrapper like IFCNORMALISEDRATIOMEASURE(0.5)
521        // This is stored as List([String("IFCNORMALISEDRATIOMEASURE"), Float(0.5)])
522        let typed_value = AttributeValue::List(vec![
523            AttributeValue::String("IFCNORMALISEDRATIOMEASURE".to_string()),
524            AttributeValue::Float(0.5),
525        ]);
526        assert_eq!(typed_value.as_float(), Some(0.5));
527
528        // Test TypedValue with integer
529        let typed_int = AttributeValue::List(vec![
530            AttributeValue::String("IFCINTEGER".to_string()),
531            AttributeValue::Integer(100),
532        ]);
533        assert_eq!(typed_int.as_float(), Some(100.0));
534
535        // Test that non-typed lists return None
536        let regular_list =
537            AttributeValue::List(vec![AttributeValue::Float(1.0), AttributeValue::Float(2.0)]);
538        assert_eq!(regular_list.as_float(), None);
539
540        // Test that empty list returns None
541        let empty_list = AttributeValue::List(vec![]);
542        assert_eq!(empty_list.as_float(), None);
543    }
544}