Keep viewport hover and click picking while a 3D mouse moves the view (#1781)

Thanks @oursland.
This commit is contained in:
Jackson Oursland authored and GitHub committed 2026-10-16 14:26:32 -04:00
1 parent 45ae1bef95
commit a9413fe385
3 files changed
+44 -51

No files matched your search

+4 -24
View File
@@ -249,8 +249,6 @@ pub(crate) struct CameraInput {
pub primary_nav: Option<NavMode>, pub primary_nav: Option<NavMode>,
/// The navigation this frame's pointer input drives. /// The navigation this frame's pointer input drives.
pub mode: Option<NavMode>, pub mode: Option<NavMode>,
/// The 3D mouse changed this page's camera this frame.
pub device_moved: bool,
} }
/// Camera input for one frame over `rect`: the navigation preset's buttons and modifiers, Orbit /// Camera input for one frame over `rect`: the navigation preset's buttons and modifiers, Orbit
@@ -288,7 +286,7 @@ pub(crate) fn handle(app: &mut SolveApp, ui: &mut egui::Ui, rect: Rect, resp: &e
crate::drag_snap::set_suppressed(mods.ctrl || mods.alt || mods.command); crate::drag_snap::set_suppressed(mods.ctrl || mods.alt || mods.command);
middle_double_click(app, ui, inside); middle_double_click(app, ui, inside);
// The 3D mouse's motion this frame, with what this page's camera may do. // The 3D mouse's motion this frame, with what this page's camera may do.
let device_moved = device(app, rect, caps); device(app, rect, caps);
// ---- navigation ---- // ---- navigation ----
let trackpad_pan = app.preferences.trackpad_pans(); let trackpad_pan = app.preferences.trackpad_pans();
@@ -506,36 +504,18 @@ pub(crate) fn handle(app: &mut SolveApp, ui: &mut egui::Ui, rect: Rect, resp: &e
if app.viewport.orbit_snap.is_some() { if app.viewport.orbit_snap.is_some() {
ui.ctx().request_repaint(); ui.ctx().request_repaint();
} }
CameraInput { CameraInput { hover, scroll, mods, middle, secondary_down, primary_down, primary_pressed, delta, inside, axes, pinch, pan, primary_nav, mode }
hover,
scroll,
mods,
middle,
secondary_down,
primary_down,
primary_pressed,
delta,
inside,
axes,
pinch,
pan,
primary_nav,
mode,
device_moved,
}
} }
/// The 3D mouse's motion this frame (#1336), on a page `rect` whose camera may do `caps`: the cap /// The 3D mouse's motion this frame (#1336), on a page `rect` whose camera may do `caps`: the cap
/// pans and zooms every page, and turns the view only where the page may orbit and no sketch is /// pans and zooms every page, and turns the view only where the page may orbit and no sketch is
/// being edited (a sketch keeps its view square to it). [`handle`] calls it for its page; the 3D /// being edited (a sketch keeps its view square to it). [`handle`] calls it for its page; the 3D
/// mouse only reads the device and queues the motion ([`crate::space_mouse::navigate_frame`]). /// mouse only reads the device and queues the motion ([`crate::space_mouse::navigate_frame`]).
pub(crate) fn device(app: &mut SolveApp, rect: Rect, caps: NavCaps) -> bool { pub(crate) fn device(app: &mut SolveApp, rect: Rect, caps: NavCaps) {
let Some(queued) = app.viewport.device_motion.take() else { return false }; let Some(queued) = app.viewport.device_motion.take() else { return };
let before = app.cam;
let turn = caps.orbit && app.session.active_sketch.is_none(); let turn = caps.orbit && app.session.active_sketch.is_none();
let height = f64::from(rect.height().max(1.0)); let height = f64::from(rect.height().max(1.0));
device_camera(&mut app.cam, queued.motion, queued.seconds, height, turn, app.preferences.zoom_reverse); device_camera(&mut app.cam, queued.motion, queued.seconds, height, turn, app.preferences.zoom_reverse);
app.cam != before
} }
/// `seconds` of 3D mouse `motion` on `cam`, in a view `height` points tall. A full deflection /// `seconds` of 3D mouse `motion` on `cam`, in a view `height` points tall. A full deflection
+35 -24
View File
@@ -2266,7 +2266,6 @@ pub fn show(app: &mut SolveApp, ui: &mut egui::Ui) {
pan, pan,
primary_nav, primary_nav,
mode, mode,
device_moved,
} = input; } = input;
app.viewport.physical_scale = ui.ctx().pixels_per_point(); app.viewport.physical_scale = ui.ctx().pixels_per_point();
let proj = projection(app, rect); let proj = projection(app, rect);
@@ -2276,8 +2275,9 @@ pub fn show(app: &mut SolveApp, ui: &mut egui::Ui) {
// Suppress hover for the whole navigation gesture, including frames between pointer // Suppress hover for the whole navigation gesture, including frames between pointer
// events. Testing delta made the highlight reappear whenever a held drag paused. // events. Testing delta made the highlight reappear whenever a held drag paused.
// Resolve it before rendering so the GPU scene and screen overlays use the same hit. // Resolve it before rendering so the GPU scene and screen overlays use the same hit.
let navigating = device_moved // A 3D mouse moving the view is not such a gesture: the pointer hand is free, and hover
|| scroll != 0.0 // and clicks keep picking while the other hand turns the view.
let navigating = scroll != 0.0
|| pinch != 1.0 || pinch != 1.0
|| pan != egui::Vec2::ZERO || pan != egui::Vec2::ZERO
|| middle || middle
@@ -4415,36 +4415,47 @@ mod tests {
} }
} }
/// Two-handed use: while a 3D mouse turns the view, the pointer still preselects what is
/// under it in the view as it is now, and a click picks it (it neither does nothing nor
/// clears the selection).
#[test] #[test]
fn device_navigation_skips_hover_picking_and_resumes_when_motion_stops() { fn device_navigation_keeps_hover_and_click_picking() {
use crate::space_mouse::{DeviceMotion, Motion}; use crate::space_mouse::{DeviceMotion, Motion};
for gpu in [false, true] { for gpu in [false, true] {
let ctx = egui::Context::default(); let ctx = egui::Context::default();
let mut app = hover_app(gpu); let mut s = solidcraft_engine::Session::default();
app.session.active_sketch = None; s.execute("solid.box", &json!({"length": 40, "width": 30, "height": 20})).unwrap();
let mut app = SolveApp::new(s, crate::Services::default());
app.ui.show_origin = false;
if gpu {
app.viewport.gpu = Some(GpuTarget { format: egui_wgpu::wgpu::TextureFormat::Rgba8Unorm, depth: None, samples: 1, tess: false });
}
app.cam.target = Vec3::new(20.0, 15.0, 10.0);
hover_frame(&mut app, &ctx, vec![]); hover_frame(&mut app, &ctx, vec![]);
let p = projection(&app, app.viewport.rect.unwrap()).to_screen(Vec3::ZERO).unwrap(); // The middle of the top face.
let p = projection(&app, app.viewport.rect.unwrap()).to_screen(Vec3::new(20.0, 15.0, 20.0)).unwrap();
hover_frame(&mut app, &ctx, vec![egui::Event::PointerMoved(p)]); hover_frame(&mut app, &ctx, vec![egui::Event::PointerMoved(p)]);
assert!(app.viewport.hover.is_some()); assert!(matches!(app.viewport.hover, Some(Hit::Face { .. })), "{:?}", app.viewport.hover);
let pick = app.viewport.last_pick.as_ref().unwrap().0; let turn = || Some(DeviceMotion { motion: Motion { translation: [0.0; 3], rotation: [0.0, 0.0, 0.1] }, seconds: 0.016 });
let before = app.cam; let mut pick = app.viewport.last_pick.as_ref().unwrap().0;
for _ in 0..3 { for _ in 0..3 {
app.viewport.device_motion = let before = app.cam;
Some(DeviceMotion { motion: Motion { translation: [0.0; 3], rotation: [0.0, 0.0, 0.1] }, seconds: 0.016 }); app.viewport.device_motion = turn();
hover_frame(&mut app, &ctx, vec![]); hover_frame(&mut app, &ctx, vec![]);
assert_ne!(app.cam, before); assert_ne!(app.cam, before, "the device turns the view");
assert!(app.viewport.device_motion.is_none()); assert!(matches!(app.viewport.hover, Some(Hit::Face { .. })), "hover stays on while the device moves the view");
assert!(app.viewport.hover.is_none(), "hover must stay off while the device moves the view"); let now = app.viewport.last_pick.as_ref().unwrap().0;
assert_eq!(app.viewport.last_pick.as_ref().unwrap().0, pick, "device navigation ran picking again"); assert_ne!(now, pick, "the pick follows the turned view");
} pick = now;
hover_frame(&mut app, &ctx, vec![]);
assert!(app.viewport.hover.is_some(), "hover must return as soon as motion stops");
assert_ne!(app.viewport.last_pick.as_ref().unwrap().0, pick);
for motion in [DeviceMotion::default(), DeviceMotion { seconds: f64::NAN, ..DeviceMotion::default() }] {
app.viewport.device_motion = Some(motion);
hover_frame(&mut app, &ctx, vec![]);
assert!(app.viewport.hover.is_some(), "an idle device must not suppress selection feedback");
} }
// A click while the device keeps turning the view selects the face under the pointer.
let button =
|pressed| egui::Event::PointerButton { pos: p, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() };
app.viewport.device_motion = turn();
hover_frame(&mut app, &ctx, vec![button(true)]);
app.viewport.device_motion = turn();
hover_frame(&mut app, &ctx, vec![button(false)]);
assert!(matches!(app.session.selection.as_slice(), [Sel::Face { .. }]), "gpu {gpu}: {:?}", app.session.selection);
} }
} }
+5 -3
View File
@@ -17,9 +17,11 @@ camera moves. Large assemblies keep their tree open without resolving every part
folder again on each frame. Geometry edits, assembly changes and uncaptured or reverted folder again on each frame. Geometry edits, assembly changes and uncaptured or reverted
positions refresh those queries; selection, visibility and locking are read live. positions refresh those queries; selection, visibility and locking are read live.
Hover picking pauses while the mouse or a 3D mouse moves the view and resumes when Hover picking pauses while the mouse moves the view and resumes when navigation stops.
navigation stops. Picking uses the cached body bounds directly in model order, so A 3D mouse moving the view leaves it on: the pointer hand is free, so preselection and
finding each body's bounds does not scan the assembly again. clicks keep working while the other hand turns the view. Picking uses the cached body
bounds directly in model order, so finding each body's bounds does not scan the assembly
again.
Hovering an assembly's Browser row retains its whole-body highlight buffers during Hovering an assembly's Browser row retains its whole-body highlight buffers during
rotation, panning and zoom; the GPU transforms those buffers with the camera. The rotation, panning and zoom; the GPU transforms those buffers with the camera. The