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)]
216pub struct MappedInstance {
217 /// The `IfcMappedItem` this came from.
218 pub item: EntityId,
219 /// `IfcAxis2Placement` locating the source geometry in its own space.
220 pub mapping_origin: EntityId,
221 /// The `IfcRepresentation` holding the reused geometry.
222 pub mapped_representation: EntityId,
223 /// `IfcCartesianTransformationOperator` placing the instance.
224 pub mapping_target: EntityId,
225}
226
227#[cfg(test)]
228mod tests {
229 use super::*;
230 use ifc_model::Value;
231
232 fn model_with_mapping() -> Model {
233 let mut model = Model::new();
234 // #14 = IFCREPRESENTATIONMAP(#2, #13)
235 model.insert(
236 EntityId(14),
237 Entity::new(
238 "IFCREPRESENTATIONMAP",
239 vec![Value::Ref(EntityId(2)), Value::Ref(EntityId(13))],
240 ),
241 );
242 // #19 = IFCMAPPEDITEM(#14, #18)
243 model.insert(
244 EntityId(19),
245 Entity::new(
246 "IFCMAPPEDITEM",
247 vec![Value::Ref(EntityId(14)), Value::Ref(EntityId(18))],
248 ),
249 );
250 model
251 }
252
253 #[test]
254 fn reads_both_halves_of_the_mapping() {
255 let model = model_with_mapping();
256 let mut walker = MappingWalker::new();
257 let resolved = walker.resolve(&model, EntityId(19)).unwrap();
258
259 assert_eq!(resolved.mapping_origin, EntityId(2));
260 assert_eq!(resolved.mapped_representation, EntityId(13));
261 assert_eq!(resolved.mapping_target, EntityId(18));
262 }
263
264 /// A map used many times is normal; that must not look like a cycle.
265 #[test]
266 fn the_same_map_may_be_instanced_repeatedly() {
267 let mut walker = MappingWalker::new();
268 for _ in 0..100 {
269 walker
270 .enter(EntityId(19))
271 .expect("sibling instances are legal");
272 walker.exit();
273 }
274 assert_eq!(walker.depth(), 0);
275 }
276
277 /// A map containing itself is not legal, and must not hang.
278 #[test]
279 fn revisiting_an_item_while_inside_it_is_a_cycle() {
280 let mut walker = MappingWalker::new();
281 walker.enter(EntityId(19)).unwrap();
282 let err = walker.enter(EntityId(19)).unwrap_err();
283 assert!(
284 matches!(err, GeometryError::CyclicChain { .. }),
285 "got {err}"
286 );
287 }
288
289 #[test]
290 fn nesting_is_bounded() {
291 let mut walker = MappingWalker::new();
292 for i in 0..MAX_NESTING_DEPTH {
293 walker.enter(EntityId(i as u64)).unwrap();
294 }
295 let err = walker.enter(EntityId(9999)).unwrap_err();
296 assert!(
297 matches!(err, GeometryError::ChainTooDeep { .. }),
298 "got {err}"
299 );
300 }
301
302 #[test]
303 fn dangling_mapping_source_is_reported() {
304 let mut model = Model::new();
305 model.insert(
306 EntityId(19),
307 Entity::new(
308 "IFCMAPPEDITEM",
309 vec![Value::Ref(EntityId(999)), Value::Ref(EntityId(18))],
310 ),
311 );
312 let mut walker = MappingWalker::new();
313 assert!(matches!(
314 walker.resolve(&model, EntityId(19)).unwrap_err(),
315 GeometryError::MissingEntity { .. }
316 ));
317 }
318}