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}