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}