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}