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
@@ -167,7 +167,7 @@ pub fn page(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
}
|
||||
ui.label(
|
||||
RichText::new(
|
||||
"Select Other: hold the left button still on the model (or right-click › Select Other…) to list everything under the cursor; click again on the same spot to select the next thing there.",
|
||||
"Select Other: hold the left button still on the model (or right-click › Select Other…) to list everything under the cursor; click again on the same spot to select the next thing there. Double-click the middle button to go Home (Preferences › Navigation).",
|
||||
)
|
||||
.size(11.0)
|
||||
.color(t.text_dim),
|
||||
|
||||
@@ -809,14 +809,14 @@ impl SolveApp {
|
||||
self.cam_anim = None;
|
||||
self.viewport.orbit_snap = None;
|
||||
self.viewport.orbit_free = None;
|
||||
let bounds = solidcraft_engine::view::bounds(&self.session);
|
||||
let bounds = viewport::view_bounds(self);
|
||||
self.pending_fit = Some((self.cam, bounds, false));
|
||||
self.cam = self.fitted(self.cam);
|
||||
}
|
||||
|
||||
/// `cam` moved to frame the whole model.
|
||||
pub fn fitted(&self, mut cam: Camera) -> Camera {
|
||||
let b = solidcraft_engine::view::bounds(&self.session);
|
||||
let b = viewport::view_bounds(self);
|
||||
if b.is_empty() {
|
||||
cam.target = solidcraft_engine::geom::Vec3::ZERO;
|
||||
cam.distance = 150.0;
|
||||
@@ -873,6 +873,15 @@ impl SolveApp {
|
||||
self.cam_anim = Some(anim);
|
||||
}
|
||||
|
||||
/// Frame everything shown, keeping the orientation (animated; the selection is ignored).
|
||||
pub fn animate_fit_all(&mut self) {
|
||||
self.pending_fit = None;
|
||||
let request = Some((self.cam, viewport::view_bounds(self), true));
|
||||
let to = self.fitted(self.cam);
|
||||
self.animate_to(to);
|
||||
self.pending_fit = request;
|
||||
}
|
||||
|
||||
/// Animated view changes for the axis view and navigation bar.
|
||||
pub fn animate_view(&mut self, v: &str) {
|
||||
self.pending_fit = None;
|
||||
@@ -885,13 +894,13 @@ impl SolveApp {
|
||||
to = self.fitted_bounds(to, &b);
|
||||
}
|
||||
None => {
|
||||
self.pending_fit = Some((to, solidcraft_engine::view::bounds(&self.session), true));
|
||||
self.pending_fit = Some((to, viewport::view_bounds(self), true));
|
||||
to = self.fitted(to);
|
||||
}
|
||||
},
|
||||
"home" => {
|
||||
to.set_view(StandardView::Iso);
|
||||
self.pending_fit = Some((to, solidcraft_engine::view::bounds(&self.session), true));
|
||||
self.pending_fit = Some((to, viewport::view_bounds(self), true));
|
||||
to = self.fitted(to);
|
||||
}
|
||||
v => match StandardView::parse(v) {
|
||||
|
||||
@@ -236,6 +236,36 @@ pub struct Prefs {
|
||||
pub up_axis: String,
|
||||
/// After a STEP or IGES file opens, offer to stand a Y-up part up (`stand_up`).
|
||||
pub offer_stand_up: bool,
|
||||
/// What a middle-button double-click in the view does: `home`, `fit` or `off`.
|
||||
pub middle_double_click: String,
|
||||
}
|
||||
|
||||
/// What a middle-button double-click in the view does.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum MiddleDoubleClick {
|
||||
/// Go Home (the iso view, framing what is shown).
|
||||
Home,
|
||||
/// Frame what is shown, keeping the orientation.
|
||||
Fit,
|
||||
Off,
|
||||
}
|
||||
|
||||
impl MiddleDoubleClick {
|
||||
pub const ALL: [(MiddleDoubleClick, &'static str, &'static str); 3] =
|
||||
[(MiddleDoubleClick::Home, "home", "Home"), (MiddleDoubleClick::Fit, "fit", "Fit"), (MiddleDoubleClick::Off, "off", "Off")];
|
||||
|
||||
/// The preference's setting (Home for an unknown one).
|
||||
pub fn of(app: &SolveApp) -> MiddleDoubleClick {
|
||||
Self::parse(&app.preferences.middle_double_click).unwrap_or(MiddleDoubleClick::Home)
|
||||
}
|
||||
|
||||
pub fn parse(id: &str) -> Option<MiddleDoubleClick> {
|
||||
Self::ALL.iter().find(|(_, i, _)| *i == id).map(|(m, _, _)| *m)
|
||||
}
|
||||
|
||||
pub fn label(self) -> &'static str {
|
||||
Self::ALL.iter().find(|(m, _, _)| *m == self).map_or("Home", |(_, _, l)| l)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Prefs {
|
||||
@@ -262,6 +292,7 @@ impl Default for Prefs {
|
||||
look_at_speed: 1.0,
|
||||
up_axis: "z".into(),
|
||||
offer_stand_up: true,
|
||||
middle_double_click: "home".into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -366,6 +397,7 @@ fn defaults(app: &mut SolveApp, page: usize) {
|
||||
p.space_mouse_speed = d.space_mouse_speed;
|
||||
p.look_at_anim = d.look_at_anim;
|
||||
p.look_at_speed = d.look_at_speed;
|
||||
p.middle_double_click = d.middle_double_click;
|
||||
}
|
||||
PAGE_SHORTCUTS => crate::keymap::reset(app, None),
|
||||
PAGE_DESIGN => {
|
||||
@@ -491,7 +523,7 @@ fn pivot_under(app: &SolveApp, proj: &crate::viewport::Proj, at: Pos2) -> Vec3 {
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
let b = solidcraft_engine::view::bounds(&app.session);
|
||||
let b = crate::viewport::view_bounds(app);
|
||||
if b.is_empty() { app.cam.target } else { b.center() }
|
||||
}
|
||||
|
||||
@@ -897,6 +929,18 @@ fn page(app: &mut SolveApp, ui: &mut egui::Ui, t: &Tokens) {
|
||||
ui.radio_value(&mut app.preferences.orbit_cursor, true, "Point under the cursor");
|
||||
});
|
||||
ui.end_row();
|
||||
ui.label("Middle double-click");
|
||||
let current = MiddleDoubleClick::of(app);
|
||||
egui::ComboBox::from_id_salt("pref_middle_double_click").selected_text(current.label()).show_ui(ui, |ui| {
|
||||
for (m, id, label) in MiddleDoubleClick::ALL {
|
||||
if ui.selectable_label(current == m, label).clicked() {
|
||||
app.preferences.middle_double_click = id.into();
|
||||
}
|
||||
}
|
||||
})
|
||||
.response
|
||||
.on_hover_text("Double-clicking the middle button in the view: Home (the home view, framing what is shown), Fit (frame what is shown, keeping the orientation) or nothing");
|
||||
ui.end_row();
|
||||
ui.label("Animate view changes");
|
||||
ui.checkbox(&mut app.preferences.look_at_anim, "Axis view, Look At, Home and Fit");
|
||||
ui.end_row();
|
||||
@@ -1051,6 +1095,9 @@ pub fn set(app: &mut SolveApp, p: &serde_json::Value) -> Result<serde_json::Valu
|
||||
if !(0.5..=2.5).contains(&new.ui_scale) || !(0.25..=4.0).contains(&new.look_at_speed) {
|
||||
return Err("`ui_scale` must be 0.5…2.5 and `look_at_speed` 0.25…4".into());
|
||||
}
|
||||
if MiddleDoubleClick::parse(&new.middle_double_click).is_none() {
|
||||
return Err("`middle_double_click` must be home, fit or off".into());
|
||||
}
|
||||
if new.theme != app.preferences.theme && new.theme != "system" {
|
||||
app.ui.dark = new.theme == "dark";
|
||||
}
|
||||
@@ -1433,7 +1480,7 @@ mod tests {
|
||||
let hits = crate::viewport::pick(&app, &proj, corner);
|
||||
use crate::viewport::Hit;
|
||||
assert!(!hits.iter().any(|h| matches!(h, Hit::Face { .. } | Hit::Edge { .. } | Hit::Vertex { .. })), "the corner is empty space");
|
||||
let centre = solidcraft_engine::view::bounds(&app.session).center();
|
||||
let centre = crate::viewport::view_bounds(&app).center();
|
||||
orbit(&mut app, rect, 40.0, 25.0, Some(corner));
|
||||
assert_eq!(app.viewport.orbit_pivot, Some(centre));
|
||||
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
// Home and Fit frame what is shown now, and a middle-button double-click goes Home.
|
||||
use crate::scenario::Harness;
|
||||
use serde_json::json;
|
||||
use solidcraft_engine::geom::{Aabb3, Vec3};
|
||||
use solidcraft_engine::render::StandardView;
|
||||
|
||||
/// A block drawn at the origin and extruded, then moved 500 mm along X: its sketch stays at the
|
||||
/// origin, used by the extrude (so not drawn).
|
||||
fn moved_block(h: &mut Harness) {
|
||||
h.app.run("sketch.create", json!({"plane": "XY"})).unwrap();
|
||||
h.app.run("sketch.rectangle.two_point", json!({"p0": [0, 0], "p1": [10, 10]})).unwrap();
|
||||
h.app.run("sketch.finish", json!({})).unwrap();
|
||||
h.app.run("solid.extrude", json!({"distance": 5})).unwrap();
|
||||
let body = h.app.session.world_state().bodies[0].name.clone();
|
||||
h.app.run("solid.move", json!({"bodies": [body], "translate": [500, 0, 0]})).unwrap();
|
||||
h.app.preferences.look_at_anim = false;
|
||||
h.settle();
|
||||
}
|
||||
|
||||
fn near_box(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:?}");
|
||||
}
|
||||
|
||||
/// The camera Home gives: the iso view framing what is shown.
|
||||
fn home(h: &Harness) -> solidcraft_engine::render::Camera {
|
||||
let mut cam = h.app.cam;
|
||||
cam.set_view(StandardView::Iso);
|
||||
h.app.fitted(cam)
|
||||
}
|
||||
|
||||
/// Somewhere in the view away from the model and the overlays.
|
||||
fn spot(h: &Harness) -> egui::Pos2 {
|
||||
let d = h.app.viewport.layout.as_ref().unwrap().drawable;
|
||||
d.left_top() + egui::vec2(d.width() * 0.3, d.height() * 0.7)
|
||||
}
|
||||
|
||||
fn same_cam(a: &solidcraft_engine::render::Camera, b: &solidcraft_engine::render::Camera) -> bool {
|
||||
a.target.dist(b.target) < 1e-6 && (a.distance - b.distance).abs() < 1e-6 && (a.yaw - b.yaw).abs() < 1e-9 && (a.pitch - b.pitch).abs() < 1e-9
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn home_and_fit_frame_the_moved_body_not_its_old_place() {
|
||||
let mut h = Harness::new();
|
||||
moved_block(&mut h);
|
||||
near_box(crate::viewport::view_bounds(&h.app), [500., 0., 0.], [510., 10., 5.]);
|
||||
for view in ["home", "fit"] {
|
||||
h.app.cam.pan(300.0, 200.0, 500.0);
|
||||
h.app.animate_view(view);
|
||||
h.settle();
|
||||
// Near the body's centre (the fit leaves room for the palettes), not 250 mm away.
|
||||
assert!(h.app.cam.target.dist(Vec3::new(505.0, 5.0, 2.5)) < 5.0, "{view} centres the body: {:?}", h.app.cam.target);
|
||||
assert!(h.app.cam.distance < 200.0, "{view} frames 10 mm, not 510: {}", h.app.cam.distance);
|
||||
}
|
||||
// The orbit pivot over empty space (#464) is the centre of the same box.
|
||||
let pivot = crate::viewport::view_bounds(&h.app).center();
|
||||
assert!(pivot.dist(Vec3::new(505.0, 5.0, 2.5)) < 1e-6);
|
||||
// The Sketches folder hidden: an unused sketch no longer counts either.
|
||||
h.app.run("sketch.create", json!({"plane": "XY"})).unwrap();
|
||||
h.app.run("sketch.circle.center", json!({"center": [-300, 0], "radius": 5})).unwrap();
|
||||
h.app.run("sketch.finish", json!({})).unwrap();
|
||||
near_box(crate::viewport::view_bounds(&h.app), [-305., -5., 0.], [510., 10., 5.]);
|
||||
h.app.ui.show_sketches = false;
|
||||
near_box(crate::viewport::view_bounds(&h.app), [500., 0., 0.], [510., 10., 5.]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn middle_double_click_goes_home_in_every_preset() {
|
||||
for preset in crate::prefs::NAV_PRESETS.iter().map(|p| p.id) {
|
||||
let mut h = Harness::new();
|
||||
h.call("ui.prefs", json!({"nav": preset}));
|
||||
moved_block(&mut h);
|
||||
h.app.cam.set_view(StandardView::Front);
|
||||
h.app.cam.pan(120.0, -80.0, 500.0);
|
||||
let p = spot(&h);
|
||||
h.call("ui.click", json!({"x": p.x, "y": p.y, "button": "middle", "double": true}));
|
||||
h.settle();
|
||||
let want = home(&h);
|
||||
assert!(same_cam(&h.app.cam, &want), "{preset}: {:?} != {:?}", h.app.cam, want);
|
||||
assert!(h.app.cam.target.dist(Vec3::new(505.0, 5.0, 2.5)) < 5.0, "{preset}: {:?}", h.app.cam.target);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn middle_drags_and_single_clicks_do_not_go_home() {
|
||||
for preset in crate::prefs::NAV_PRESETS.iter().map(|p| p.id) {
|
||||
let mut h = Harness::new();
|
||||
h.call("ui.prefs", json!({"nav": preset}));
|
||||
moved_block(&mut h);
|
||||
h.app.cam.set_view(StandardView::Front);
|
||||
h.app.cam.pan(120.0, -80.0, 500.0);
|
||||
let p = spot(&h);
|
||||
// A middle drag (pan or orbit), then a click where it ended: a drag and one click.
|
||||
h.call("ui.drag", json!({"x0": p.x, "y0": p.y, "x1": p.x + 40.0, "y1": p.y + 25.0, "button": "middle", "steps": 4}));
|
||||
h.settle();
|
||||
let dragged = h.app.cam;
|
||||
let q = p + egui::vec2(40.0, 25.0);
|
||||
h.call("ui.click", json!({"x": q.x, "y": q.y, "button": "middle"}));
|
||||
h.settle();
|
||||
assert!(same_cam(&h.app.cam, &dragged), "{preset}: a drag and a click went home");
|
||||
// A left click and then a middle click are not a middle double-click.
|
||||
h.call("ui.click", json!({"x": q.x, "y": q.y}));
|
||||
h.call("ui.click", json!({"x": q.x, "y": q.y, "button": "middle"}));
|
||||
h.settle();
|
||||
assert!(same_cam(&h.app.cam, &dragged), "{preset}: left + middle went home");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn middle_double_click_preference_fit_or_off() {
|
||||
let mut h = Harness::new();
|
||||
moved_block(&mut h);
|
||||
let bad = h.call("ui.prefs", json!({"middle_double_click": "zoom"}));
|
||||
assert!(bad.to_string().contains("middle_double_click"), "{bad}");
|
||||
assert_eq!(h.app.preferences.middle_double_click, "home");
|
||||
|
||||
h.call("ui.prefs", json!({"middle_double_click": "fit"}));
|
||||
h.app.cam.set_view(StandardView::Front);
|
||||
h.app.cam.pan(120.0, -80.0, 500.0);
|
||||
let (yaw, pitch) = (h.app.cam.yaw, h.app.cam.pitch);
|
||||
let p = spot(&h);
|
||||
h.call("ui.click", json!({"x": p.x, "y": p.y, "button": "middle", "double": true}));
|
||||
h.settle();
|
||||
assert_eq!((h.app.cam.yaw, h.app.cam.pitch), (yaw, pitch), "Fit keeps the orientation");
|
||||
assert!(h.app.cam.target.dist(Vec3::new(505.0, 5.0, 2.5)) < 5.0, "{:?}", h.app.cam.target);
|
||||
// Fit frames everything shown, not the selection.
|
||||
let body = h.app.session.world_state().bodies[0].name.clone();
|
||||
h.app.run("select.set", json!({"items": [{"type": "body", "name": body}]})).unwrap();
|
||||
let fitted = h.app.cam;
|
||||
h.app.cam.pan(60.0, 0.0, 500.0);
|
||||
// Well after the last pair (a third click soon after a double-click is a triple-click).
|
||||
h.frames(60);
|
||||
h.call("ui.click", json!({"x": p.x, "y": p.y, "button": "middle", "double": true}));
|
||||
h.settle();
|
||||
assert!(same_cam(&h.app.cam, &fitted));
|
||||
|
||||
h.call("ui.prefs", json!({"middle_double_click": "off"}));
|
||||
h.app.cam.pan(60.0, 0.0, 500.0);
|
||||
let moved = h.app.cam;
|
||||
h.frames(60);
|
||||
h.call("ui.click", json!({"x": p.x, "y": p.y, "button": "middle", "double": true}));
|
||||
h.settle();
|
||||
assert!(same_cam(&h.app.cam, &moved), "Off does nothing");
|
||||
}
|
||||
@@ -7,7 +7,7 @@ use solidcraft_engine::Sel;
|
||||
use solidcraft_engine::geom::{Vec2, Vec3};
|
||||
use solidcraft_engine::render::{Camera, Mat4, Rgb};
|
||||
use solidcraft_engine::sketch::CurveKind;
|
||||
use solidcraft_engine::view::{colors, grid_step, sketch_consumed, sketch_lines_with_sampler};
|
||||
use solidcraft_engine::view::{colors, grid_step, sketch_lines_with_sampler};
|
||||
use std::hash::{Hash, Hasher};
|
||||
|
||||
use crate::gpu::{GpuScene, GpuTarget, SceneSlot, ViewportCallback};
|
||||
@@ -35,8 +35,11 @@ pub enum NavMode {
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ViewportState {
|
||||
/// Per-body bounding boxes for fast picking, keyed by the model they were taken from.
|
||||
pick_bounds: std::sync::Mutex<Option<(usize, std::sync::Arc<Vec<(String, solidcraft_engine::geom::Aabb3)>>)>>,
|
||||
/// Per-body boxes of one world state, held (not just its address, which a later state can
|
||||
/// reuse once the old one is freed, so stale boxes would come back).
|
||||
pick_bounds: std::sync::Mutex<Option<BodyBounds>>,
|
||||
/// The last click in the view was the middle button's (for the middle double-click).
|
||||
middle_clicked: bool,
|
||||
/// The last pick (its key and hits): reused while nothing it depends on changed.
|
||||
last_pick: Option<(u64, Vec<Hit>)>,
|
||||
pub rect: Option<Rect>,
|
||||
@@ -239,16 +242,13 @@ pub fn projection(app: &SolveApp, rect: Rect) -> Proj {
|
||||
/// Sketches drawn in the viewport: the active one, and finished ones not yet used by a feature.
|
||||
fn visible_sketches(app: &SolveApp) -> Vec<u64> {
|
||||
let st = app.session.world_state();
|
||||
st.sketches
|
||||
.iter()
|
||||
.filter(|s| {
|
||||
app.session.active_sketch == Some(s.feature)
|
||||
|| (!app.session.visibility.hidden_sketches.contains(&s.feature)
|
||||
&& (app.session.visibility.shown_sketches.contains(&s.feature)
|
||||
|| (app.ui.show_sketches && !sketch_consumed(&app.session, s.feature))))
|
||||
})
|
||||
.map(|s| s.feature)
|
||||
.collect()
|
||||
st.sketches.iter().filter(|s| solidcraft_engine::view::sketch_drawn(&app.session, s.feature, app.ui.show_sketches)).map(|s| s.feature).collect()
|
||||
}
|
||||
|
||||
/// What Home, Fit and the orbit pivot frame: the bodies and sketches shown now, where they are
|
||||
/// now (see [`solidcraft_engine::view::visible_bounds`]).
|
||||
pub fn view_bounds(app: &SolveApp) -> solidcraft_engine::geom::Aabb3 {
|
||||
solidcraft_engine::view::visible_bounds(&app.session, app.ui.show_sketches)
|
||||
}
|
||||
|
||||
fn scene_key(app: &SolveApp) -> u64 {
|
||||
@@ -593,7 +593,7 @@ pub fn origin_axes(app: &SolveApp) -> Vec<(&'static str, Vec3)> {
|
||||
/// from, to).
|
||||
pub fn construction_axis_segments(app: &SolveApp) -> Vec<(String, Vec3, Vec3)> {
|
||||
let h = origin_size(app.cam.half_height()) * AXIS_LEN;
|
||||
let bb = solidcraft_engine::view::bounds(&app.session);
|
||||
let bb = view_bounds(app);
|
||||
solidcraft_engine::view::construction_axes(&app.session)
|
||||
.into_iter()
|
||||
.filter(|(_, n, _, _)| !app.session.visibility.hidden_origin.contains(n))
|
||||
@@ -645,20 +645,21 @@ fn body_bounds(
|
||||
app: &SolveApp,
|
||||
st: &std::sync::Arc<solidcraft_engine::doc::ModelState>,
|
||||
) -> std::sync::Arc<Vec<(String, solidcraft_engine::geom::Aabb3)>> {
|
||||
let key = std::sync::Arc::as_ptr(st) as usize;
|
||||
if let Ok(mut g) = app.viewport.pick_bounds.lock() {
|
||||
if let Some((k, b)) = g.as_ref()
|
||||
&& *k == key
|
||||
&& std::sync::Arc::ptr_eq(k, st)
|
||||
{
|
||||
return b.clone();
|
||||
}
|
||||
let b = std::sync::Arc::new(st.bodies.iter().map(|b| (b.name.clone(), b.mesh().bounds())).collect::<Vec<_>>());
|
||||
*g = Some((key, b.clone()));
|
||||
*g = Some((st.clone(), b.clone()));
|
||||
return b;
|
||||
}
|
||||
std::sync::Arc::new(Vec::new())
|
||||
}
|
||||
|
||||
type BodyBounds = (std::sync::Arc<solidcraft_engine::doc::ModelState>, std::sync::Arc<Vec<(String, solidcraft_engine::geom::Aabb3)>>);
|
||||
|
||||
/// The model's extent: bodies (from the cached boxes) and sketches.
|
||||
pub fn model_bounds(app: &SolveApp) -> solidcraft_engine::geom::Aabb3 {
|
||||
let st = app.session.world_state();
|
||||
@@ -1812,6 +1813,7 @@ pub fn show(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
app.viewport.mouse = hover.filter(|_| inside);
|
||||
// Ctrl or Alt held: drawing and dragging don't snap.
|
||||
crate::drag_snap::set_suppressed(mods.ctrl || mods.alt || mods.command);
|
||||
middle_double_click(app, ui, inside);
|
||||
|
||||
// ---- navigation ----
|
||||
let trackpad_pan = app.preferences.trackpad_pan;
|
||||
@@ -2949,6 +2951,34 @@ fn signed_axis_view(app: &mut SolveApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
}
|
||||
}
|
||||
|
||||
/// A middle-button double-click goes Home (or fits, or nothing: Preferences › Navigation). Only
|
||||
/// clicks count: a press that moves (a pan or orbit drag) or is held is not one, and the two
|
||||
/// clicks must both be the middle button's, close together in time and place.
|
||||
fn middle_double_click(app: &mut SolveApp, ui: &egui::Ui, inside: bool) {
|
||||
use egui::PointerButton::{Extra1, Extra2, Middle, Primary, Secondary};
|
||||
let (double, releases, middle, other, dragging) = ui.input(|i| {
|
||||
(
|
||||
i.pointer.button_double_clicked(Middle),
|
||||
i.events.iter().filter(|e| matches!(e, egui::Event::PointerButton { button: Middle, pressed: false, .. })).count(),
|
||||
i.pointer.button_clicked(Middle),
|
||||
[Primary, Secondary, Extra1, Extra2].into_iter().any(|b| i.pointer.button_clicked(b)),
|
||||
i.pointer.is_decidedly_dragging(),
|
||||
)
|
||||
});
|
||||
// egui pairs clicks of any buttons; the first click must have been the middle one too (or
|
||||
// both arrive in this frame).
|
||||
let go = double && inside && !other && (app.viewport.middle_clicked || releases >= 2);
|
||||
app.viewport.middle_clicked = if other || dragging || go { false } else { middle || app.viewport.middle_clicked };
|
||||
if !go {
|
||||
return;
|
||||
}
|
||||
match crate::prefs::MiddleDoubleClick::of(app) {
|
||||
crate::prefs::MiddleDoubleClick::Home => app.animate_view("home"),
|
||||
crate::prefs::MiddleDoubleClick::Fit => app.animate_fit_all(),
|
||||
crate::prefs::MiddleDoubleClick::Off => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// At a halfway tie, prefer the square heading nearer the standard plan view.
|
||||
fn nearest_quarter_turn(yaw: f64) -> f64 {
|
||||
if !yaw.is_finite() {
|
||||
@@ -4389,3 +4419,7 @@ mod tests {
|
||||
assert!(construction_axis_segments(&app).is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "view_home_tests.rs"]
|
||||
mod view_home_tests;
|
||||
Reference in new issue
Block a user