use crate::error::{GeometryError, GeometryResult};
use crate::resource::placement::axis_placement_transform;
use crate::slots::Slots;
use crate::transform::Transform;
use ifc_model::{EntityId, Model};
use std::collections::HashMap;
pub(crate) mod slot {
pub const PLACEMENT_REL_TO: usize = 0;
pub const RELATIVE_PLACEMENT: usize = 1;
}
#[derive(Debug, Clone, Copy)]
pub struct LocalPlacement<'m> {
slots: Slots<'m>,
}
impl<'m> LocalPlacement<'m> {
pub fn new(id: EntityId, entity: &'m ifc_model::Entity) -> Self {
Self {
slots: Slots::new(id, entity),
}
}
pub fn id(&self) -> EntityId {
self.slots.id()
}
pub fn parent(&self) -> Option<EntityId> {
self.slots.opt_ref(slot::PLACEMENT_REL_TO)
}
pub fn relative_placement(&self) -> GeometryResult<EntityId> {
self.slots
.req_ref(slot::RELATIVE_PLACEMENT, "RelativePlacement")
}
pub fn local_transform(&self, model: &'m Model) -> GeometryResult<Transform> {
let placement_id = self.relative_placement()?;
let entity = self.slots.resolve(model, placement_id)?;
axis_placement_transform(model, placement_id, entity)
}
}
const MAX_CHAIN_DEPTH: usize = 64;
#[derive(Debug, Default)]
pub struct PlacementResolver {
cache: HashMap<EntityId, Transform>,
}
impl PlacementResolver {
pub fn new() -> Self {
Self::default()
}
pub fn cached(&self) -> usize {
self.cache.len()
}
pub fn world_transform(
&mut self,
model: &Model,
placement: EntityId,
) -> GeometryResult<Transform> {
if let Some(cached) = self.cache.get(&placement) {
return Ok(*cached);
}
let mut chain = Vec::new();
let mut visited = Vec::new();
let mut current = Some(placement);
while let Some(id) = current {
if visited.contains(&id) {
return Err(GeometryError::CyclicChain {
entity: id,
kind: "placement",
});
}
if chain.len() >= MAX_CHAIN_DEPTH {
return Err(GeometryError::ChainTooDeep {
entity: id,
kind: "placement",
limit: MAX_CHAIN_DEPTH,
});
}
visited.push(id);
if self.cache.contains_key(&id) {
break;
}
let entity = model.get(id).ok_or(GeometryError::MissingEntity {
referrer: placement,
missing: id,
})?;
match entity.type_name.as_ref() {
"IFCLOCALPLACEMENT" => {
let view = LocalPlacement::new(id, entity);
chain.push(id);
current = view.parent();
}
"IFCGRIDPLACEMENT" => {
return Err(GeometryError::Unsupported {
entity: id,
type_name: entity.type_name.to_string(),
detail: "grid placement resolution",
});
}
other => {
return Err(GeometryError::WrongEntityType {
entity: id,
actual: other.to_string(),
expected: "IfcLocalPlacement",
});
}
}
}
let mut world = current
.and_then(|id| self.cache.get(&id).copied())
.unwrap_or_else(Transform::identity);
for id in chain.iter().rev() {
let entity = model.get(*id).ok_or(GeometryError::MissingEntity {
referrer: placement,
missing: *id,
})?;
let local = LocalPlacement::new(*id, entity).local_transform(model)?;
world = world.compose(&local);
self.cache.insert(*id, world);
}
Ok(world)
}
}
#[cfg(test)]
mod tests {
use super::*;
use ifc_model::{Entity, Value};
fn placement_at(model: &mut Model, id: u64, point_id: u64, xyz: [f64; 3]) {
model.insert(
EntityId(point_id),
Entity::new(
"IFCCARTESIANPOINT",
vec![Value::List(
xyz.iter().map(|v| Value::Real(*v)).collect::<Vec<_>>(),
)],
),
);
model.insert(
EntityId(id),
Entity::new(
"IFCAXIS2PLACEMENT3D",
vec![Value::Ref(EntityId(point_id)), Value::Null, Value::Null],
),
);
}
fn local(model: &mut Model, id: u64, parent: Option<u64>, axis: u64) {
model.insert(
EntityId(id),
Entity::new(
"IFCLOCALPLACEMENT",
vec![
parent.map_or(Value::Null, |p| Value::Ref(EntityId(p))),
Value::Ref(EntityId(axis)),
],
),
);
}
fn three_level_model() -> Model {
let mut model = Model::new();
placement_at(&mut model, 10, 11, [0.0, 0.0, 0.0]);
placement_at(&mut model, 20, 21, [0.0, 0.0, 3.0]);
placement_at(&mut model, 30, 31, [1.0, 0.0, 0.0]);
local(&mut model, 1, None, 10);
local(&mut model, 2, Some(1), 20);
local(&mut model, 3, Some(2), 30);
model
}
#[test]
fn absent_parent_means_world_coordinates() {
let model = three_level_model();
let mut resolver = PlacementResolver::new();
let t = resolver.world_transform(&model, EntityId(1)).unwrap();
assert!(t.is_identity(1e-12));
}
#[test]
fn chain_composes_from_root_downward() {
let model = three_level_model();
let mut resolver = PlacementResolver::new();
let t = resolver.world_transform(&model, EntityId(3)).unwrap();
assert_eq!(t.origin, [1.0, 0.0, 3.0], "storey height must accumulate");
}
#[test]
fn cyclic_chains_are_detected_not_stack_overflowed() {
let mut model = Model::new();
placement_at(&mut model, 10, 11, [0.0, 0.0, 0.0]);
local(&mut model, 1, Some(2), 10);
local(&mut model, 2, Some(1), 10);
let mut resolver = PlacementResolver::new();
let err = resolver.world_transform(&model, EntityId(1)).unwrap_err();
assert!(
matches!(err, GeometryError::CyclicChain { .. }),
"expected a cycle error, got {err}"
);
}
#[test]
fn self_reference_is_a_cycle() {
let mut model = Model::new();
placement_at(&mut model, 10, 11, [0.0, 0.0, 0.0]);
local(&mut model, 1, Some(1), 10);
let mut resolver = PlacementResolver::new();
assert!(matches!(
resolver.world_transform(&model, EntityId(1)).unwrap_err(),
GeometryError::CyclicChain { .. }
));
}
#[test]
fn shared_ancestors_are_resolved_once() {
let model = three_level_model();
let mut resolver = PlacementResolver::new();
resolver.world_transform(&model, EntityId(3)).unwrap();
let after_first = resolver.cached();
resolver.world_transform(&model, EntityId(2)).unwrap();
assert_eq!(
resolver.cached(),
after_first,
"resolving an already-cached ancestor must not recompute"
);
}
#[test]
fn dangling_parent_reference_is_reported() {
let mut model = Model::new();
placement_at(&mut model, 10, 11, [0.0, 0.0, 0.0]);
local(&mut model, 1, Some(999), 10);
let mut resolver = PlacementResolver::new();
assert!(matches!(
resolver.world_transform(&model, EntityId(1)).unwrap_err(),
GeometryError::MissingEntity { .. }
));
}
#[test]
fn grid_placement_reports_unsupported_rather_than_defaulting_to_origin() {
let mut model = Model::new();
model.insert(
EntityId(1),
Entity::new("IFCGRIDPLACEMENT", vec![Value::Null, Value::Null]),
);
let mut resolver = PlacementResolver::new();
let err = resolver.world_transform(&model, EntityId(1)).unwrap_err();
assert!(err.is_unsupported(), "got {err}");
}
}