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, 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}