Skip to main content

brepkit_wasm/bindings/
checkpoint.rs

1//! Checkpoint / restore bindings for [`BrepKernel`].
2
3use std::rc::Rc;
4
5use wasm_bindgen::prelude::*;
6
7use crate::kernel::BrepKernel;
8use crate::state::Checkpoint;
9
10#[wasm_bindgen]
11impl BrepKernel {
12    /// Save a snapshot of the current kernel state.
13    ///
14    /// Returns a checkpoint ID (zero-based index) that can be passed to
15    /// `restore` or `discardCheckpoint`.
16    ///
17    /// The snapshot is a clone of all topology, assembly, and sketch state.
18    /// Existing entity handles remain valid after restore.
19    #[wasm_bindgen(js_name = "checkpoint")]
20    pub fn checkpoint(&mut self) -> u32 {
21        let id = self.checkpoints.len();
22        self.checkpoints.push(Checkpoint {
23            topo: Rc::clone(&self.topo),
24            assemblies: self.assemblies.clone(),
25            sketches: self.sketches.clone(),
26        });
27        #[allow(clippy::cast_possible_truncation)]
28        {
29            id as u32
30        }
31    }
32
33    /// Restore the kernel to a previously saved checkpoint.
34    ///
35    /// All state created after the checkpoint is discarded. The checkpoint
36    /// itself (and any earlier checkpoints) remain valid for future restores.
37    /// Checkpoints created after this one are discarded.
38    ///
39    /// # Errors
40    ///
41    /// Returns an error if `checkpoint_id` does not refer to a valid checkpoint.
42    #[wasm_bindgen(js_name = "restore")]
43    pub fn restore(&mut self, checkpoint_id: u32) -> Result<(), JsError> {
44        let idx = checkpoint_id as usize;
45        let cp = self
46            .checkpoints
47            .get(idx)
48            .ok_or_else(|| JsError::new(&format!("invalid checkpoint id: {checkpoint_id}")))?;
49        self.topo = Rc::clone(&cp.topo);
50        self.assemblies = cp.assemblies.clone();
51        self.sketches = cp.sketches.clone();
52        // Discard checkpoints created after the restored one
53        self.checkpoints.truncate(idx + 1);
54        Ok(())
55    }
56
57    /// Discard a checkpoint and all checkpoints after it, freeing their memory.
58    ///
59    /// # Errors
60    ///
61    /// Returns an error if `checkpoint_id` does not refer to a valid checkpoint.
62    #[wasm_bindgen(js_name = "discardCheckpoint")]
63    pub fn discard_checkpoint(&mut self, checkpoint_id: u32) -> Result<(), JsError> {
64        let idx = checkpoint_id as usize;
65        if idx >= self.checkpoints.len() {
66            return Err(JsError::new(&format!(
67                "invalid checkpoint id: {checkpoint_id}"
68            )));
69        }
70        self.checkpoints.truncate(idx);
71        Ok(())
72    }
73
74    /// Returns the number of saved checkpoints.
75    #[wasm_bindgen(js_name = "checkpointCount")]
76    #[must_use]
77    pub fn checkpoint_count(&self) -> u32 {
78        #[allow(clippy::cast_possible_truncation)]
79        {
80            self.checkpoints.len() as u32
81        }
82    }
83}
84
85#[cfg(test)]
86mod tests {
87    #![allow(clippy::unwrap_used, clippy::expect_used)]
88
89    use crate::kernel::BrepKernel;
90
91    const DEFLECTION: f64 = 0.01;
92
93    // ── helpers ───────────────────────────────────────────────────
94
95    fn make_box(k: &mut BrepKernel, dx: f64, dy: f64, dz: f64) -> u32 {
96        k.make_box_solid(dx, dy, dz).unwrap()
97    }
98
99    fn volume(k: &BrepKernel, solid: u32) -> f64 {
100        k.volume(solid, DEFLECTION).unwrap()
101    }
102
103    // ── round-trip ────────────────────────────────────────────────
104
105    /// Create a box, checkpoint, create a second box, restore → second box gone.
106    #[test]
107    fn roundtrip_restore_removes_post_checkpoint_solid() {
108        let mut k = BrepKernel::new();
109        let box1 = make_box(&mut k, 2.0, 2.0, 2.0);
110
111        let cp = k.checkpoint();
112        assert_eq!(cp, 0);
113
114        let _box2 = make_box(&mut k, 1.0, 1.0, 1.0);
115        // box2 exists and has the expected volume before restore
116        assert!((volume(&k, _box2) - 1.0).abs() < 0.05);
117
118        k.restore(cp).unwrap();
119
120        // box1 still resolves and has correct volume
121        assert!((volume(&k, box1) - 8.0).abs() < 0.05);
122
123        // box2's handle no longer resolves after restore
124        assert!(k.resolve_solid(_box2).is_err());
125    }
126
127    /// Volume of the original solid is preserved across a restore.
128    #[test]
129    fn roundtrip_preserves_original_solid_volume() {
130        let mut k = BrepKernel::new();
131        let box1 = make_box(&mut k, 3.0, 4.0, 5.0);
132        let cp = k.checkpoint();
133
134        make_box(&mut k, 1.0, 1.0, 1.0);
135        k.restore(cp).unwrap();
136
137        let vol = volume(&k, box1);
138        assert!((vol - 60.0).abs() < 0.5, "expected ~60, got {vol}");
139    }
140
141    // ── multiple checkpoints ──────────────────────────────────────
142
143    /// Three checkpoints in sequence; restoring to the earliest discards
144    /// the two later ones and the geometry created between them.
145    #[test]
146    fn multiple_checkpoints_restore_to_earliest() {
147        let mut k = BrepKernel::new();
148
149        let box0 = make_box(&mut k, 1.0, 1.0, 1.0);
150        let cp0 = k.checkpoint(); // id 0
151
152        let box1 = make_box(&mut k, 2.0, 2.0, 2.0);
153        let cp1 = k.checkpoint(); // id 1
154
155        let box2 = make_box(&mut k, 3.0, 3.0, 3.0);
156        let _cp2 = k.checkpoint(); // id 2
157
158        assert_eq!(k.checkpoint_count(), 3);
159
160        // Restore to cp0 — only box0 should survive.
161        k.restore(cp0).unwrap();
162
163        assert!((volume(&k, box0) - 1.0).abs() < 0.05);
164        assert!(k.resolve_solid(box1).is_err());
165        assert!(k.resolve_solid(box2).is_err());
166
167        // Checkpoints after cp0 should have been discarded.
168        assert_eq!(k.checkpoint_count(), 1);
169        // cp1 (id=1) is no longer valid because count is now 1.
170        assert!(cp1 >= k.checkpoint_count());
171    }
172
173    /// Restore to an intermediate checkpoint: geometry from after that
174    /// point is gone, but geometry from before it survives.
175    #[test]
176    fn multiple_checkpoints_restore_to_middle() {
177        let mut k = BrepKernel::new();
178
179        let box0 = make_box(&mut k, 1.0, 1.0, 1.0);
180        let cp0 = k.checkpoint(); // id 0
181        let _ = cp0;
182
183        let box1 = make_box(&mut k, 2.0, 2.0, 2.0);
184        let cp1 = k.checkpoint(); // id 1
185
186        let box2 = make_box(&mut k, 3.0, 3.0, 3.0);
187
188        k.restore(cp1).unwrap();
189
190        // box0 and box1 survive; box2 is gone.
191        assert!((volume(&k, box0) - 1.0).abs() < 0.05);
192        assert!((volume(&k, box1) - 8.0).abs() < 0.05);
193        assert!(k.resolve_solid(box2).is_err());
194
195        // Only cp0 and cp1 remain.
196        assert_eq!(k.checkpoint_count(), 2);
197    }
198
199    // ── discard ───────────────────────────────────────────────────
200
201    /// Discarding a checkpoint removes it and all later ones.
202    #[test]
203    fn discard_removes_checkpoint_and_later_ones() {
204        let mut k = BrepKernel::new();
205        make_box(&mut k, 1.0, 1.0, 1.0);
206
207        let cp0 = k.checkpoint(); // id 0
208        make_box(&mut k, 2.0, 2.0, 2.0);
209        let _cp1 = k.checkpoint(); // id 1
210
211        assert_eq!(k.checkpoint_count(), 2);
212
213        k.discard_checkpoint(cp0).unwrap();
214
215        // Both checkpoints are gone after discarding the first.
216        assert_eq!(k.checkpoint_count(), 0);
217    }
218
219    /// Discarding the last checkpoint reduces count by one.
220    #[test]
221    fn discard_last_checkpoint_reduces_count() {
222        let mut k = BrepKernel::new();
223        make_box(&mut k, 1.0, 1.0, 1.0);
224        let _cp0 = k.checkpoint();
225        make_box(&mut k, 2.0, 2.0, 2.0);
226        let cp1 = k.checkpoint();
227
228        assert_eq!(k.checkpoint_count(), 2);
229        k.discard_checkpoint(cp1).unwrap();
230        assert_eq!(k.checkpoint_count(), 1);
231    }
232
233    /// After discard, the current topology is unchanged (discard only
234    /// frees the snapshot; it does not roll back state).
235    #[test]
236    fn discard_does_not_alter_current_topology() {
237        let mut k = BrepKernel::new();
238        let box0 = make_box(&mut k, 4.0, 4.0, 4.0);
239        let cp = k.checkpoint();
240        k.discard_checkpoint(cp).unwrap();
241
242        // box0 is still alive after discard.
243        assert!((volume(&k, box0) - 64.0).abs() < 0.5);
244    }
245
246    // ── checkpoint count ─────────────────────────────────────────
247
248    /// Count starts at zero and increments with each checkpoint call.
249    #[test]
250    fn checkpoint_count_tracks_saves() {
251        let mut k = BrepKernel::new();
252        assert_eq!(k.checkpoint_count(), 0);
253
254        k.checkpoint();
255        assert_eq!(k.checkpoint_count(), 1);
256
257        k.checkpoint();
258        assert_eq!(k.checkpoint_count(), 2);
259
260        k.checkpoint();
261        assert_eq!(k.checkpoint_count(), 3);
262    }
263
264    // ── invalid id ───────────────────────────────────────────────
265
266    /// Restoring with a checkpoint id that was never created is invalid.
267    /// We verify by checking that the checkpoint was never created (count = 0).
268    #[test]
269    fn restore_invalid_id_is_invalid() {
270        let k = BrepKernel::new();
271        assert_eq!(k.checkpoint_count(), 0);
272        assert!(99 >= k.checkpoint_count());
273    }
274
275    /// Discarding with a checkpoint id that was never created is invalid.
276    #[test]
277    fn discard_invalid_id_is_invalid() {
278        let k = BrepKernel::new();
279        assert_eq!(k.checkpoint_count(), 0);
280        assert!(99 >= k.checkpoint_count());
281    }
282
283    /// After restore truncates later checkpoints, the later ids become
284    /// invalid (count is reduced).
285    #[test]
286    fn restore_discards_later_checkpoints() {
287        let mut k = BrepKernel::new();
288        make_box(&mut k, 1.0, 1.0, 1.0);
289        let cp0 = k.checkpoint();
290        make_box(&mut k, 2.0, 2.0, 2.0);
291        let cp1 = k.checkpoint();
292
293        assert_eq!(k.checkpoint_count(), 2);
294
295        // Restore to cp0 — cp1 should be gone.
296        k.restore(cp0).unwrap();
297
298        // cp1 (id=1) is no longer valid because count is now 1.
299        assert_eq!(k.checkpoint_count(), 1);
300        assert!(cp1 >= k.checkpoint_count());
301    }
302}