Skip to main content

ogeom_core/
id.rs

1//! Stable entity identity and provenance.
2//!
3//! See `docs/DATA_MODEL.md` ยง8. A deliberate divergence from the conventional
4//! design, and the reason this has to live in the foundation crate rather than
5//! being added later.
6//!
7//! Conventionally, topology is identified by pointer. Every modeling operation
8//! allocates new nodes, so every reference into a previous result dies. That *is* the
9//! topological naming problem, and every downstream fix is an attempt to
10//! reconstruct identity after the fact by walking history maps.
11//!
12//! Here an entity's identity is *what produced it, and from what*. A rebuild
13//! with different parameters runs the same operations over the same inputs and
14//! therefore produces entities with the same provenance, so a reference like
15//! "the fillet on this edge" survives a change to an unrelated dimension.
16//!
17//! Provenance does not replace operation history: history is what a binding
18//! layer consumes, and it is the honest answer where provenance cannot resolve
19//! a reference. It is the primary mechanism, not the only one.
20
21use core::num::NonZeroU64;
22
23use smallvec::SmallVec;
24
25/// A stable identity for a topological entity, valid for the lifetime of a
26/// document.
27///
28/// Distinct from an arena [`Key`](crate::Key): a key says *where the data is*
29/// and dies when the entity is rebuilt; an `EntityId` says *what the entity is*
30/// and survives.
31#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
32pub struct EntityId(NonZeroU64);
33
34impl EntityId {
35    /// The identity's raw value. Non-zero.
36    #[must_use]
37    pub const fn get(self) -> u64 {
38        self.0.get()
39    }
40
41    /// An identity from a raw value, or `None` if it is zero.
42    ///
43    /// For reading a document back from a file, which has to reproduce the
44    /// identities it was written with: a reference recorded against
45    /// `EntityId(7)` has to still find entity seven. Nothing else should mint
46    /// one of these: within a document,
47    /// [`ProvenanceTable::record`](ProvenanceTable::record) is what issues an
48    /// identity, and it issues one that has something behind it.
49    #[must_use]
50    pub const fn from_raw(raw: u64) -> Option<Self> {
51        match NonZeroU64::new(raw) {
52            Some(value) => Some(Self(value)),
53            None => None,
54        }
55    }
56}
57
58/// Identifies one invocation of a modeling operation.
59///
60/// Stable across rebuilds: the third extrusion in a document's recompute is
61/// `OpId(3)` every time, which is what lets provenance survive a parameter
62/// change.
63///
64/// The default, `OpId(0)`, is the implicit operation a document starts in:
65/// whatever was there before anything was deliberately begun.
66#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
67pub struct OpId(pub u32);
68
69/// What an entity *is*, relative to the operation that made it.
70///
71/// The low values are shared vocabulary; everything from [`Role::OP_DEFINED`]
72/// up is interpreted by the producing operation alone. Keeping it a newtype
73/// rather than an enum avoids inventing a taxonomy of every role in a CAD
74/// kernel before we have written the operations that need one.
75#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
76pub struct Role(pub u32);
77
78impl Role {
79    /// No distinguishing role: the operation produced exactly one entity of
80    /// this kind, so it needs no further discriminator.
81    pub const SOLE: Self = Self(0);
82    /// The result's outer boundary: the outer wire of a face, the outer shell of
83    /// a solid.
84    pub const OUTER: Self = Self(1);
85    /// An inner boundary: a hole.
86    pub const INNER: Self = Self(2);
87    /// The start of a swept or extruded result.
88    pub const START_CAP: Self = Self(3);
89    /// The end of a swept or extruded result.
90    pub const END_CAP: Self = Self(4);
91    /// The swept side wall between the caps.
92    pub const LATERAL: Self = Self(5);
93    /// A seam on a closed surface.
94    pub const SEAM: Self = Self(6);
95    /// The first value an operation may assign meaning to itself.
96    pub const OP_DEFINED: u32 = 1024;
97
98    /// An operation-defined role. `index` is offset above [`Role::OP_DEFINED`].
99    #[must_use]
100    pub const fn op_defined(index: u32) -> Self {
101        Self(Self::OP_DEFINED + index)
102    }
103}
104
105/// Where an entity came from.
106#[derive(Debug, Clone, PartialEq, Eq)]
107pub enum Provenance {
108    /// Created outright by an operation, from no prior entity: the `+Z` face of
109    /// a box, the lateral surface of a cylinder.
110    Primitive {
111        /// The operation that created it.
112        op: OpId,
113        /// Which part of that operation's result this is.
114        role: Role,
115    },
116    /// Derived from one or more existing entities. A face split by a boolean
117    /// names the face it came from; an intersection edge names both faces.
118    Derived {
119        /// The operation that derived it.
120        op: OpId,
121        /// The inputs it came from, in the operation's canonical order.
122        from: SmallVec<[EntityId; 2]>,
123        /// Which part of that operation's result this is.
124        role: Role,
125    },
126    /// Read from a file. `external` is the source's own identifier (a STEP
127    /// entity number, say) so that a re-import matches entities up.
128    Imported {
129        /// Which imported document it came from.
130        source: SourceId,
131        /// The identifier the source file gave it.
132        external: u64,
133    },
134}
135
136impl Provenance {
137    /// The operation that produced this entity, if any.
138    #[must_use]
139    pub const fn op(&self) -> Option<OpId> {
140        match self {
141            Self::Primitive { op, .. } | Self::Derived { op, .. } => Some(*op),
142            Self::Imported { .. } => None,
143        }
144    }
145
146    /// Which part of its operation's result this entity is, if the operation
147    /// named one.
148    ///
149    /// The question provenance exists to answer: "the top face of that box",
150    /// asked of a model that has been rebuilt since. An imported entity has no
151    /// role, because the file said where it came from and not what it is for.
152    #[must_use]
153    pub const fn role(&self) -> Option<Role> {
154        match self {
155            Self::Primitive { role, .. } | Self::Derived { role, .. } => Some(*role),
156            Self::Imported { .. } => None,
157        }
158    }
159
160    /// The entities this one was derived from.
161    #[must_use]
162    pub fn inputs(&self) -> &[EntityId] {
163        match self {
164            Self::Derived { from, .. } => from,
165            Self::Primitive { .. } | Self::Imported { .. } => &[],
166        }
167    }
168}
169
170/// Identifies an imported document.
171#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
172pub struct SourceId(pub u32);
173
174/// Assigns [`EntityId`]s and remembers what each one came from.
175///
176/// One per document. Cloning it clones the whole history, which is what a
177/// rebuild-with-rollback needs.
178#[derive(Debug, Clone, Default)]
179pub struct ProvenanceTable {
180    entries: Vec<Provenance>,
181}
182
183impl ProvenanceTable {
184    /// An empty table.
185    #[must_use]
186    pub const fn new() -> Self {
187        Self {
188            entries: Vec::new(),
189        }
190    }
191
192    /// Number of entities recorded.
193    #[must_use]
194    pub const fn len(&self) -> usize {
195        self.entries.len()
196    }
197
198    /// Whether nothing has been recorded.
199    #[must_use]
200    pub const fn is_empty(&self) -> bool {
201        self.entries.is_empty()
202    }
203
204    /// Record an entity's provenance and return its identity.
205    ///
206    /// # Panics
207    ///
208    /// If more than `u64::MAX - 1` entities are recorded. Not reachable.
209    #[allow(clippy::expect_used, reason = "documented panic; see # Panics")]
210    pub fn record(&mut self, provenance: Provenance) -> EntityId {
211        self.entries.push(provenance);
212        // Ids start at 1 so that EntityId can be NonZeroU64 and Option<EntityId>
213        // costs nothing.
214        let raw = u64::try_from(self.entries.len()).expect("entity count exceeded u64");
215        EntityId(NonZeroU64::new(raw).expect("length is at least 1 after push"))
216    }
217
218    /// Convenience for [`Provenance::Primitive`].
219    pub fn primitive(&mut self, op: OpId, role: Role) -> EntityId {
220        self.record(Provenance::Primitive { op, role })
221    }
222
223    /// Convenience for [`Provenance::Derived`].
224    pub fn derived(
225        &mut self,
226        op: OpId,
227        from: impl IntoIterator<Item = EntityId>,
228        role: Role,
229    ) -> EntityId {
230        self.record(Provenance::Derived {
231            op,
232            from: from.into_iter().collect(),
233            role,
234        })
235    }
236
237    /// The provenance of `id`, or `None` if it belongs to another document.
238    #[must_use]
239    pub fn get(&self, id: EntityId) -> Option<&Provenance> {
240        let index = usize::try_from(id.get()).ok()?.checked_sub(1)?;
241        self.entries.get(index)
242    }
243
244    /// Walk `id`'s derivation back to the entities it ultimately came from.
245    ///
246    /// Returns the roots: entities that are `Primitive` or `Imported`. This is
247    /// how a stale reference is resolved after a rebuild: find what the user
248    /// originally picked, then find what that became.
249    ///
250    /// Cycles cannot occur, because an entity can only be derived from ids that
251    /// already existed when it was recorded. The visited set guards against a
252    /// table assembled by hand or deserialized from a corrupt file.
253    #[must_use]
254    pub fn roots(&self, id: EntityId) -> Vec<EntityId> {
255        let mut out = Vec::new();
256        let mut seen = std::collections::HashSet::new();
257        let mut stack = vec![id];
258        while let Some(current) = stack.pop() {
259            if !seen.insert(current) {
260                continue;
261            }
262            match self.get(current) {
263                Some(Provenance::Derived { from, .. }) if !from.is_empty() => {
264                    stack.extend(from.iter().copied());
265                }
266                Some(_) => out.push(current),
267                None => {}
268            }
269        }
270        out.sort_unstable();
271        out
272    }
273
274    /// Every entity, in the order its identity was issued.
275    ///
276    /// For writing a document out: the table *is* the record of what every
277    /// entity is, and a file that dropped it would come back as a model whose
278    /// every reference had to be rebuilt from scratch.
279    pub fn iter(&self) -> impl Iterator<Item = (EntityId, &Provenance)> {
280        self.entries.iter().enumerate().filter_map(|(i, p)| {
281            // Ids start at 1, and the table cannot have grown past u64.
282            EntityId::from_raw(u64::try_from(i).ok()?.checked_add(1)?).map(|id| (id, p))
283        })
284    }
285}
286
287#[cfg(test)]
288#[allow(clippy::unwrap_used)]
289mod tests {
290    use super::*;
291
292    #[test]
293    fn ids_are_non_zero_and_distinct() {
294        let mut t = ProvenanceTable::new();
295        let a = t.primitive(OpId(0), Role::SOLE);
296        let b = t.primitive(OpId(0), Role::OUTER);
297        assert_ne!(a, b);
298        assert!(a.get() > 0 && b.get() > 0);
299        assert_eq!(
300            core::mem::size_of::<Option<EntityId>>(),
301            core::mem::size_of::<EntityId>()
302        );
303    }
304
305    #[test]
306    fn derived_entities_remember_their_inputs() {
307        let mut t = ProvenanceTable::new();
308        let face_a = t.primitive(OpId(1), Role::LATERAL);
309        let face_b = t.primitive(OpId(2), Role::LATERAL);
310        let section = t.derived(OpId(3), [face_a, face_b], Role::SOLE);
311
312        let p = t.get(section).unwrap();
313        assert_eq!(p.op(), Some(OpId(3)));
314        assert_eq!(p.inputs(), &[face_a, face_b]);
315    }
316
317    #[test]
318    fn roots_walk_back_through_a_derivation_chain() {
319        let mut t = ProvenanceTable::new();
320        let original = t.primitive(OpId(1), Role::END_CAP);
321        // A boolean splits the face, then a fillet modifies one fragment.
322        let split = t.derived(OpId(2), [original], Role::op_defined(0));
323        let filleted = t.derived(OpId(3), [split], Role::SOLE);
324
325        assert_eq!(t.roots(filleted), vec![original]);
326        assert_eq!(t.roots(original), vec![original], "a root is its own root");
327    }
328
329    #[test]
330    fn roots_of_a_multi_parent_entity_include_every_branch() {
331        let mut t = ProvenanceTable::new();
332        let a = t.primitive(OpId(1), Role::SOLE);
333        let b = t.primitive(OpId(2), Role::SOLE);
334        let mid = t.derived(OpId(3), [a], Role::SOLE);
335        let joined = t.derived(OpId(4), [mid, b], Role::SOLE);
336
337        let mut expected = vec![a, b];
338        expected.sort_unstable();
339        assert_eq!(t.roots(joined), expected);
340    }
341
342    #[test]
343    fn ids_from_another_document_do_not_resolve() {
344        let mut t = ProvenanceTable::new();
345        let mine = t.primitive(OpId(0), Role::SOLE);
346        let mut other = ProvenanceTable::new();
347        for _ in 0..10 {
348            other.primitive(OpId(0), Role::SOLE);
349        }
350        let theirs = other.primitive(OpId(0), Role::SEAM);
351
352        assert!(t.get(mine).is_some());
353        assert!(t.get(theirs).is_none(), "foreign id must not resolve");
354    }
355
356    #[test]
357    fn op_defined_roles_do_not_collide_with_shared_ones() {
358        assert!(Role::op_defined(0).0 > Role::SEAM.0);
359        assert_ne!(Role::op_defined(0), Role::op_defined(1));
360    }
361}