Skip to main content

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::{SchemaResolutionError, 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    /// `IfcRoot.Name`.
31    pub const NAME: usize = 2;
32}
33
34/// A system as the file states it, with its members resolved.
35#[derive(Debug, Clone, PartialEq, Eq)]
36#[non_exhaustive]
37pub struct System {
38    /// The `IfcSystem` (or subtype) entity.
39    pub id: EntityId,
40    /// Declared type, e.g. `IFCDISTRIBUTIONSYSTEM`.
41    pub type_name: String,
42    /// `Name`, when present.
43    pub name: Option<String>,
44    /// Members, in file order.
45    ///
46    /// Order is preserved because IFC states no ordering and re-sorting would
47    /// invent one; a caller comparing two exports needs the file's own order.
48    pub members: Vec<EntityId>,
49}
50
51fn text(model: &Model, id: EntityId, slot: usize) -> Option<String> {
52    match model.get(id)?.attributes.get(slot)? {
53        Value::Text(t) => Some(t.to_string()),
54        _ => None,
55    }
56}
57
58fn refs(value: Option<&Value>) -> Vec<EntityId> {
59    match value {
60        Some(Value::List(items)) => items
61            .iter()
62            .filter_map(|v| match v {
63                Value::Ref(id) => Some(*id),
64                _ => None,
65            })
66            .collect(),
67        Some(Value::Ref(id)) => vec![*id],
68        _ => Vec::new(),
69    }
70}
71
72/// Every system in the file, with members resolved and anomalies reported.
73///
74/// Systems are found by type, not by walking memberships: a file may declare
75/// a system that nothing is assigned to yet, and dropping it would understate
76/// the model. Subtypes are included, so `IfcDistributionSystem` and
77/// `IfcBuildingSystem` are both found in IFC4, and `IfcElectricalCircuit` is
78/// found in IFC2X3 -- but `IfcZone` is NOT, because it subtypes `IfcGroup`
79/// rather than `IfcSystem` in IFC2X3 (issue #52).
80///
81/// Reads against the release the model's `FILE_SCHEMA` header declares:
82/// IFC2X3, IFC4 or IFC4X3.
83///
84/// # Errors
85///
86/// [`SchemaResolutionError`] when the model's `FILE_SCHEMA` binds no release
87/// this crate is verified for (see [`crate::schema_of`]). Nothing is read
88/// against a release the file did not declare.
89pub fn systems(model: &Model) -> Result<(Vec<System>, Vec<SystemAnomaly>), SchemaResolutionError> {
90    let release = release::resolve(model)?;
91    let mut anomalies = Vec::new();
92
93    // Membership is stated by the relationship, not the system, so index the
94    // relationships once instead of rescanning per system.
95    let mut members: std::collections::BTreeMap<EntityId, Vec<EntityId>> =
96        std::collections::BTreeMap::new();
97
98    for &relation in model.ids_of_type("IFCRELASSIGNSTOGROUP") {
99        let Some(entity) = model.get(relation) else {
100            continue;
101        };
102        let group = match entity.attributes.get(slot::ASSIGNS_GROUP) {
103            Some(Value::Ref(id)) => *id,
104            _ => continue,
105        };
106        let Some(group_entity) = model.get(group) else {
107            anomalies.push(SystemAnomaly::Dangling {
108                relation,
109                missing: group,
110            });
111            continue;
112        };
113        // The relationship is shared with every group kind; only systems are
114        // this crate's concern, and the rest are reported rather than dropped.
115        if !release.is_a(&group_entity.type_name.to_ascii_uppercase(), "IFCSYSTEM") {
116            anomalies.push(SystemAnomaly::NotASystem {
117                relation,
118                group,
119                // Upper-cased: a STEP file writes IFCINVENTORY while an
120                // in-memory model may carry IfcInventory, and a caller
121                // matching on this string must not have to know which.
122                type_name: group_entity.type_name.to_ascii_uppercase(),
123            });
124            continue;
125        }
126        for member in refs(entity.attributes.get(slot::ASSIGNS_RELATED)) {
127            if model.get(member).is_none() {
128                anomalies.push(SystemAnomaly::Dangling {
129                    relation,
130                    missing: member,
131                });
132                continue;
133            }
134            members.entry(group).or_default().push(member);
135        }
136    }
137
138    // `ids_of_type` is an EXACT index: asking it for IFCSYSTEM misses every
139    // IfcDistributionSystem in the file, which is the common case. Systems are
140    // therefore selected by schema ancestry over the file's own type keys.
141    let mut systems = Vec::new();
142    for id in system_ids(model, release) {
143        let Some(entity) = model.get(id) else {
144            continue;
145        };
146        systems.push(System {
147            id,
148            // Upper-cased for the same reason as `NotASystem::type_name`.
149            type_name: entity.type_name.to_ascii_uppercase(),
150            name: text(model, id, slot::NAME),
151            members: members.remove(&id).unwrap_or_default(),
152        });
153    }
154    Ok((systems, anomalies))
155}
156
157/// Ids of every entity whose declared type is `IfcSystem` or a subtype,
158/// under `release`.
159///
160/// Deliberately not `Model::ids_of_type`, which indexes the exact type name
161/// only: a file whose systems are all `IfcDistributionSystem` would return
162/// nothing and the crate would report a model with no systems at all.
163fn system_ids(model: &Model, release: Release) -> Vec<EntityId> {
164    let mut out = Vec::new();
165    for (type_name, _) in model.type_histogram() {
166        if release.is_a(type_name, "IFCSYSTEM") {
167            out.extend_from_slice(model.ids_of_type(type_name));
168        }
169    }
170    out.sort_unstable();
171    out
172}