ifc_lite_server/services/data_model/relationships.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//! Relationship extraction.
6
7use super::generated::{relationship_slots, RelationshipSlots};
8use super::types::{EntityJob, Relationship};
9use ifc_lite_core::{DecodedEntity, EntityDecoder};
10use rayon::prelude::*;
11use std::sync::Arc;
12
13/// Extract all relationships.
14pub(super) fn extract_relationships(
15 jobs: &[EntityJob],
16 content: &Arc<Vec<u8>>,
17 entity_index: &Arc<ifc_lite_core::EntityIndex>,
18) -> Vec<Relationship> {
19 // The schema-derived gate (issue #4205): a type is a relationship job iff
20 // the generated table (apps/server/.../generated/relationship_slots.rs,
21 // derived from @ifc-lite/parser's relationship-schema-slots.ts) has a
22 // slot plan for it. This replaces a hand-written 13-entry array that
23 // silently dropped every `IfcRelationship` subtype outside it — using the
24 // slot lookup itself as the gate means there is exactly one table to keep
25 // in sync with the schema, not two that can drift apart.
26 let rel_jobs: Vec<_> = jobs
27 .iter()
28 .filter(|job| {
29 let type_upper = job.type_name.to_uppercase();
30 relationship_slots(&type_upper).is_some()
31 })
32 .collect();
33
34 tracing::debug!(count = rel_jobs.len(), "Extracting relationships");
35
36 let mut rels: Vec<Relationship> = rel_jobs
37 .par_iter()
38 .filter_map(|job| {
39 let mut local_decoder =
40 EntityDecoder::with_arc_index(content.as_slice(), entity_index.clone());
41 let entity = local_decoder.decode_at(job.start, job.end).ok()?;
42
43 extract_relationship(&entity, &job.type_name, job.id)
44 })
45 .flatten()
46 .collect();
47
48 // Attach each type's own property/quantity sets (issue #1751). Type sets in
49 // IFC live on `IfcTypeObject.HasPropertySets` (attr 5), NOT via
50 // IfcRelDefinesByProperties, so they carry no relationship the client can
51 // follow. Emit a synthetic `TYPEHASPROPERTYSETS` edge (set -> type) per
52 // member so the viewer converter can key type-owned sets by the type id and
53 // resolve the WASM path's type fallback — mirroring
54 // `extractTypeEntityOwnProperties`. A distinct rel_type keeps these out of
55 // the DefinesByProperties graph (no phantom edges for inspector/IDS).
56 let type_links = extract_type_property_links(&rels, content, entity_index);
57 rels.extend(type_links);
58 rels
59}
60
61/// For every type referenced by an `IfcRelDefinesByType`, read its
62/// `HasPropertySets` (attr 5) and emit `{rel_type:"TYPEHASPROPERTYSETS",
63/// relating_id: setId, related_id: typeId}` for each member. Types are
64/// discovered from the DefinesByType `relating_id`s (never by name suffix,
65/// which would catch IfcSurfaceStyle / the rel itself).
66fn extract_type_property_links(
67 rels: &[Relationship],
68 content: &Arc<Vec<u8>>,
69 entity_index: &Arc<ifc_lite_core::EntityIndex>,
70) -> Vec<Relationship> {
71 use std::collections::BTreeSet;
72
73 let type_ids: BTreeSet<u32> = rels
74 .iter()
75 .filter(|r| r.rel_type.eq_ignore_ascii_case("IFCRELDEFINESBYTYPE"))
76 .map(|r| r.relating_id)
77 .collect();
78
79 if type_ids.is_empty() {
80 return Vec::new();
81 }
82
83 type_ids
84 .par_iter()
85 .flat_map_iter(|&type_id| {
86 let mut decoder =
87 EntityDecoder::with_arc_index(content.as_slice(), entity_index.clone());
88 let mut out = Vec::new();
89 if let Ok(entity) = decoder.decode_by_id(type_id) {
90 // IfcTypeObject.HasPropertySets is at index 5 across IFC2X3/4/4X3.
91 if let Some(set_list) = entity.get_list(5) {
92 for set_ref in set_list.iter() {
93 if let Some(set_id) = set_ref.as_entity_ref() {
94 out.push(Relationship {
95 rel_type: "TYPEHASPROPERTYSETS".to_string(),
96 // Synthetic: read off IfcTypeObject.HasPropertySets,
97 // so there is no IfcRel entity to name here.
98 rel_id: 0,
99 relating_id: set_id,
100 related_id: type_id,
101 });
102 }
103 }
104 }
105 }
106 out
107 })
108 .collect()
109}
110
111/// Extract relationship from entity (may return multiple if related[] has multiple items).
112fn extract_relationship(
113 entity: &DecodedEntity,
114 type_name: &str,
115 rel_id: u32,
116) -> Option<Vec<Relationship>> {
117 let type_upper = type_name.to_uppercase();
118
119 // Schema-derived slot plan (issue #4205): the generated table already
120 // encodes ABSOLUTE attribute positions and whether `related` is a
121 // LIST/SET or a single reference — derived from the same
122 // relationship-schema-slots.ts the TS/WASM columnar parser uses, so this
123 // stays correct for every concrete `IfcRelationship` subtype without a
124 // per-type match here. `extract_relationships` already filtered `jobs`
125 // down to types this resolves, but re-check defensively (`?` below) in
126 // case of a future caller.
127 let slots: RelationshipSlots = relationship_slots(&type_upper)?;
128
129 // `relating` is schema-legally a grouped aggregate for exactly one
130 // concrete subtype (`IFCRELDEFINESBYPROPERTIES.RelatingPropertyDefinition`
131 // — see `relating_is_list`'s doc comment in the generated table).
132 // `get_ref` returns `None` for a list-valued attribute (it delegates to
133 // `AttributeValue::as_entity_ref`, which only recognises a bare
134 // reference), so calling it unconditionally silently dropped the WHOLE
135 // relationship for a grouped `IfcPropertySetDefinitionSet` — every
136 // related object lost all properties/quantities from that pset group.
137 // `get_refs` accepts both a bare ref and a list, mirroring the TS/WASM
138 // path's `readRefList` (`columnar-parser-relationships.ts`), which reads
139 // `RelatingPropertyDefinition` as a ref list unconditionally and applies
140 // EVERY entry in the group to EVERY related object — reproduced below as
141 // the same cross product, not just the first entry.
142 let relating_ids: Vec<u32> = if slots.relating_is_list {
143 entity.get_refs(slots.relating_idx as usize)?
144 } else {
145 vec![entity.get_ref(slots.relating_idx as usize)?]
146 };
147
148 let related_ids: Vec<u32> = if slots.related_is_list {
149 let related_list = entity.get_list(slots.related_idx as usize)?;
150 related_list
151 .iter()
152 .filter_map(|v| v.as_entity_ref())
153 .collect()
154 } else {
155 vec![entity.get_ref(slots.related_idx as usize)?]
156 };
157
158 if relating_ids.is_empty() || related_ids.is_empty() {
159 return None;
160 }
161
162 Some(
163 relating_ids
164 .into_iter()
165 .flat_map(|relating_id| {
166 related_ids.iter().map(move |&related_id| Relationship {
167 rel_type: type_name.to_string(),
168 rel_id,
169 relating_id,
170 related_id,
171 })
172 })
173 .collect(),
174 )
175}