Skip to main content

ifc_systems/port/
definition.rs

1//! `IfcPort` and `IfcDistributionPort` definitions.
2//!
3//! # Two attachment mechanisms, not one
4//!
5//! A port is attached to its element in one of two ways, and real files use
6//! both:
7//!
8//! ```text
9//! IfcRelNests                   4 = RelatingObject (element)  5 = RelatedObjects (ports)
10//! IfcRelConnectsPortToElement   4 = RelatingPort              5 = RelatedElement
11//! ```
12//!
13//! `IfcRelNests` is the IFC4 mechanism and `IfcRelConnectsPortToElement` is
14//! the IFC2x3 one, retained in IFC4 for compatibility. They are REVERSED with
15//! respect to each other: nesting names the element first, the legacy
16//! relationship names the port first. Reading one layout for the other
17//! silently swaps ports and elements.
18//!
19//! Supporting only the IFC4 form would drop every port in a file exported by
20//! an older tool, which is a large share of what exists.
21
22use crate::error::SystemAnomaly;
23use crate::flow::FlowDirection;
24use crate::release::{self, Release};
25use ifc_model::{EntityId, Model, Value};
26
27pub(crate) mod slot {
28    /// `IfcRelNests.RelatingObject` -- the nesting element.
29    pub const NESTS_PARENT: usize = 4;
30    /// `IfcRelNests.RelatedObjects` -- the nested ports.
31    pub const NESTS_CHILDREN: usize = 5;
32    /// `IfcRelConnectsPortToElement.RelatingPort` -- the PORT, not the element.
33    pub const PORT_TO_ELEMENT_PORT: usize = 4;
34    /// `IfcRelConnectsPortToElement.RelatedElement`.
35    pub const PORT_TO_ELEMENT_ELEMENT: usize = 5;
36    /// `IfcDistributionPort.FlowDirection`.
37    pub const FLOW_DIRECTION: usize = 7;
38}
39
40/// How a port was attached to its element.
41///
42/// Recorded rather than normalised away: a file mixing both mechanisms is
43/// worth knowing about, and a consumer migrating away from the legacy form
44/// needs to see which files still use it.
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub enum Attachment {
47    /// `IfcRelNests` -- the IFC4 mechanism.
48    Nests,
49    /// `IfcRelConnectsPortToElement` -- the IFC2x3 mechanism.
50    ConnectsPortToElement,
51}
52
53/// A port as the file states it.
54#[derive(Debug, Clone, PartialEq, Eq)]
55pub struct Port {
56    /// The `IfcPort` subtype entity.
57    pub id: EntityId,
58    /// Declared type, upper-cased.
59    pub type_name: String,
60    /// `Name`, when present.
61    pub name: Option<String>,
62    /// Flow direction, `NotDefined` when the file omits it.
63    pub flow: FlowDirection,
64    /// The element this port belongs to, when the file attaches it.
65    ///
66    /// `IfcPort.ContainedIn` is `SET [0:1]`, so a port has at most one owning
67    /// element. A free-floating port is legal and is reported as `None`.
68    pub element: Option<EntityId>,
69    /// Which relationship attached it.
70    pub attachment: Option<Attachment>,
71}
72
73/// Ids of every entity whose declared type is an `IfcPort` subtype.
74///
75/// `IfcPort` is ABSTRACT, so no entity is ever literally an `IFCPORT`: every
76/// port in a file is an `IfcDistributionPort`. Selecting by ancestry rather
77/// than by the exact-type index is what makes that work, and it keeps working
78/// if a later schema adds another subtype.
79fn port_ids(model: &Model, release: Release) -> Vec<EntityId> {
80    let mut out = Vec::new();
81    for (type_name, _) in model.type_histogram() {
82        if release.is_a(type_name, "IFCPORT") {
83            out.extend_from_slice(model.ids_of_type(type_name));
84        }
85    }
86    out.sort_unstable();
87    out
88}
89
90fn text(model: &Model, id: EntityId, slot: usize) -> Option<String> {
91    match model.get(id)?.attributes.get(slot)? {
92        Value::Text(t) => Some(t.to_string()),
93        _ => None,
94    }
95}
96
97/// Every port in the file, with its owning element resolved.
98///
99/// Both attachment mechanisms are read. `IfcRelNests` is applied first
100/// because it is the IFC4 form, so when an exporter writes both and they
101/// disagree the modern one wins and the conflict is reported.
102///
103/// Port ancestry is read against the release the model's `FILE_SCHEMA`
104/// declares (IFC2X3 or IFC4); see [`crate::systems`] for how an undeclared
105/// or unsupported header is handled.
106pub fn ports(model: &Model) -> (Vec<Port>, Vec<SystemAnomaly>) {
107    let release = release::resolve_or_ifc4(model);
108    let mut anomalies = Vec::new();
109    let mut owner: std::collections::BTreeMap<EntityId, (EntityId, Attachment)> =
110        std::collections::BTreeMap::new();
111
112    let mut attach =
113        |port: EntityId, element: EntityId, how: Attachment, anomalies: &mut Vec<SystemAnomaly>| {
114            match owner.get(&port) {
115                Some((kept, _)) if *kept != element => {
116                    anomalies.push(SystemAnomaly::PortAttachedTwice {
117                        port,
118                        kept: *kept,
119                        rejected: element,
120                    });
121                }
122                Some(_) => {}
123                None => {
124                    owner.insert(port, (element, how));
125                }
126            }
127        };
128
129    // IFC4: the element nests its ports.
130    for &relation in model.ids_of_type("IFCRELNESTS") {
131        let Some(entity) = model.get(relation) else {
132            continue;
133        };
134        let Some(Value::Ref(parent)) = entity.attributes.get(slot::NESTS_PARENT) else {
135            continue;
136        };
137        for child in refs(entity.attributes.get(slot::NESTS_CHILDREN)) {
138            let Some(child_entity) = model.get(child) else {
139                anomalies.push(SystemAnomaly::Dangling {
140                    relation,
141                    missing: child,
142                });
143                continue;
144            };
145            // IfcRelNests nests anything, not just ports: a distribution
146            // element nests its ports, but an element type nests its
147            // components too. Only ports are this module's concern.
148            if !release.is_a(&child_entity.type_name.to_ascii_uppercase(), "IFCPORT") {
149                continue;
150            }
151            attach(child, *parent, Attachment::Nests, &mut anomalies);
152        }
153    }
154
155    // IFC2x3 compatibility form, still emitted by real exporters.
156    for &relation in model.ids_of_type("IFCRELCONNECTSPORTTOELEMENT") {
157        let Some(entity) = model.get(relation) else {
158            continue;
159        };
160        let port = match entity.attributes.get(slot::PORT_TO_ELEMENT_PORT) {
161            Some(Value::Ref(id)) => *id,
162            _ => continue,
163        };
164        let element = match entity.attributes.get(slot::PORT_TO_ELEMENT_ELEMENT) {
165            Some(Value::Ref(id)) => *id,
166            _ => continue,
167        };
168        if model.get(port).is_none() {
169            anomalies.push(SystemAnomaly::Dangling {
170                relation,
171                missing: port,
172            });
173            continue;
174        }
175        if model.get(element).is_none() {
176            anomalies.push(SystemAnomaly::Dangling {
177                relation,
178                missing: element,
179            });
180            continue;
181        }
182        attach(
183            port,
184            element,
185            Attachment::ConnectsPortToElement,
186            &mut anomalies,
187        );
188    }
189
190    let mut ports = Vec::new();
191    for id in port_ids(model, release) {
192        let Some(entity) = model.get(id) else {
193            continue;
194        };
195        let attached = owner.get(&id);
196        ports.push(Port {
197            id,
198            type_name: entity.type_name.to_ascii_uppercase(),
199            name: text(model, id, 2),
200            flow: FlowDirection::parse(entity.attributes.get(slot::FLOW_DIRECTION)),
201            element: attached.map(|(e, _)| *e),
202            attachment: attached.map(|(_, how)| *how),
203        });
204    }
205    (ports, anomalies)
206}
207
208fn refs(value: Option<&Value>) -> Vec<EntityId> {
209    match value {
210        Some(Value::List(items)) => items
211            .iter()
212            .filter_map(|v| match v {
213                Value::Ref(id) => Some(*id),
214                _ => None,
215            })
216            .collect(),
217        Some(Value::Ref(id)) => vec![*id],
218        _ => Vec::new(),
219    }
220}