Skip to main content

ifc_properties/query/
assignment.rs

1//! Resolving which properties apply to an object.
2//!
3//! # Precedence is a real decision, and it is made explicit
4//!
5//! An occurrence inherits property sets from its type
6//! (`IfcRelDefinesByType` -> `IfcTypeObject.HasPropertySets`) and may carry
7//! its own (`IfcRelDefinesByProperties`). When both state a set of the same
8//! NAME, the occurrence wins: that is the IFC4 rule, and it is what every
9//! authoring tool means by overriding a type default.
10//!
11//! Nothing about that is inferable from the entity graph -- both routes are
12//! just relationships -- so this module states it once, applies it uniformly,
13//! and reports which source each surviving set came from. A caller that needs
14//! the type's original value can still see it: overridden sets are not
15//! discarded, they are recorded as shadowed.
16//!
17//! # Slots
18//!
19//! ```text
20//! IfcRelDefinesByType  4 = RelatedObjects  5 = RelatingType
21//! ```
22//!
23//! Note this is the OPPOSITE arrangement to `IfcRelDefinesByProperties`,
24//! where the definition is slot 5 and the objects slot 4 -- the same two slot
25//! numbers carrying different roles.
26
27use std::collections::BTreeMap;
28
29use ifc_model::{EntityId, Model, Value};
30
31use crate::error::PropertyAnomaly;
32use crate::pset::{property_sets_by_object, AttachedSets, Attachment, PropertySet};
33
34const REL_TYPE_RELATED_OBJECTS: usize = 4;
35const REL_TYPE_RELATING_TYPE: usize = 5;
36
37/// Where a resolved property set came from.
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub enum Source {
40    /// Stated directly on the occurrence.
41    Occurrence,
42    /// Inherited from the object's type.
43    Type(EntityId),
44}
45
46/// A property set that applies to an object, with its provenance.
47#[derive(Debug, Clone, PartialEq)]
48pub struct ResolvedSet {
49    /// Where it came from.
50    pub source: Source,
51    /// The set itself.
52    pub set: PropertySet,
53    /// A same-named set this one overrode, if any.
54    ///
55    /// Populated when an occurrence set shadows a type set. Kept rather than
56    /// dropped so a caller can explain WHY a value differs from the type
57    /// default, which is a question every model checker eventually asks.
58    pub shadowed: Option<Box<ResolvedSet>>,
59}
60
61/// Resolved property sets per object, after precedence is applied.
62pub type ResolvedProperties = BTreeMap<EntityId, Vec<ResolvedSet>>;
63
64/// Every property set applying to every object, precedence applied.
65///
66/// Sets are ordered by name so output is deterministic regardless of file
67/// ordering or hash iteration.
68pub fn resolved_properties(model: &Model) -> (ResolvedProperties, Vec<PropertyAnomaly>) {
69    let (direct, mut anomalies) = property_sets_by_object(model);
70    let types = type_assignments(model, &mut anomalies);
71
72    // Every object that has properties of its own or a type that does.
73    let mut objects: Vec<EntityId> = direct.keys().copied().collect();
74    objects.extend(types.keys().copied());
75    objects.sort_unstable();
76    objects.dedup();
77
78    duplicate_set_names(&direct, &mut anomalies);
79
80    let mut out: BTreeMap<EntityId, Vec<ResolvedSet>> = BTreeMap::new();
81    for object in objects {
82        // Type sets first, so occurrence sets can overwrite by name. Within
83        // one source, sets arrive in id order and the first of a name wins.
84        let mut by_name: BTreeMap<String, ResolvedSet> = BTreeMap::new();
85        if let Some(&type_id) = types.get(&object) {
86            for (attachment, set) in direct.get(&type_id).into_iter().flatten() {
87                // A type's own sets are what an occurrence inherits. Sets
88                // attached to the type by the forbidden relationship are
89                // included: the file states them, and the anomaly already
90                // records that it should not have.
91                let _ = attachment;
92                if by_name.contains_key(&key(set)) {
93                    continue;
94                }
95                by_name.insert(
96                    key(set),
97                    ResolvedSet {
98                        source: Source::Type(type_id),
99                        set: set.clone(),
100                        shadowed: None,
101                    },
102                );
103            }
104        }
105        for (attachment, set) in direct.get(&object).into_iter().flatten() {
106            if *attachment != Attachment::Occurrence {
107                continue;
108            }
109            // A same-named occurrence set already resolved is a duplicate,
110            // not something to shadow: shadowing is occurrence over type.
111            if by_name
112                .get(&key(set))
113                .is_some_and(|r| r.source == Source::Occurrence)
114            {
115                continue;
116            }
117            let shadowed = by_name.remove(&key(set)).map(Box::new);
118            by_name.insert(
119                key(set),
120                ResolvedSet {
121                    source: Source::Occurrence,
122                    set: set.clone(),
123                    shadowed,
124                },
125            );
126        }
127        if !by_name.is_empty() {
128            out.insert(object, by_name.into_values().collect());
129        }
130    }
131    (out, anomalies)
132}
133
134/// Report every owner holding two same-named sets through one route.
135///
136/// Checked per owner and per attachment route, so a type's duplicates are
137/// reported once even when many occurrences inherit them, and even when none
138/// does. An occurrence set overriding a same-named type set is precedence,
139/// not a duplicate, and is not reported. Sets arrive in id order, so the
140/// kept set is the one resolution keeps.
141fn duplicate_set_names(direct: &AttachedSets, anomalies: &mut Vec<PropertyAnomaly>) {
142    for (&owner, sets) in direct {
143        let mut kept: BTreeMap<(bool, String), EntityId> = BTreeMap::new();
144        for (attachment, set) in sets {
145            let slot = (*attachment == Attachment::Type, key(set));
146            match kept.get(&slot) {
147                Some(&first) => anomalies.push(PropertyAnomaly::DuplicateSetName {
148                    owner,
149                    kept: first,
150                    rejected: set.id,
151                }),
152                None => {
153                    kept.insert(slot, set.id);
154                }
155            }
156        }
157    }
158}
159
160/// Property sets applying to one object.
161pub fn properties_of(model: &Model, object: EntityId) -> Vec<ResolvedSet> {
162    resolved_properties(model)
163        .0
164        .remove(&object)
165        .unwrap_or_default()
166}
167
168/// Find a single property by set name and property name.
169///
170/// Returns the winning value after precedence, so a caller asking for
171/// `Pset_WallCommon.IsExternal` gets the occurrence's answer when it has one
172/// and the type's otherwise -- which is what the question means.
173pub fn property_value<'a>(
174    resolved: &'a [ResolvedSet],
175    set_name: &str,
176    property_name: &str,
177) -> Option<&'a crate::pset::Property> {
178    resolved
179        .iter()
180        .find(|r| r.set.name.as_deref() == Some(set_name))
181        .and_then(|r| r.set.property(property_name))
182}
183
184/// Map each object to its type, via `IfcRelDefinesByType`.
185fn type_assignments(
186    model: &Model,
187    anomalies: &mut Vec<PropertyAnomaly>,
188) -> BTreeMap<EntityId, EntityId> {
189    let mut out: BTreeMap<EntityId, EntityId> = BTreeMap::new();
190    // First relationship by id wins, whatever order the file lists them in.
191    let mut relations = model.ids_of_type("IFCRELDEFINESBYTYPE").to_vec();
192    relations.sort_unstable();
193    for id in relations {
194        let Some(rel) = model.get(id) else { continue };
195        let Some(type_id) = rel.attributes.get(REL_TYPE_RELATING_TYPE).and_then(one_ref) else {
196            continue;
197        };
198        if model.get(type_id).is_none() {
199            anomalies.push(PropertyAnomaly::MissingDefinition {
200                relationship: id,
201                definition: type_id,
202            });
203            continue;
204        }
205        for object in rel
206            .attributes
207            .get(REL_TYPE_RELATED_OBJECTS)
208            .and_then(refs)
209            .unwrap_or_default()
210        {
211            if model.get(object).is_none() {
212                anomalies.push(PropertyAnomaly::MissingObject {
213                    relationship: id,
214                    object,
215                });
216                continue;
217            }
218            match out.get(&object) {
219                None => {
220                    out.insert(object, type_id);
221                }
222                Some(&kept) if kept != type_id => anomalies.push(PropertyAnomaly::TypedTwice {
223                    object,
224                    kept,
225                    rejected: type_id,
226                    relation: id,
227                }),
228                Some(_) => {}
229            }
230        }
231    }
232    out
233}
234
235/// Key a set by name, falling back to its id when the file omits the name.
236///
237/// `ExistsName` requires one, so the fallback only fires on malformed files.
238/// Keying those by id keeps them distinct instead of collapsing every
239/// unnamed set into one bucket.
240fn key(set: &PropertySet) -> String {
241    match &set.name {
242        Some(name) => name.to_string(),
243        None => format!("#{}", set.id.0),
244    }
245}
246
247fn one_ref(value: &Value) -> Option<EntityId> {
248    match value.unwrap_typed() {
249        Value::Ref(id) => Some(*id),
250        _ => None,
251    }
252}
253
254fn refs(value: &Value) -> Option<Vec<EntityId>> {
255    match value {
256        Value::List(items) => Some(items.iter().filter_map(one_ref).collect()),
257        _ => None,
258    }
259}