use crate::error::GeometryResult;
use crate::slots::Slots;
use ifc_model::{Entity, EntityId};
pub(crate) mod slot {
pub const AT_RELATING: usize = 0;
pub const AT_RELATED: usize = 1;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConnectionKind {
Point,
PointEccentricity,
Curve,
Surface,
Volume,
}
impl ConnectionKind {
pub fn classify(type_name: &str) -> Option<Self> {
match type_name.to_ascii_uppercase().as_str() {
"IFCCONNECTIONPOINTGEOMETRY" => Some(Self::Point),
"IFCCONNECTIONPOINTECCENTRICITY" => Some(Self::PointEccentricity),
"IFCCONNECTIONCURVEGEOMETRY" => Some(Self::Curve),
"IFCCONNECTIONSURFACEGEOMETRY" => Some(Self::Surface),
"IFCCONNECTIONVOLUMEGEOMETRY" => Some(Self::Volume),
_ => None,
}
}
}
pub(crate) mod eccentricity_slot {
pub const IN_X: usize = 2;
pub const IN_Y: usize = 3;
pub const IN_Z: usize = 4;
}
#[derive(Debug, Clone, Copy)]
pub struct ConnectionGeometry<'m> {
slots: Slots<'m>,
kind: ConnectionKind,
}
impl<'m> ConnectionGeometry<'m> {
pub fn new(id: EntityId, entity: &'m Entity) -> Option<Self> {
let kind = ConnectionKind::classify(&entity.type_name)?;
Some(Self {
slots: Slots::new(id, entity),
kind,
})
}
pub fn id(&self) -> EntityId {
self.slots.id()
}
pub fn kind(&self) -> ConnectionKind {
self.kind
}
pub fn at_relating(&self) -> GeometryResult<EntityId> {
self.slots.req_ref(slot::AT_RELATING, "AtRelatingElement")
}
pub fn at_related(&self) -> Option<EntityId> {
self.slots.opt_ref(slot::AT_RELATED)
}
pub fn eccentricity(&self) -> Option<[f64; 3]> {
if self.kind != ConnectionKind::PointEccentricity {
return None;
}
Some([
self.slots.opt_f64(eccentricity_slot::IN_X).unwrap_or(0.0),
self.slots.opt_f64(eccentricity_slot::IN_Y).unwrap_or(0.0),
self.slots.opt_f64(eccentricity_slot::IN_Z).unwrap_or(0.0),
])
}
}
#[cfg(test)]
mod tests {
use super::*;
use ifc_model::Value;
#[test]
fn classifies_every_connection_subtype() {
for (name, expected) in [
("IFCCONNECTIONPOINTGEOMETRY", ConnectionKind::Point),
(
"IFCCONNECTIONPOINTECCENTRICITY",
ConnectionKind::PointEccentricity,
),
("IFCCONNECTIONCURVEGEOMETRY", ConnectionKind::Curve),
("IFCCONNECTIONSURFACEGEOMETRY", ConnectionKind::Surface),
("IFCCONNECTIONVOLUMEGEOMETRY", ConnectionKind::Volume),
] {
assert_eq!(ConnectionKind::classify(name), Some(expected));
}
assert_eq!(ConnectionKind::classify("IFCWALL"), None);
}
#[test]
fn related_end_is_optional() {
let e = Entity::new(
"IFCCONNECTIONPOINTGEOMETRY",
vec![Value::Ref(EntityId(5)), Value::Null],
);
let c = ConnectionGeometry::new(EntityId(1), &e).unwrap();
assert_eq!(c.at_relating().unwrap(), EntityId(5));
assert_eq!(c.at_related(), None);
}
#[test]
fn eccentricity_is_absent_rather_than_zero_on_plain_point_connections() {
let plain = Entity::new(
"IFCCONNECTIONPOINTGEOMETRY",
vec![Value::Ref(EntityId(5)), Value::Null],
);
assert_eq!(
ConnectionGeometry::new(EntityId(1), &plain)
.unwrap()
.eccentricity(),
None
);
let eccentric = Entity::new(
"IFCCONNECTIONPOINTECCENTRICITY",
vec![
Value::Ref(EntityId(5)),
Value::Null,
Value::Real(0.1),
Value::Real(0.2),
Value::Null,
],
);
assert_eq!(
ConnectionGeometry::new(EntityId(2), &eccentric)
.unwrap()
.eccentricity(),
Some([0.1, 0.2, 0.0]),
"an omitted axis offset is genuinely zero"
);
}
#[test]
fn non_connection_entities_are_rejected() {
let e = Entity::new("IFCWALL", vec![]);
assert!(ConnectionGeometry::new(EntityId(1), &e).is_none());
}
}