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