Skip to main content

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}