use crate::error::{GeometryError, GeometryResult};
use crate::resource::axes::{base_axes_2d, base_axes_3d};
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 AXIS1: usize = 0;
pub const AXIS2: usize = 1;
pub const LOCAL_ORIGIN: usize = 2;
pub const SCALE: usize = 3;
pub const AXIS3: usize = 4;
pub const SCALE2_2D: usize = 4;
pub const SCALE2_3D: usize = 5;
pub const SCALE3_3D: usize = 6;
}
#[derive(Debug, Clone, Copy)]
pub struct CartesianTransformationOperator<'m> {
slots: Slots<'m>,
}
impl<'m> CartesianTransformationOperator<'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 local_origin_ref(&self) -> GeometryResult<EntityId> {
self.slots.req_ref(slot::LOCAL_ORIGIN, "LocalOrigin")
}
pub fn local_origin(&self, model: &'m Model) -> GeometryResult<[f64; 3]> {
cartesian_point_3d(model, self.id(), self.local_origin_ref()?)
}
pub fn scale_attribute(&self) -> Option<f64> {
self.slots.opt_f64(slot::SCALE)
}
pub fn scale(&self) -> GeometryResult<f64> {
let value = self.scale_attribute().unwrap_or(1.0);
self.checked_scale(value, "Scale")
}
pub fn axis1(&self, model: &'m Model) -> GeometryResult<Option<[f64; 3]>> {
self.optional_direction(model, slot::AXIS1)
}
pub fn axis2(&self, model: &'m Model) -> GeometryResult<Option<[f64; 3]>> {
self.optional_direction(model, slot::AXIS2)
}
fn optional_direction(
&self,
model: &'m Model,
index: usize,
) -> GeometryResult<Option<[f64; 3]>> {
match self.slots.opt_ref(index) {
Some(id) => resolve_unit(model, self.id(), id).map(Some),
None => Ok(None),
}
}
fn checked_scale(&self, value: f64, attribute: &str) -> GeometryResult<f64> {
if value > 0.0 {
Ok(value)
} else {
Err(self.slots.degenerate(format!(
"{attribute} is {value}, but the schema requires a scale greater than zero"
)))
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct CartesianTransformationOperator2D<'m> {
base: CartesianTransformationOperator<'m>,
}
impl<'m> CartesianTransformationOperator2D<'m> {
pub fn new(id: EntityId, entity: &'m Entity) -> Self {
Self {
base: CartesianTransformationOperator::new(id, entity),
}
}
pub fn base(&self) -> CartesianTransformationOperator<'m> {
self.base
}
pub fn id(&self) -> EntityId {
self.base.id()
}
pub fn transform(&self, model: &'m Model) -> GeometryResult<Transform> {
let scale = self.base.scale()?;
self.scaled_transform(model, [scale, scale])
}
fn scaled_transform(&self, model: &'m Model, factors: [f64; 2]) -> GeometryResult<Transform> {
let origin = self.base.local_origin(model)?;
let axis1 = self.base.axis1(model)?;
let axis2 = self.base.axis2(model)?;
let frame = base_axes_2d(origin, axis1, axis2).ok_or_else(|| {
self.base
.slots
.degenerate("Axis1 and Axis2 do not define a 2D frame")
})?;
Ok(frame.scaled_nonuniform([factors[0], factors[1], 1.0]))
}
}
#[derive(Debug, Clone, Copy)]
pub struct CartesianTransformationOperator2DnonUniform<'m> {
inner: CartesianTransformationOperator2D<'m>,
}
impl<'m> CartesianTransformationOperator2DnonUniform<'m> {
pub fn new(id: EntityId, entity: &'m Entity) -> Self {
Self {
inner: CartesianTransformationOperator2D::new(id, entity),
}
}
pub fn id(&self) -> EntityId {
self.inner.id()
}
pub fn base(&self) -> CartesianTransformationOperator<'m> {
self.inner.base()
}
pub fn scale2(&self) -> GeometryResult<f64> {
let base = self.base();
let value = base.slots.opt_f64(slot::SCALE2_2D).unwrap_or(base.scale()?);
base.checked_scale(value, "Scale2")
}
pub fn transform(&self, model: &'m Model) -> GeometryResult<Transform> {
let factors = [self.base().scale()?, self.scale2()?];
self.inner.scaled_transform(model, factors)
}
}
#[derive(Debug, Clone, Copy)]
pub struct CartesianTransformationOperator3D<'m> {
base: CartesianTransformationOperator<'m>,
}
impl<'m> CartesianTransformationOperator3D<'m> {
pub fn new(id: EntityId, entity: &'m Entity) -> Self {
Self {
base: CartesianTransformationOperator::new(id, entity),
}
}
pub fn base(&self) -> CartesianTransformationOperator<'m> {
self.base
}
pub fn id(&self) -> EntityId {
self.base.id()
}
pub fn axis3(&self, model: &'m Model) -> GeometryResult<Option<[f64; 3]>> {
self.base.optional_direction(model, slot::AXIS3)
}
pub fn transform(&self, model: &'m Model) -> GeometryResult<Transform> {
let scale = self.base.scale()?;
self.scaled_transform(model, [scale; 3])
}
fn scaled_transform(&self, model: &'m Model, factors: [f64; 3]) -> GeometryResult<Transform> {
let origin = self.base.local_origin(model)?;
let axis1 = self.base.axis1(model)?;
let axis2 = self.base.axis2(model)?;
let axis3 = self.axis3(model)?;
let frame = base_axes_3d(origin, axis1, axis2, axis3).ok_or_else(|| {
self.base
.slots
.degenerate("Axis1 and Axis3 are parallel, so they define no frame")
})?;
Ok(frame.scaled_nonuniform(factors))
}
}
#[derive(Debug, Clone, Copy)]
pub struct CartesianTransformationOperator3DnonUniform<'m> {
inner: CartesianTransformationOperator3D<'m>,
}
impl<'m> CartesianTransformationOperator3DnonUniform<'m> {
pub fn new(id: EntityId, entity: &'m Entity) -> Self {
Self {
inner: CartesianTransformationOperator3D::new(id, entity),
}
}
pub fn id(&self) -> EntityId {
self.inner.id()
}
pub fn base(&self) -> CartesianTransformationOperator<'m> {
self.inner.base()
}
pub fn scale2(&self) -> GeometryResult<f64> {
self.derived_scale(slot::SCALE2_3D, "Scale2")
}
pub fn scale3(&self) -> GeometryResult<f64> {
self.derived_scale(slot::SCALE3_3D, "Scale3")
}
pub fn transform(&self, model: &'m Model) -> GeometryResult<Transform> {
let factors = [self.base().scale()?, self.scale2()?, self.scale3()?];
self.inner.scaled_transform(model, factors)
}
fn derived_scale(&self, index: usize, attribute: &str) -> GeometryResult<f64> {
let base = self.base();
let value = base.slots.opt_f64(index).unwrap_or(base.scale()?);
base.checked_scale(value, attribute)
}
}
pub fn operator_transform(
model: &Model,
id: EntityId,
entity: &Entity,
) -> GeometryResult<Transform> {
match entity.type_name.to_ascii_uppercase().as_str() {
"IFCCARTESIANTRANSFORMATIONOPERATOR3D" => {
CartesianTransformationOperator3D::new(id, entity).transform(model)
}
"IFCCARTESIANTRANSFORMATIONOPERATOR3DNONUNIFORM" => {
CartesianTransformationOperator3DnonUniform::new(id, entity).transform(model)
}
"IFCCARTESIANTRANSFORMATIONOPERATOR2D" => {
CartesianTransformationOperator2D::new(id, entity).transform(model)
}
"IFCCARTESIANTRANSFORMATIONOPERATOR2DNONUNIFORM" => {
CartesianTransformationOperator2DnonUniform::new(id, entity).transform(model)
}
other => Err(GeometryError::WrongEntityType {
entity: id,
actual: other.to_string(),
expected: "IfcCartesianTransformationOperator",
}),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::GeometryError;
use ifc_model::Value;
fn coords(values: &[f64]) -> Value {
Value::List(values.iter().copied().map(Value::Real).collect())
}
fn model() -> 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(&[1.0, 0.0, 0.0])]),
);
model.insert(
EntityId(3),
Entity::new("IFCDIRECTION", vec![coords(&[0.0, 1.0, 0.0])]),
);
model.insert(
EntityId(4),
Entity::new("IFCDIRECTION", vec![coords(&[0.0, 0.0, 1.0])]),
);
model
}
fn operator_3d(scale: Value) -> Entity {
Entity::new(
"IFCCARTESIANTRANSFORMATIONOPERATOR3D",
vec![
Value::Null,
Value::Null,
Value::Ref(EntityId(1)),
scale,
Value::Null,
],
)
}
fn close(a: [f64; 3], b: [f64; 3]) -> bool {
a.iter().zip(b).all(|(x, y)| (x - y).abs() < 1e-9)
}
#[test]
fn absent_scale_defaults_to_one_not_zero() {
let e = operator_3d(Value::Null);
let op = CartesianTransformationOperator::new(EntityId(9), &e);
assert_eq!(op.scale_attribute(), None);
assert_eq!(op.scale().unwrap(), 1.0);
}
#[test]
fn local_origin_is_read_after_the_two_optional_axes() {
let model = model();
let e = operator_3d(Value::Null);
let op = CartesianTransformationOperator::new(EntityId(9), &e);
assert_eq!(op.local_origin(&model).unwrap(), [1.0, 2.0, 3.0]);
}
#[test]
fn a_uniform_operator_scales_every_axis_by_scale() {
let model = model();
let e = operator_3d(Value::Real(2.0));
let t = CartesianTransformationOperator3D::new(EntityId(9), &e)
.transform(&model)
.unwrap();
assert!(close(t.basis[0], [2.0, 0.0, 0.0]));
assert!(close(t.basis[1], [0.0, 2.0, 0.0]));
assert!(close(t.basis[2], [0.0, 0.0, 2.0]));
assert_eq!(t.origin, [1.0, 2.0, 3.0]);
}
#[test]
fn axis1_is_the_local_x_and_axis3_the_local_z() {
let model = model();
let e = Entity::new(
"IFCCARTESIANTRANSFORMATIONOPERATOR3D",
vec![
Value::Ref(EntityId(3)), Value::Null,
Value::Ref(EntityId(1)),
Value::Null,
Value::Ref(EntityId(4)), ],
);
let t = CartesianTransformationOperator3D::new(EntityId(9), &e)
.transform(&model)
.unwrap();
assert!(close(t.basis[0], [0.0, 1.0, 0.0]), "got {:?}", t.basis[0]);
assert!(close(t.basis[2], [0.0, 0.0, 1.0]));
}
#[test]
fn nonuniform_secondary_scales_default_to_scale_not_to_one() {
let model = model();
let e = Entity::new(
"IFCCARTESIANTRANSFORMATIONOPERATOR3DNONUNIFORM",
vec![
Value::Null,
Value::Null,
Value::Ref(EntityId(1)),
Value::Real(3.0),
Value::Null,
Value::Null, Value::Null, ],
);
let op = CartesianTransformationOperator3DnonUniform::new(EntityId(9), &e);
assert_eq!(op.scale2().unwrap(), 3.0);
assert_eq!(op.scale3().unwrap(), 3.0);
let t = op.transform(&model).unwrap();
assert!(close(t.basis[1], [0.0, 3.0, 0.0]));
assert!(close(t.basis[2], [0.0, 0.0, 3.0]));
}
#[test]
fn nonuniform_scales_are_applied_per_axis_when_given() {
let model = model();
let e = Entity::new(
"IFCCARTESIANTRANSFORMATIONOPERATOR3DNONUNIFORM",
vec![
Value::Null,
Value::Null,
Value::Ref(EntityId(1)),
Value::Real(2.0),
Value::Null,
Value::Real(5.0),
Value::Real(7.0),
],
);
let t = CartesianTransformationOperator3DnonUniform::new(EntityId(9), &e)
.transform(&model)
.unwrap();
assert!(close(t.basis[0], [2.0, 0.0, 0.0]));
assert!(close(t.basis[1], [0.0, 5.0, 0.0]));
assert!(close(t.basis[2], [0.0, 0.0, 7.0]));
}
#[test]
fn two_d_nonuniform_reads_scale2_one_slot_earlier_than_the_three_d_one() {
let model = model();
let e = Entity::new(
"IFCCARTESIANTRANSFORMATIONOPERATOR2DNONUNIFORM",
vec![
Value::Null,
Value::Null,
Value::Ref(EntityId(1)),
Value::Real(2.0),
Value::Real(6.0), ],
);
let op = CartesianTransformationOperator2DnonUniform::new(EntityId(9), &e);
assert_eq!(op.scale2().unwrap(), 6.0);
let t = op.transform(&model).unwrap();
assert!(close(t.basis[0], [2.0, 0.0, 0.0]));
assert!(close(t.basis[1], [0.0, 6.0, 0.0]));
}
#[test]
fn two_d_operator_leaves_the_z_axis_unscaled() {
let model = model();
let e = Entity::new(
"IFCCARTESIANTRANSFORMATIONOPERATOR2D",
vec![
Value::Null,
Value::Null,
Value::Ref(EntityId(1)),
Value::Real(4.0),
],
);
let t = CartesianTransformationOperator2D::new(EntityId(9), &e)
.transform(&model)
.unwrap();
assert!(close(t.basis[0], [4.0, 0.0, 0.0]));
assert!(close(t.basis[1], [0.0, 4.0, 0.0]));
assert!(close(t.basis[2], [0.0, 0.0, 1.0]), "got {:?}", t.basis[2]);
}
#[test]
fn two_d_y_axis_is_the_orthogonal_complement_of_axis1() {
let mut model = model();
model.insert(
EntityId(5),
Entity::new("IFCDIRECTION", vec![coords(&[0.0, 1.0])]),
);
let e = Entity::new(
"IFCCARTESIANTRANSFORMATIONOPERATOR2D",
vec![
Value::Ref(EntityId(5)),
Value::Null,
Value::Ref(EntityId(1)),
Value::Null,
],
);
let t = CartesianTransformationOperator2D::new(EntityId(9), &e)
.transform(&model)
.unwrap();
assert!(close(t.basis[0], [0.0, 1.0, 0.0]));
assert!(close(t.basis[1], [-1.0, 0.0, 0.0]), "got {:?}", t.basis[1]);
}
#[test]
fn an_opposing_axis2_flips_the_derived_y_axis() {
let mut model = model();
model.insert(
EntityId(6),
Entity::new("IFCDIRECTION", vec![coords(&[0.0, -1.0])]),
);
model.insert(
EntityId(7),
Entity::new("IFCDIRECTION", vec![coords(&[1.0, 0.0])]),
);
let e = Entity::new(
"IFCCARTESIANTRANSFORMATIONOPERATOR2D",
vec![
Value::Ref(EntityId(7)), Value::Ref(EntityId(6)), Value::Ref(EntityId(1)),
Value::Null,
],
);
let t = CartesianTransformationOperator2D::new(EntityId(9), &e)
.transform(&model)
.unwrap();
assert!(close(t.basis[1], [0.0, -1.0, 0.0]), "got {:?}", t.basis[1]);
}
#[test]
fn an_opposing_axis2_flips_handedness_in_three_d_too() {
let mut model = model();
model.insert(
EntityId(8),
Entity::new("IFCDIRECTION", vec![coords(&[0.0, -1.0, 0.0])]),
);
let e = Entity::new(
"IFCCARTESIANTRANSFORMATIONOPERATOR3D",
vec![
Value::Ref(EntityId(2)),
Value::Ref(EntityId(8)),
Value::Ref(EntityId(1)),
Value::Null,
Value::Ref(EntityId(4)),
],
);
let t = CartesianTransformationOperator3D::new(EntityId(9), &e)
.transform(&model)
.unwrap();
assert!(close(t.basis[1], [0.0, -1.0, 0.0]), "got {:?}", t.basis[1]);
}
#[test]
fn zero_scale_is_degenerate() {
let e = operator_3d(Value::Real(0.0));
let err = CartesianTransformationOperator::new(EntityId(9), &e)
.scale()
.unwrap_err();
assert!(matches!(err, GeometryError::Degenerate { .. }), "{err}");
}
#[test]
fn negative_scale_is_degenerate() {
let e = operator_3d(Value::Real(-1.0));
assert!(CartesianTransformationOperator::new(EntityId(9), &e)
.scale()
.is_err());
}
#[test]
fn a_negative_secondary_scale_names_that_attribute() {
let e = Entity::new(
"IFCCARTESIANTRANSFORMATIONOPERATOR3DNONUNIFORM",
vec![
Value::Null,
Value::Null,
Value::Ref(EntityId(1)),
Value::Real(1.0),
Value::Null,
Value::Real(-2.0),
Value::Null,
],
);
let err = CartesianTransformationOperator3DnonUniform::new(EntityId(9), &e)
.scale2()
.unwrap_err();
assert!(err.to_string().contains("Scale2"), "got: {err}");
}
#[test]
fn a_zero_length_axis_is_degenerate_rather_than_nan() {
let mut model = model();
model.insert(
EntityId(9),
Entity::new("IFCDIRECTION", vec![coords(&[0.0, 0.0, 0.0])]),
);
let e = Entity::new(
"IFCCARTESIANTRANSFORMATIONOPERATOR3D",
vec![
Value::Null,
Value::Null,
Value::Ref(EntityId(1)),
Value::Null,
Value::Ref(EntityId(9)),
],
);
let err = CartesianTransformationOperator3D::new(EntityId(20), &e)
.transform(&model)
.unwrap_err();
assert!(matches!(err, GeometryError::Degenerate { .. }), "{err}");
}
#[test]
fn axis1_parallel_to_axis3_is_degenerate() {
let model = model();
let e = Entity::new(
"IFCCARTESIANTRANSFORMATIONOPERATOR3D",
vec![
Value::Ref(EntityId(4)),
Value::Null,
Value::Ref(EntityId(1)),
Value::Null,
Value::Ref(EntityId(4)),
],
);
assert!(CartesianTransformationOperator3D::new(EntityId(20), &e)
.transform(&model)
.is_err());
}
#[test]
fn a_missing_local_origin_names_the_entity_and_attribute() {
let e = Entity::new("IFCCARTESIANTRANSFORMATIONOPERATOR3D", vec![]);
let err = CartesianTransformationOperator::new(EntityId(42), &e)
.local_origin_ref()
.unwrap_err();
assert!(err.to_string().contains("#42"), "got: {err}");
assert!(err.to_string().contains("LocalOrigin"), "got: {err}");
}
#[test]
fn an_operator_without_axes_is_a_pure_translation_and_scale() {
let model = model();
let e = operator_3d(Value::Null);
let t = CartesianTransformationOperator3D::new(EntityId(9), &e)
.transform(&model)
.unwrap();
assert_eq!(t, Transform::translation([1.0, 2.0, 3.0]));
}
#[test]
fn a_two_d_operator_with_only_axis2_derives_x_from_it() {
let mut model = model();
model.insert(
EntityId(5),
Entity::new("IFCDIRECTION", vec![coords(&[0.0, 1.0])]),
);
let e = Entity::new(
"IFCCARTESIANTRANSFORMATIONOPERATOR2D",
vec![
Value::Null,
Value::Ref(EntityId(5)),
Value::Ref(EntityId(1)),
Value::Null,
],
);
let t = CartesianTransformationOperator2D::new(EntityId(9), &e)
.transform(&model)
.unwrap();
assert!(close(t.basis[0], [1.0, 0.0, 0.0]), "got {:?}", t.basis[0]);
assert!(close(t.basis[1], [0.0, 1.0, 0.0]), "got {:?}", t.basis[1]);
}
}