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) => {
61                // Decode STEP escapes (\X2\, \X4\, \X\, \S\, \P\) so every
62                // consumer of a string attribute sees native UTF-8, matching
63                // the TS decodeIfcString. No-escape strings stay zero-cost.
64                let raw = String::from_utf8_lossy(s);
65                AttributeValue::String(crate::step_encoding::decode_ifc_string(&raw).into_owned())
66            }
67            Token::Integer(i) => AttributeValue::Integer(*i),
68            Token::Float(f) => AttributeValue::Float(*f),
69            Token::Enum(e) => AttributeValue::Enum(String::from_utf8_lossy(e).into_owned()),
70            Token::List(items) => {
71                AttributeValue::List(items.iter().map(Self::from_token).collect())
72            }
73            Token::TypedValue(type_name, args) => {
74                // For typed values like IFCPARAMETERVALUE(0.), extract the inner value
75                // Store as a list with the type name first, followed by args
76                let mut values = vec![AttributeValue::String(
77                    String::from_utf8_lossy(type_name).into_owned(),
78                )];
79                values.extend(args.iter().map(Self::from_token));
80                AttributeValue::List(values)
81            }
82            Token::Null => AttributeValue::Null,
83            Token::Derived => AttributeValue::Derived,
84        }
85    }
86
87    /// Get as entity reference
88    #[inline]
89    pub fn as_entity_ref(&self) -> Option<u32> {
90        match self {
91            AttributeValue::EntityRef(id) => Some(*id),
92            _ => None,
93        }
94    }
95
96    /// Get as string
97    #[inline]
98    pub fn as_string(&self) -> Option<&str> {
99        match self {
100            AttributeValue::String(s) => Some(s),
101            _ => None,
102        }
103    }
104
105    /// Get as enum value (strips the dots from .ENUM.)
106    #[inline]
107    pub fn as_enum(&self) -> Option<&str> {
108        match self {
109            AttributeValue::Enum(s) => Some(s),
110            _ => None,
111        }
112    }
113
114    /// Get as float
115    /// Also handles TypedValue wrappers like IFCNORMALISEDRATIOMEASURE(0.5)
116    /// which are stored as List([String("typename"), Float(value)])
117    #[inline]
118    pub fn as_float(&self) -> Option<f64> {
119        match self {
120            AttributeValue::Float(f) => Some(*f),
121            AttributeValue::Integer(i) => Some(*i as f64),
122            // Handle TypedValue wrappers (stored as List with type name + value)
123            AttributeValue::List(items) if items.len() >= 2 => {
124                // Check if first item is a string (type name) and second is numeric
125                if matches!(items.first(), Some(AttributeValue::String(_))) {
126                    // Try to get the numeric value from the second element
127                    match items.get(1) {
128                        Some(AttributeValue::Float(f)) => Some(*f),
129                        Some(AttributeValue::Integer(i)) => Some(*i as f64),
130                        _ => None,
131                    }
132                } else {
133                    None
134                }
135            }
136            _ => None,
137        }
138    }
139
140    /// Get as integer (more efficient than as_float for indices)
141    #[inline]
142    pub fn as_int(&self) -> Option<i64> {
143        match self {
144            AttributeValue::Integer(i) => Some(*i),
145            AttributeValue::Float(f) => Some(*f as i64),
146            _ => None,
147        }
148    }
149
150    /// Get as list
151    #[inline]
152    pub fn as_list(&self) -> Option<&[AttributeValue]> {
153        match self {
154            AttributeValue::List(items) => Some(items),
155            _ => None,
156        }
157    }
158
159    /// Check if null/derived
160    #[inline]
161    pub fn is_null(&self) -> bool {
162        matches!(self, AttributeValue::Null | AttributeValue::Derived)
163    }
164
165    /// Batch parse 3D coordinates from a list of coordinate triples
166    /// Returns flattened f32 array: [x0, y0, z0, x1, y1, z1, ...]
167    /// Optimized for large coordinate lists
168    #[inline]
169    pub fn parse_coordinate_list_3d(coord_list: &[AttributeValue]) -> Vec<f32> {
170        let mut result = Vec::with_capacity(coord_list.len() * 3);
171
172        for coord_attr in coord_list {
173            if let Some(coord) = coord_attr.as_list() {
174                // Fast path: extract x, y, z directly
175                let x = coord.first().and_then(|v| v.as_float()).unwrap_or(0.0) as f32;
176                let y = coord.get(1).and_then(|v| v.as_float()).unwrap_or(0.0) as f32;
177                let z = coord.get(2).and_then(|v| v.as_float()).unwrap_or(0.0) as f32;
178
179                result.push(x);
180                result.push(y);
181                result.push(z);
182            }
183        }
184
185        result
186    }
187
188    /// Batch parse 2D coordinates from a list of coordinate pairs
189    /// Returns flattened f32 array: [x0, y0, x1, y1, ...]
190    #[inline]
191    pub fn parse_coordinate_list_2d(coord_list: &[AttributeValue]) -> Vec<f32> {
192        let mut result = Vec::with_capacity(coord_list.len() * 2);
193
194        for coord_attr in coord_list {
195            if let Some(coord) = coord_attr.as_list() {
196                let x = coord.first().and_then(|v| v.as_float()).unwrap_or(0.0) as f32;
197                let y = coord.get(1).and_then(|v| v.as_float()).unwrap_or(0.0) as f32;
198
199                result.push(x);
200                result.push(y);
201            }
202        }
203
204        result
205    }
206
207    /// Batch parse triangle indices from a list of index triples
208    /// Converts from 1-based IFC indices to 0-based indices
209    /// Returns flattened u32 array: [i0, i1, i2, ...]
210    #[inline]
211    pub fn parse_index_list(face_list: &[AttributeValue]) -> Vec<u32> {
212        let mut result = Vec::with_capacity(face_list.len() * 3);
213
214        // Convert a 1-based i64 IFC index to a 0-based u32. Anything outside the
215        // valid u32 vertex range — non-positive, or beyond u32::MAX — maps to
216        // u32::MAX, an out-of-range sentinel the downstream bounds check drops,
217        // instead of an `(i64 - 1) as u32` truncation/wrap to a valid-looking
218        // (wrong) vertex. NOTE: this sentinel is u32::MAX while fast_parse's
219        // saturating path yields u32::MAX - 1 — consumers must bounds-check
220        // (i >= vertex_count), never compare against a single sentinel value.
221        let to_zero_based = |i: i64| -> u32 {
222            i.checked_sub(1)
223                .and_then(|z| u32::try_from(z).ok())
224                .unwrap_or(u32::MAX)
225        };
226
227        for face_attr in face_list {
228            if let Some(face) = face_attr.as_list() {
229                // Use as_int for faster parsing, convert from 1-based to 0-based
230                let i0 = to_zero_based(face.first().and_then(|v| v.as_int()).unwrap_or(1));
231                let i1 = to_zero_based(face.get(1).and_then(|v| v.as_int()).unwrap_or(1));
232                let i2 = to_zero_based(face.get(2).and_then(|v| v.as_int()).unwrap_or(1));
233
234                result.push(i0);
235                result.push(i1);
236                result.push(i2);
237            }
238        }
239
240        result
241    }
242
243    /// Batch parse coordinate list with f64 precision
244    /// Returns Vec of (x, y, z) tuples
245    #[inline]
246    pub fn parse_coordinate_list_3d_f64(coord_list: &[AttributeValue]) -> Vec<(f64, f64, f64)> {
247        coord_list
248            .iter()
249            .filter_map(|coord_attr| {
250                let coord = coord_attr.as_list()?;
251                let x = coord.first().and_then(|v| v.as_float()).unwrap_or(0.0);
252                let y = coord.get(1).and_then(|v| v.as_float()).unwrap_or(0.0);
253                let z = coord.get(2).and_then(|v| v.as_float()).unwrap_or(0.0);
254                Some((x, y, z))
255            })
256            .collect()
257    }
258}
259
260/// Decoded IFC entity. `attributes` is behind an `Arc` so cloning a
261/// `DecodedEntity` (the decoder clones on every cache insert AND every cache
262/// hit) is a refcount bump, not a deep clone of the attribute tree. Attributes
263/// are never mutated after construction, so sharing is sound and byte-identical.
264#[derive(Debug, Clone)]
265pub struct DecodedEntity {
266    pub id: u32,
267    pub ifc_type: IfcType,
268    pub attributes: std::sync::Arc<Vec<AttributeValue>>,
269}
270
271impl DecodedEntity {
272    /// Create new decoded entity
273    pub fn new(id: u32, ifc_type: IfcType, attributes: Vec<AttributeValue>) -> Self {
274        Self {
275            id,
276            ifc_type,
277            attributes: std::sync::Arc::new(attributes),
278        }
279    }
280
281    /// Get attribute by index
282    pub fn get(&self, index: usize) -> Option<&AttributeValue> {
283        self.attributes.get(index)
284    }
285
286    /// Get entity reference attribute
287    pub fn get_ref(&self, index: usize) -> Option<u32> {
288        self.get(index).and_then(|v| v.as_entity_ref())
289    }
290
291    /// Get string attribute
292    pub fn get_string(&self, index: usize) -> Option<&str> {
293        self.get(index).and_then(|v| v.as_string())
294    }
295
296    /// Get float attribute
297    pub fn get_float(&self, index: usize) -> Option<f64> {
298        self.get(index).and_then(|v| v.as_float())
299    }
300
301    /// Get list attribute
302    pub fn get_list(&self, index: usize) -> Option<&[AttributeValue]> {
303        self.get(index).and_then(|v| v.as_list())
304    }
305}
306
307/// IFC schema metadata for dynamic processing
308#[derive(Clone)]
309pub struct IfcSchema {
310    /// Geometry representation types (for routing)
311    pub geometry_types: HashMap<IfcType, GeometryCategory>,
312    /// Profile types
313    pub profile_types: HashMap<IfcType, ProfileCategory>,
314}
315
316impl IfcSchema {
317    /// Create schema with geometry type mappings
318    pub fn new() -> Self {
319        let mut geometry_types = HashMap::new();
320        let mut profile_types = HashMap::new();
321
322        // Swept solids (P0)
323        geometry_types.insert(IfcType::IfcExtrudedAreaSolid, GeometryCategory::SweptSolid);
324        geometry_types.insert(IfcType::IfcRevolvedAreaSolid, GeometryCategory::SweptSolid);
325
326        // Boolean operations (P0)
327        geometry_types.insert(IfcType::IfcBooleanResult, GeometryCategory::Boolean);
328        geometry_types.insert(IfcType::IfcBooleanClippingResult, GeometryCategory::Boolean);
329
330        // Explicit meshes (P0)
331        geometry_types.insert(IfcType::IfcFacetedBrep, GeometryCategory::ExplicitMesh);
332        geometry_types.insert(
333            IfcType::IfcTriangulatedFaceSet,
334            GeometryCategory::ExplicitMesh,
335        );
336        geometry_types.insert(IfcType::IfcPolygonalFaceSet, GeometryCategory::ExplicitMesh);
337        geometry_types.insert(IfcType::IfcFaceBasedSurfaceModel, GeometryCategory::Surface);
338        geometry_types.insert(
339            IfcType::IfcSurfaceOfLinearExtrusion,
340            GeometryCategory::Surface,
341        );
342        geometry_types.insert(
343            IfcType::IfcShellBasedSurfaceModel,
344            GeometryCategory::Surface,
345        );
346
347        // Instancing (P0)
348        geometry_types.insert(IfcType::IfcMappedItem, GeometryCategory::MappedItem);
349
350        // Profile types - Parametric
351        profile_types.insert(IfcType::IfcRectangleProfileDef, ProfileCategory::Parametric);
352        profile_types.insert(
353            IfcType::IfcRoundedRectangleProfileDef,
354            ProfileCategory::Parametric,
355        );
356        profile_types.insert(IfcType::IfcCircleProfileDef, ProfileCategory::Parametric);
357        profile_types.insert(
358            IfcType::IfcCircleHollowProfileDef,
359            ProfileCategory::Parametric,
360        );
361        profile_types.insert(
362            IfcType::IfcRectangleHollowProfileDef,
363            ProfileCategory::Parametric,
364        );
365        profile_types.insert(IfcType::IfcIShapeProfileDef, ProfileCategory::Parametric);
366        profile_types.insert(
367            IfcType::IfcAsymmetricIShapeProfileDef,
368            ProfileCategory::Parametric,
369        );
370        profile_types.insert(IfcType::IfcLShapeProfileDef, ProfileCategory::Parametric);
371        profile_types.insert(IfcType::IfcUShapeProfileDef, ProfileCategory::Parametric);
372        profile_types.insert(IfcType::IfcTShapeProfileDef, ProfileCategory::Parametric);
373        profile_types.insert(IfcType::IfcCShapeProfileDef, ProfileCategory::Parametric);
374        profile_types.insert(IfcType::IfcZShapeProfileDef, ProfileCategory::Parametric);
375
376        // Profile types - Arbitrary
377        profile_types.insert(
378            IfcType::IfcArbitraryClosedProfileDef,
379            ProfileCategory::Arbitrary,
380        );
381        profile_types.insert(
382            IfcType::IfcArbitraryProfileDefWithVoids,
383            ProfileCategory::Arbitrary,
384        );
385
386        // Profile types - Composite
387        profile_types.insert(IfcType::IfcCompositeProfileDef, ProfileCategory::Composite);
388
389        Self {
390            geometry_types,
391            profile_types,
392        }
393    }
394
395    /// Get geometry category for a type
396    pub fn geometry_category(&self, ifc_type: &IfcType) -> Option<GeometryCategory> {
397        self.geometry_types.get(ifc_type).copied()
398    }
399
400    /// Get profile category for a type
401    pub fn profile_category(&self, ifc_type: &IfcType) -> Option<ProfileCategory> {
402        self.profile_types.get(ifc_type).copied()
403    }
404
405    /// Check if type is a geometry representation
406    pub fn is_geometry_type(&self, ifc_type: &IfcType) -> bool {
407        self.geometry_types.contains_key(ifc_type)
408    }
409
410    /// Check if type is a profile
411    pub fn is_profile_type(&self, ifc_type: &IfcType) -> bool {
412        self.profile_types.contains_key(ifc_type)
413    }
414
415    /// Check if type has geometry
416    pub fn has_geometry(&self, ifc_type: &IfcType) -> bool {
417        // Building elements, furnishing, etc.
418        let name = ifc_type.name();
419        (matches!(
420            ifc_type,
421            IfcType::IfcWall
422                | IfcType::IfcWallStandardCase
423                | IfcType::IfcSlab
424                | IfcType::IfcBeam
425                | IfcType::IfcColumn
426                | IfcType::IfcRoof
427                | IfcType::IfcStair
428                | IfcType::IfcRamp
429                | IfcType::IfcRailing
430                | IfcType::IfcPlate
431                | IfcType::IfcMember
432                | IfcType::IfcFooting
433                | IfcType::IfcPile
434                | IfcType::IfcCovering
435                | IfcType::IfcCurtainWall
436                | IfcType::IfcDoor
437                | IfcType::IfcWindow
438                | IfcType::IfcChimney
439                | IfcType::IfcShadingDevice
440                | IfcType::IfcBuildingElementProxy
441                | IfcType::IfcBuildingElementPart
442        ) || name.contains("Reinforc"))
443            || matches!(
444                ifc_type,
445                IfcType::IfcFurnishingElement
446                | IfcType::IfcFurniture
447                | IfcType::IfcDuctSegment
448                | IfcType::IfcPipeSegment
449                | IfcType::IfcCableSegment
450                | IfcType::IfcProduct // Base type for all products
451                | IfcType::IfcDistributionElement
452                | IfcType::IfcFlowSegment
453                | IfcType::IfcFlowFitting
454                | IfcType::IfcFlowTerminal
455            )
456            // Spatial elements with geometry (for visibility toggling)
457            || matches!(
458                ifc_type,
459                IfcType::IfcSpace
460                | IfcType::IfcOpeningElement
461                | IfcType::IfcSite
462            )
463    }
464}
465
466impl Default for IfcSchema {
467    fn default() -> Self {
468        Self::new()
469    }
470}
471
472// Note: IFC types are now defined as proper enum variants in schema.rs
473// This avoids the issue where from_str() would return Unknown(hash) instead of matching the constant.
474
475#[cfg(test)]
476#[path = "schema_gen_tests.rs"]
477mod tests;