ifc_geometry/constraint/
connection.rs1use crate::error::GeometryResult;
16use crate::slots::Slots;
17use ifc_model::{Entity, EntityId};
18
19pub(crate) mod slot {
25 pub const AT_RELATING: usize = 0;
27 pub const AT_RELATED: usize = 1;
29}
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum ConnectionKind {
34 Point,
36 PointEccentricity,
38 Curve,
40 Surface,
42 Volume,
44}
45
46impl ConnectionKind {
47 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
60pub(crate) mod eccentricity_slot {
64 pub const IN_X: usize = 2;
66 pub const IN_Y: usize = 3;
68 pub const IN_Z: usize = 4;
70}
71
72#[derive(Debug, Clone, Copy)]
74pub struct ConnectionGeometry<'m> {
75 slots: Slots<'m>,
76 kind: ConnectionKind,
77}
78
79impl<'m> ConnectionGeometry<'m> {
80 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 pub fn id(&self) -> EntityId {
91 self.slots.id()
92 }
93
94 pub fn kind(&self) -> ConnectionKind {
96 self.kind
97 }
98
99 pub fn at_relating(&self) -> GeometryResult<EntityId> {
101 self.slots.req_ref(slot::AT_RELATING, "AtRelatingElement")
102 }
103
104 pub fn at_related(&self) -> Option<EntityId> {
109 self.slots.opt_ref(slot::AT_RELATED)
110 }
111
112 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 #[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}