Skip to main content

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}