Files
solidcraft-upstream/crates/engine/src/view_bounds_tests.rs
T
bherbruckandClaude Opus 5.5 7249623855 Home and Fit frame what is shown now; middle double-click goes Home (#701)
Home, Fit, the orbit pivot and the scene's depth range counted sketches the view doesn't draw (ones used by an extrude or revolve, ones in hidden components), so after a body moved away from its sketch Home framed the empty spot it came from. They now use the world-space bounds of what is shown now (view::visible_bounds / view::sketch_drawn, viewport::view_bounds). The pick-box cache holds its world state instead of its address. A middle-button double-click goes Home in every preset (Preferences › Navigation: Home, Fit or Off).

Fixes #689

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 09:33:55 -04:00

163 lines
7.3 KiB
Rust

// Home and Fit frame what is visible now: bodies where they are placed (occurrence moves,
// joints, motion), sketches only while they are drawn, nothing hidden or consumed.
use serde_json::{Value, json};
use solidcraft_geom::{Aabb3, Vec3};
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}"),
}
}
/// What the viewport draws: visible bodies' meshes and the sketches it shows, from the scene.
fn drawn(s: &Session) -> Aabb3 {
let cam = solidcraft_render::Camera { distance: 1e5, ..Default::default() };
let scene = crate::view::scene(s, &cam);
let mut b = Aabb3::EMPTY;
for m in &scene.meshes {
b = b.union(&m.mesh.bounds());
}
use crate::view::colors::{AXIS_X, AXIS_Y, AXIS_Z, GRID, GRID_MAJOR};
for l in scene.lines.iter().filter(|l| ![GRID, GRID_MAJOR, AXIS_X, AXIS_Y, AXIS_Z].contains(&l.color)) {
for p in &l.points {
b.add(*p);
}
}
b
}
fn same(a: Aabb3, b: Aabb3) {
assert!(!a.is_empty() && !b.is_empty(), "{a:?} {b:?}");
assert!(a.min.dist(b.min) < 1e-6 && a.max.dist(b.max) < 1e-6, "Home frames {a:?}, the view shows {b:?}");
}
fn near(a: Aabb3, lo: [f64; 3], hi: [f64; 3]) {
let (lo, hi) = (Vec3::new(lo[0], lo[1], lo[2]), Vec3::new(hi[0], hi[1], hi[2]));
assert!(a.min.dist(lo) < 1e-6 && a.max.dist(hi) < 1e-6, "{a:?} != {lo:?}..{hi:?}");
}
/// A 10 x 10 x 5 block drawn as a sketch and extruded, in the active component.
fn block(s: &mut Session) {
run(s, "sketch.create", json!({"plane": "XY"}));
run(s, "sketch.rectangle.two_point", json!({"p0": [0, 0], "p1": [10, 10]}));
run(s, "sketch.finish", json!({}));
run(s, "solid.extrude", json!({"distance": 5}));
}
#[test]
fn moved_body_leaves_its_used_sketch_out() {
let mut s = Session::default();
block(&mut s);
near(crate::view::bounds(&s), [0., 0., 0.], [10., 10., 5.]);
let body = s.world_state().bodies[0].name.clone();
run(&mut s, "solid.move", json!({"bodies": [body], "translate": [500, 0, 0]}));
near(crate::view::bounds(&s), [500., 0., 0.], [510., 10., 5.]);
same(crate::view::bounds(&s), drawn(&s));
}
#[test]
fn moved_component_frames_where_it_is_now() {
let mut s = Session::default();
run(&mut s, "component.create", json!({"name": "A"}));
block(&mut s);
run(&mut s, "component.activate", json!({"component": "root"}));
let b = run(&mut s, "component.create", json!({"name": "B"}))["occurrence"].as_u64().unwrap();
block(&mut s);
run(&mut s, "component.activate", json!({"component": "root"}));
run(&mut s, "occurrence.move", json!({"occurrence": b, "translate": [800, 0, 0]}));
near(crate::view::bounds(&s), [0., 0., 0.], [810., 10., 5.]);
run(&mut s, "occurrence.move", json!({"occurrence": b, "translate": [-780, 0, 0]}));
near(crate::view::bounds(&s), [0., 0., 0.], [30., 10., 5.]);
same(crate::view::bounds(&s), drawn(&s));
// Pending (not captured) moves too.
run(&mut s, "occurrence.move", json!({"occurrence": b, "translate": [0, 300, 0], "capture": false}));
near(crate::view::bounds(&s), [0., 0., 0.], [30., 310., 5.]);
same(crate::view::bounds(&s), drawn(&s));
}
#[test]
fn body_consumed_by_a_join_or_cut_is_not_framed() {
let mut s = Session::default();
run(&mut s, "solid.box", json!({"length": 10, "width": 10, "height": 10}));
run(&mut s, "solid.box", json!({"length": 10, "width": 10, "height": 10, "corner": [200, 0, 0], "operation": "new"}));
let names: Vec<String> = s.world_state().bodies.iter().map(|b| b.name.clone()).collect();
run(&mut s, "solid.move", json!({"bodies": [names[1].clone()], "translate": [-195, 0, 0]}));
run(&mut s, "solid.combine", json!({"target": names[0].clone(), "tools": [names[1].clone()], "operation": "cut"}));
near(crate::view::bounds(&s), [0., 0., 0.], [5., 10., 10.]);
same(crate::view::bounds(&s), drawn(&s));
}
#[test]
fn hidden_component_is_not_framed() {
let mut s = Session::default();
run(&mut s, "component.create", json!({"name": "A"}));
block(&mut s);
run(&mut s, "component.activate", json!({"component": "root"}));
let b = run(&mut s, "component.create", json!({"name": "B"}))["occurrence"].as_u64().unwrap();
run(&mut s, "sketch.create", json!({"plane": "XY"}));
run(&mut s, "sketch.rectangle.two_point", json!({"p0": [0, 0], "p1": [10, 10]}));
run(&mut s, "sketch.finish", json!({}));
run(&mut s, "solid.extrude", json!({"distance": 5}));
// An unused sketch in B, drawn while B is shown.
run(&mut s, "sketch.create", json!({"plane": "XY"}));
run(&mut s, "sketch.rectangle.two_point", json!({"p0": [0, 0], "p1": [40, 40]}));
run(&mut s, "sketch.finish", json!({}));
run(&mut s, "component.activate", json!({"component": "root"}));
run(&mut s, "occurrence.move", json!({"occurrence": b, "translate": [400, 0, 0]}));
near(crate::view::bounds(&s), [0., 0., 0.], [440., 40., 5.]);
same(crate::view::bounds(&s), drawn(&s));
run(&mut s, "browser.visibility", json!({"visible": false, "paths": [[b]]}));
near(crate::view::bounds(&s), [0., 0., 0.], [10., 10., 5.]);
same(crate::view::bounds(&s), drawn(&s));
}
/// Two leaves joined by a hinge (as in the joint tests).
fn hinge(s: &mut Session) {
for name in ["LeafA", "LeafB"] {
run(s, "component.activate", json!({"component": "root"}));
run(s, "component.create", json!({"name": name}));
block_leaf(s);
}
run(s, "component.activate", json!({"component": "root"}));
let a = s.doc.occurrence_of(s.doc.find_component("LeafA").unwrap()).unwrap().id;
run(s, "occurrence.ground", json!({"occurrence": a, "grounded": true}));
let b = s.doc.occurrence_of(s.doc.find_component("LeafB").unwrap()).unwrap().id;
// B starts far away; the joint brings it onto A.
run(s, "occurrence.move", json!({"occurrence": b, "translate": [600, 0, 0]}));
run(
s,
"joint.create",
json!({"type": "revolute", "a": {"occurrence": "LeafA", "circle": [1, 37, 4]}, "b": {"occurrence": "LeafB", "circle": [601, 37, 4]}, "flip": true, "name": "Hinge"}),
);
}
fn block_leaf(s: &mut Session) {
// The plate drawn as a sketch (used by its extrude), and the knuckle.
run(s, "sketch.create", json!({"plane": "XY"}));
run(s, "sketch.rectangle.two_point", json!({"p0": [0, 0], "p1": [40, 30]}));
run(s, "sketch.finish", json!({}));
run(s, "solid.extrude", json!({"distance": 3}));
run(s, "solid.cylinder", json!({"base": [0, 33, 4], "axis": [1, 0, 0], "radius": 4, "height": 40, "operation": "new"}));
}
#[test]
fn joint_and_drive_frame_the_joined_place() {
let mut s = Session::default();
hinge(&mut s);
let b = crate::view::bounds(&s);
assert!(b.max.x < 50.0, "the joint brought B onto A: {b:?}");
same(b, drawn(&s));
run(&mut s, "joint.drive", json!({"joint": "Hinge", "value": 90}));
let b = crate::view::bounds(&s);
assert!(b.max.x < 50.0 && b.max.z > 30.0, "{b:?}");
same(b, drawn(&s));
run(&mut s, "motion.study", json!({"name": "Swing", "steps": 10, "keys": [{"joint": "Hinge", "points": [[0, 0], [10, 180]]}]}));
run(&mut s, "motion.play", json!({"study": "Swing", "step": 10}));
let b = crate::view::bounds(&s);
assert!(b.max.x < 50.0 && b.max.y > 60.0, "{b:?}");
same(b, drawn(&s));
}