Skip to main content

ifc_geometry/constraint/
connection.rs

1//! `IfcConnectionGeometry`: where two elements actually meet.
2//!
3//! Used by `IfcRelConnectsWithRealizingElements` and the structural analysis
4//! model to say *where* a connection happens: at a point, along a curve,
5//! across a surface, or through a volume.
6//!
7//! # Why the geometry is optional per end
8//!
9//! Each connection names geometry "at the related element" and "at the
10//! relating element". Both exist because the two elements may disagree about
11//! where the joint is -- a beam's idealised centreline meets a column's
12//! centreline at a point the physical parts do not touch. Modelling both ends
13//! rather than one shared point is what makes eccentricity representable.
14
15use crate::error::GeometryResult;
16use crate::slots::Slots;
17use ifc_model::{Entity, EntityId};
18
19/// Attribute slots shared by all `IfcConnectionGeometry` subtypes.
20///
21/// The supertype declares no explicit attributes, so each subtype's own
22/// attributes start at 0. All four subtypes follow the same
23/// `(AtRelatingElement, AtRelatedElement)` shape.
24pub(crate) mod slot {
25    /// Geometry in the relating element's coordinate system.
26    pub const AT_RELATING: usize = 0;
27    /// Geometry in the related element's coordinate system.
28    pub const AT_RELATED: usize = 1;
29}
30
31/// Which kind of connection geometry an entity carries.
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum ConnectionKind {
34    /// `IfcConnectionPointGeometry`: a vertex point.
35    Point,
36    /// `IfcConnectionPointEccentricity`: a point plus an offset vector.
37    PointEccentricity,
38    /// `IfcConnectionCurveGeometry`: a curve or edge curve.
39    Curve,
40    /// `IfcConnectionSurfaceGeometry`: a surface or face surface.
41    Surface,
42    /// `IfcConnectionVolumeGeometry`: a solid or shell.
43    Volume,
44}
45
46impl ConnectionKind {
47    /// Classify by IFC type name, or `None` if not a connection geometry.
48    pub fn classify(type_name: &str) -> Option<Self> {
49        match type_name.to_ascii_uppercase().as_str() {
50            "IFCCONNECTIONPOINTGEOMETRY" => Some(Self::Point),
51            "IFCCONNECTIONPOINTECCENTRICITY" => Some(Self::PointEccentricity),
52            "IFCCONNECTIONCURVEGEOMETRY" => Some(Self::Curve),
53            "IFCCONNECTIONSURFACEGEOMETRY" => Some(Self::Surface),
54            "IFCCONNECTIONVOLUMEGEOMETRY" => Some(Self::Volume),
55            _ => None,
56        }
57    }
58}
59
60/// `IfcConnectionPointEccentricity` eccentricity slots.
61///
62/// These follow the two inherited connection attributes.
63pub(crate) mod eccentricity_slot {
64    /// Offset along the connection X axis.
65    pub const IN_X: usize = 2;
66    /// Offset along the connection Y axis.
67    pub const IN_Y: usize = 3;
68    /// Offset along the connection Z axis.
69    pub const IN_Z: usize = 4;
70}
71
72/// A borrowed view of any `IfcConnectionGeometry` subtype.
73#[derive(Debug, Clone, Copy)]
74pub struct ConnectionGeometry<'m> {
75    slots: Slots<'m>,
76    kind: ConnectionKind,
77}
78
79impl<'m> ConnectionGeometry<'m> {
80    /// Wrap an entity, or `None` if it is not a connection geometry.
81    pub fn new(id: EntityId, entity: &'m Entity) -> Option<Self> {
82        let kind = ConnectionKind::classify(&entity.type_name)?;
83        Some(Self {
84            slots: Slots::new(id, entity),
85            kind,
86        })
87    }
88
89    /// The entity id.
90    pub fn id(&self) -> EntityId {
91        self.slots.id()
92    }
93
94    /// Which kind of connection this is.
95    pub fn kind(&self) -> ConnectionKind {
96        self.kind
97    }
98
99    /// Geometry in the relating element's coordinate system.
100    pub fn at_relating(&self) -> GeometryResult<EntityId> {
101        self.slots.req_ref(slot::AT_RELATING, "AtRelatingElement")
102    }
103
104    /// Geometry in the related element's coordinate system, if given.
105    ///
106    /// Optional because the two ends often coincide, in which case the file
107    /// states the geometry once.
108    pub fn at_related(&self) -> Option<EntityId> {
109        self.slots.opt_ref(slot::AT_RELATED)
110    }
111
112    /// Eccentricity offsets, for `IfcConnectionPointEccentricity` only.
113    ///
114    /// Returns `None` for every other kind rather than silently reporting
115    /// zeros, because "no eccentricity modelled" and "eccentricity of zero"
116    /// are different statements about a structural joint.
117    pub fn eccentricity(&self) -> Option<[f64; 3]> {
118        if self.kind != ConnectionKind::PointEccentricity {
119            return None;
120        }
121        Some([
122            self.slots.opt_f64(eccentricity_slot::IN_X).unwrap_or(0.0),
123            self.slots.opt_f64(eccentricity_slot::IN_Y).unwrap_or(0.0),
124            self.slots.opt_f64(eccentricity_slot::IN_Z).unwrap_or(0.0),
125        ])
126    }
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132    use ifc_model::Value;
133
134    #[test]
135    fn classifies_every_connection_subtype() {
136        for (name, expected) in [
137            ("IFCCONNECTIONPOINTGEOMETRY", ConnectionKind::Point),
138            (
139                "IFCCONNECTIONPOINTECCENTRICITY",
140                ConnectionKind::PointEccentricity,
141            ),
142            ("IFCCONNECTIONCURVEGEOMETRY", ConnectionKind::Curve),
143            ("IFCCONNECTIONSURFACEGEOMETRY", ConnectionKind::Surface),
144            ("IFCCONNECTIONVOLUMEGEOMETRY", ConnectionKind::Volume),
145        ] {
146            assert_eq!(ConnectionKind::classify(name), Some(expected));
147        }
148        assert_eq!(ConnectionKind::classify("IFCWALL"), None);
149    }
150
151    #[test]
152    fn related_end_is_optional() {
153        let e = Entity::new(
154            "IFCCONNECTIONPOINTGEOMETRY",
155            vec![Value::Ref(EntityId(5)), Value::Null],
156        );
157        let c = ConnectionGeometry::new(EntityId(1), &e).unwrap();
158        assert_eq!(c.at_relating().unwrap(), EntityId(5));
159        assert_eq!(c.at_related(), None);
160    }
161
162    /// "No eccentricity modelled" differs from "eccentricity is zero".
163    #[test]
164    fn eccentricity_is_absent_rather_than_zero_on_plain_point_connections() {
165        let plain = Entity::new(
166            "IFCCONNECTIONPOINTGEOMETRY",
167            vec![Value::Ref(EntityId(5)), Value::Null],
168        );
169        assert_eq!(
170            ConnectionGeometry::new(EntityId(1), &plain)
171                .unwrap()
172                .eccentricity(),
173            None
174        );
175
176        let eccentric = Entity::new(
177            "IFCCONNECTIONPOINTECCENTRICITY",
178            vec![
179                Value::Ref(EntityId(5)),
180                Value::Null,
181                Value::Real(0.1),
182                Value::Real(0.2),
183                Value::Null,
184            ],
185        );
186        assert_eq!(
187            ConnectionGeometry::new(EntityId(2), &eccentric)
188                .unwrap()
189                .eccentricity(),
190            Some([0.1, 0.2, 0.0]),
191            "an omitted axis offset is genuinely zero"
192        );
193    }
194
195    #[test]
196    fn non_connection_entities_are_rejected() {
197        let e = Entity::new("IFCWALL", vec![]);
198        assert!(ConnectionGeometry::new(EntityId(1), &e).is_none());
199    }
200}