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}