ifc_properties/exact/predefined.rs
1//! Predefined property sets in exact resolution (#149, #66).
2//!
3//! A predefined set (`IfcDoorLiningProperties`, `IfcDoorPanelProperties`,
4//! their window counterparts, and every other `IfcPropertySetDefinition`
5//! that is neither an `IfcPropertySet` nor a quantity set) keeps its values
6//! in attributes of its own entity rather than in named properties. Its
7//! members are therefore the attributes its entity declares below
8//! `IfcPropertySetDefinition`, in the bound release's order and spelling:
9//!
10//! ```text
11//! IfcDoorLiningProperties 4 LiningDepth 5 LiningThickness ...
12//! IfcDoorPanelProperties 4 PanelDepth 5 PanelOperation 6 PanelWidth
13//! 7 PanelPosition 8 ShapeAspectStyle
14//! ```
15//!
16//! Every attribute is read from the release's table, never by a hard-wired
17//! slot, so IFC2X3's `LiningThickness : IfcPositiveLengthMeasure` and IFC4's
18//! `IfcNonNegativeLengthMeasure`, or IFC4's added `LiningToPanelOffsetX`,
19//! come out as each release declares them. A value is typed by its declared
20//! type:
21//!
22//! - a defined type or EXPRESS simple type gives a scalar with that type as
23//! `value_type` (`IFCPOSITIVELENGTHMEASURE`, `IFCNORMALISEDRATIOMEASURE`);
24//! no unit is stated, so the project unit applies (`exact_unit`);
25//! - an enumeration gives [`ExactValue::Enum`] checked against the
26//! release's members (`FIXEDPANEL` is an IFC4 addition);
27//! - a select gives the typed member it holds, as a single value does;
28//! - an entity gives [`ExactValue::Entity`], the target checked but not
29//! followed;
30//! - `$` on an `OPTIONAL` attribute is [`ExactValue::Null`] with the declared
31//! type, an exact absence of the value; `$` on a required one is refused.
32//!
33//! An aggregate attribute (`IfcReinforcementDefinitionProperties.
34//! ReinforcementSectionDefinitions`) is not read and refuses when selected,
35//! as the whole set did before (#66).
36
37use std::{collections::BTreeSet, sync::Arc};
38
39use ifc_model::{Entity, EntityId, Model, Value};
40use ifc_schema::TypeKind;
41
42use super::assignment::assigned_sets;
43use super::composite::{entity_target, enum_accepts};
44use super::refs::text_at;
45use super::release::{validate_model, Release};
46use super::set::{load_named, Member, SetKind};
47use super::value::{
48 exact_value, select_member, simple_payload_matches, typed_payload_matches, ResolvedValue,
49};
50use super::{
51 ExactEntityRef, ExactLogical, ExactProperty, ExactPropertyEntry, ExactPropertyError,
52 ExactSource, ExactValue,
53};
54
55/// A predefined property set assigned to an object, with every attribute
56/// its entity declares itself resolved exactly.
57///
58/// Returned by [`exact_predefined_sets`].
59#[derive(Debug, Clone, PartialEq)]
60#[non_exhaustive]
61pub struct ExactPredefinedSet {
62 /// Whether the set is assigned to the occurrence or inherited from its
63 /// `IfcTypeObject` (an `IfcDoorType`, or an IFC2X3 `IfcDoorStyle`); a
64 /// queried type object's own sets are `Type` of that object.
65 pub source: ExactSource,
66 /// Entity id of the set.
67 pub set_id: EntityId,
68 /// The set's entity in the release's spelling, e.g.
69 /// `IfcDoorPanelProperties`.
70 pub entity: Arc<str>,
71 /// The set's `Name`, if stated.
72 pub name: Option<Arc<str>>,
73 /// Each attribute the entity declares below `IfcPropertySetDefinition`,
74 /// in schema order, named as the schema names it (`PanelWidth`). Each
75 /// [`ExactProperty`] has the set's id as both `set_id` and
76 /// `property_id`, the attribute's declared type as `value_type`, no
77 /// `unit_id`, and `property_set` equal to `name` or else `entity`.
78 pub attributes: Vec<ExactPropertyEntry>,
79}
80
81impl ExactPredefinedSet {
82 /// The attribute named `name` (schema spelling, e.g. `LiningDepth`), or
83 /// `None` when the entity declares no such attribute in this release.
84 #[must_use]
85 pub fn attribute(&self, name: &str) -> Option<&ExactProperty> {
86 self.attributes
87 .iter()
88 .find(|entry| entry.name.as_ref() == name)
89 .map(|entry| &entry.property)
90 }
91}
92
93/// Every predefined property set of entity type `entity` (or a subtype)
94/// assigned to `object`, each with all its own attributes resolved.
95///
96/// A door can carry several sets of one type, one
97/// `IfcDoorPanelProperties` per leaf, which [`exact_property`] would refuse
98/// as ambiguous; this lists them all. Occurrence sets come first, then
99/// those of the object's `IfcTypeObject`, each in assignment order. No
100/// override is applied between the two sources: every set is reported with
101/// its [`ExactSource`], and which one governs is the caller's decision (for
102/// example the occurrence's sets when it has any, else the type's). An
103/// empty result is a proven absence of such sets. The traversal, model and
104/// assignment validation are those of [`exact_property`], so `object` may
105/// be a type object (an `IfcDoorType`, an IFC2X3 `IfcDoorStyle`), whose own
106/// `HasPropertySets` are listed with [`ExactSource::Type`] of `object`
107/// (#193).
108///
109/// # Door and window geometry
110///
111/// This is the read path for derived door operation geometry (#148): per
112/// leaf, `PanelOperation` and `PanelPosition` come as
113/// [`ExactValue::Enum`], and `PanelWidth` as an `IFCNORMALISEDRATIOMEASURE`
114/// fraction of the door's `OverallWidth`; `LiningDepth` and
115/// `LiningThickness` are lengths in the project length unit, which
116/// [`exact_unit`] resolves from their `value_type`.
117///
118/// ```
119/// use ifc_model::{EntityId, Model};
120/// use ifc_properties::{exact_predefined_sets, exact_unit, ExactValue};
121///
122/// /// (operation, position, width fraction) of each leaf of `door`.
123/// fn leaves(model: &Model, door: EntityId) -> Option<Vec<(String, String, f64)>> {
124/// let panels = exact_predefined_sets(model, door, "IfcDoorPanelProperties").ok()?;
125/// panels
126/// .iter()
127/// .map(|panel| {
128/// let text = |name| match &panel.attribute(name)?.value {
129/// ExactValue::Enum(value) => Some(value.to_string()),
130/// _ => None,
131/// };
132/// let width = match panel.attribute("PanelWidth")?.value {
133/// ExactValue::Real(fraction) => fraction,
134/// _ => return None, // `$`: no width stated
135/// };
136/// Some((text("PanelOperation")?, text("PanelPosition")?, width))
137/// })
138/// .collect()
139/// }
140///
141/// /// The lining depth of `door` in metres.
142/// fn lining_depth(model: &Model, door: EntityId) -> Option<f64> {
143/// let linings = exact_predefined_sets(model, door, "IfcDoorLiningProperties").ok()?;
144/// let depth = linings.first()?.attribute("LiningDepth")?;
145/// let ExactValue::Real(value) = depth.value else { return None };
146/// let unit = exact_unit(model, depth.value_type.as_deref()?, depth.unit_id).ok()?;
147/// Some(value * unit.scale)
148/// }
149/// # let _ = (leaves, lining_depth);
150/// ```
151///
152/// # Errors
153///
154/// [`ExactPropertyError::NotAPredefinedSet`] when the declared release does
155/// not declare `entity` as a predefined property set, and otherwise any
156/// [`ExactPropertyError`] of [`exact_property`], including a set attribute
157/// that cannot be read exactly (an aggregate refuses as
158/// [`ExactPropertyError::UnsupportedDefinition`]).
159///
160/// [`exact_property`]: super::exact_property
161/// [`exact_unit`]: super::exact_unit
162pub fn exact_predefined_sets(
163 model: &Model,
164 object: EntityId,
165 entity: &str,
166) -> Result<Vec<ExactPredefinedSet>, ExactPropertyError> {
167 let release = validate_model(model)?;
168 let schema = release.schema;
169 let predefined = schema.entity(entity).is_some()
170 && schema.is_a(entity, "IFCPROPERTYSETDEFINITION")
171 && !schema.is_a("IFCPROPERTYSET", entity)
172 && !schema.is_a("IFCELEMENTQUANTITY", entity);
173 if !predefined {
174 return Err(ExactPropertyError::NotAPredefinedSet {
175 name: entity.into(),
176 schema: release.version,
177 });
178 }
179 let assigned = assigned_sets(model, release, object)?;
180 let mut sources = vec![(ExactSource::Occurrence, &assigned.occurrence_sets)];
181 if let Some((type_id, sets)) = &assigned.type_sets {
182 sources.push((ExactSource::Type(*type_id), sets));
183 }
184 let mut found = Vec::new();
185 for (source, sets) in sources {
186 for &set_id in sets {
187 let set = load_named(model, release, set_id)?;
188 if set.kind != SetKind::Predefined || !schema.is_a(&set.entity.type_name, entity) {
189 continue;
190 }
191 let attributes = own_attributes(release, set.entity)
192 .map(|(slot, name)| {
193 let member = Member::Attribute(slot);
194 let resolved = set.value(model, release, member)?;
195 Ok(ExactPropertyEntry {
196 name: name.into(),
197 property: set.exact(source, member, resolved),
198 })
199 })
200 .collect::<Result<_, ExactPropertyError>>()?;
201 found.push(ExactPredefinedSet {
202 source,
203 set_id,
204 entity: canonical_name(release, set.entity).into(),
205 name: set.named.then(|| set.name.into()),
206 attributes,
207 });
208 }
209 }
210 Ok(found)
211}
212
213/// The name a set selector sees for a predefined or material set, and
214/// whether it is a stated `Name`: its `Name`, or its entity name when `Name`
215/// is `$` where the release allows it, or when the entity declares no
216/// `Name` (an IFC2X3 typed `IfcMaterialProperties` subtype, #218).
217///
218/// # Errors
219///
220/// [`ExactPropertyError::MalformedName`] for a `Name` that is neither text
221/// nor an allowed `$`.
222pub(super) fn predefined_key(
223 release: Release,
224 set_id: EntityId,
225 set: &Entity,
226) -> Result<(&str, bool), ExactPropertyError> {
227 let Some((slot, attribute)) = release.attribute(&set.type_name, "Name") else {
228 return Ok((canonical_name(release, set), false));
229 };
230 match set.attributes.get(slot) {
231 Some(Value::Null) if attribute.optional => Ok((canonical_name(release, set), false)),
232 name => text_at(set_id, name, "Name").map(|name| (name, true)),
233 }
234}
235
236/// The entity's name as the release spells it (`IfcDoorLiningProperties`).
237fn canonical_name(release: Release, set: &Entity) -> &'static str {
238 release
239 .schema
240 .entity(&set.type_name)
241 .map_or("", |definition| definition.name.as_str())
242}
243
244/// The slot and name of each attribute a predefined set declares below
245/// `IfcPropertySetDefinition` (whose `IfcRoot` attributes every set shares),
246/// or an IFC2X3 typed material property set below `IfcMaterialProperties`
247/// (whose `Material` every such set shares, #218).
248pub(super) fn own_attributes(
249 release: Release,
250 set: &Entity,
251) -> impl Iterator<Item = (usize, &'static str)> {
252 let schema = release.schema;
253 let base = if schema.is_a(set.type_name.as_ref(), "IFCMATERIALPROPERTIES") {
254 "IFCMATERIALPROPERTIES"
255 } else {
256 "IFCPROPERTYSETDEFINITION"
257 };
258 let inherited = schema.attribute_names(base).len();
259 schema
260 .attributes(set.type_name.as_ref())
261 .into_iter()
262 .enumerate()
263 .skip(inherited)
264 .map(|(slot, attribute)| (slot, attribute.name.as_str()))
265}
266
267/// The value of a predefined set's attribute at `slot`, typed by its
268/// declaration in the bound release.
269///
270/// # Errors
271///
272/// [`ExactPropertyError::UnsupportedDefinition`] for an aggregate
273/// attribute, [`ExactPropertyError::MissingValueSlot`] for `$` on a required
274/// one, and [`ExactPropertyError::UnsupportedValue`] (or a reference or
275/// release error) for a value its declared type does not accept.
276pub(super) fn attribute_value(
277 model: &Model,
278 release: Release,
279 set_id: EntityId,
280 set: &Entity,
281 slot: usize,
282) -> Result<ResolvedValue, ExactPropertyError> {
283 let schema = release.schema;
284 let attribute = schema.attributes(&set.type_name)[slot];
285 if attribute.aggregate {
286 return Err(ExactPropertyError::UnsupportedDefinition {
287 entity: set_id,
288 type_name: set.type_name.clone(),
289 });
290 }
291 let declared: Arc<str> = attribute.type_name.to_ascii_uppercase().into();
292 let value = &set.attributes[slot];
293 let scalar = |value, value_type| {
294 Ok(ResolvedValue {
295 value,
296 value_type: Some(value_type),
297 unit_id: None,
298 })
299 };
300 let unsupported = || ExactPropertyError::UnsupportedValue { property: set_id };
301 match value {
302 Value::Null if attribute.optional => return scalar(ExactValue::Null, declared),
303 Value::Null => return Err(ExactPropertyError::MissingValueSlot { property: set_id }),
304 _ => {}
305 }
306 let entity_ref = |target: EntityId| {
307 let entity = entity_target(model, release, set_id, target, attribute)?;
308 Ok(ExactValue::Entity(ExactEntityRef {
309 id: target,
310 type_name: entity.type_name.clone(),
311 }))
312 };
313 if schema.entity(&declared).is_some() {
314 let Value::Ref(target) = value else {
315 return Err(unsupported());
316 };
317 return scalar(entity_ref(*target)?, declared);
318 }
319 match schema
320 .type_def(&declared)
321 .map(|definition| &definition.kind)
322 {
323 Some(TypeKind::Enumeration(_)) => match value {
324 Value::Enum(member) if enum_accepts(release, &declared, member) => {
325 scalar(ExactValue::Enum(member.clone()), declared)
326 }
327 _ => Err(unsupported()),
328 },
329 Some(TypeKind::Select(_)) => match value {
330 Value::Ref(target) => scalar(entity_ref(*target)?, declared),
331 value => {
332 let typed = select_member(release, set_id, &declared, value)?;
333 scalar(typed.value, typed.value_type)
334 }
335 },
336 Some(TypeKind::Defined(_))
337 if typed_payload_matches(schema, &declared, value, &mut BTreeSet::new()) =>
338 {
339 let logical = schema
340 .resolve_defined(&declared)
341 .eq_ignore_ascii_case("LOGICAL");
342 scalar(bare(set_id, value, logical)?, declared)
343 }
344 None if simple_payload_matches(&declared, value) => {
345 scalar(bare(set_id, value, &*declared == "LOGICAL")?, declared)
346 }
347 _ => Err(unsupported()),
348 }
349}
350
351/// A bare attribute payload, keeping a `LOGICAL` three-state.
352fn bare(set_id: EntityId, value: &Value, logical: bool) -> Result<ExactValue, ExactPropertyError> {
353 Ok(match (exact_value(set_id, Some(value))?, logical) {
354 (ExactValue::Bool(true), true) => ExactValue::Logical(ExactLogical::True),
355 (ExactValue::Bool(false), true) => ExactValue::Logical(ExactLogical::False),
356 (other, _) => other,
357 })
358}
359
360#[cfg(test)]
361mod tests {
362 //! No predefined set of IFC2X3, IFC4 or IFC4X3 declares a `LOGICAL`
363 //! attribute, so the three-state reading is pinned on a minimal schema.
364
365 use ifc_model::{Entity, EntityId, Model, Value};
366 use ifc_schema::{Schema, SchemaVersion};
367
368 use super::{attribute_value, Release};
369 use crate::{ExactLogical, ExactValue};
370
371 const EXPRESS: &str = "SCHEMA IFC4;
372TYPE IfcLogical = LOGICAL;
373END_TYPE;
374ENTITY IfcPropertySetDefinition;
375 GlobalId : STRING;
376 OwnerHistory : OPTIONAL STRING;
377 Name : OPTIONAL STRING;
378 Description : OPTIONAL STRING;
379END_ENTITY;
380ENTITY IfcFlagProperties
381 SUBTYPE OF (IfcPropertySetDefinition);
382 Flag : OPTIONAL IfcLogical;
383 Bare : OPTIONAL LOGICAL;
384END_ENTITY;
385END_SCHEMA;";
386
387 #[test]
388 fn a_logical_attribute_keeps_three_states() {
389 let release = Release {
390 version: SchemaVersion::Ifc4,
391 schema: Box::leak(Box::new(Schema::from_express(EXPRESS))),
392 };
393 let model = Model::new();
394 for (value, expected) in [
395 (Value::Bool(true), ExactLogical::True),
396 (Value::Bool(false), ExactLogical::False),
397 (Value::LogicalUnknown, ExactLogical::Unknown),
398 ] {
399 for slot in [4, 5] {
400 let mut attributes = vec![Value::Null; 6];
401 attributes[slot] = value.clone();
402 let set = Entity::new("IFCFLAGPROPERTIES", attributes);
403 let resolved = attribute_value(&model, release, EntityId(1), &set, slot)
404 .expect("a logical resolves");
405 assert_eq!(resolved.value, ExactValue::Logical(expected), "slot {slot}");
406 }
407 }
408 }
409}