ifc_geometry/lower/provenance.rs
1//! IFC source attribution for format-neutral geometry nodes.
2//!
3//! Provenance is deliberately a side table. `axiolid-model` stays IFC-agnostic,
4//! while diagnostics and consumers of a lowered result can still trace a node
5//! back to the entity that emitted it.
6
7use axiolid_model::NodeId;
8use ifc_model::EntityId;
9use std::collections::BTreeMap;
10
11/// Source IFC entities for nodes in one lowered geometry graph.
12///
13/// The map is partial: caller-synthesized nodes created outside an active IFC
14/// entity scope have no source instead of receiving a fabricated entity id.
15#[derive(Debug, Clone, Default, PartialEq, Eq)]
16pub struct ProvenanceMap {
17 sources: BTreeMap<NodeId, EntityId>,
18}
19
20impl ProvenanceMap {
21 /// Return the IFC entity that emitted `node`, if it has an IFC source.
22 pub fn source(&self, node: NodeId) -> Option<EntityId> {
23 self.sources.get(&node).copied()
24 }
25
26 /// Number of attributed graph nodes.
27 pub fn len(&self) -> usize {
28 self.sources.len()
29 }
30
31 /// Whether no graph nodes carry IFC attribution.
32 pub fn is_empty(&self) -> bool {
33 self.sources.is_empty()
34 }
35
36 /// Iterate deterministically over `(node, source entity)` pairs.
37 pub fn iter(&self) -> impl Iterator<Item = (NodeId, EntityId)> + '_ {
38 self.sources.iter().map(|(&node, &source)| (node, source))
39 }
40
41 pub(crate) fn record(&mut self, node: NodeId, source: EntityId) {
42 if let Some(previous) = self.sources.insert(node, source) {
43 debug_assert_eq!(
44 previous, source,
45 "one graph node cannot have two IFC sources"
46 );
47 }
48 }
49}