ifc_systems/system/group.rs
1//! `IfcSystem` and group semantics.
2//!
3//! # The slot trap
4//!
5//! Membership and service use different attribute layouts, and neither is
6//! guessable from the other:
7//!
8//! ```text
9//! IfcRelAssignsToGroup 4 = RelatedObjects 6 = RelatingGroup
10//! IfcRelServicesBuildings 4 = RelatingSystem 5 = RelatedBuildings
11//! ```
12//!
13//! `IfcRelAssignsToGroup` also carries `RelatedObjectsType` at slot 5, so the
14//! group is at 6 and NOT at 5 where every other `IfcRel*` in this crate puts
15//! its relating end. Reading slot 5 yields an enumeration, not a reference,
16//! and a membership silently vanishes.
17
18use ifc_model::{EntityId, Model, Value};
19
20use crate::error::SystemAnomaly;
21use crate::release::{self, Release};
22
23/// Attribute slots, named so a misread is a compile error rather than a
24/// silently empty result.
25pub(crate) mod slot {
26 /// `IfcRelAssignsToGroup.RelatedObjects`.
27 pub const ASSIGNS_RELATED: usize = 4;
28 /// `IfcRelAssignsToGroup.RelatingGroup` -- 6, not 5.
29 pub const ASSIGNS_GROUP: usize = 6;
30}
31
32/// A system as the file states it, with its members resolved.
33#[derive(Debug, Clone, PartialEq, Eq)]
34pub struct System {
35 /// The `IfcSystem` (or subtype) entity.
36 pub id: EntityId,
37 /// Declared type, e.g. `IFCDISTRIBUTIONSYSTEM`.
38 pub type_name: String,
39 /// `Name`, when present.
40 pub name: Option<String>,
41 /// Members, in file order.
42 ///
43 /// Order is preserved because IFC states no ordering and re-sorting would
44 /// invent one; a caller comparing two exports needs the file's own order.
45 pub members: Vec<EntityId>,
46}
47
48fn text(model: &Model, id: EntityId, slot: usize) -> Option<String> {
49 match model.get(id)?.attributes.get(slot)? {
50 Value::Text(t) => Some(t.to_string()),
51 _ => None,
52 }
53}
54
55fn refs(value: Option<&Value>) -> Vec<EntityId> {
56 match value {
57 Some(Value::List(items)) => items
58 .iter()
59 .filter_map(|v| match v {
60 Value::Ref(id) => Some(*id),
61 _ => None,
62 })
63 .collect(),
64 Some(Value::Ref(id)) => vec![*id],
65 _ => Vec::new(),
66 }
67}
68
69/// Every system in the file, with members resolved and anomalies reported.
70///
71/// Systems are found by type, not by walking memberships: a file may declare
72/// a system that nothing is assigned to yet, and dropping it would understate
73/// the model. Subtypes are included, so `IfcDistributionSystem` and
74/// `IfcBuildingSystem` are both found in IFC4, and `IfcElectricalCircuit` is
75/// found in IFC2X3 -- but `IfcZone` is NOT, because it subtypes `IfcGroup`
76/// rather than `IfcSystem` in IFC2X3 (issue #52).
77///
78/// Reads against the release the model's `FILE_SCHEMA` header declares
79/// (IFC2X3 or IFC4). A model that declares neither -- including every
80/// hand-built model in this crate's own tests, which set no header at all
81/// -- reads as IFC4, matching this function's pre-#52 behaviour. A model
82/// that explicitly declares an unsupported release (e.g. IFC4X3) also
83/// falls back to IFC4 here, because this function's `(Vec<_>, Vec<_>)`
84/// signature has no slot for a hard refusal; call [`crate::schema_of`]
85/// first if that distinction matters to the caller.
86pub fn systems(model: &Model) -> (Vec<System>, Vec<SystemAnomaly>) {
87 let release = release::resolve_or_ifc4(model);
88 let mut anomalies = Vec::new();
89
90 // Membership is stated by the relationship, not the system, so index the
91 // relationships once instead of rescanning per system.
92 let mut members: std::collections::BTreeMap<EntityId, Vec<EntityId>> =
93 std::collections::BTreeMap::new();
94
95 for &relation in model.ids_of_type("IFCRELASSIGNSTOGROUP") {
96 let Some(entity) = model.get(relation) else {
97 continue;
98 };
99 let group = match entity.attributes.get(slot::ASSIGNS_GROUP) {
100 Some(Value::Ref(id)) => *id,
101 _ => continue,
102 };
103 let Some(group_entity) = model.get(group) else {
104 anomalies.push(SystemAnomaly::Dangling {
105 relation,
106 missing: group,
107 });
108 continue;
109 };
110 // The relationship is shared with every group kind; only systems are
111 // this crate's concern, and the rest are reported rather than dropped.
112 if !release.is_a(&group_entity.type_name.to_ascii_uppercase(), "IFCSYSTEM") {
113 anomalies.push(SystemAnomaly::NotASystem {
114 relation,
115 group,
116 // Upper-cased: a STEP file writes IFCINVENTORY while an
117 // in-memory model may carry IfcInventory, and a caller
118 // matching on this string must not have to know which.
119 type_name: group_entity.type_name.to_ascii_uppercase(),
120 });
121 continue;
122 }
123 for member in refs(entity.attributes.get(slot::ASSIGNS_RELATED)) {
124 if model.get(member).is_none() {
125 anomalies.push(SystemAnomaly::Dangling {
126 relation,
127 missing: member,
128 });
129 continue;
130 }
131 members.entry(group).or_default().push(member);
132 }
133 }
134
135 // `ids_of_type` is an EXACT index: asking it for IFCSYSTEM misses every
136 // IfcDistributionSystem in the file, which is the common case. Systems are
137 // therefore selected by schema ancestry over the file's own type keys.
138 let mut systems = Vec::new();
139 for id in system_ids(model, release) {
140 let Some(entity) = model.get(id) else {
141 continue;
142 };
143 systems.push(System {
144 id,
145 // Upper-cased for the same reason as `NotASystem::type_name`.
146 type_name: entity.type_name.to_ascii_uppercase(),
147 name: text(model, id, 2),
148 members: members.remove(&id).unwrap_or_default(),
149 });
150 }
151 (systems, anomalies)
152}
153
154/// Ids of every entity whose declared type is `IfcSystem` or a subtype,
155/// under `release`.
156///
157/// Deliberately not `Model::ids_of_type`, which indexes the exact type name
158/// only: a file whose systems are all `IfcDistributionSystem` would return
159/// nothing and the crate would report a model with no systems at all.
160fn system_ids(model: &Model, release: Release) -> Vec<EntityId> {
161 let mut out = Vec::new();
162 for (type_name, _) in model.type_histogram() {
163 if release.is_a(type_name, "IFCSYSTEM") {
164 out.extend_from_slice(model.ids_of_type(type_name));
165 }
166 }
167 out.sort_unstable();
168 out
169}