use super::super::GeometryRouter;
use crate::{Point3, Result, Vector3};
use ifc_lite_core::{DecodedEntity, EntityDecoder, IfcType};
use nalgebra::Matrix4;
impl GeometryRouter {
#[inline]
pub(crate) fn parse_cartesian_transformation_operator(
&self,
entity: &DecodedEntity,
decoder: &mut EntityDecoder,
) -> Result<Matrix4<f64>> {
parse_transformation_operator(entity, decoder)
}
}
pub(crate) fn parse_transformation_operator(
entity: &DecodedEntity,
decoder: &mut EntityDecoder,
) -> Result<Matrix4<f64>> {
let is_2d = matches!(
entity.ifc_type,
IfcType::IfcCartesianTransformationOperator2D
| IfcType::IfcCartesianTransformationOperator2DnonUniform
);
let origin = if let Some(origin_attr) = entity.get(2) {
if !origin_attr.is_null() {
if let Some(origin_entity) = decoder.resolve_ref(origin_attr)? {
if origin_entity.ifc_type == IfcType::IfcCartesianPoint {
let coords_attr = origin_entity.get(0);
if let Some(coords) = coords_attr.and_then(|a| a.as_list()) {
Point3::new(
coords.first().and_then(|v| v.as_float()).unwrap_or(0.0),
coords.get(1).and_then(|v| v.as_float()).unwrap_or(0.0),
coords.get(2).and_then(|v| v.as_float()).unwrap_or(0.0),
)
} else {
Point3::origin()
}
} else {
Point3::origin()
}
} else {
Point3::origin()
}
} else {
Point3::origin()
}
} else {
Point3::origin()
};
let scale = match entity.get_float(3) {
Some(v) if v.is_finite() => v,
_ => 1.0,
};
let is_non_uniform = matches!(
entity.ifc_type,
IfcType::IfcCartesianTransformationOperator2DnonUniform
| IfcType::IfcCartesianTransformationOperator3DnonUniform
);
let (scale_y, scale_z) = match (is_non_uniform, is_2d) {
(false, _) => (scale, scale),
(true, true) => (finite_scale(entity.get_float(4), scale), scale),
(true, false) => (
finite_scale(entity.get_float(5), scale),
finite_scale(entity.get_float(6), scale),
),
};
let x_axis_raw = if let Some(axis1_attr) = entity.get(0) {
if !axis1_attr.is_null() {
if let Some(axis1_entity) = decoder.resolve_ref(axis1_attr)? {
parse_direction_ratios(&axis1_entity)?
} else {
Vector3::new(1.0, 0.0, 0.0)
}
} else {
Vector3::new(1.0, 0.0, 0.0)
}
} else {
Vector3::new(1.0, 0.0, 0.0)
};
let y_axis_raw = if let Some(axis2_attr) = entity.get(1) {
if !axis2_attr.is_null() {
decoder
.resolve_ref(axis2_attr)?
.and_then(|e| parse_direction_ratios(&e).ok())
} else {
None
}
} else {
None
};
let z_axis_raw = match entity.get(4).filter(|_| !is_2d) {
Some(axis3_attr) if !axis3_attr.is_null() => {
if let Some(axis3_entity) = decoder.resolve_ref(axis3_attr)? {
parse_direction_ratios(&axis3_entity)?
} else {
Vector3::new(0.0, 0.0, 1.0)
}
}
_ => Vector3::new(0.0, 0.0, 1.0),
};
let z_axis = z_axis_raw
.try_normalize(1e-9)
.unwrap_or_else(|| Vector3::new(0.0, 0.0, 1.0));
let x_norm = x_axis_raw
.try_normalize(1e-9)
.unwrap_or_else(|| Vector3::new(1.0, 0.0, 0.0));
let x_ortho = x_norm - z_axis * x_norm.dot(&z_axis);
let x_axis = x_ortho.try_normalize(1e-6).unwrap_or_else(|| {
if z_axis.z.abs() < 0.9 {
Vector3::new(0.0, 0.0, 1.0).cross(&z_axis).normalize()
} else {
Vector3::new(1.0, 0.0, 0.0).cross(&z_axis).normalize()
}
});
let cross_y = z_axis.cross(&x_axis).normalize();
let y_axis = match y_axis_raw {
Some(raw) => {
let projected = raw - z_axis * raw.dot(&z_axis);
let projected = projected - x_axis * projected.dot(&x_axis);
match projected.try_normalize(1e-6) {
Some(v) if v.dot(&cross_y) < 1.0 - 1e-9 => v,
_ => cross_y,
}
}
None => cross_y,
};
let mut transform = Matrix4::identity();
transform[(0, 0)] = x_axis.x * scale;
transform[(1, 0)] = x_axis.y * scale;
transform[(2, 0)] = x_axis.z * scale;
transform[(0, 1)] = y_axis.x * scale_y;
transform[(1, 1)] = y_axis.y * scale_y;
transform[(2, 1)] = y_axis.z * scale_y;
transform[(0, 2)] = z_axis.x * scale_z;
transform[(1, 2)] = z_axis.y * scale_z;
transform[(2, 2)] = z_axis.z * scale_z;
transform[(0, 3)] = origin.x;
transform[(1, 3)] = origin.y;
transform[(2, 3)] = origin.z;
Ok(transform)
}
#[inline]
fn finite_scale(value: Option<f64>, default: f64) -> f64 {
match value {
Some(v) if v.is_finite() => v,
_ => default,
}
}
pub(super) fn parse_direction_ratios(entity: &DecodedEntity) -> Result<Vector3<f64>> {
if entity.ifc_type != IfcType::IfcDirection {
return Err(crate::Error::geometry(format!(
"Expected IfcDirection, got {}",
entity.ifc_type
)));
}
let ratios = entity
.get(0)
.and_then(|a| a.as_list())
.ok_or_else(|| crate::Error::geometry("IfcDirection missing ratios".to_string()))?;
Ok(Vector3::new(
ratios.first().and_then(|v| v.as_float()).unwrap_or(0.0),
ratios.get(1).and_then(|v| v.as_float()).unwrap_or(0.0),
ratios.get(2).and_then(|v| v.as_float()).unwrap_or(0.0),
))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cartesian_transformation_operator_zero_axis_stays_finite() {
let content = "\
#1=IFCDIRECTION((0.,0.,0.));
#3=IFCCARTESIANTRANSFORMATIONOPERATOR3D(#1,$,$,$,$);
";
let mut decoder = EntityDecoder::new(content);
let router = GeometryRouter::new();
let entity = decoder.decode_by_id(3).unwrap();
let m = router
.parse_cartesian_transformation_operator(&entity, &mut decoder)
.expect("operator should still parse");
assert!(m.iter().all(|v| v.is_finite()), "NaN in transform matrix: {m:?}");
}
#[test]
fn operator_2d_non_uniform_reads_scale2_from_attribute_4() {
let content = "\
#1=IFCCARTESIANPOINT((0.,0.));
#2=IFCCARTESIANTRANSFORMATIONOPERATOR2DNONUNIFORM($,$,#1,2.,5.);
";
let mut decoder = EntityDecoder::new(content);
let router = GeometryRouter::new();
let entity = decoder.decode_by_id(2).unwrap();
let m = router
.parse_cartesian_transformation_operator(&entity, &mut decoder)
.expect("2D non-uniform operator should parse");
assert!((m[(0, 0)] - 2.0).abs() < 1e-12, "X scale: {m:?}");
assert!((m[(1, 1)] - 5.0).abs() < 1e-12, "Y scale (Scale2, attr 4): {m:?}");
assert!((m[(2, 2)] - 2.0).abs() < 1e-12, "Z scale: {m:?}");
assert!(m[(0, 2)].abs() < 1e-12 && m[(1, 2)].abs() < 1e-12, "Z axis: {m:?}");
}
#[test]
fn operator_honours_a_mirroring_axis2() {
let content = "\
#1=IFCDIRECTION((1.,0.,0.));
#2=IFCDIRECTION((0.,-1.,0.));
#3=IFCDIRECTION((0.,0.,1.));
#4=IFCCARTESIANPOINT((0.,0.,0.));
#5=IFCCARTESIANTRANSFORMATIONOPERATOR3D(#1,#2,#4,$,#3);
";
let mut decoder = EntityDecoder::new(content);
let router = GeometryRouter::new();
let entity = decoder.decode_by_id(5).unwrap();
let m = router
.parse_cartesian_transformation_operator(&entity, &mut decoder)
.expect("mirroring operator should parse");
assert!(
(m[(1, 1)] + 1.0).abs() < 1e-12,
"Y axis must follow the supplied Axis2 (mirror), got {m:?}"
);
assert!(
m.determinant() < 0.0,
"a mirroring frame must keep its negative determinant: {m:?}"
);
}
#[test]
fn consistent_axis2_keeps_the_cross_product_bits() {
let with_axis2 = "\
#1=IFCDIRECTION((0.,0.6,0.8));
#2=IFCDIRECTION((0.,-0.8,0.6));
#3=IFCDIRECTION((1.,0.,0.));
#4=IFCCARTESIANPOINT((0.,0.,0.));
#5=IFCCARTESIANTRANSFORMATIONOPERATOR3D(#1,#2,#4,$,#3);
";
let without_axis2 = "\
#1=IFCDIRECTION((0.,0.6,0.8));
#3=IFCDIRECTION((1.,0.,0.));
#4=IFCCARTESIANPOINT((0.,0.,0.));
#5=IFCCARTESIANTRANSFORMATIONOPERATOR3D(#1,$,#4,$,#3);
";
let router = GeometryRouter::new();
let parse = |content: &str| {
let mut decoder = EntityDecoder::new(content);
let entity = decoder.decode_by_id(5).unwrap();
router
.parse_cartesian_transformation_operator(&entity, &mut decoder)
.expect("operator should parse")
};
let a = parse(with_axis2);
let b = parse(without_axis2);
assert_eq!(
a.iter().map(|v| v.to_bits()).collect::<Vec<_>>(),
b.iter().map(|v| v.to_bits()).collect::<Vec<_>>(),
"a consistent Axis2 must not perturb the derived frame"
);
}
}