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 identifying
4//! topology by pointer, and the reason this lives in the foundation crate
5//! rather than being added later.
6//!
7//! Where topology is identified by pointer, every modeling operation allocates
8//! new nodes, so every reference into a previous result dies: the topological
9//! naming problem, which identity reconstructed afterwards from history maps
10//! only patches.
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 fixing a taxonomy of every role in a CAD
74/// kernel before the operations that need one exist.
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///
179/// An entry can be forgotten ([`ProvenanceTable::forget`],
180/// [`ProvenanceTable::retain`]): its id stays issued, so no later entity
181/// takes it, and asking for it answers `None` as for an id from another
182/// document.
183#[derive(Debug, Clone, Default)]
184pub struct ProvenanceTable {
185    /// The entries, ascending by id. From `tail_at` on the ids run without
186    /// a gap up to the last one issued, so they are found by arithmetic;
187    /// the ones before are found by search. An entry forgotten one at a
188    /// time stays as `None` until [`ProvenanceTable::retain`] packs it out.
189    entries: Vec<(EntityId, Option<Provenance>)>,
190    /// Where the gapless run of ids starts in `entries`.
191    tail_at: usize,
192    /// How many ids have been issued.
193    issued: u64,
194}
195
196impl ProvenanceTable {
197    /// An empty table.
198    #[must_use]
199    pub const fn new() -> Self {
200        Self {
201            entries: Vec::new(),
202            tail_at: 0,
203            issued: 0,
204        }
205    }
206
207    /// Number of identities issued, forgotten ones included: the next
208    /// recorded entity is this plus one.
209    #[must_use]
210    #[allow(
211        clippy::cast_possible_truncation,
212        reason = "every issued id has had an entry in memory"
213    )]
214    pub const fn len(&self) -> usize {
215        self.issued as usize
216    }
217
218    /// Whether no identity has been issued.
219    #[must_use]
220    pub const fn is_empty(&self) -> bool {
221        self.issued == 0
222    }
223
224    /// Record an entity's provenance and return its identity.
225    ///
226    /// # Panics
227    ///
228    /// If more than `u64::MAX - 1` entities are recorded. Not reachable.
229    #[allow(clippy::expect_used, reason = "documented panic; see # Panics")]
230    pub fn record(&mut self, provenance: Provenance) -> EntityId {
231        // Ids start at 1 so that EntityId can be NonZeroU64 and Option<EntityId>
232        // costs nothing.
233        self.issued = self
234            .issued
235            .checked_add(1)
236            .expect("entity count exceeded u64");
237        let id = EntityId(NonZeroU64::new(self.issued).expect("at least 1 after an increment"));
238        self.entries.push((id, Some(provenance)));
239        id
240    }
241
242    /// Convenience for [`Provenance::Primitive`].
243    pub fn primitive(&mut self, op: OpId, role: Role) -> EntityId {
244        self.record(Provenance::Primitive { op, role })
245    }
246
247    /// Convenience for [`Provenance::Derived`].
248    pub fn derived(
249        &mut self,
250        op: OpId,
251        from: impl IntoIterator<Item = EntityId>,
252        role: Role,
253    ) -> EntityId {
254        self.record(Provenance::Derived {
255            op,
256            from: from.into_iter().collect(),
257            role,
258        })
259    }
260
261    /// Where `id`'s entry sits in `entries`, if it has one.
262    fn position(&self, id: EntityId) -> Option<usize> {
263        let tail_len = (self.entries.len() - self.tail_at) as u64;
264        let tail_first = self.issued - tail_len + 1;
265        if id.get() >= tail_first {
266            let at = self.tail_at + usize::try_from(id.get() - tail_first).ok()?;
267            return (at < self.entries.len()).then_some(at);
268        }
269        self.entries[..self.tail_at]
270            .binary_search_by_key(&id, |(id, _)| *id)
271            .ok()
272    }
273
274    /// The provenance of `id`, or `None` if it belongs to another document
275    /// or its entry was forgotten.
276    #[must_use]
277    pub fn get(&self, id: EntityId) -> Option<&Provenance> {
278        self.entries.get(self.position(id)?)?.1.as_ref()
279    }
280
281    /// Drop `id`'s entry and keep its id issued.
282    ///
283    /// For a document that keeps only what its shapes can still reach: an
284    /// entity no shape carries and no kept entry derives from answers
285    /// nothing a reference could ask. Returns whether there was an entry
286    /// to drop.
287    pub fn forget(&mut self, id: EntityId) -> bool {
288        let Some(at) = self.position(id) else {
289            return false;
290        };
291        self.entries
292            .get_mut(at)
293            .and_then(|(_, entry)| entry.take())
294            .is_some()
295    }
296
297    /// Keep the entries `keep` answers `true` for and forget the rest, as
298    /// [`ProvenanceTable::forget`] does each one. What is forgotten stops
299    /// costing memory.
300    pub fn retain(&mut self, mut keep: impl FnMut(EntityId, &Provenance) -> bool) {
301        self.entries
302            .retain(|(id, entry)| entry.as_ref().is_some_and(|entry| keep(*id, entry)));
303        let mut tail_len = 0_usize;
304        while tail_len < self.entries.len() {
305            let (id, _) = self.entries[self.entries.len() - 1 - tail_len];
306            if id.get() + tail_len as u64 != self.issued {
307                break;
308            }
309            tail_len += 1;
310        }
311        self.tail_at = self.entries.len() - tail_len;
312        self.entries.shrink_to_fit();
313    }
314
315    /// Walk `id`'s derivation back to the entities it ultimately came from.
316    ///
317    /// Returns the roots: entities that are `Primitive` or `Imported`. This is
318    /// how a stale reference is resolved after a rebuild: find what the user
319    /// originally picked, then find what that became.
320    ///
321    /// Cycles cannot occur, because an entity can only be derived from ids that
322    /// already existed when it was recorded. The visited set guards against a
323    /// table assembled by hand or deserialized from a corrupt file.
324    #[must_use]
325    pub fn roots(&self, id: EntityId) -> Vec<EntityId> {
326        let mut out = Vec::new();
327        let mut seen = crate::collections::FastSet::default();
328        let mut stack = vec![id];
329        while let Some(current) = stack.pop() {
330            if !seen.insert(current) {
331                continue;
332            }
333            match self.get(current) {
334                Some(Provenance::Derived { from, .. }) if !from.is_empty() => {
335                    stack.extend(from.iter().copied());
336                }
337                Some(_) => out.push(current),
338                None => {}
339            }
340        }
341        out.sort_unstable();
342        out
343    }
344
345    /// Every entity with an entry, in the order its identity was issued;
346    /// forgotten entries are skipped.
347    ///
348    /// For writing a document out: the table *is* the record of what every
349    /// entity is, and a file that dropped it would come back as a model whose
350    /// every reference had to be rebuilt from scratch.
351    pub fn iter(&self) -> impl Iterator<Item = (EntityId, &Provenance)> {
352        self.entries
353            .iter()
354            .filter_map(|(id, entry)| Some((*id, entry.as_ref()?)))
355    }
356}
357
358#[cfg(test)]
359#[allow(clippy::unwrap_used)]
360mod tests {
361    use super::*;
362
363    #[test]
364    fn ids_are_non_zero_and_distinct() {
365        let mut t = ProvenanceTable::new();
366        let a = t.primitive(OpId(0), Role::SOLE);
367        let b = t.primitive(OpId(0), Role::OUTER);
368        assert_ne!(a, b);
369        assert!(a.get() > 0 && b.get() > 0);
370        assert_eq!(
371            core::mem::size_of::<Option<EntityId>>(),
372            core::mem::size_of::<EntityId>()
373        );
374    }
375
376    #[test]
377    fn derived_entities_remember_their_inputs() {
378        let mut t = ProvenanceTable::new();
379        let face_a = t.primitive(OpId(1), Role::LATERAL);
380        let face_b = t.primitive(OpId(2), Role::LATERAL);
381        let section = t.derived(OpId(3), [face_a, face_b], Role::SOLE);
382
383        let p = t.get(section).unwrap();
384        assert_eq!(p.op(), Some(OpId(3)));
385        assert_eq!(p.inputs(), &[face_a, face_b]);
386    }
387
388    #[test]
389    fn roots_walk_back_through_a_derivation_chain() {
390        let mut t = ProvenanceTable::new();
391        let original = t.primitive(OpId(1), Role::END_CAP);
392        // A boolean splits the face, then a fillet modifies one fragment.
393        let split = t.derived(OpId(2), [original], Role::op_defined(0));
394        let filleted = t.derived(OpId(3), [split], Role::SOLE);
395
396        assert_eq!(t.roots(filleted), vec![original]);
397        assert_eq!(t.roots(original), vec![original], "a root is its own root");
398    }
399
400    #[test]
401    fn roots_of_a_multi_parent_entity_include_every_branch() {
402        let mut t = ProvenanceTable::new();
403        let a = t.primitive(OpId(1), Role::SOLE);
404        let b = t.primitive(OpId(2), Role::SOLE);
405        let mid = t.derived(OpId(3), [a], Role::SOLE);
406        let joined = t.derived(OpId(4), [mid, b], Role::SOLE);
407
408        let mut expected = vec![a, b];
409        expected.sort_unstable();
410        assert_eq!(t.roots(joined), expected);
411    }
412
413    #[test]
414    fn ids_from_another_document_do_not_resolve() {
415        let mut t = ProvenanceTable::new();
416        let mine = t.primitive(OpId(0), Role::SOLE);
417        let mut other = ProvenanceTable::new();
418        for _ in 0..10 {
419            other.primitive(OpId(0), Role::SOLE);
420        }
421        let theirs = other.primitive(OpId(0), Role::SEAM);
422
423        assert!(t.get(mine).is_some());
424        assert!(t.get(theirs).is_none(), "foreign id must not resolve");
425    }
426
427    #[test]
428    fn a_forgotten_entry_keeps_its_id_issued() {
429        let mut t = ProvenanceTable::new();
430        let a = t.primitive(OpId(1), Role::SOLE);
431        let b = t.primitive(OpId(2), Role::SOLE);
432        assert!(t.forget(a));
433        assert!(!t.forget(a), "already forgotten");
434        assert!(t.get(a).is_none());
435        assert_eq!(t.len(), 2);
436        assert_eq!(t.iter().map(|(id, _)| id).collect::<Vec<_>>(), vec![b]);
437        let c = t.primitive(OpId(3), Role::SOLE);
438        assert_eq!(c.get(), 3, "the next id follows every id issued");
439    }
440
441    #[test]
442    fn retained_entries_answer_under_their_ids_and_new_ones_follow() {
443        let mut t = ProvenanceTable::new();
444        let ids: Vec<_> = (0..8).map(|i| t.primitive(OpId(i), Role::SOLE)).collect();
445        let kept = [ids[1], ids[4], ids[6], ids[7]];
446        t.retain(|id, _| kept.contains(&id));
447        for (i, id) in ids.iter().enumerate() {
448            let want = kept.contains(id).then_some(OpId(u32::try_from(i).unwrap()));
449            assert_eq!(t.get(*id).and_then(Provenance::op), want);
450        }
451        assert_eq!(t.len(), 8);
452        let next = t.derived(OpId(9), [ids[4]], Role::SOLE);
453        assert_eq!(next.get(), 9);
454        assert_eq!(t.roots(next), vec![ids[4]]);
455        assert_eq!(
456            t.iter().map(|(id, _)| id.get()).collect::<Vec<_>>(),
457            vec![2, 5, 7, 8, 9]
458        );
459    }
460
461    #[test]
462    fn op_defined_roles_do_not_collide_with_shared_ones() {
463        assert!(Role::op_defined(0).0 > Role::SEAM.0);
464        assert_ne!(Role::op_defined(0), Role::op_defined(1));
465    }
466}