1use crate::error::{GeometryError, GeometryResult};
25use crate::resource::placement::axis_placement_transform;
26use crate::slots::Slots;
27use crate::transform::Transform;
28use ifc_model::{EntityId, Model};
29use std::collections::HashMap;
30
31pub(crate) mod slot {
37 pub const PLACEMENT_REL_TO: usize = 0;
39 pub const RELATIVE_PLACEMENT: usize = 1;
41}
42
43#[derive(Debug, Clone, Copy)]
45pub struct LocalPlacement<'m> {
46 slots: Slots<'m>,
47}
48
49impl<'m> LocalPlacement<'m> {
50 pub fn new(id: EntityId, entity: &'m ifc_model::Entity) -> Self {
52 Self {
53 slots: Slots::new(id, entity),
54 }
55 }
56
57 pub fn id(&self) -> EntityId {
59 self.slots.id()
60 }
61
62 pub fn parent(&self) -> Option<EntityId> {
66 self.slots.opt_ref(slot::PLACEMENT_REL_TO)
67 }
68
69 pub fn relative_placement(&self) -> GeometryResult<EntityId> {
71 self.slots
72 .req_ref(slot::RELATIVE_PLACEMENT, "RelativePlacement")
73 }
74
75 pub fn local_transform(&self, model: &'m Model) -> GeometryResult<Transform> {
77 let placement_id = self.relative_placement()?;
78 let entity = self.slots.resolve(model, placement_id)?;
79 axis_placement_transform(model, placement_id, entity)
80 }
81}
82
83const MAX_CHAIN_DEPTH: usize = 64;
89
90#[derive(Debug, Default)]
99pub struct PlacementResolver {
100 cache: HashMap<EntityId, Transform>,
101}
102
103impl PlacementResolver {
104 pub fn new() -> Self {
106 Self::default()
107 }
108
109 pub fn cached(&self) -> usize {
111 self.cache.len()
112 }
113
114 pub fn world_transform(
120 &mut self,
121 model: &Model,
122 placement: EntityId,
123 ) -> GeometryResult<Transform> {
124 if let Some(cached) = self.cache.get(&placement) {
125 return Ok(*cached);
126 }
127
128 let mut chain = Vec::new();
131 let mut visited = Vec::new();
132 let mut current = Some(placement);
133
134 while let Some(id) = current {
135 if visited.contains(&id) {
136 return Err(GeometryError::CyclicChain {
137 entity: id,
138 kind: "placement",
139 });
140 }
141 if chain.len() >= MAX_CHAIN_DEPTH {
142 return Err(GeometryError::ChainTooDeep {
143 entity: id,
144 kind: "placement",
145 limit: MAX_CHAIN_DEPTH,
146 });
147 }
148 visited.push(id);
149
150 if self.cache.contains_key(&id) {
152 break;
153 }
154
155 let entity = model.get(id).ok_or(GeometryError::MissingEntity {
156 referrer: placement,
157 missing: id,
158 })?;
159
160 match entity.type_name.as_ref() {
161 "IFCLOCALPLACEMENT" => {
162 let view = LocalPlacement::new(id, entity);
163 chain.push(id);
164 current = view.parent();
165 }
166 "IFCGRIDPLACEMENT" => {
171 return Err(GeometryError::Unsupported {
172 entity: id,
173 type_name: entity.type_name.to_string(),
174 detail: "grid placement resolution",
175 });
176 }
177 other => {
178 return Err(GeometryError::WrongEntityType {
179 entity: id,
180 actual: other.to_string(),
181 expected: "IfcLocalPlacement",
182 });
183 }
184 }
185 }
186
187 let mut world = current
189 .and_then(|id| self.cache.get(&id).copied())
190 .unwrap_or_else(Transform::identity);
191
192 for id in chain.iter().rev() {
194 let entity = model.get(*id).ok_or(GeometryError::MissingEntity {
195 referrer: placement,
196 missing: *id,
197 })?;
198 let local = LocalPlacement::new(*id, entity).local_transform(model)?;
199 world = world.compose(&local);
200 self.cache.insert(*id, world);
201 }
202
203 Ok(world)
204 }
205}
206
207#[cfg(test)]
208mod tests {
209 use super::*;
210 use ifc_model::{Entity, Value};
211
212 fn placement_at(model: &mut Model, id: u64, point_id: u64, xyz: [f64; 3]) {
214 model.insert(
215 EntityId(point_id),
216 Entity::new(
217 "IFCCARTESIANPOINT",
218 vec![Value::List(
219 xyz.iter().map(|v| Value::Real(*v)).collect::<Vec<_>>(),
220 )],
221 ),
222 );
223 model.insert(
224 EntityId(id),
225 Entity::new(
226 "IFCAXIS2PLACEMENT3D",
227 vec![Value::Ref(EntityId(point_id)), Value::Null, Value::Null],
228 ),
229 );
230 }
231
232 fn local(model: &mut Model, id: u64, parent: Option<u64>, axis: u64) {
234 model.insert(
235 EntityId(id),
236 Entity::new(
237 "IFCLOCALPLACEMENT",
238 vec![
239 parent.map_or(Value::Null, |p| Value::Ref(EntityId(p))),
240 Value::Ref(EntityId(axis)),
241 ],
242 ),
243 );
244 }
245
246 fn three_level_model() -> Model {
248 let mut model = Model::new();
249 placement_at(&mut model, 10, 11, [0.0, 0.0, 0.0]);
250 placement_at(&mut model, 20, 21, [0.0, 0.0, 3.0]);
251 placement_at(&mut model, 30, 31, [1.0, 0.0, 0.0]);
252 local(&mut model, 1, None, 10);
253 local(&mut model, 2, Some(1), 20);
254 local(&mut model, 3, Some(2), 30);
255 model
256 }
257
258 #[test]
259 fn absent_parent_means_world_coordinates() {
260 let model = three_level_model();
261 let mut resolver = PlacementResolver::new();
262 let t = resolver.world_transform(&model, EntityId(1)).unwrap();
263 assert!(t.is_identity(1e-12));
264 }
265
266 #[test]
267 fn chain_composes_from_root_downward() {
268 let model = three_level_model();
269 let mut resolver = PlacementResolver::new();
270 let t = resolver.world_transform(&model, EntityId(3)).unwrap();
271 assert_eq!(t.origin, [1.0, 0.0, 3.0], "storey height must accumulate");
272 }
273
274 #[test]
277 fn cyclic_chains_are_detected_not_stack_overflowed() {
278 let mut model = Model::new();
279 placement_at(&mut model, 10, 11, [0.0, 0.0, 0.0]);
280 local(&mut model, 1, Some(2), 10);
281 local(&mut model, 2, Some(1), 10);
282
283 let mut resolver = PlacementResolver::new();
284 let err = resolver.world_transform(&model, EntityId(1)).unwrap_err();
285 assert!(
286 matches!(err, GeometryError::CyclicChain { .. }),
287 "expected a cycle error, got {err}"
288 );
289 }
290
291 #[test]
293 fn self_reference_is_a_cycle() {
294 let mut model = Model::new();
295 placement_at(&mut model, 10, 11, [0.0, 0.0, 0.0]);
296 local(&mut model, 1, Some(1), 10);
297
298 let mut resolver = PlacementResolver::new();
299 assert!(matches!(
300 resolver.world_transform(&model, EntityId(1)).unwrap_err(),
301 GeometryError::CyclicChain { .. }
302 ));
303 }
304
305 #[test]
306 fn shared_ancestors_are_resolved_once() {
307 let model = three_level_model();
308 let mut resolver = PlacementResolver::new();
309 resolver.world_transform(&model, EntityId(3)).unwrap();
310 let after_first = resolver.cached();
311
312 resolver.world_transform(&model, EntityId(2)).unwrap();
314 assert_eq!(
315 resolver.cached(),
316 after_first,
317 "resolving an already-cached ancestor must not recompute"
318 );
319 }
320
321 #[test]
322 fn dangling_parent_reference_is_reported() {
323 let mut model = Model::new();
324 placement_at(&mut model, 10, 11, [0.0, 0.0, 0.0]);
325 local(&mut model, 1, Some(999), 10);
326
327 let mut resolver = PlacementResolver::new();
328 assert!(matches!(
329 resolver.world_transform(&model, EntityId(1)).unwrap_err(),
330 GeometryError::MissingEntity { .. }
331 ));
332 }
333
334 #[test]
337 fn grid_placement_reports_unsupported_rather_than_defaulting_to_origin() {
338 let mut model = Model::new();
339 model.insert(
340 EntityId(1),
341 Entity::new("IFCGRIDPLACEMENT", vec![Value::Null, Value::Null]),
342 );
343 let mut resolver = PlacementResolver::new();
344 let err = resolver.world_transform(&model, EntityId(1)).unwrap_err();
345 assert!(err.is_unsupported(), "got {err}");
346 }
347}