Files
solidcraft-upstream/crates/engine/src/sketch_edit_removed_tests.rs
T
Lasse Lian 9fbbf8dc3a Edit Sketch no longer shows bodies a later feature deletes (#1329)
Fixes #1167. While a sketch is edited, a body that a rolled-off, unsuppressed Remove deletes is not shown (unless the sketch is built on it); the history stays rolled back, only the view changes. Drawing, picking, screenshots, thumbnails and the browser's Bodies folder agree; document.inspect reports removed_later. Checked on main + this branch: cargo check --workspace, the engine sketch_edit_removed/occurrences tests and the UI viewport/browser tests.
2026-10-15 06:34:01 -04:00

162 lines
7.0 KiB
Rust

//! Edit Sketch rolls the timeline back to the sketch. A body made before the sketch and deleted
//! after it is then part of the model again, but it must not be shown while the sketch is edited
//! (#1167): the history is rolled back, the view of it is not. A body the sketch is built on stays.
use serde_json::{Value, json};
use solidcraft_geom::Vec3;
use solidcraft_sketch::LinkSource;
use crate::Session;
fn run(s: &mut Session, id: &str, p: Value) -> Value {
match s.execute(id, &p) {
Ok(v) => v,
Err(e) => panic!("{id} {p}: {e}"),
}
}
/// A 10 x 10 x 5 block `name`, drawn on the XY plane at `x`.
fn block(s: &mut Session, name: &str, x: f64) {
run(s, "sketch.create", json!({"plane": "XY", "name": format!("{name}Sketch")}));
run(s, "sketch.rectangle.two_point", json!({"p0": [x, 0], "p1": [x + 10.0, 10]}));
run(s, "sketch.finish", json!({}));
run(s, "solid.extrude", json!({"sketch": format!("{name}Sketch"), "distance": 5, "body_name": name}));
}
/// Whether `inspect` lists the body, and what it says of it.
fn inspected(s: &mut Session, body: &str) -> Value {
let r = run(s, "document.inspect", json!({"measure": false, "sketches": false}));
r["bodies"].as_array().and_then(|bodies| bodies.iter().find(|b| b["name"] == body)).cloned().unwrap_or(Value::Null)
}
/// `Kept` and `Gone` (removed after the sketch `Edited`), then Edit Sketch on `Edited`.
fn edit_with_a_later_removal(s: &mut Session) {
block(s, "Kept", 0.0);
block(s, "Gone", 20.0);
run(s, "sketch.create", json!({"plane": "XY", "offset": "30 mm", "name": "Edited"}));
run(s, "sketch.rectangle.two_point", json!({"p0": [0, 0], "p1": [5, 5]}));
run(s, "sketch.finish", json!({}));
run(s, "solid.remove", json!({"bodies": ["Gone"]}));
run(s, "timeline.roll_to", json!({"feature": "Edited"}));
run(s, "sketch.edit", json!({"sketch": "Edited"}));
}
#[test]
fn a_body_deleted_later_is_not_shown_while_a_sketch_is_edited() {
let mut s = Session::default();
edit_with_a_later_removal(&mut s);
// The rolled-back history still has the body: commands and measurements see it.
assert!(s.model.state().body("Gone").is_some());
assert!(!s.body_visible("Gone"));
assert!(s.body_removed_later("Gone"));
let gone = inspected(&mut s, "Gone");
assert_eq!(gone["visible"], json!(false), "{gone}");
assert_eq!(gone["removed_later"], json!(true), "{gone}");
// What is not deleted is shown as before.
assert!(s.body_visible("Kept"));
let kept = inspected(&mut s, "Kept");
assert_eq!(kept["visible"], json!(true), "{kept}");
assert!(kept.get("removed_later").is_none(), "{kept}");
}
#[test]
fn the_body_is_shown_again_when_the_sketch_is_finished() {
let mut s = Session::default();
edit_with_a_later_removal(&mut s);
run(&mut s, "sketch.finish", json!({}));
// Rolled back and not editing: the history as it was, which has the body.
assert!(s.body_visible("Gone"));
assert!(inspected(&mut s, "Gone").get("removed_later").is_none());
// Rolled forward, it is gone.
run(&mut s, "timeline.roll_to", json!({}));
assert!(s.model.state().body("Gone").is_none());
}
#[test]
fn rolling_back_by_hand_shows_the_history() {
let mut s = Session::default();
block(&mut s, "Gone", 0.0);
run(&mut s, "solid.remove", json!({"bodies": ["Gone"]}));
run(&mut s, "timeline.roll_to", json!({"position": 2}));
assert!(s.body_visible("Gone"));
}
#[test]
fn a_suppressed_removal_does_not_hide_the_body() {
let mut s = Session::default();
edit_with_a_later_removal(&mut s);
run(&mut s, "sketch.finish", json!({}));
let removal = s.doc.features.iter().find(|f| f.kind.type_name() == "RemoveFeature").map(|f| f.id).unwrap_or(0);
run(&mut s, "timeline.suppress", json!({"feature": removal, "suppressed": true}));
run(&mut s, "sketch.edit", json!({"sketch": "Edited"}));
assert!(s.body_visible("Gone"));
}
#[test]
fn a_body_the_sketch_projects_stays_shown() {
let mut s = Session::default();
block(&mut s, "Gone", 0.0);
run(&mut s, "sketch.create", json!({"plane": "XY", "offset": "5 mm", "name": "Edited"}));
run(&mut s, "sketch.project", json!({"refs": [{"edge": [10, 5, 5], "body": "Gone"}]}));
run(&mut s, "sketch.finish", json!({}));
run(&mut s, "solid.remove", json!({"bodies": ["Gone"]}));
run(&mut s, "timeline.roll_to", json!({"feature": "Edited"}));
run(&mut s, "sketch.edit", json!({"sketch": "Edited"}));
assert!(s.body_visible("Gone"));
}
#[test]
fn a_body_the_sketch_sits_on_stays_shown() {
let mut s = Session::default();
block(&mut s, "Gone", 0.0);
let top = {
let world = s.world_state();
let face = world
.body("Gone")
.and_then(|body| body.body.faces(0.01).ok())
.and_then(|faces| faces.into_iter().find(|f| f.plane_normal.is_some_and(|n| n.dot(Vec3::Z) > 0.999)))
.unwrap();
json!({"face": [face.centroid.x, face.centroid.y, face.centroid.z], "body": "Gone", "index": face.index})
};
run(&mut s, "sketch.create", json!({"plane": top, "name": "Edited"}));
let anchored = match s.doc.find_feature("Edited").map(|f| &f.kind) {
Some(solidcraft_doc::FeatureKind::Sketch { plane: solidcraft_doc::PlaneRef::Face { anchor, .. }, .. }) => anchor.is_some(),
other => panic!("not attached to a face: {other:?}"),
};
assert!(anchored, "the face attachment names its body");
run(&mut s, "sketch.finish", json!({}));
run(&mut s, "solid.remove", json!({"bodies": ["Gone"]}));
run(&mut s, "timeline.roll_to", json!({"feature": "Edited"}));
run(&mut s, "sketch.edit", json!({"sketch": "Edited"}));
assert!(s.body_visible("Gone"));
}
#[test]
fn every_placement_of_a_component_body_is_left_out() {
let mut s = Session::default();
let component = run(&mut s, "component.create", json!({"name": "Block"}))["component"].as_u64().unwrap_or(0);
block(&mut s, "Gone", 0.0);
run(&mut s, "sketch.create", json!({"plane": "XY", "offset": "30 mm", "name": "Edited"}));
run(&mut s, "sketch.rectangle.two_point", json!({"p0": [0, 0], "p1": [5, 5]}));
run(&mut s, "sketch.finish", json!({}));
run(&mut s, "solid.remove", json!({"bodies": ["Gone"]}));
run(&mut s, "occurrence.copy", json!({"component": component, "translate": [50, 0, 0]}));
run(&mut s, "timeline.roll_to", json!({"feature": "Edited"}));
run(&mut s, "sketch.edit", json!({"sketch": "Edited"}));
let world = s.world_state();
assert_eq!(world.bodies.len(), 2, "one body, placed twice");
for body in &world.bodies {
assert!(!s.body_visible(&body.name), "{} is deleted later", body.name);
}
}
#[test]
fn a_link_reads_the_bodies_of_its_sources() {
let at = Vec3::ZERO;
let edge = LinkSource::Edge { body: "A".into(), at };
let face = LinkSource::Face { body: "B".into(), at };
assert_eq!(edge.bodies(), ["A"]);
assert_eq!(LinkSource::Intersection { a: Box::new(edge), b: Box::new(face) }.bodies(), ["A", "B"]);
assert!(LinkSource::Origin.bodies().is_empty());
}