ifc_model/index/reverse.rs
1//! Target-to-referrer index: "which entities point at this one?"
2//!
3//! # Why this is not built during insertion
4//!
5//! The type index is built eagerly because almost every consumer asks for
6//! "every IfcWall". The reverse index is different: a codec that parses a file
7//! and writes it straight back never asks who references what, and paying for
8//! the index on every load would tax the common path to serve the rarer one.
9//!
10//! So it is built on demand, from a `&Model`, and handed back as a value the
11//! caller owns and can drop. A caller doing one lookup pays for one scan; a
12//! caller doing thousands builds it once.
13//!
14//! # Slots are recorded, not just referrers
15//!
16//! IFC relationships are objectified: `IfcRelContainedInSpatialStructure` holds
17//! its elements in one attribute and its container in another. "Who references
18//! this storey" is not enough -- a consumer must know *which slot* the
19//! reference sat in to tell containment from the inverse. So each hit carries
20//! the attribute index.
21
22use ahash::AHashMap;
23
24use crate::value::EntityId;
25use crate::Model;
26
27/// One reference to a target entity.
28#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
29pub struct Referrer {
30 /// The entity holding the reference.
31 pub from: EntityId,
32 /// Which top-level attribute slot of `from` the reference sits in.
33 ///
34 /// Nested references inside a `List` or `Typed` wrapper report the index of
35 /// the outermost attribute, because that is the slot the schema names.
36 pub slot: usize,
37}
38
39/// Reverse-reference index over a model snapshot.
40///
41/// Derived state: it reflects the model as it was when built. Mutating the
42/// model afterwards does not update it, and the borrow checker enforces that
43/// for the common `&Model` case.
44#[derive(Debug, Clone, Default)]
45pub struct ReverseIndex {
46 /// Target to the entities referencing it, sorted and deduplicated so
47 /// results are deterministic across runs.
48 incoming: AHashMap<EntityId, Vec<Referrer>>,
49}
50
51impl ReverseIndex {
52 /// Scan every entity once and record each reference by target.
53 #[must_use]
54 pub fn build(model: &Model) -> Self {
55 let mut incoming: AHashMap<EntityId, Vec<Referrer>> = AHashMap::new();
56 // File order, so the result is stable rather than hash-ordered.
57 for from in model.ids() {
58 let Some(entity) = model.get(from) else {
59 continue;
60 };
61 for (slot, attribute) in entity.attributes.iter().enumerate() {
62 attribute.for_each_ref(&mut |target| {
63 incoming
64 .entry(target)
65 .or_default()
66 .push(Referrer { from, slot });
67 });
68 }
69 }
70 // An attribute may name the same target twice (a degenerate polyline
71 // closing on its start point). Report the pair once per slot.
72 for referrers in incoming.values_mut() {
73 referrers.sort_unstable();
74 referrers.dedup();
75 }
76 Self { incoming }
77 }
78
79 /// Every reference pointing at `target`, in ascending `(from, slot)` order.
80 #[must_use]
81 pub fn referrers(&self, target: EntityId) -> &[Referrer] {
82 self.incoming.get(&target).map_or(&[], Vec::as_slice)
83 }
84
85 /// Entities referencing `target` from the given attribute slot.
86 ///
87 /// This is the query objectified relationships need: "which relationship
88 /// entities name me in their *RelatingStructure* slot".
89 pub fn referrers_in_slot(
90 &self,
91 target: EntityId,
92 slot: usize,
93 ) -> impl Iterator<Item = EntityId> + '_ {
94 self.referrers(target)
95 .iter()
96 .filter(move |r| r.slot == slot)
97 .map(|r| r.from)
98 }
99
100 /// Whether anything references `target`.
101 #[must_use]
102 pub fn is_referenced(&self, target: EntityId) -> bool {
103 !self.referrers(target).is_empty()
104 }
105
106 /// Number of distinct targets that have at least one referrer.
107 #[must_use]
108 pub fn len(&self) -> usize {
109 self.incoming.len()
110 }
111
112 /// Whether no entity references any other.
113 #[must_use]
114 pub fn is_empty(&self) -> bool {
115 self.incoming.is_empty()
116 }
117}