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}