Skip to main content

ifc_properties/pset/
scalar.rs

1//! Single, bounded, list and enumerated property values.
2//!
3//! # Slots, verified against the IFC4 EXPRESS schema
4//!
5//! Every `IfcProperty` starts with `Name` (0) and `Description` (1); the
6//! subtype attributes follow. The families disagree about what comes next,
7//! and reading slot 2 as "the value" is wrong for four of the six:
8//!
9//! ```text
10//! IfcPropertySingleValue      2 = NominalValue      3 = Unit
11//! IfcPropertyEnumeratedValue  2 = EnumerationValues 3 = EnumerationReference
12//! IfcPropertyBoundedValue     2 = UpperBoundValue   3 = LowerBoundValue
13//!                             4 = Unit              5 = SetPointValue
14//! IfcPropertyListValue        2 = ListValues        3 = Unit
15//! IfcPropertyTableValue       2 = DefiningValues    3 = DefinedValues
16//!                             4 = Expression        5 = DefiningUnit
17//!                             6 = DefinedUnit       7 = CurveInterpolation
18//! IfcPropertyReferenceValue   2 = UsageName         3 = PropertyReference
19//! ```
20//!
21//! Note `IfcPropertyBoundedValue`: UPPER bound is slot 2, LOWER is slot 3.
22//! Reading them in the intuitive order silently inverts every range in the
23//! file, and the result still looks like a plausible range.
24
25use std::sync::Arc;
26
27use ifc_model::{EntityId, Model};
28
29use crate::error::PropertyAnomaly;
30use crate::nesting::Nesting;
31use crate::pset::complex::complex_members;
32use crate::value::MeasureValue;
33
34/// Attribute slots shared by every `IfcProperty`.
35const NAME: usize = 0;
36const DESCRIPTION: usize = 1;
37
38/// What a property actually states.
39///
40/// One variant per concrete `IfcProperty` subtype in IFC4. A file stating a
41/// subtype this crate does not model yields [`PropertyValue::Unsupported`]
42/// carrying the type name, so an unknown property is reported rather than
43/// silently dropped from its set.
44#[derive(Debug, Clone, PartialEq)]
45pub enum PropertyValue {
46    /// `IfcPropertySingleValue`: one measure, optionally with a unit.
47    Single {
48        /// The nominal value. `None` when the file omits it, which is legal.
49        value: Option<MeasureValue>,
50        /// `IfcNamedUnit`/`IfcDerivedUnit` override for this property.
51        unit: Option<EntityId>,
52    },
53    /// `IfcPropertyEnumeratedValue`: chosen constants plus their enumeration.
54    Enumerated {
55        /// The selected values.
56        values: Vec<MeasureValue>,
57        /// The `IfcPropertyEnumeration` they were selected from.
58        enumeration: Option<EntityId>,
59    },
60    /// `IfcPropertyBoundedValue`: a range, optionally with a set point.
61    Bounded {
62        /// Slot 2. Named explicitly because the schema order is upper-first.
63        upper: Option<MeasureValue>,
64        /// Slot 3.
65        lower: Option<MeasureValue>,
66        /// Slot 5.
67        set_point: Option<MeasureValue>,
68        /// Slot 4.
69        unit: Option<EntityId>,
70    },
71    /// `IfcPropertyListValue`: an ordered list of measures.
72    List {
73        /// The values, in file order. Order is meaningful in a LIST.
74        values: Vec<MeasureValue>,
75        /// Unit shared by every entry.
76        unit: Option<EntityId>,
77    },
78    /// `IfcPropertyTableValue`: a defining/defined value mapping.
79    Table {
80        /// The x column.
81        defining: Vec<MeasureValue>,
82        /// The y column.
83        defined: Vec<MeasureValue>,
84        /// How to interpolate between rows, when stated.
85        interpolation: Option<Arc<str>>,
86    },
87    /// `IfcPropertyReferenceValue`: a pointer to another entity.
88    Reference {
89        /// What the reference is for.
90        usage: Option<Arc<str>>,
91        /// The referenced entity.
92        reference: Option<EntityId>,
93    },
94    /// `IfcComplexProperty`: nested properties under a usage name.
95    Complex {
96        /// The grouping name.
97        usage: Option<Arc<str>>,
98        /// Contained properties, in file order.
99        properties: Vec<Property>,
100    },
101    /// A concrete `IfcProperty` subtype this crate does not model.
102    Unsupported {
103        /// The declared type, upper-cased.
104        type_name: Arc<str>,
105    },
106}
107
108/// One property: its name, description and value.
109#[derive(Debug, Clone, PartialEq)]
110pub struct Property {
111    /// The `IfcProperty` entity.
112    pub id: EntityId,
113    /// `Name`. Required by the schema; `None` means the file broke that.
114    pub name: Option<Arc<str>>,
115    /// `Description`, when stated.
116    pub description: Option<Arc<str>>,
117    /// The value, by family.
118    pub value: PropertyValue,
119}
120
121/// Read one property by id.
122///
123/// Returns `None` when `id` is not in the file. `IfcComplexProperty` may
124/// nest, and a malformed file can make one reach itself; such a member, one
125/// nested deeper than the reader follows, and one naming an absent entity are
126/// left out of the resolved [`PropertyValue::Complex`]. This function does
127/// not say which members were cut: use [`property_checked`], which returns
128/// the same value together with the [`PropertyAnomaly`] for each.
129pub fn property(model: &Model, id: EntityId) -> Option<Property> {
130    property_checked(model, id).map(|(property, _)| property)
131}
132
133/// Read one property by id, reporting every nested member left unread.
134///
135/// Nested complex properties are followed along a tracked path with a depth
136/// bound and a member budget. A member that re-enters the path
137/// ([`PropertyAnomaly::ComplexCycle`]), a complex property nested too deep
138/// ([`PropertyAnomaly::ComplexTooDeep`]), members past the budget
139/// ([`PropertyAnomaly::ComplexBudgetExceeded`]) and member ids absent from
140/// the file ([`PropertyAnomaly::MissingMember`]) are reported, never
141/// silently dropped.
142pub fn property_checked(model: &Model, id: EntityId) -> Option<(Property, Vec<PropertyAnomaly>)> {
143    let mut anomalies = Vec::new();
144    let property = read_property(model, id, &mut Nesting::new(&mut anomalies))?;
145    Some((property, anomalies))
146}
147
148pub(crate) fn read_property(
149    model: &Model,
150    id: EntityId,
151    nesting: &mut Nesting<'_>,
152) -> Option<Property> {
153    let entity = model.get(id)?;
154    let ty = entity.type_name.to_ascii_uppercase();
155    let name = entity.attributes.get(NAME).and_then(text);
156    let description = entity.attributes.get(DESCRIPTION).and_then(text);
157
158    let value = match ty.as_str() {
159        "IFCPROPERTYSINGLEVALUE" => PropertyValue::Single {
160            value: entity.attributes.get(2).and_then(MeasureValue::read),
161            unit: entity.attributes.get(3).and_then(entity_ref),
162        },
163        "IFCPROPERTYENUMERATEDVALUE" => PropertyValue::Enumerated {
164            values: entity
165                .attributes
166                .get(2)
167                .and_then(MeasureValue::read_list)
168                .unwrap_or_default(),
169            enumeration: entity.attributes.get(3).and_then(entity_ref),
170        },
171        // Upper is slot 2, lower is slot 3. See the module note.
172        "IFCPROPERTYBOUNDEDVALUE" => PropertyValue::Bounded {
173            upper: entity.attributes.get(2).and_then(MeasureValue::read),
174            lower: entity.attributes.get(3).and_then(MeasureValue::read),
175            unit: entity.attributes.get(4).and_then(entity_ref),
176            set_point: entity.attributes.get(5).and_then(MeasureValue::read),
177        },
178        "IFCPROPERTYLISTVALUE" => PropertyValue::List {
179            values: entity
180                .attributes
181                .get(2)
182                .and_then(MeasureValue::read_list)
183                .unwrap_or_default(),
184            unit: entity.attributes.get(3).and_then(entity_ref),
185        },
186        "IFCPROPERTYTABLEVALUE" => PropertyValue::Table {
187            defining: entity
188                .attributes
189                .get(2)
190                .and_then(MeasureValue::read_list)
191                .unwrap_or_default(),
192            defined: entity
193                .attributes
194                .get(3)
195                .and_then(MeasureValue::read_list)
196                .unwrap_or_default(),
197            interpolation: entity.attributes.get(7).and_then(enum_text),
198        },
199        "IFCPROPERTYREFERENCEVALUE" => PropertyValue::Reference {
200            usage: entity.attributes.get(2).and_then(text),
201            reference: entity.attributes.get(3).and_then(entity_ref),
202        },
203        "IFCCOMPLEXPROPERTY" => PropertyValue::Complex {
204            usage: entity.attributes.get(2).and_then(text),
205            properties: complex_members(model, id, entity.attributes.get(3), nesting),
206        },
207        _ => PropertyValue::Unsupported {
208            type_name: ty.as_str().into(),
209        },
210    };
211
212    Some(Property {
213        id,
214        name,
215        description,
216        value,
217    })
218}
219
220/// Read an enumeration constant such as `.LINEAR.`
221///
222/// Distinct from `text`: an IFC enum is written unquoted, so a reader
223/// expecting `Value::Text` finds nothing and silently reports "unstated".
224fn enum_text(value: &ifc_model::Value) -> Option<Arc<str>> {
225    match value.unwrap_typed() {
226        ifc_model::Value::Enum(t) | ifc_model::Value::Text(t) => Some(t.clone()),
227        _ => None,
228    }
229}
230
231fn text(value: &ifc_model::Value) -> Option<Arc<str>> {
232    match value.unwrap_typed() {
233        ifc_model::Value::Text(t) => Some(t.clone()),
234        _ => None,
235    }
236}
237
238fn entity_ref(value: &ifc_model::Value) -> Option<EntityId> {
239    match value.unwrap_typed() {
240        ifc_model::Value::Ref(id) => Some(*id),
241        _ => None,
242    }
243}