use crate::error::{GeometryError, GeometryResult};
use crate::resource::direction::resolve_unit;
use crate::resource::point::cartesian_point_3d;
use crate::slots::Slots;
use crate::transform::Transform;
use ifc_model::{Entity, EntityId, Model};
pub(crate) mod slot {
pub const LOCATION: usize = 0;
pub mod axis1 {
pub const AXIS: usize = 1;
}
pub mod axis2_2d {
pub const REF_DIRECTION: usize = 1;
}
pub mod axis2_3d {
pub const AXIS: usize = 1;
pub const REF_DIRECTION: usize = 2;
}
}
#[derive(Debug, Clone, Copy)]
pub struct Placement<'m> {
slots: Slots<'m>,
}
impl<'m> Placement<'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 location_ref(&self) -> GeometryResult<EntityId> {
self.slots.req_ref(slot::LOCATION, "Location")
}
pub fn location(&self, model: &'m Model) -> GeometryResult<[f64; 3]> {
cartesian_point_3d(model, self.id(), self.location_ref()?)
}
}
#[derive(Debug, Clone, Copy)]
pub struct Axis1Placement<'m> {
placement: Placement<'m>,
}
impl<'m> Axis1Placement<'m> {
pub fn new(id: EntityId, entity: &'m Entity) -> Self {
Self {
placement: Placement::new(id, entity),
}
}
pub fn id(&self) -> EntityId {
self.placement.id()
}
pub fn location(&self, model: &'m Model) -> GeometryResult<[f64; 3]> {
self.placement.location(model)
}
pub fn axis(&self, model: &'m Model) -> GeometryResult<[f64; 3]> {
match self.placement.slots.opt_ref(slot::axis1::AXIS) {
Some(id) => resolve_unit(model, self.id(), id),
None => Ok([0.0, 0.0, 1.0]),
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct Axis2Placement2D<'m> {
placement: Placement<'m>,
}
impl<'m> Axis2Placement2D<'m> {
pub fn new(id: EntityId, entity: &'m Entity) -> Self {
Self {
placement: Placement::new(id, entity),
}
}
pub fn id(&self) -> EntityId {
self.placement.id()
}
pub fn location(&self, model: &'m Model) -> GeometryResult<[f64; 3]> {
self.placement.location(model)
}
pub fn ref_direction(&self, model: &'m Model) -> GeometryResult<[f64; 3]> {
match self.placement.slots.opt_ref(slot::axis2_2d::REF_DIRECTION) {
Some(id) => resolve_unit(model, self.id(), id),
None => Ok([1.0, 0.0, 0.0]),
}
}
pub fn transform(&self, model: &'m Model) -> GeometryResult<Transform> {
let origin = self.location(model)?;
let x = self.ref_direction(model)?;
Transform::from_axes(origin, Some([0.0, 0.0, 1.0]), Some(x)).ok_or_else(|| {
self.placement
.slots
.degenerate("RefDirection is parallel to the plane normal or zero-length")
})
}
}
#[derive(Debug, Clone, Copy)]
pub struct Axis2Placement3D<'m> {
placement: Placement<'m>,
}
impl<'m> Axis2Placement3D<'m> {
pub fn new(id: EntityId, entity: &'m Entity) -> Self {
Self {
placement: Placement::new(id, entity),
}
}
pub fn id(&self) -> EntityId {
self.placement.id()
}
pub fn location(&self, model: &'m Model) -> GeometryResult<[f64; 3]> {
self.placement.location(model)
}
pub fn axis(&self, model: &'m Model) -> GeometryResult<Option<[f64; 3]>> {
self.optional_direction(model, slot::axis2_3d::AXIS)
}
pub fn ref_direction(&self, model: &'m Model) -> GeometryResult<Option<[f64; 3]>> {
self.optional_direction(model, slot::axis2_3d::REF_DIRECTION)
}
pub fn transform(&self, model: &'m Model) -> GeometryResult<Transform> {
let origin = self.location(model)?;
let axis = self.axis(model)?;
let ref_direction = self.ref_direction(model)?;
Transform::from_axes(origin, axis, ref_direction).ok_or_else(|| {
self.placement
.slots
.degenerate("Axis and RefDirection are parallel, so they define no frame")
})
}
fn optional_direction(
&self,
model: &'m Model,
index: usize,
) -> GeometryResult<Option<[f64; 3]>> {
match self.placement.slots.opt_ref(index) {
Some(id) => resolve_unit(model, self.id(), id).map(Some),
None => Ok(None),
}
}
}
#[derive(Debug, Clone, Copy)]
pub enum Axis2Placement<'m> {
TwoD(Axis2Placement2D<'m>),
ThreeD(Axis2Placement3D<'m>),
}
impl<'m> Axis2Placement<'m> {
pub fn id(&self) -> EntityId {
match self {
Self::TwoD(p) => p.id(),
Self::ThreeD(p) => p.id(),
}
}
pub fn location(&self, model: &'m Model) -> GeometryResult<[f64; 3]> {
match self {
Self::TwoD(p) => p.location(model),
Self::ThreeD(p) => p.location(model),
}
}
pub fn transform(&self, model: &'m Model) -> GeometryResult<Transform> {
match self {
Self::TwoD(p) => p.transform(model),
Self::ThreeD(p) => p.transform(model),
}
}
}
pub fn axis_placement_transform(
model: &Model,
id: EntityId,
entity: &Entity,
) -> GeometryResult<Transform> {
match entity.type_name.as_ref() {
"IFCAXIS2PLACEMENT3D" => Axis2Placement3D::new(id, entity).transform(model),
"IFCAXIS2PLACEMENT2D" => Axis2Placement2D::new(id, entity).transform(model),
other => Err(GeometryError::WrongEntityType {
entity: id,
actual: other.to_string(),
expected: "IfcAxis2Placement2D or IfcAxis2Placement3D",
}),
}
}
#[cfg(test)]
mod tests {
use super::*;
use ifc_model::Value;
fn coords(values: &[f64]) -> Value {
Value::List(values.iter().copied().map(Value::Real).collect())
}
fn model_with_frame() -> Model {
let mut model = Model::new();
model.insert(
EntityId(1),
Entity::new("IFCCARTESIANPOINT", vec![coords(&[1.0, 2.0, 3.0])]),
);
model.insert(
EntityId(2),
Entity::new("IFCDIRECTION", vec![coords(&[0.0, 0.0, 1.0])]),
);
model.insert(
EntityId(3),
Entity::new("IFCDIRECTION", vec![coords(&[1.0, 0.0, 0.0])]),
);
model
}
fn close(a: [f64; 3], b: [f64; 3]) -> bool {
a.iter().zip(b).all(|(x, y)| (x - y).abs() < 1e-9)
}
#[test]
fn inherited_location_is_slot_zero_not_the_subtypes_first_attribute() {
let model = model_with_frame();
let e = Entity::new(
"IFCAXIS2PLACEMENT3D",
vec![
Value::Ref(EntityId(1)),
Value::Ref(EntityId(2)),
Value::Ref(EntityId(3)),
],
);
let p = Axis2Placement3D::new(EntityId(10), &e);
assert_eq!(p.location(&model).unwrap(), [1.0, 2.0, 3.0]);
assert_eq!(p.axis(&model).unwrap(), Some([0.0, 0.0, 1.0]));
}
#[test]
fn absent_axis_and_ref_direction_default_to_global_z_and_x() {
let model = model_with_frame();
let e = Entity::new(
"IFCAXIS2PLACEMENT3D",
vec![Value::Ref(EntityId(1)), Value::Null, Value::Null],
);
let p = Axis2Placement3D::new(EntityId(10), &e);
assert_eq!(p.axis(&model).unwrap(), None);
assert_eq!(p.ref_direction(&model).unwrap(), None);
let t = p.transform(&model).unwrap();
assert!(close(t.basis[0], [1.0, 0.0, 0.0]));
assert!(close(t.basis[1], [0.0, 1.0, 0.0]));
assert!(close(t.basis[2], [0.0, 0.0, 1.0]));
assert_eq!(t.origin, [1.0, 2.0, 3.0]);
}
#[test]
fn a_record_missing_its_trailing_optionals_still_places() {
let model = model_with_frame();
let e = Entity::new("IFCAXIS2PLACEMENT3D", vec![Value::Ref(EntityId(1))]);
let t = Axis2Placement3D::new(EntityId(10), &e)
.transform(&model)
.unwrap();
assert_eq!(t.origin, [1.0, 2.0, 3.0]);
assert!(close(t.basis[2], [0.0, 0.0, 1.0]));
}
#[test]
fn non_perpendicular_ref_direction_is_projected_into_an_orthonormal_basis() {
let mut model = model_with_frame();
model.insert(
EntityId(4),
Entity::new("IFCDIRECTION", vec![coords(&[1.0, 0.0, 1.0])]),
);
let e = Entity::new(
"IFCAXIS2PLACEMENT3D",
vec![
Value::Ref(EntityId(1)),
Value::Ref(EntityId(2)),
Value::Ref(EntityId(4)),
],
);
let t = Axis2Placement3D::new(EntityId(10), &e)
.transform(&model)
.unwrap();
assert!(close(t.basis[0], [1.0, 0.0, 0.0]), "got {:?}", t.basis[0]);
assert!(
t.basis[0]
.iter()
.zip(t.basis[2])
.map(|(a, b)| a * b)
.sum::<f64>()
.abs()
< 1e-12,
"X must end up perpendicular to Z"
);
}
#[test]
fn axis_parallel_to_ref_direction_is_degenerate() {
let model = model_with_frame();
let e = Entity::new(
"IFCAXIS2PLACEMENT3D",
vec![
Value::Ref(EntityId(1)),
Value::Ref(EntityId(2)),
Value::Ref(EntityId(2)),
],
);
let err = Axis2Placement3D::new(EntityId(10), &e)
.transform(&model)
.unwrap_err();
assert!(matches!(err, GeometryError::Degenerate { .. }), "{err}");
}
#[test]
fn zero_length_axis_is_degenerate_rather_than_nan() {
let mut model = model_with_frame();
model.insert(
EntityId(5),
Entity::new("IFCDIRECTION", vec![coords(&[0.0, 0.0, 0.0])]),
);
let e = Entity::new(
"IFCAXIS2PLACEMENT3D",
vec![
Value::Ref(EntityId(1)),
Value::Ref(EntityId(5)),
Value::Null,
],
);
let err = Axis2Placement3D::new(EntityId(10), &e)
.transform(&model)
.unwrap_err();
assert!(matches!(err, GeometryError::Degenerate { .. }), "{err}");
}
#[test]
fn only_one_of_the_paired_axes_still_yields_a_frame() {
let model = model_with_frame();
let e = Entity::new(
"IFCAXIS2PLACEMENT3D",
vec![
Value::Ref(EntityId(1)),
Value::Null,
Value::Ref(EntityId(3)),
],
);
let t = Axis2Placement3D::new(EntityId(10), &e)
.transform(&model)
.unwrap();
assert!(close(t.basis[0], [1.0, 0.0, 0.0]));
assert!(close(t.basis[2], [0.0, 0.0, 1.0]));
}
#[test]
fn two_d_placement_derives_y_as_a_quarter_turn_from_x() {
let mut model = Model::new();
model.insert(
EntityId(1),
Entity::new("IFCCARTESIANPOINT", vec![coords(&[4.0, 5.0])]),
);
model.insert(
EntityId(2),
Entity::new("IFCDIRECTION", vec![coords(&[0.0, 1.0])]),
);
let e = Entity::new(
"IFCAXIS2PLACEMENT2D",
vec![Value::Ref(EntityId(1)), Value::Ref(EntityId(2))],
);
let t = Axis2Placement2D::new(EntityId(10), &e)
.transform(&model)
.unwrap();
assert_eq!(t.origin, [4.0, 5.0, 0.0], "2D location pads z with 0");
assert!(close(t.basis[0], [0.0, 1.0, 0.0]));
assert!(
close(t.basis[1], [-1.0, 0.0, 0.0]),
"Y must be X rotated a quarter turn counter-clockwise, got {:?}",
t.basis[1]
);
}
#[test]
fn two_d_placement_without_ref_direction_defaults_to_global_x() {
let mut model = Model::new();
model.insert(
EntityId(1),
Entity::new("IFCCARTESIANPOINT", vec![coords(&[0.0, 0.0])]),
);
let e = Entity::new("IFCAXIS2PLACEMENT2D", vec![Value::Ref(EntityId(1))]);
let p = Axis2Placement2D::new(EntityId(10), &e);
assert_eq!(p.ref_direction(&model).unwrap(), [1.0, 0.0, 0.0]);
assert!(p.transform(&model).unwrap().is_identity(1e-12));
}
#[test]
fn axis1_placement_defaults_its_axis_to_global_z() {
let model = model_with_frame();
let e = Entity::new("IFCAXIS1PLACEMENT", vec![Value::Ref(EntityId(1))]);
let p = Axis1Placement::new(EntityId(10), &e);
assert_eq!(p.axis(&model).unwrap(), [0.0, 0.0, 1.0]);
assert_eq!(p.location(&model).unwrap(), [1.0, 2.0, 3.0]);
}
#[test]
fn axis1_placement_reads_its_axis_after_the_inherited_location() {
let model = model_with_frame();
let e = Entity::new(
"IFCAXIS1PLACEMENT",
vec![Value::Ref(EntityId(1)), Value::Ref(EntityId(3))],
);
assert_eq!(
Axis1Placement::new(EntityId(10), &e).axis(&model).unwrap(),
[1.0, 0.0, 0.0]
);
}
#[test]
fn a_missing_location_names_the_entity_and_attribute() {
let model = Model::new();
let e = Entity::new("IFCAXIS2PLACEMENT3D", vec![]);
let err = Axis2Placement3D::new(EntityId(77), &e)
.location(&model)
.unwrap_err();
assert!(err.to_string().contains("#77"), "got: {err}");
assert!(err.to_string().contains("Location"), "got: {err}");
}
#[test]
fn a_dangling_location_reference_names_the_placement_as_referrer() {
let model = Model::new();
let e = Entity::new("IFCAXIS2PLACEMENT3D", vec![Value::Ref(EntityId(99))]);
let err = Axis2Placement3D::new(EntityId(7), &e)
.location(&model)
.unwrap_err();
assert_eq!(err.entity(), Some(EntityId(7)));
}
}