diff --git a/crates/engine/src/cmd/delete.rs b/crates/engine/src/cmd/delete.rs index 05b1cfd809..d330dd79bd 100644 --- a/crates/engine/src/cmd/delete.rs +++ b/crates/engine/src/cmd/delete.rs @@ -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(); diff --git a/crates/engine/src/cmd/edit.rs b/crates/engine/src/cmd/edit.rs index 886d40b3a5..c998019df0 100644 --- a/crates/engine/src/cmd/edit.rs +++ b/crates/engine/src/cmd/edit.rs @@ -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