use crate::error::GeometryResult;
use crate::slots::Slots;
use ifc_model::{Entity, EntityId, Model};
pub(crate) mod slot {
pub mod direction {
pub const DIRECTION_RATIOS: usize = 0;
}
pub mod vector {
pub const ORIENTATION: usize = 0;
pub const MAGNITUDE: usize = 1;
}
}
#[derive(Debug, Clone, Copy)]
pub struct Direction<'m> {
slots: Slots<'m>,
}
impl<'m> Direction<'m> {
pub fn new(id: EntityId, entity: &'m Entity) -> Self {
Self {
slots: Slots::new(id, entity),
}
}
pub fn id(&self) -> EntityId {
self.slots.id()
}
pub fn ratios(&self) -> GeometryResult<Vec<f64>> {
self.slots
.req_f64_list(slot::direction::DIRECTION_RATIOS, "DirectionRatios")
}
pub fn dimension(&self) -> GeometryResult<usize> {
let n = self.ratios()?.len();
match n {
2 | 3 => Ok(n),
other => Err(self.slots.degenerate(format!(
"DirectionRatios has {other} entries, expected 2 or 3"
))),
}
}
pub fn ratios_3d(&self) -> GeometryResult<[f64; 3]> {
let r = self.ratios()?;
match r.len() {
2 => Ok([r[0], r[1], 0.0]),
3 => Ok([r[0], r[1], r[2]]),
other => Err(self.slots.degenerate(format!(
"DirectionRatios has {other} entries, expected 2 or 3"
))),
}
}
pub fn unit(&self) -> GeometryResult<[f64; 3]> {
let v = self.ratios_3d()?;
let scale = v
.iter()
.map(|component| component.abs())
.fold(0.0, f64::max);
if scale == 0.0 {
return Err(self
.slots
.degenerate("DirectionRatios are all zero, so there is no direction"));
}
if !scale.is_finite() {
return Err(self
.slots
.degenerate("DirectionRatios must be finite before normalization"));
}
let scaled = [v[0] / scale, v[1] / scale, v[2] / scale];
let length = (scaled[0] * scaled[0] + scaled[1] * scaled[1] + scaled[2] * scaled[2]).sqrt();
let unit = [scaled[0] / length, scaled[1] / length, scaled[2] / length];
if !unit.iter().all(|component| component.is_finite()) {
return Err(self
.slots
.degenerate("DirectionRatios did not derive a finite unit direction"));
}
Ok(unit)
}
}
#[derive(Debug, Clone, Copy)]
pub struct Vector<'m> {
slots: Slots<'m>,
}
impl<'m> Vector<'m> {
pub fn new(id: EntityId, entity: &'m Entity) -> Self {
Self {
slots: Slots::new(id, entity),
}
}
pub fn id(&self) -> EntityId {
self.slots.id()
}
pub fn orientation_ref(&self) -> GeometryResult<EntityId> {
self.slots.req_ref(slot::vector::ORIENTATION, "Orientation")
}
pub fn magnitude(&self) -> GeometryResult<f64> {
self.slots.req_f64(slot::vector::MAGNITUDE, "Magnitude")
}
pub fn components(&self, model: &'m Model) -> GeometryResult<[f64; 3]> {
let magnitude = self.magnitude()?;
let unit = resolve_unit(model, self.id(), self.orientation_ref()?)?;
Ok([
unit[0] * magnitude,
unit[1] * magnitude,
unit[2] * magnitude,
])
}
}
pub fn resolve_unit(model: &Model, referrer: EntityId, id: EntityId) -> GeometryResult<[f64; 3]> {
direction_view(model, referrer, id)?.unit()
}
pub fn resolve_ratios_3d(
model: &Model,
referrer: EntityId,
id: EntityId,
) -> GeometryResult<[f64; 3]> {
direction_view(model, referrer, id)?.ratios_3d()
}
fn direction_view<'m>(
model: &'m Model,
referrer: EntityId,
id: EntityId,
) -> GeometryResult<Direction<'m>> {
crate::resource::resolve::direction(model, referrer, id)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::GeometryError;
use ifc_model::Value;
fn direction_entity(values: &[f64]) -> Entity {
Entity::new(
"IFCDIRECTION",
vec![Value::List(
values.iter().copied().map(Value::Real).collect(),
)],
)
}
fn close(a: [f64; 3], b: [f64; 3]) -> bool {
a.iter().zip(b).all(|(x, y)| (x - y).abs() < 1e-12)
}
#[test]
fn direction_ratios_are_returned_unnormalized() {
let e = direction_entity(&[3.0, 4.0, 0.0]);
let d = Direction::new(EntityId(1), &e);
assert_eq!(d.ratios().unwrap(), vec![3.0, 4.0, 0.0]);
assert!(close(d.unit().unwrap(), [0.6, 0.8, 0.0]));
}
#[test]
fn two_dimensional_directions_report_their_dimension() {
let e = direction_entity(&[1.0, 0.0]);
let d = Direction::new(EntityId(1), &e);
assert_eq!(d.dimension().unwrap(), 2);
assert_eq!(d.ratios_3d().unwrap(), [1.0, 0.0, 0.0]);
}
#[test]
fn zero_length_direction_is_degenerate_rather_than_nan() {
let e = direction_entity(&[0.0, 0.0, 0.0]);
let d = Direction::new(EntityId(3), &e);
let err = d.unit().unwrap_err();
assert!(matches!(err, GeometryError::Degenerate { .. }));
assert!(err.to_string().contains("#3"), "got: {err}");
}
#[test]
fn finite_extreme_ratios_normalize_without_underflow_or_overflow() {
let tiny = direction_entity(&[1e-300, 0.0, 0.0]);
assert_eq!(
Direction::new(EntityId(1), &tiny).unit().unwrap(),
[1.0, 0.0, 0.0]
);
let huge = direction_entity(&[f64::MAX, f64::MAX, 0.0]);
let unit = Direction::new(EntityId(2), &huge).unit().unwrap();
let expected = 1.0 / 2.0_f64.sqrt();
assert!(close(unit, [expected, expected, 0.0]));
}
#[test]
fn a_single_ratio_is_degenerate() {
let e = direction_entity(&[1.0]);
let d = Direction::new(EntityId(1), &e);
assert!(d.dimension().is_err());
assert!(d.ratios_3d().is_err());
}
#[test]
fn missing_direction_ratios_names_the_entity_and_attribute() {
let e = Entity::new("IFCDIRECTION", vec![]);
let err = Direction::new(EntityId(8), &e).ratios().unwrap_err();
assert!(err.to_string().contains("#8"), "got: {err}");
assert!(err.to_string().contains("DirectionRatios"), "got: {err}");
}
#[test]
fn vector_components_are_orientation_times_magnitude() {
let mut model = Model::new();
model.insert(EntityId(1), direction_entity(&[3.0, 4.0, 0.0]));
let v = Entity::new(
"IFCVECTOR",
vec![
Value::Ref(EntityId(1)),
Value::Typed {
type_name: "IFCLENGTHMEASURE".into(),
value: Box::new(Value::Real(10.0)),
},
],
);
let view = Vector::new(EntityId(2), &v);
assert_eq!(view.magnitude().unwrap(), 10.0);
assert!(close(view.components(&model).unwrap(), [6.0, 8.0, 0.0]));
}
#[test]
fn zero_magnitude_vector_is_legal_and_yields_the_zero_vector() {
let mut model = Model::new();
model.insert(EntityId(1), direction_entity(&[0.0, 0.0, 1.0]));
let v = Entity::new("IFCVECTOR", vec![Value::Ref(EntityId(1)), Value::Real(0.0)]);
assert_eq!(
Vector::new(EntityId(2), &v).components(&model).unwrap(),
[0.0, 0.0, 0.0]
);
}
#[test]
fn a_vector_pointing_at_a_non_direction_reports_the_wrong_type() {
let mut model = Model::new();
model.insert(EntityId(1), Entity::new("IFCCARTESIANPOINT", vec![]));
let v = Entity::new("IFCVECTOR", vec![Value::Ref(EntityId(1)), Value::Real(1.0)]);
let err = Vector::new(EntityId(2), &v).components(&model).unwrap_err();
assert!(matches!(
err,
GeometryError::WrongEntityType {
expected: "IfcDirection",
..
}
));
}
#[test]
fn a_dangling_direction_reference_names_the_referrer() {
let model = Model::new();
let err = resolve_unit(&model, EntityId(5), EntityId(99)).unwrap_err();
assert_eq!(err.entity(), Some(EntityId(5)));
}
#[test]
fn resolving_ratios_keeps_them_unnormalized() {
let mut model = Model::new();
model.insert(EntityId(1), direction_entity(&[0.0, 2.0]));
assert_eq!(
resolve_ratios_3d(&model, EntityId(2), EntityId(1)).unwrap(),
[0.0, 2.0, 0.0]
);
}
}