use super::super::GeometryRouter;
use crate::profiles::ProfileProcessor;
use crate::{Point3, Result, TessellationQuality, Vector3};
use ifc_lite_core::{DecodedEntity, EntityDecoder, IfcSchema, IfcType};
use nalgebra::Matrix4;
impl GeometryRouter {
pub(super) fn resolve_linear_placement_with_depth(
&self,
placement: &DecodedEntity,
decoder: &mut EntityDecoder,
depth: usize,
) -> Result<Matrix4<f64>> {
let parent_transform = if let Some(parent_attr) = placement.get(0) {
if !parent_attr.is_null() {
if let Some(parent) = decoder.resolve_ref(parent_attr)? {
self.get_placement_transform_with_depth(&parent, decoder, depth + 1)?
} else {
Matrix4::identity()
}
} else {
Matrix4::identity()
}
} else {
Matrix4::identity()
};
let local = match self.try_resolve_axis2_placement_linear(placement, decoder) {
Some(m) => m,
None => self.try_resolve_cartesian_fallback(placement, decoder),
};
Ok(parent_transform * local)
}
fn try_resolve_axis2_placement_linear(
&self,
placement: &DecodedEntity,
decoder: &mut EntityDecoder,
) -> Option<Matrix4<f64>> {
let rel_attr = placement.get(1)?;
if rel_attr.is_null() {
return None;
}
let rel = decoder.resolve_ref(rel_attr).ok().flatten()?;
if rel.ifc_type != IfcType::IfcAxis2PlacementLinear {
return None;
}
let location_attr = rel.get(0)?;
if location_attr.is_null() {
return None;
}
let location = decoder.resolve_ref(location_attr).ok().flatten()?;
if location.ifc_type != IfcType::IfcPointByDistanceExpression {
return None;
}
let distance_along = location.get_float(0)?;
let offset_lateral = location.get_float(1).unwrap_or(0.0);
let offset_vertical = location.get_float(2).unwrap_or(0.0);
let offset_longitudinal = location.get_float(3).unwrap_or(0.0);
let basis_attr = location.get(4)?;
if basis_attr.is_null() {
return None;
}
let basis_curve = decoder.resolve_ref(basis_attr).ok().flatten()?;
let processor = ProfileProcessor::new(IfcSchema::new());
let samples = processor
.get_curve_points(&basis_curve, decoder, TessellationQuality::Medium)
.ok()
.filter(|pts| pts.len() >= 2)?;
let (origin, tangent) = sample_polyline_at_distance(&samples, distance_along)?;
let world_up = Vector3::new(0.0, 0.0, 1.0);
let tangent_horiz_norm =
(tangent - world_up * tangent.dot(&world_up)).norm();
let (x_axis, y_axis, z_axis) = if tangent_horiz_norm > 1e-9 {
let x = tangent.normalize();
let y = world_up.cross(&x).normalize();
let z = x.cross(&y).normalize();
(x, y, z)
} else {
(
Vector3::new(1.0, 0.0, 0.0),
Vector3::new(0.0, 1.0, 0.0),
Vector3::new(0.0, 0.0, 1.0),
)
};
let position = origin.coords
+ x_axis * offset_longitudinal
+ y_axis * offset_lateral
+ z_axis * offset_vertical;
let mut m = Matrix4::<f64>::identity();
m.fixed_view_mut::<3, 1>(0, 0).copy_from(&x_axis);
m.fixed_view_mut::<3, 1>(0, 1).copy_from(&y_axis);
m.fixed_view_mut::<3, 1>(0, 2).copy_from(&z_axis);
m[(0, 3)] = position.x;
m[(1, 3)] = position.y;
m[(2, 3)] = position.z;
Some(m)
}
fn try_resolve_cartesian_fallback(
&self,
placement: &DecodedEntity,
decoder: &mut EntityDecoder,
) -> Matrix4<f64> {
let Some(cart_attr) = placement.get(2) else {
return Matrix4::identity();
};
if cart_attr.is_null() {
return Matrix4::identity();
}
let Ok(Some(cart)) = decoder.resolve_ref(cart_attr) else {
return Matrix4::identity();
};
if cart.ifc_type != IfcType::IfcAxis2Placement3D {
return Matrix4::identity();
}
self.parse_axis2_placement_3d(&cart, decoder)
.unwrap_or_else(|_| Matrix4::identity())
}
}
fn sample_polyline_at_distance(
samples: &[Point3<f64>],
distance: f64,
) -> Option<(Point3<f64>, Vector3<f64>)> {
if samples.len() < 2 {
return None;
}
if distance <= 0.0 {
let tangent = (samples[1] - samples[0])
.try_normalize(1e-12)
.unwrap_or_else(|| Vector3::new(1.0, 0.0, 0.0));
return Some((samples[0], tangent));
}
let mut acc = 0.0;
for window in samples.windows(2) {
let a = window[0];
let b = window[1];
let seg = b - a;
let len = seg.norm();
if len < 1e-12 {
continue;
}
if acc + len >= distance {
let t = ((distance - acc) / len).clamp(0.0, 1.0);
let position = a + seg * t;
let tangent = (seg / len)
.try_normalize(1e-12)
.unwrap_or_else(|| Vector3::new(1.0, 0.0, 0.0));
return Some((position, tangent));
}
acc += len;
}
let last = samples[samples.len() - 1];
let prev = samples[samples.len() - 2];
let tangent = (last - prev)
.try_normalize(1e-12)
.unwrap_or_else(|| Vector3::new(1.0, 0.0, 0.0));
Some((last, tangent))
}
#[cfg(test)]
mod sample_polyline_tests {
use super::*;
#[test]
fn samples_at_start_middle_end() {
let samples: Vec<Point3<f64>> = (0..=10)
.map(|i| Point3::new(i as f64, 0.0, 0.0))
.collect();
let (p0, t0) = sample_polyline_at_distance(&samples, 0.0).unwrap();
assert!((p0 - Point3::new(0.0, 0.0, 0.0)).norm() < 1e-9);
assert!((t0 - Vector3::new(1.0, 0.0, 0.0)).norm() < 1e-9);
let (p5, t5) = sample_polyline_at_distance(&samples, 5.0).unwrap();
assert!((p5 - Point3::new(5.0, 0.0, 0.0)).norm() < 1e-9);
assert!((t5 - Vector3::new(1.0, 0.0, 0.0)).norm() < 1e-9);
let (p10, _) = sample_polyline_at_distance(&samples, 10.0).unwrap();
assert!((p10 - Point3::new(10.0, 0.0, 0.0)).norm() < 1e-9);
}
#[test]
fn clamps_past_end() {
let samples = vec![
Point3::new(0.0, 0.0, 0.0),
Point3::new(3.0, 4.0, 0.0), ];
let (p, t) = sample_polyline_at_distance(&samples, 99.0).unwrap();
assert!((p - Point3::new(3.0, 4.0, 0.0)).norm() < 1e-9);
assert!((t.norm() - 1.0).abs() < 1e-9, "tangent must be unit");
}
#[test]
fn empty_returns_none() {
let none = sample_polyline_at_distance(&[], 0.0);
assert!(none.is_none());
let single = sample_polyline_at_distance(&[Point3::new(0.0, 0.0, 0.0)], 0.0);
assert!(single.is_none());
}
}