Skip to main content

ifc_spatial/tree/
build.rs

1//! Assembling the containment tree from relationship entities.
2//!
3//! # Tolerating real files
4//!
5//! The canonical hierarchy is project → site → building → storey → element, and
6//! plenty of real exports do not follow it: sites are omitted, elements hang
7//! directly off the building, storeys are duplicated, and occasionally a
8//! relationship points at an entity that is not in the file. The tree records
9//! what the file says rather than asserting the ideal shape, and reports the
10//! anomalies separately so a caller can decide whether to care.
11
12use std::collections::BTreeMap;
13
14use ifc_model::{EntityId, Model};
15
16use super::kind::SpatialKind;
17use crate::relation::{Relationship, RelationshipKind};
18
19/// One entity's place in the containment tree.
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub struct SpatialNode {
22    /// The entity this node describes.
23    pub id: EntityId,
24    /// Its spatial role.
25    pub kind: SpatialKind,
26    /// Its container, if any relationship names one.
27    pub parent: Option<EntityId>,
28    /// Sub-containers, in file order.
29    pub children: Vec<EntityId>,
30    /// Physical elements placed directly in this container, in file order.
31    pub elements: Vec<EntityId>,
32}
33
34/// The containment tree of one model.
35#[derive(Debug, Clone, Default)]
36pub struct SpatialTree {
37    nodes: BTreeMap<EntityId, SpatialNode>,
38    /// Element to its direct container, so `container_of` is a lookup.
39    container_of_element: BTreeMap<EntityId, EntityId>,
40    roots: Vec<EntityId>,
41    orphans: Vec<EntityId>,
42    dangling: Vec<(EntityId, EntityId)>,
43}
44
45impl SpatialTree {
46    /// Build the containment tree of `model`.
47    ///
48    /// One pass over the aggregation and containment relationships. Cost is
49    /// linear in the number of relationship entities, not in model size,
50    /// because relationships are found through the type index.
51    #[must_use]
52    pub fn build(model: &Model) -> Self {
53        let mut tree = Self::default();
54
55        // Ensure every spatial entity has a node, even one no relationship
56        // mentions -- a lone IfcBuildingStorey is still part of the file.
57        for id in model.ids() {
58            let Some(entity) = model.get(id) else {
59                continue;
60            };
61            let kind = SpatialKind::classify(&entity.type_name);
62            if kind.is_container() {
63                tree.nodes.insert(
64                    id,
65                    SpatialNode {
66                        id,
67                        kind,
68                        parent: None,
69                        children: Vec::new(),
70                        elements: Vec::new(),
71                    },
72                );
73            }
74        }
75
76        for relationship in crate::relation::all(model) {
77            tree.apply(model, &relationship);
78        }
79
80        // A container with no parent is a root. Sorted for determinism, then
81        // ordered so the project (if any) leads.
82        tree.roots = tree
83            .nodes
84            .values()
85            .filter(|node| node.parent.is_none())
86            .map(|node| node.id)
87            .collect();
88        tree.roots.sort_by_key(|id| {
89            let kind = tree.nodes[id].kind;
90            (kind, *id)
91        });
92
93        // Every root beyond the most-general one is a detached branch: the file
94        // declares a container that nothing aggregates. Reported rather than
95        // hidden, because a viewer would otherwise silently lose it.
96        if tree.roots.len() > 1 {
97            tree.orphans = tree.roots[1..].to_vec();
98        }
99        tree
100    }
101
102    /// Record one relationship's effect on the tree.
103    fn apply(&mut self, model: &Model, relationship: &Relationship) {
104        let Some(parent) = relationship.relating else {
105            return;
106        };
107        // A relationship naming an entity the file does not contain is a
108        // defect worth reporting, not a reason to abandon the tree.
109        if model.get(parent).is_none() {
110            self.dangling.push((relationship.id, parent));
111            return;
112        }
113        if !self.nodes.contains_key(&parent) {
114            // The relating end is not a container -- an element aggregating
115            // its parts. Valid IFC, but not spatial containment.
116            return;
117        }
118
119        for &child in &relationship.related {
120            let Some(child_entity) = model.get(child) else {
121                self.dangling.push((relationship.id, child));
122                continue;
123            };
124            let child_kind = SpatialKind::classify(&child_entity.type_name);
125
126            if child_kind.is_container() {
127                // Containment relationships name elements, not containers;
128                // a container arriving here means an aggregation edge.
129                if relationship.kind == RelationshipKind::ContainedIn {
130                    continue;
131                }
132                if let Some(node) = self.nodes.get_mut(&child) {
133                    // A second parent is a malformed file. Keep the first so
134                    // the tree stays a tree, and record nothing further --
135                    // `orphans` and `dangling` cover the reportable defects.
136                    if node.parent.is_none() {
137                        node.parent = Some(parent);
138                        if let Some(parent_node) = self.nodes.get_mut(&parent) {
139                            parent_node.children.push(child);
140                        }
141                    }
142                }
143            } else if let Some(parent_node) = self.nodes.get_mut(&parent) {
144                if !parent_node.elements.contains(&child) {
145                    parent_node.elements.push(child);
146                    // First container wins: an element named by two containment
147                    // relationships is malformed, and picking the first keeps
148                    // the answer stable across runs.
149                    self.container_of_element.entry(child).or_insert(parent);
150                }
151            }
152        }
153    }
154}
155
156impl SpatialTree {
157    /// The node describing `id`, if it is a spatial container.
158    #[must_use]
159    pub fn node(&self, id: EntityId) -> Option<&SpatialNode> {
160        self.nodes.get(&id)
161    }
162
163    /// Containers with no parent, most-general kind first.
164    ///
165    /// A conformant file yields exactly one root, the `IfcProject`. More than
166    /// one means the file omits an aggregation relationship somewhere.
167    #[must_use]
168    pub fn roots(&self) -> &[EntityId] {
169        &self.roots
170    }
171
172    /// Every spatial container in the model, ordered by entity id.
173    pub fn containers(&self) -> impl Iterator<Item = &SpatialNode> {
174        self.nodes.values()
175    }
176
177    /// Containers of a given kind, ordered by entity id.
178    pub fn of_kind(&self, kind: SpatialKind) -> impl Iterator<Item = &SpatialNode> + '_ {
179        self.nodes.values().filter(move |node| node.kind == kind)
180    }
181
182    /// Elements placed directly in `container`.
183    ///
184    /// Direct only: an element in a space inside a storey is not returned for
185    /// the storey. Use [`elements_recursive`](Self::elements_recursive) for the
186    /// transitive set.
187    #[must_use]
188    pub fn elements_of(&self, container: EntityId) -> &[EntityId] {
189        self.nodes
190            .get(&container)
191            .map_or(&[], |node| node.elements.as_slice())
192    }
193
194    /// Every element in `container` or any container beneath it.
195    ///
196    /// Breadth-first, so a container's own elements precede those of its
197    /// children. Bounded by the tree's depth, which is finite because each node
198    /// holds at most one parent.
199    #[must_use]
200    pub fn elements_recursive(&self, container: EntityId) -> Vec<EntityId> {
201        let mut out = Vec::new();
202        let mut queue = std::collections::VecDeque::from([container]);
203        let mut seen = std::collections::BTreeSet::new();
204        while let Some(current) = queue.pop_front() {
205            if !seen.insert(current) {
206                continue;
207            }
208            let Some(node) = self.nodes.get(&current) else {
209                continue;
210            };
211            out.extend_from_slice(&node.elements);
212            queue.extend(node.children.iter().copied());
213        }
214        out
215    }
216
217    /// The chain of containers above `id`, nearest first.
218    ///
219    /// Empty for a root. Terminates even if the file contains a containment
220    /// cycle, because the walk stops at the first repeated entity.
221    #[must_use]
222    pub fn ancestors(&self, id: EntityId) -> Vec<EntityId> {
223        let mut out = Vec::new();
224        let mut seen = std::collections::BTreeSet::new();
225        let mut current = self.nodes.get(&id).and_then(|node| node.parent);
226        while let Some(parent) = current {
227            if !seen.insert(parent) {
228                break;
229            }
230            out.push(parent);
231            current = self.nodes.get(&parent).and_then(|node| node.parent);
232        }
233        out
234    }
235
236    /// The container holding `element`, if any relationship places it.
237    ///
238    /// Answers the question a drawing or take-off actually asks: which storey
239    /// is this wall on? Backed by a map built during assembly, so this is a
240    /// lookup rather than a scan over every container.
241    #[must_use]
242    pub fn container_of(&self, element: EntityId) -> Option<EntityId> {
243        self.container_of_element.get(&element).copied()
244    }
245
246    /// Relationship/target pairs naming an entity the model does not contain.
247    #[must_use]
248    pub fn dangling(&self) -> &[(EntityId, EntityId)] {
249        &self.dangling
250    }
251
252    /// Containers that no relationship places under a parent, excluding the
253    /// most-general root.
254    ///
255    /// A conformant file has none: every site is aggregated into the project,
256    /// every building into a site. Entries here mean the file omits an
257    /// aggregation relationship, which a viewer would show as a detached
258    /// branch.
259    #[must_use]
260    pub fn orphans(&self) -> &[EntityId] {
261        &self.orphans
262    }
263}