ifc_lite_server/services/data_model/
spatial_lookups.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5//! The element -> spatial-container lookup tables of `SpatialHierarchyData`
6//! (`element_to_storey` and friends), split out of `spatial.rs` for the
7//! module-size ratchet. Built after the node tree so the storey ruling can
8//! tell a project-reachable storey from a rescued orphan (#4310).
9
10use super::super::types::{EntityMetadata, Relationship, SpatialNode};
11use rustc_hash::{FxHashMap, FxHashSet};
12
13pub(super) struct ElementLookups {
14    pub storey: Vec<(u32, u32)>,
15    pub building: Vec<(u32, u32)>,
16    pub site: Vec<(u32, u32)>,
17    pub space: Vec<(u32, u32)>,
18}
19
20/// `IFCRELCONTAINEDINSPATIALSTRUCTURE` pairs bucketed by container type.
21/// `project_reachable` is the node set BEFORE orphan rescue; only those
22/// storeys compete for the first-declared storey slot.
23pub(super) fn build_element_lookups(
24    relationships: &[Relationship],
25    entity_map: &FxHashMap<u32, &EntityMetadata>,
26    nodes_map: &FxHashMap<u32, SpatialNode>,
27    project_reachable: &FxHashSet<u32>,
28    is_spatial_type: impl Fn(&str) -> bool,
29    is_building_like_spatial_type: impl Fn(&str) -> bool,
30    is_space_like_spatial_type: impl Fn(&str) -> bool,
31) -> ElementLookups {
32    let mut element_to_storey = Vec::new();
33    let mut element_to_building = Vec::new();
34    let mut element_to_site = Vec::new();
35    let mut element_to_space = Vec::new();
36
37    // #4310 parity: `packages/parser`'s `elementToStorey` resolves a malformed
38    // file naming the same element in more than one `IfcRelContainedInSpatialStructure`
39    // edge (two different storeys both containing the same wall) to the
40    // FIRST-declared edge, matching `packages/query`'s `containedIn()`. Before
41    // this fix, `element_to_storey` here just pushed every matching relation
42    // pair in file order with no winner picked at this layer at all - the
43    // Parquet lookup table then carried BOTH `(element, StoreyA)` and
44    // `(element, StoreyB)` rows, and `packages/server-client`'s
45    // `data-model-decoder.ts` flattens that table into a `Map` with a plain
46    // forward `for` loop and unconditional `map.set`, so the LAST row in file
47    // order silently won - the opposite of the ruling. Deduping to the
48    // first-declared row here, before serialization, makes that flattening
49    // step correct regardless of its own iteration order and keeps this one
50    // lookup table consistent with the wasm/parser path's `elementToStorey`.
51    //
52    // Only storeys reachable from IfcProject compete for the first-declared
53    // slot - packages/parser never visits a rescued orphan storey, so an
54    // orphan declared before a reachable storey must not claim the element
55    // there either. An element contained ONLY in orphan storeys still keeps
56    // its first-declared orphan row (the orphan is a rescued root here, and
57    // dropping the row would lose data the parser has no equivalent for).
58    let mut storey_claimed: FxHashSet<u32> = FxHashSet::default();
59    let mut orphan_storey_claimed: FxHashSet<u32> = FxHashSet::default();
60    let mut element_to_orphan_storey: Vec<(u32, u32)> = Vec::new();
61
62    for rel in relationships {
63        if rel.rel_type.to_uppercase() == "IFCRELCONTAINEDINSPATIALSTRUCTURE" {
64            let spatial_id = rel.relating_id;
65            let element_id = rel.related_id;
66
67            // Skip a target that was promoted to a spatial-structure node above: it is
68            // not a leaf element, so it has no place in these element_to_* lookups
69            // (mirrors containedElements excluding containedSpatialChildren in
70            // packages/parser/src/spatial-hierarchy-builder.ts).
71            let target_is_spatial = entity_map
72                .get(&element_id)
73                .map(|e| {
74                    let target_type_upper = e.type_name.to_uppercase();
75                    is_spatial_type(&target_type_upper) && target_type_upper != "IFCPROJECT"
76                })
77                .unwrap_or(false);
78            if target_is_spatial {
79                continue;
80            }
81
82            if let Some(spatial_node) = nodes_map.get(&spatial_id) {
83                let type_upper = spatial_node.type_name.to_uppercase();
84                if type_upper == "IFCBUILDINGSTOREY" {
85                    // First-declared wins: an element already claimed by an
86                    // earlier-declared storey ignores every later-declared one.
87                    if !project_reachable.contains(&spatial_id) {
88                        if orphan_storey_claimed.insert(element_id) {
89                            element_to_orphan_storey.push((element_id, spatial_id));
90                        }
91                    } else if storey_claimed.insert(element_id) {
92                        element_to_storey.push((element_id, spatial_id));
93                    }
94                } else if is_building_like_spatial_type(&type_upper) {
95                    element_to_building.push((element_id, spatial_id));
96                } else if type_upper == "IFCSITE" {
97                    element_to_site.push((element_id, spatial_id));
98                } else if is_space_like_spatial_type(&type_upper) {
99                    element_to_space.push((element_id, spatial_id));
100                }
101            }
102        }
103    }
104
105    // Orphan-only elements keep their first-declared orphan storey.
106    for (element_id, spatial_id) in element_to_orphan_storey {
107        if !storey_claimed.contains(&element_id) {
108            element_to_storey.push((element_id, spatial_id));
109        }
110    }
111
112    ElementLookups {
113        storey: element_to_storey,
114        building: element_to_building,
115        site: element_to_site,
116        space: element_to_space,
117    }
118}