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}