use super::*;
use ifc_model::{Entity, Value};
fn axes_at(model: &mut Model, id: u64, point: u64, xyz: [f64; 3]) {
model.insert(
EntityId(point),
Entity::new(
"IFCCARTESIANPOINT",
vec![Value::List(xyz.iter().map(|v| Value::Real(*v)).collect())],
),
);
model.insert(
EntityId(id),
Entity::new(
"IFCAXIS2PLACEMENT3D",
vec![Value::Ref(EntityId(point)), Value::Null, Value::Null],
),
);
}
fn local_placement(model: &mut Model, id: u64, parent: Option<u64>, axes: u64) {
let rel_to = parent.map_or(Value::Null, |p| Value::Ref(EntityId(p)));
model.insert(
EntityId(id),
Entity::new(
"IFCLOCALPLACEMENT",
vec![rel_to, Value::Ref(EntityId(axes))],
),
);
}
fn product(model: &mut Model, id: u64, placement: Option<u64>) {
let mut slots = vec![Value::Null; 7];
if let Some(p) = placement {
slots[5] = Value::Ref(EntityId(p));
}
model.insert(EntityId(id), Entity::new("IFCWALL", slots));
}
#[test]
fn nested_placement_composes_outermost_last() {
let mut model = Model::new();
axes_at(&mut model, 10, 11, [0.0, 0.0, 3.0]); axes_at(&mut model, 20, 21, [0.0, 0.0, 2.0]); local_placement(&mut model, 30, None, 10);
local_placement(&mut model, 40, Some(30), 20);
product(&mut model, 50, Some(40));
let world = product_world_transform(&model, &UnitScale::default(), EntityId(50)).unwrap();
assert_eq!(world.origin, [0.0, 0.0, 5.0]);
}
#[test]
fn unit_scale_applies_once_not_per_link() {
let mut model = Model::new();
axes_at(&mut model, 10, 11, [0.0, 0.0, 3000.0]);
axes_at(&mut model, 20, 21, [0.0, 0.0, 2000.0]);
local_placement(&mut model, 30, None, 10);
local_placement(&mut model, 40, Some(30), 20);
product(&mut model, 50, Some(40));
let millimetres = UnitScale {
length_to_metres: 0.001,
..UnitScale::default()
};
let world = product_world_transform(&model, &millimetres, EntityId(50)).unwrap();
assert_eq!(world.origin, [0.0, 0.0, 5.0]);
}
#[test]
fn missing_object_placement_is_identity() {
let mut model = Model::new();
product(&mut model, 50, None);
let world = product_world_transform(&model, &UnitScale::default(), EntityId(50)).unwrap();
assert!(world.is_identity(1e-12));
}
#[test]
fn cyclic_chain_is_reported() {
let mut model = Model::new();
axes_at(&mut model, 10, 11, [1.0, 0.0, 0.0]);
local_placement(&mut model, 30, Some(40), 10);
local_placement(&mut model, 40, Some(30), 10);
product(&mut model, 50, Some(40));
let err = product_world_transform(&model, &UnitScale::default(), EntityId(50)).unwrap_err();
assert!(
matches!(err, GeometryError::CyclicChain { .. }),
"expected a cycle report, got {err:?}"
);
}
#[test]
fn missing_product_is_an_error() {
let model = Model::new();
let err = product_world_transform(&model, &UnitScale::default(), EntityId(99)).unwrap_err();
assert!(matches!(err, GeometryError::MissingEntity { .. }));
}
#[test]
fn batch_matches_single_and_isolates_failures() {
let mut model = Model::new();
axes_at(&mut model, 10, 11, [0.0, 0.0, 3.0]);
local_placement(&mut model, 30, None, 10);
product(&mut model, 50, Some(30));
axes_at(&mut model, 60, 61, [1.0, 0.0, 0.0]);
local_placement(&mut model, 70, Some(80), 60);
local_placement(&mut model, 80, Some(70), 60);
product(&mut model, 90, Some(80));
let units = UnitScale::default();
let results = products_world_transforms(&model, &units, [EntityId(50), EntityId(90)]);
assert_eq!(results.len(), 2);
let good = results[0].1.as_ref().unwrap();
assert_eq!(
good.origin,
product_world_transform(&model, &units, EntityId(50))
.unwrap()
.origin
);
assert!(
results[1].1.is_err(),
"the cyclic product must still report"
);
}
#[test]
fn shared_cache_agrees_with_independent_resolution() {
let mut model = Model::new();
axes_at(&mut model, 10, 11, [0.0, 0.0, 3.0]); local_placement(&mut model, 30, None, 10);
for (i, offset) in [(0u64, 1.0), (1, 2.0), (2, 3.0)] {
let base = 100 + i * 10;
axes_at(&mut model, base, base + 1, [0.0, 0.0, offset]);
local_placement(&mut model, base + 2, Some(30), base);
product(&mut model, base + 3, Some(base + 2));
}
let units = UnitScale::default();
let ids = [EntityId(103), EntityId(113), EntityId(123)];
let batched = products_world_transforms(&model, &units, ids);
for (id, result) in batched {
let independent = product_world_transform(&model, &units, id).unwrap();
assert_eq!(result.unwrap().origin, independent.origin);
}
}