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, BTreeSet};
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 /// Faces left out under `DegenerateFacePolicy::DropAndReport` (#46).
19 dropped_faces: BTreeSet<EntityId>,
20}
21
22impl ProvenanceMap {
23 /// Return the IFC entity that emitted `node`, if it has an IFC source.
24 pub fn source(&self, node: NodeId) -> Option<EntityId> {
25 self.sources.get(&node).copied()
26 }
27
28 /// Number of attributed graph nodes.
29 pub fn len(&self) -> usize {
30 self.sources.len()
31 }
32
33 /// Whether no graph nodes carry IFC attribution.
34 pub fn is_empty(&self) -> bool {
35 self.sources.is_empty()
36 }
37
38 /// Iterate deterministically over `(node, source entity)` pairs.
39 pub fn iter(&self) -> impl Iterator<Item = (NodeId, EntityId)> + '_ {
40 self.sources.iter().map(|(&node, &source)| (node, source))
41 }
42
43 /// `IfcFace` entities left out because their outer loop collapsed, in
44 /// ascending id.
45 ///
46 /// Always empty under the default `DegenerateFacePolicy::Refuse`. A face
47 /// shared by several items is listed once. A non-empty list means the
48 /// lowered geometry is NOT exactly what the file authored: a consumer
49 /// using it as evidence decides whether that is acceptable.
50 pub fn dropped_faces(&self) -> impl Iterator<Item = EntityId> + '_ {
51 self.dropped_faces.iter().copied()
52 }
53
54 pub(crate) fn record_dropped_face(&mut self, face: EntityId) {
55 self.dropped_faces.insert(face);
56 }
57
58 pub(crate) fn record(&mut self, node: NodeId, source: EntityId) {
59 if let Some(previous) = self.sources.insert(node, source) {
60 debug_assert_eq!(
61 previous, source,
62 "one graph node cannot have two IFC sources"
63 );
64 }
65 }
66}