ifc_geometry/resource/mapped.rs
1//! `IfcMappedItem` and `IfcRepresentationMap`: geometry reuse by reference.
2//!
3//! # What it means
4//!
5//! A mapped item is IFC's block/instance mechanism. One geometry definition
6//! (the [`RepresentationMap`]) is authored once, and every place it appears is
7//! an [`MappedItem`] that references it plus a transform. A file with 400
8//! identical windows stores one window and 400 small records.
9//!
10//! ```text
11//! IfcMappedItem ---- MappingSource ---> IfcRepresentationMap
12//! | |
13//! | MappingOrigin (IfcAxis2Placement)
14//! +---- MappingTarget ---> |
15//! IfcCartesianTransformationOperator
16//! MappedRepresentation
17//! |
18//! v
19//! the actual geometry
20//! ```
21//!
22//! # The trap: mapped items nest
23//!
24//! The spec is explicit that "an IfcMappedItem can reuse other mapped items
25//! (ako nested blocks)": the `MappedRepresentation` may itself contain mapped
26//! items. A naive resolver that assumes one level silently drops geometry, and
27//! one that recurses without a guard hangs on the cyclic files that real
28//! exporters have produced. The fixture corpus contains
29//! `nested_mapped_item_cycle.ifc` for exactly this reason.
30//!
31//! # The full transform
32//!
33//! Placing a mapped item is **two** transforms composed, not one:
34//!
35//! 1. `MappingOrigin` on the representation map -- where the source geometry
36//! sits in its own definition space;
37//! 2. `MappingTarget` on the mapped item -- where that space lands in the
38//! consuming representation.
39//!
40//! Applying only the target is a common bug, and it is invisible whenever
41//! `MappingOrigin` happens to be the identity, which it usually is.
42
43use crate::error::{GeometryError, GeometryResult};
44use crate::slots::Slots;
45use ifc_model::{Entity, EntityId, Model};
46
47/// `IfcMappedItem` attribute slots.
48///
49/// `IfcMappedItem` inherits from `IfcRepresentationItem`, which declares no
50/// explicit attributes, so these indices are its own.
51pub(crate) mod item_slot {
52 /// `MappingSource`: the `IfcRepresentationMap` being instanced.
53 pub const MAPPING_SOURCE: usize = 0;
54 /// `MappingTarget`: an `IfcCartesianTransformationOperator`.
55 pub const MAPPING_TARGET: usize = 1;
56}
57
58/// `IfcRepresentationMap` attribute slots.
59pub(crate) mod map_slot {
60 /// `MappingOrigin`: an `IfcAxis2Placement` for the source geometry.
61 pub const MAPPING_ORIGIN: usize = 0;
62 /// `MappedRepresentation`: the `IfcRepresentation` being reused.
63 pub const MAPPED_REPRESENTATION: usize = 1;
64}
65
66/// How deep mapped items may nest before the file is called malformed.
67///
68/// Legitimate nesting is shallow (a window in a wall assembly in a facade
69/// module). 32 leaves ample room while terminating quickly on a cycle.
70const MAX_NESTING_DEPTH: usize = 32;
71
72/// A borrowed view of an `IfcMappedItem`.
73#[derive(Debug, Clone, Copy)]
74pub struct MappedItem<'m> {
75 slots: Slots<'m>,
76}
77
78impl<'m> MappedItem<'m> {
79 /// Wrap an entity assumed to be an `IfcMappedItem`.
80 pub fn new(id: EntityId, entity: &'m Entity) -> Self {
81 Self {
82 slots: Slots::new(id, entity),
83 }
84 }
85
86 /// The entity id.
87 pub fn id(&self) -> EntityId {
88 self.slots.id()
89 }
90
91 /// The `IfcRepresentationMap` this item instances.
92 pub fn mapping_source(&self) -> GeometryResult<EntityId> {
93 self.slots
94 .req_ref(item_slot::MAPPING_SOURCE, "MappingSource")
95 }
96
97 /// The `IfcCartesianTransformationOperator` placing the instance.
98 pub fn mapping_target(&self) -> GeometryResult<EntityId> {
99 self.slots
100 .req_ref(item_slot::MAPPING_TARGET, "MappingTarget")
101 }
102}
103
104/// A borrowed view of an `IfcRepresentationMap`.
105#[derive(Debug, Clone, Copy)]
106pub struct RepresentationMap<'m> {
107 slots: Slots<'m>,
108}
109
110impl<'m> RepresentationMap<'m> {
111 /// Wrap an entity assumed to be an `IfcRepresentationMap`.
112 pub fn new(id: EntityId, entity: &'m Entity) -> Self {
113 Self {
114 slots: Slots::new(id, entity),
115 }
116 }
117
118 /// The entity id.
119 pub fn id(&self) -> EntityId {
120 self.slots.id()
121 }
122
123 /// The `IfcAxis2Placement` locating the source geometry.
124 pub fn mapping_origin(&self) -> GeometryResult<EntityId> {
125 self.slots
126 .req_ref(map_slot::MAPPING_ORIGIN, "MappingOrigin")
127 }
128
129 /// The `IfcRepresentation` being reused.
130 pub fn mapped_representation(&self) -> GeometryResult<EntityId> {
131 self.slots
132 .req_ref(map_slot::MAPPED_REPRESENTATION, "MappedRepresentation")
133 }
134}
135
136/// Walks mapped-item nesting, detecting cycles and excessive depth.
137///
138/// Separate from the views because the views are stateless borrows while a
139/// safe walk needs to remember where it has been.
140#[derive(Debug, Default)]
141pub struct MappingWalker {
142 visited: Vec<EntityId>,
143}
144
145impl MappingWalker {
146 /// A walker with no history.
147 pub fn new() -> Self {
148 Self::default()
149 }
150
151 /// Current nesting depth.
152 pub fn depth(&self) -> usize {
153 self.visited.len()
154 }
155
156 /// Enter a mapped item, or fail if that would revisit or go too deep.
157 ///
158 /// Call [`Self::exit`] when done with the item, so sibling instances of
159 /// the same map do not look like a cycle. A map legitimately appears many
160 /// times; what is illegal is a map containing *itself*.
161 pub fn enter(&mut self, item: EntityId) -> GeometryResult<()> {
162 if self.visited.contains(&item) {
163 return Err(GeometryError::CyclicChain {
164 entity: item,
165 kind: "mapped item",
166 });
167 }
168 if self.visited.len() >= MAX_NESTING_DEPTH {
169 return Err(GeometryError::ChainTooDeep {
170 entity: item,
171 kind: "mapped item",
172 limit: MAX_NESTING_DEPTH,
173 });
174 }
175 self.visited.push(item);
176 Ok(())
177 }
178
179 /// Leave the most recently entered item.
180 pub fn exit(&mut self) {
181 self.visited.pop();
182 }
183
184 /// Resolve the source map and target operator of a mapped item.
185 ///
186 /// Returns both ids so a caller can compose `MappingOrigin` with
187 /// `MappingTarget`. Composing only the target is the bug this signature
188 /// is shaped to prevent.
189 pub fn resolve(&mut self, model: &Model, item_id: EntityId) -> GeometryResult<MappedInstance> {
190 let entity = model.get(item_id).ok_or(GeometryError::MissingEntity {
191 referrer: item_id,
192 missing: item_id,
193 })?;
194 let item = MappedItem::new(item_id, entity);
195
196 let source_id = item.mapping_source()?;
197 let target_id = item.mapping_target()?;
198
199 let source = model.get(source_id).ok_or(GeometryError::MissingEntity {
200 referrer: item_id,
201 missing: source_id,
202 })?;
203 let map = RepresentationMap::new(source_id, source);
204
205 Ok(MappedInstance {
206 item: item_id,
207 mapping_origin: map.mapping_origin()?,
208 mapped_representation: map.mapped_representation()?,
209 mapping_target: target_id,
210 })
211 }
212}
213
214/// One resolved mapped item: everything needed to place the reused geometry.
215#[derive(Debug, Clone, Copy, PartialEq, Eq)]
216#[non_exhaustive]
217pub struct MappedInstance {
218 /// The `IfcMappedItem` this came from.
219 pub item: EntityId,
220 /// `IfcAxis2Placement` locating the source geometry in its own space.
221 pub mapping_origin: EntityId,
222 /// The `IfcRepresentation` holding the reused geometry.
223 pub mapped_representation: EntityId,
224 /// `IfcCartesianTransformationOperator` placing the instance.
225 pub mapping_target: EntityId,
226}
227
228#[cfg(test)]
229mod tests {
230 use super::*;
231 use ifc_model::Value;
232
233 fn model_with_mapping() -> Model {
234 let mut model = Model::new();
235 // #14 = IFCREPRESENTATIONMAP(#2, #13)
236 model.insert(
237 EntityId(14),
238 Entity::new(
239 "IFCREPRESENTATIONMAP",
240 vec![Value::Ref(EntityId(2)), Value::Ref(EntityId(13))],
241 ),
242 );
243 // #19 = IFCMAPPEDITEM(#14, #18)
244 model.insert(
245 EntityId(19),
246 Entity::new(
247 "IFCMAPPEDITEM",
248 vec![Value::Ref(EntityId(14)), Value::Ref(EntityId(18))],
249 ),
250 );
251 model
252 }
253
254 #[test]
255 fn reads_both_halves_of_the_mapping() {
256 let model = model_with_mapping();
257 let mut walker = MappingWalker::new();
258 let resolved = walker.resolve(&model, EntityId(19)).unwrap();
259
260 assert_eq!(resolved.mapping_origin, EntityId(2));
261 assert_eq!(resolved.mapped_representation, EntityId(13));
262 assert_eq!(resolved.mapping_target, EntityId(18));
263 }
264
265 /// A map used many times is normal; that must not look like a cycle.
266 #[test]
267 fn the_same_map_may_be_instanced_repeatedly() {
268 let mut walker = MappingWalker::new();
269 for _ in 0..100 {
270 walker
271 .enter(EntityId(19))
272 .expect("sibling instances are legal");
273 walker.exit();
274 }
275 assert_eq!(walker.depth(), 0);
276 }
277
278 /// A map containing itself is not legal, and must not hang.
279 #[test]
280 fn revisiting_an_item_while_inside_it_is_a_cycle() {
281 let mut walker = MappingWalker::new();
282 walker.enter(EntityId(19)).unwrap();
283 let err = walker.enter(EntityId(19)).unwrap_err();
284 assert!(
285 matches!(err, GeometryError::CyclicChain { .. }),
286 "got {err}"
287 );
288 }
289
290 #[test]
291 fn nesting_is_bounded() {
292 let mut walker = MappingWalker::new();
293 for i in 0..MAX_NESTING_DEPTH {
294 walker.enter(EntityId(i as u64)).unwrap();
295 }
296 let err = walker.enter(EntityId(9999)).unwrap_err();
297 assert!(
298 matches!(err, GeometryError::ChainTooDeep { .. }),
299 "got {err}"
300 );
301 }
302
303 #[test]
304 fn dangling_mapping_source_is_reported() {
305 let mut model = Model::new();
306 model.insert(
307 EntityId(19),
308 Entity::new(
309 "IFCMAPPEDITEM",
310 vec![Value::Ref(EntityId(999)), Value::Ref(EntityId(18))],
311 ),
312 );
313 let mut walker = MappingWalker::new();
314 assert!(matches!(
315 walker.resolve(&model, EntityId(19)).unwrap_err(),
316 GeometryError::MissingEntity { .. }
317 ));
318 }
319}