Skip to main content

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}