1use std::rc::Rc;
4
5use wasm_bindgen::prelude::*;
6
7use crate::kernel::BrepKernel;
8use crate::state::Checkpoint;
9
10#[wasm_bindgen]
11impl BrepKernel {
12 #[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 #[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 self.checkpoints.truncate(idx + 1);
54 Ok(())
55 }
56
57 #[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 #[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 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 #[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 assert!((volume(&k, _box2) - 1.0).abs() < 0.05);
117
118 k.restore(cp).unwrap();
119
120 assert!((volume(&k, box1) - 8.0).abs() < 0.05);
122
123 assert!(k.resolve_solid(_box2).is_err());
125 }
126
127 #[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 #[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(); let box1 = make_box(&mut k, 2.0, 2.0, 2.0);
153 let cp1 = k.checkpoint(); let box2 = make_box(&mut k, 3.0, 3.0, 3.0);
156 let _cp2 = k.checkpoint(); assert_eq!(k.checkpoint_count(), 3);
159
160 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 assert_eq!(k.checkpoint_count(), 1);
169 assert!(cp1 >= k.checkpoint_count());
171 }
172
173 #[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(); let _ = cp0;
182
183 let box1 = make_box(&mut k, 2.0, 2.0, 2.0);
184 let cp1 = k.checkpoint(); let box2 = make_box(&mut k, 3.0, 3.0, 3.0);
187
188 k.restore(cp1).unwrap();
189
190 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 assert_eq!(k.checkpoint_count(), 2);
197 }
198
199 #[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(); make_box(&mut k, 2.0, 2.0, 2.0);
209 let _cp1 = k.checkpoint(); assert_eq!(k.checkpoint_count(), 2);
212
213 k.discard_checkpoint(cp0).unwrap();
214
215 assert_eq!(k.checkpoint_count(), 0);
217 }
218
219 #[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 #[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 assert!((volume(&k, box0) - 64.0).abs() < 0.5);
244 }
245
246 #[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 #[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 #[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 #[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 k.restore(cp0).unwrap();
297
298 assert_eq!(k.checkpoint_count(), 1);
300 assert!(cp1 >= k.checkpoint_count());
301 }
302}