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