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::{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}