Skip to main content

ogeom_algo/
history.rs

1//! What an operation did to its inputs.
2//!
3//! `docs/DATA_MODEL.md` ยง7. Every operation in this crate and above it reports
4//! three things about each shape it was given:
5//!
6//! - **generated**: new entities made *from* it that did not exist before. A
7//!   prism's side faces are generated from the profile's edges.
8//! - **modified**: what it *became*. A face split in two is modified into both
9//!   halves.
10//! - **deleted**: it has no image in the result at all.
11//!
12//! This is not bookkeeping for its own sake. A parametric application records
13//! "fillet *that* edge" and must still find that edge after the model is
14//! rebuilt with different dimensions; it does so by walking history. Half-
15//! populated history does not error; it reopens the document with the wrong
16//! faces filleted, which is why every operation populates it from the commit
17//! that introduces it rather than later.
18//!
19//! # Composition is the hard part
20//!
21//! Operations chain. If A modifies `x` into `y` and B then modifies `y` into
22//! `z`, the composed history must say A-then-B modified `x` into `z`. Getting
23//! that wrong is how a reference survives one rebuild and dies on the next,
24//! which is far harder to diagnose than dying immediately. See
25//! [`History::then`].
26
27use std::collections::{HashMap, HashSet};
28
29use ogeom_topo::{SameKey, Shape};
30
31/// A record of what one operation, or a chain of them, did.
32///
33/// Keyed by [`SameKey`]: node and placement, ignoring orientation. An edge and
34/// its reverse are the same edge, and history about one is history about both;
35/// keying on orientation would silently split every record in two.
36#[derive(Debug, Clone, Default)]
37pub struct History {
38    generated: HashMap<SameKey, Vec<Shape>>,
39    modified: HashMap<SameKey, Vec<Shape>>,
40    deleted: HashSet<SameKey>,
41}
42
43impl History {
44    /// An empty history.
45    #[must_use]
46    pub fn new() -> Self {
47        Self::default()
48    }
49
50    /// A history in which nothing happened to anything.
51    ///
52    /// The identity for [`History::then`]: composing with it changes nothing.
53    #[must_use]
54    pub fn identity() -> Self {
55        Self::new()
56    }
57
58    /// Record that `input` produced `output` as a new entity.
59    pub fn generate(&mut self, input: &Shape, output: Shape) {
60        self.generated
61            .entry(SameKey(input.clone()))
62            .or_default()
63            .push(output);
64    }
65
66    /// Record that `input` became `output`.
67    ///
68    /// Withdraws any deletion recorded for the same shape. Deletion and
69    /// modification are contradictory claims, and the guard has to run both
70    /// ways: clearing modifications on delete but not deletions on modify
71    /// leaves a history that says both, and two callers reach opposite
72    /// conclusions from it.
73    pub fn modify(&mut self, input: &Shape, output: Shape) {
74        let key = SameKey(input.clone());
75        self.deleted.remove(&key);
76        self.modified.entry(key).or_default().push(output);
77    }
78
79    /// Record that `input` has no image in the result.
80    ///
81    /// A shape cannot be both deleted and modified: if it became something, it
82    /// was not deleted. Recording a deletion drops any modification record for
83    /// the same shape, so the two can never disagree.
84    ///
85    /// Deletion and *generation* are a different matter, and coexist freely: a
86    /// swept profile edge is consumed by the sweep (deleted) while generating
87    /// the side face that grew from it. Anything that treats a deletion as the
88    /// end of the story about a shape loses that face's ancestry.
89    pub fn delete(&mut self, input: &Shape) {
90        let key = SameKey(input.clone());
91        self.modified.remove(&key);
92        self.deleted.insert(key);
93    }
94
95    /// New entities made from `input`.
96    #[must_use]
97    pub fn generated(&self, input: &Shape) -> &[Shape] {
98        self.generated
99            .get(&SameKey(input.clone()))
100            .map_or(&[], Vec::as_slice)
101    }
102
103    /// What `input` became.
104    ///
105    /// Empty for a shape the operation left alone: "unchanged" and "modified
106    /// into nothing" are different, and the second is [`History::is_deleted`].
107    #[must_use]
108    pub fn modified(&self, input: &Shape) -> &[Shape] {
109        self.modified
110            .get(&SameKey(input.clone()))
111            .map_or(&[], Vec::as_slice)
112    }
113
114    /// Whether `input` has no image in the result.
115    #[must_use]
116    pub fn is_deleted(&self, input: &Shape) -> bool {
117        self.deleted.contains(&SameKey(input.clone()))
118    }
119
120    /// Whether the operation touched `input` at all.
121    #[must_use]
122    pub fn is_affected(&self, input: &Shape) -> bool {
123        let key = SameKey(input.clone());
124        self.deleted.contains(&key)
125            || self.modified.contains_key(&key)
126            || self.generated.contains_key(&key)
127    }
128
129    /// Where `input` ended up: what it became, or itself if it was untouched.
130    ///
131    /// The question a caller resolving a stored reference actually has. A shape
132    /// an operation ignored is still there, and reporting nothing for it would
133    /// make every caller special-case the common path.
134    ///
135    /// Returns an empty slice only for a shape that was deleted.
136    #[must_use]
137    pub fn trace<'a>(&'a self, input: &'a Shape) -> &'a [Shape] {
138        if self.is_deleted(input) {
139            return &[];
140        }
141        let images = self.modified(input);
142        if images.is_empty() {
143            core::slice::from_ref(input)
144        } else {
145            images
146        }
147    }
148
149    /// Every shape this history has something to say about.
150    #[must_use]
151    pub fn inputs(&self) -> Vec<Shape> {
152        let mut out: Vec<Shape> = Vec::new();
153        let mut seen = HashSet::new();
154        for key in self
155            .generated
156            .keys()
157            .chain(self.modified.keys())
158            .chain(self.deleted.iter())
159        {
160            if seen.insert(key.clone()) {
161                out.push(key.0.clone());
162            }
163        }
164        out
165    }
166
167    /// Whether this history records nothing.
168    #[must_use]
169    pub fn is_empty(&self) -> bool {
170        self.generated.is_empty() && self.modified.is_empty() && self.deleted.is_empty()
171    }
172
173    /// This history followed by `later`.
174    ///
175    /// Composition, and the operation everything chained depends on. For each
176    /// shape either history knows about, the result answers where it ended up
177    /// after both steps.
178    ///
179    /// The subtlety is telling *unchanged* from *modified into itself*. A shape
180    /// neither step touched must come out with no record at all, not a
181    /// modification saying it became itself; otherwise composing with an empty
182    /// history would invent records, and composition would not have an
183    /// identity. So a modification is recorded only when one of the two steps
184    /// actually reported one.
185    #[must_use]
186    pub fn then(&self, later: &Self) -> Self {
187        let mut out = Self::new();
188
189        let mut subjects: Vec<Shape> = self.inputs();
190        for input in later.inputs() {
191            if !subjects.iter().any(|s| s.is_same(&input)) {
192                subjects.push(input);
193            }
194        }
195
196        for input in subjects {
197            let gone_already = self.is_deleted(&input);
198
199            // What this input is after the first step. Nothing, if it was
200            // consumed; its images, if it changed; itself, if it was left alone.
201            let first_images = self.modified(&input);
202            let changed_by_self = !first_images.is_empty();
203            let after_first: Vec<Shape> = if gone_already {
204                Vec::new()
205            } else if changed_by_self {
206                first_images.to_vec()
207            } else {
208                vec![input.clone()]
209            };
210
211            if gone_already {
212                out.delete(&input);
213            } else {
214                let mut changed_by_later = false;
215                let mut final_images: Vec<Shape> = Vec::new();
216                for image in &after_first {
217                    if later.is_deleted(image) {
218                        // Surviving the first step but not the second is a
219                        // change, and one that ends in nothing.
220                        changed_by_later = true;
221                        continue;
222                    }
223                    if !later.modified(image).is_empty() {
224                        changed_by_later = true;
225                    }
226                    final_images.extend(later.trace(image).iter().cloned());
227                }
228
229                if final_images.is_empty() {
230                    out.delete(&input);
231                } else if changed_by_self || changed_by_later {
232                    for image in final_images {
233                        out.modify(&input, image);
234                    }
235                }
236                // Otherwise neither step touched it, and silence is the answer.
237            }
238
239            // Generation is reported whether or not the input survived: a
240            // consumed edge still explains where the face that replaced it came
241            // from, and that ancestry is the whole point of the record.
242            for made in self.generated(&input) {
243                for image in later.trace(made) {
244                    out.generate(&input, image.clone());
245                }
246            }
247            for image in &after_first {
248                for made in later.generated(image) {
249                    out.generate(&input, made.clone());
250                }
251            }
252        }
253
254        out
255    }
256
257    /// Fold a sequence of histories into one, in order.
258    #[must_use]
259    pub fn chain(steps: &[Self]) -> Self {
260        steps
261            .iter()
262            .fold(Self::identity(), |acc, step| acc.then(step))
263    }
264}
265
266/// A shape together with the history of the operation that produced it.
267///
268/// The return type of every operation. Bundling them means an operation cannot
269/// return a result without saying what it did to get there.
270#[derive(Debug, Clone)]
271pub struct Built {
272    /// The result.
273    pub shape: Shape,
274    /// What the operation did to its inputs.
275    pub history: History,
276}
277
278impl Built {
279    /// A result with its history.
280    #[must_use]
281    pub const fn new(shape: Shape, history: History) -> Self {
282        Self { shape, history }
283    }
284
285    /// A result of an operation that had no inputs to report on.
286    ///
287    /// For a primitive built from numbers rather than from existing topology,
288    /// there is nothing for the history to say.
289    #[must_use]
290    pub fn from_nothing(shape: Shape) -> Self {
291        Self::new(shape, History::identity())
292    }
293}
294
295#[cfg(test)]
296#[allow(clippy::unwrap_used)]
297mod tests {
298    use super::*;
299    use ogeom_math::Point;
300    use ogeom_topo::Model;
301
302    fn shapes(n: usize) -> (Model, Vec<Shape>) {
303        let mut model = Model::new();
304        #[allow(clippy::cast_precision_loss)]
305        let shapes = (0..n)
306            .map(|i| model.add_point(Point::new(i as f64, 0.0, 0.0)))
307            .collect();
308        (model, shapes)
309    }
310
311    #[test]
312    fn an_empty_history_reports_everything_as_untouched() {
313        let (_, s) = shapes(1);
314        let h = History::new();
315        assert!(h.is_empty());
316        assert!(!h.is_affected(&s[0]));
317        assert!(!h.is_deleted(&s[0]));
318        assert!(h.generated(&s[0]).is_empty());
319        assert!(h.modified(&s[0]).is_empty());
320        // Tracing an untouched shape finds the shape itself, not nothing.
321        assert_eq!(h.trace(&s[0]).len(), 1);
322        assert!(h.trace(&s[0])[0].is_same(&s[0]));
323    }
324
325    #[test]
326    fn generated_modified_and_deleted_are_distinct_claims() {
327        let (_, s) = shapes(4);
328        let mut h = History::new();
329        h.generate(&s[0], s[1].clone());
330        h.modify(&s[2], s[3].clone());
331        h.delete(&s[1]);
332
333        assert_eq!(h.generated(&s[0]).len(), 1);
334        assert!(h.modified(&s[0]).is_empty(), "generating is not modifying");
335        assert_eq!(h.modified(&s[2]).len(), 1);
336        assert!(h.is_deleted(&s[1]));
337        assert!(!h.is_deleted(&s[2]));
338    }
339
340    #[test]
341    fn a_shape_cannot_be_both_deleted_and_modified() {
342        // If it became something it was not deleted, and a record claiming both
343        // would let two callers reach opposite conclusions.
344        let (_, s) = shapes(2);
345        let mut h = History::new();
346        h.modify(&s[0], s[1].clone());
347        assert_eq!(h.modified(&s[0]).len(), 1);
348
349        h.delete(&s[0]);
350        assert!(h.is_deleted(&s[0]));
351        assert!(
352            h.modified(&s[0]).is_empty(),
353            "the modification was withdrawn"
354        );
355        assert!(h.trace(&s[0]).is_empty());
356    }
357
358    #[test]
359    fn history_is_keyed_ignoring_orientation() {
360        // An edge and its reverse are the same edge. Keying on orientation
361        // would split every record in two, so a caller holding the reversed
362        // handle would find nothing.
363        let (_, s) = shapes(2);
364        let mut h = History::new();
365        h.modify(&s[0], s[1].clone());
366
367        let reversed = s[0].reversed();
368        assert_eq!(h.modified(&reversed).len(), 1, "same edge, other way round");
369        assert!(h.is_affected(&reversed));
370    }
371
372    #[test]
373    fn composition_follows_a_shape_through_two_operations() {
374        // a -> b in the first, b -> c in the second. The composed history must
375        // say a -> c; anything else and a stored reference survives one rebuild
376        // and dies on the next.
377        let (_, s) = shapes(3);
378        let (a, b, c) = (&s[0], &s[1], &s[2]);
379
380        let mut first = History::new();
381        first.modify(a, b.clone());
382        let mut second = History::new();
383        second.modify(b, c.clone());
384
385        let composed = first.then(&second);
386        assert_eq!(composed.modified(a).len(), 1);
387        assert!(composed.modified(a)[0].is_same(c));
388        assert!(!composed.is_deleted(a));
389    }
390
391    #[test]
392    fn composition_reports_a_shape_deleted_by_the_second_step() {
393        let (_, s) = shapes(2);
394        let (a, b) = (&s[0], &s[1]);
395
396        let mut first = History::new();
397        first.modify(a, b.clone());
398        let mut second = History::new();
399        second.delete(b);
400
401        let composed = first.then(&second);
402        assert!(
403            composed.is_deleted(a),
404            "a survived the first step but not the second, so it is gone"
405        );
406        assert!(composed.trace(a).is_empty());
407    }
408
409    #[test]
410    fn composition_keeps_a_deletion_from_the_first_step() {
411        let (_, s) = shapes(2);
412        let mut first = History::new();
413        first.delete(&s[0]);
414        let second = History::new();
415        assert!(first.then(&second).is_deleted(&s[0]));
416    }
417
418    #[test]
419    fn composition_splits_when_the_second_step_splits() {
420        // a -> b, then b -> {c, d}. The composed answer is a -> {c, d}, which is
421        // what a caller filleting "that edge" needs after two rebuilds.
422        let (_, s) = shapes(4);
423        let (a, b, c, d) = (&s[0], &s[1], &s[2], &s[3]);
424
425        let mut first = History::new();
426        first.modify(a, b.clone());
427        let mut second = History::new();
428        second.modify(b, c.clone());
429        second.modify(b, d.clone());
430
431        let composed = first.then(&second);
432        assert_eq!(composed.modified(a).len(), 2);
433        assert!(composed.modified(a).iter().any(|s| s.is_same(c)));
434        assert!(composed.modified(a).iter().any(|s| s.is_same(d)));
435    }
436
437    #[test]
438    fn composition_carries_generated_entities_forward() {
439        // The first step generates b from a; the second turns b into c. What a
440        // generated from the pair is c, not b; the intermediate is gone.
441        let (_, s) = shapes(3);
442        let (a, b, c) = (&s[0], &s[1], &s[2]);
443
444        let mut first = History::new();
445        first.generate(a, b.clone());
446        let mut second = History::new();
447        second.modify(b, c.clone());
448
449        let composed = first.then(&second);
450        assert_eq!(composed.generated(a).len(), 1);
451        assert!(composed.generated(a)[0].is_same(c));
452    }
453
454    #[test]
455    fn composition_passes_through_shapes_only_the_later_step_knows() {
456        let (_, s) = shapes(3);
457        let (a, b, c) = (&s[0], &s[1], &s[2]);
458
459        let mut first = History::new();
460        first.modify(a, a.clone());
461        let mut second = History::new();
462        second.modify(b, c.clone());
463
464        let composed = first.then(&second);
465        assert_eq!(composed.modified(b).len(), 1, "the first step never saw b");
466        assert!(composed.modified(b)[0].is_same(c));
467    }
468
469    #[test]
470    fn the_empty_history_is_an_identity_for_composition() {
471        let (_, s) = shapes(3);
472        let mut h = History::new();
473        h.modify(&s[0], s[1].clone());
474        h.generate(&s[0], s[2].clone());
475        h.delete(&s[1]);
476
477        for composed in [h.then(&History::identity()), History::identity().then(&h)] {
478            assert_eq!(composed.modified(&s[0]).len(), h.modified(&s[0]).len());
479            assert_eq!(composed.generated(&s[0]).len(), h.generated(&s[0]).len());
480            assert_eq!(composed.is_deleted(&s[1]), h.is_deleted(&s[1]));
481        }
482    }
483
484    #[test]
485    fn composition_is_associative() {
486        // Three chained operations must give the same answer however the chain
487        // is bracketed, or a caller that batches differently gets a different
488        // model.
489        let (_, s) = shapes(4);
490        let (a, b, c, d) = (&s[0], &s[1], &s[2], &s[3]);
491
492        let mut one = History::new();
493        one.modify(a, b.clone());
494        let mut two = History::new();
495        two.modify(b, c.clone());
496        let mut three = History::new();
497        three.modify(c, d.clone());
498
499        let left = one.then(&two).then(&three);
500        let right = one.then(&two.then(&three));
501
502        assert_eq!(left.modified(a).len(), right.modified(a).len());
503        assert!(left.modified(a)[0].is_same(&right.modified(a)[0]));
504        assert!(left.modified(a)[0].is_same(d));
505    }
506
507    #[test]
508    fn chaining_a_sequence_matches_folding_it_by_hand() {
509        let (_, s) = shapes(4);
510        let mut steps = Vec::new();
511        for i in 0..3 {
512            let mut h = History::new();
513            h.modify(&s[i], s[i + 1].clone());
514            steps.push(h);
515        }
516        let chained = History::chain(&steps);
517        assert!(chained.modified(&s[0])[0].is_same(&s[3]));
518        assert!(History::chain(&[]).is_empty());
519    }
520
521    #[test]
522    fn inputs_lists_every_shape_the_history_mentions_once() {
523        let (_, s) = shapes(3);
524        let mut h = History::new();
525        h.modify(&s[0], s[1].clone());
526        h.generate(&s[0], s[2].clone());
527        h.delete(&s[1]);
528
529        let inputs = h.inputs();
530        assert_eq!(inputs.len(), 2, "s0 appears twice but is listed once");
531        assert!(inputs.iter().any(|x| x.is_same(&s[0])));
532        assert!(inputs.iter().any(|x| x.is_same(&s[1])));
533    }
534
535    #[test]
536    fn a_consumed_shape_still_reports_what_it_generated() {
537        // A sweep consumes its profile edge and grows a side face from it. Both
538        // facts are true at once, and losing the second loses that face's
539        // ancestry, which is what a rebuild needs to find it again.
540        let (_, s) = shapes(2);
541        let (edge, face) = (&s[0], &s[1]);
542
543        let mut sweep = History::new();
544        sweep.generate(edge, face.clone());
545        sweep.delete(edge);
546
547        assert!(sweep.is_deleted(edge));
548        assert_eq!(
549            sweep.generated(edge).len(),
550            1,
551            "deletion is not the end of the story"
552        );
553
554        // And composition must carry it through.
555        let composed = sweep.then(&History::identity());
556        assert!(composed.is_deleted(edge));
557        assert_eq!(composed.generated(edge).len(), 1);
558        assert!(composed.generated(edge)[0].is_same(face));
559    }
560
561    #[test]
562    fn a_modification_withdraws_an_earlier_deletion() {
563        // The guard has to run both ways. Clearing modifications on delete but
564        // not deletions on modify leaves a history saying both, and two callers
565        // reach opposite conclusions from it.
566        let (_, s) = shapes(2);
567        let mut h = History::new();
568        h.delete(&s[0]);
569        h.modify(&s[0], s[1].clone());
570        assert!(!h.is_deleted(&s[0]));
571        assert_eq!(h.modified(&s[0]).len(), 1);
572    }
573
574    #[test]
575    fn a_primitive_reports_a_history_with_nothing_in_it() {
576        // Built from numbers, not from topology: there is nothing to say, and
577        // saying nothing is different from failing to say anything.
578        let (_, s) = shapes(1);
579        let built = Built::from_nothing(s[0].clone());
580        assert!(built.history.is_empty());
581        assert!(built.shape.is_same(&s[0]));
582    }
583}