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