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