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>
This commit is contained in:
1 parent
7b34d57b25
commit
7249623855
11 files changed
+474
-31
No files matched your search
@@ -823,6 +823,8 @@ mod tests;
|
||||
#[cfg(test)]
|
||||
mod thread_tests;
|
||||
#[cfg(test)]
|
||||
mod view_bounds_tests;
|
||||
#[cfg(test)]
|
||||
mod world_placement_cache_tests;
|
||||
|
||||
/// Per sketch feature: its parameter-driven dimensions' values (see [`own_values`]).
|
||||
|
||||
@@ -39,20 +39,32 @@ pub mod colors {
|
||||
pub const SECTION_CAP: Rgb = solidcraft_render::SECTION_CAP;
|
||||
}
|
||||
|
||||
/// Bounds of everything visible.
|
||||
/// Bounds of everything visible now, in world space: shown bodies where their occurrences,
|
||||
/// joints and moves place them, and the sketches the view draws (not ones used by a feature,
|
||||
/// hidden ones, or ones in hidden components). What Home, Fit and the orbit pivot frame.
|
||||
pub fn bounds(s: &Session) -> Aabb3 {
|
||||
visible_bounds(s, true)
|
||||
}
|
||||
|
||||
/// [`bounds`], with the browser's Sketches folder shown (`sketches`) or hidden.
|
||||
pub fn visible_bounds(s: &Session, sketches: bool) -> Aabb3 {
|
||||
let st = s.world_state();
|
||||
let mut b = Aabb3::EMPTY;
|
||||
let v = &s.visibility;
|
||||
for body in st.bodies.iter().filter(|x| s.body_visible(&x.name)) {
|
||||
b = b.union(&body.mesh().bounds());
|
||||
}
|
||||
for ss in st.sketches.iter().filter(|x| !v.hidden_sketches.contains(&x.feature)) {
|
||||
for ss in st.sketches.iter().filter(|x| sketch_drawn(s, x.feature, sketches)) {
|
||||
// All four corners: on a tilted plane two opposite ones don't span the rectangle.
|
||||
if let Some((lo, hi)) = ss.sketch.bounds() {
|
||||
for p in [lo, hi] {
|
||||
for p in [lo, hi, solidcraft_geom::Vec2::new(lo.x, hi.y), solidcraft_geom::Vec2::new(hi.x, lo.y)] {
|
||||
b.add(ss.plane.to_world(p));
|
||||
}
|
||||
}
|
||||
for w in ss.sketch.wires.iter().filter(|w| !w.fit.is_empty() || !ss.sketch.view.hide_projected) {
|
||||
for p in &w.pts {
|
||||
b.add(*p);
|
||||
}
|
||||
}
|
||||
}
|
||||
b
|
||||
}
|
||||
@@ -284,8 +296,38 @@ pub fn section_segments(m: &solidcraft_geom::Mesh, o: Vec3, n: Vec3) -> Vec<(Vec
|
||||
/// Is a sketch drawn? The active one is; others unless hidden. Finished sketches are hidden
|
||||
/// once a feature uses them, like Fusion's default, unless shown one by one.
|
||||
pub fn sketch_shown(s: &Session, id: u64) -> bool {
|
||||
sketch_drawn(s, id, true)
|
||||
}
|
||||
|
||||
/// Is a sketch drawn, with the browser's Sketches folder shown (`folder`) or hidden? The sketch
|
||||
/// being edited always is; otherwise not when hidden, nor in a hidden component, and one used by
|
||||
/// a feature only when shown on its own.
|
||||
pub fn sketch_drawn(s: &Session, id: u64, folder: bool) -> bool {
|
||||
let v = &s.visibility;
|
||||
s.active_sketch == Some(id) || (!v.hidden_sketches.contains(&id) && (v.shown_sketches.contains(&id) || !sketch_consumed(s, id)))
|
||||
if s.active_sketch == Some(id) {
|
||||
return true;
|
||||
}
|
||||
if v.hidden_sketches.contains(&id) || !sketch_component_visible(s, id) {
|
||||
return false;
|
||||
}
|
||||
v.shown_sketches.contains(&id) || (folder && !sketch_consumed(s, id))
|
||||
}
|
||||
|
||||
/// Is the placement a sketch is drawn at shown (the active one while its component is being
|
||||
/// edited, else the component's first)?
|
||||
fn sketch_component_visible(s: &Session, id: u64) -> bool {
|
||||
if s.visibility.hidden_occurrences.is_empty() {
|
||||
return true;
|
||||
}
|
||||
let component = s.doc.feature(id).map_or(0, |f| f.component);
|
||||
if component == 0 {
|
||||
return true;
|
||||
}
|
||||
let path = match &s.active_occurrence_path {
|
||||
Some(p) if s.active_component == component => p.clone(),
|
||||
_ => s.doc.first_path(component),
|
||||
};
|
||||
s.occurrence_visible(&path)
|
||||
}
|
||||
|
||||
/// Is a sketch used by a later feature?
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
// 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));
|
||||
}
|
||||
Reference in new issue
Block a user