Delete: in a rolled-back history, the check builds the history once when nothing breaks (#9917) (#9972)

* Delete: the check in a rolled-back history builds the history once when nothing breaks (#9917)

With the history rolled back, the check before a delete (which features
the delete would break) built the whole history twice, before and after
the change, with the marker removed. On a large design each build ran
every boolean after the marker: deleting twelve splines took half a
minute on the window thread. Only a feature that fails after the change
can be newly broken, so the history after it is built first, and when
nothing fails there the history before it isn't built at all.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Delete: test that the check in a rolled-back history still reports what breaks (#9917)

Deleting a hole in the plate changes the plate and every cut after the
marker and breaks none of them: the check reports nothing. Deleting a
side of the outline breaks the plate, and the check still says so, with
a cache too small to keep a state.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
semmelrinde
2026-10-31 12:29:30 +00:00
committed by GitHub
parent c512c8e86a
commit d4eeedbf95
2 changed files with 38 additions and 1 deletions
+30
View File
@@ -336,6 +336,36 @@ mod tests {
assert!(s.model.state().sketch(sk).unwrap().sketch.constraints.iter().all(|c| c.id != angle));
}
/// #9917: the check before a delete in a sketch with a rolled-back history after it builds the
/// history before the delete only when a feature fails after it. Deleting a hole in the plate
/// changes the plate and every cut after it and breaks none of them: the check reports
/// nothing. Deleting a side of the plate's outline breaks the plate, and the check still says
/// so, with the cache too small to keep a state (as on a large design).
#[test]
fn a_delete_in_a_rolled_back_history_reports_what_breaks_after_the_marker_and_nothing_else() {
let mut s = Session::default();
let sk = run(&mut s, "sketch.create", json!({"plane": "XY"}))["sketch"].as_u64().unwrap();
let outline = run(&mut s, "sketch.line", json!({"points": [[0, 0], [40, 0], [30, 20], [10, 20]], "closed": true}))["curves"][0]
.as_str()
.unwrap()
.to_string();
let hole = run(&mut s, "sketch.circle.center", json!({"center": [30, 15], "radius": 2}))["curves"][0].as_str().unwrap().to_string();
run(&mut s, "sketch.finish", json!({}));
run(&mut s, "solid.extrude", json!({"distance": 5, "name": "Plate"}));
for i in 0..6 {
run(&mut s, "solid.cylinder", json!({"base": [12 + 3 * i, 8, -1], "radius": 1, "height": 10, "operation": "cut"}));
}
run(&mut s, "timeline.roll_to", json!({"position": 1}));
run(&mut s, "sketch.edit", json!({"sketch": sk}));
*s.model.cache.lock().unwrap() = solidcraft_doc::EvalCache::with_budget(1);
let doc = s.doc.clone();
let dry = run(&mut s, "selection.delete", json!({"items": [{"type": "sketch_curve", "id": hole}], "dry_run": true}));
assert_eq!(dry["would_fail"], json!([]), "{dry}");
let dry = run(&mut s, "selection.delete", json!({"items": [{"type": "sketch_curve", "id": outline}], "dry_run": true}));
assert!(dry["would_fail"].as_array().unwrap().contains(&json!("Plate")), "{dry}");
assert_eq!(*s.doc, *doc, "the checks change nothing");
}
#[test]
fn mixed_selection_is_one_undo_step() {
let (mut s, _) = part();
+8 -1
View File
@@ -1353,7 +1353,14 @@ pub(super) fn newly_broken_in_history(
model.evaluate(&doc);
model
};
newly_broken(&whole(before_doc, before), &whole(after_doc, after))
// Only a feature that fails after the change can be newly broken: when none does, the
// history before it needn't be built again (#9917: each build ran every boolean after the
// marker, half a minute on a large design).
let after = whole(after_doc, after);
if after.results.iter().all(|r| r.error.is_none()) {
return Vec::new();
}
newly_broken(&whole(before_doc, before), &after)
}
/// Whether `after_doc` differs from `before_doc` only in sketch constraints and dimensions (and