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