Ctrl+left drag on empty space orbits in every navigation preset (#471)
A left press with Ctrl (or Cmd) held, no Shift or Alt, on empty space orbits for that press, unless the preset binds Ctrl+left itself. It only orbits once the pointer moves: a Ctrl+click still adds to the selection, and Ctrl+drag on sketch geometry still drags it without snapping. Works with a tool active, without adding geometry. Ctrl+box selection from empty space becomes the orbit; Shift+box selection still adds. Preferences › Navigation and the UI tour mention it. Test drives every preset through real pointer events. Claude-Session: https://claude.ai/code/session_01YDDPmNdRoBgkHcLeUDKVfb Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
fc1d70cc3b
commit
1ae6e9beb4
3 files changed
+74
-12
No files matched your search
@@ -816,7 +816,7 @@ fn page(app: &mut SolveApp, ui: &mut egui::Ui, t: &Tokens) {
|
|||||||
});
|
});
|
||||||
ui.end_row();
|
ui.end_row();
|
||||||
let snap_help = if app.preferences.nav == OPENSCAD_NAV { "" } else { "; hold Shift during orbit to snap" };
|
let snap_help = if app.preferences.nav == OPENSCAD_NAV { "" } else { "; hold Shift during orbit to snap" };
|
||||||
note(ui, &format!("{}{snap_help}; the wheel zooms", nav_preset(&app.preferences.nav).help));
|
note(ui, &format!("{}; Ctrl+left drag on empty space orbits{snap_help}; the wheel zooms", nav_preset(&app.preferences.nav).help));
|
||||||
ui.label("Context menu");
|
ui.label("Context menu");
|
||||||
ui.checkbox(&mut app.preferences.radial_context_menu, "Radial context menu");
|
ui.checkbox(&mut app.preferences.radial_context_menu, "Radial context menu");
|
||||||
ui.end_row();
|
ui.end_row();
|
||||||
|
|||||||
@@ -1726,20 +1726,24 @@ pub fn show(app: &mut SolveApp, ui: &mut egui::Ui) {
|
|||||||
// A left press with the preset's keys (Alias: Shift+Alt) navigates until the button is let
|
// A left press with the preset's keys (Alias: Shift+Alt) navigates until the button is let
|
||||||
// go (kept through the release frame, so the end of the drag doesn't select either).
|
// go (kept through the release frame, so the end of the drag doesn't select either).
|
||||||
if primary_pressed && inside {
|
if primary_pressed && inside {
|
||||||
let openscad_orbit = app.preferences.nav == crate::prefs::OPENSCAD_NAV
|
let empty_space = |app: &SolveApp| {
|
||||||
&& app.tool.is_none()
|
app.viewport.context_menu.is_none()
|
||||||
&& app.dialog.is_none()
|
&& hover.is_some_and(|p| {
|
||||||
&& app.viewport.context_menu.is_none()
|
let proj = projection(app, rect);
|
||||||
&& !mods.shift
|
pick(app, &proj, p).is_empty() && crate::dim_view::hit(p).is_none() && crate::sketch_tools::glyph_at(p).is_none()
|
||||||
&& hover.is_some_and(|p| {
|
})
|
||||||
let proj = projection(app, rect);
|
};
|
||||||
pick(app, &proj, p).is_empty() && crate::dim_view::hit(p).is_none() && crate::sketch_tools::glyph_at(p).is_none()
|
let openscad_orbit =
|
||||||
});
|
app.preferences.nav == crate::prefs::OPENSCAD_NAV && app.tool.is_none() && app.dialog.is_none() && !mods.shift && empty_space(app);
|
||||||
app.viewport.primary_nav = if app.preferences.nav != crate::prefs::OPENSCAD_NAV || openscad_orbit {
|
let preset = if app.preferences.nav != crate::prefs::OPENSCAD_NAV || openscad_orbit {
|
||||||
crate::prefs::nav_mode(app, crate::prefs::NavButton::Left, mods)
|
crate::prefs::nav_mode(app, crate::prefs::NavButton::Left, mods)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
// Ctrl+left drag from empty space orbits in every preset (Cmd on macOS). A Ctrl+click
|
||||||
|
// still selects, and Ctrl on geometry still drags it without snapping.
|
||||||
|
let ctrl_orbit = (mods.ctrl || mods.command) && !mods.shift && !mods.alt && empty_space(app);
|
||||||
|
app.viewport.primary_nav = preset.or(ctrl_orbit.then_some(NavMode::Orbit));
|
||||||
} else if !primary_down && !primary_released {
|
} else if !primary_down && !primary_released {
|
||||||
app.viewport.primary_nav = None;
|
app.viewport.primary_nav = None;
|
||||||
}
|
}
|
||||||
@@ -3094,6 +3098,63 @@ mod tests {
|
|||||||
app
|
app
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ctrl_left_drag_from_empty_space_orbits_in_every_preset() {
|
||||||
|
for preset in crate::prefs::NAV_PRESETS.iter().map(|p| p.id) {
|
||||||
|
for action in ["orbit", "tool", "point", "click"] {
|
||||||
|
let ctx = egui::Context::default();
|
||||||
|
let mut app = hover_app(false);
|
||||||
|
app.preferences.nav = preset.into();
|
||||||
|
app.ui.show_origin = false;
|
||||||
|
hover_frame(&mut app, &ctx, vec![]);
|
||||||
|
let point = projection(&app, app.viewport.rect.unwrap()).to_screen(Vec3::ZERO).unwrap();
|
||||||
|
let (start, end) = match action {
|
||||||
|
"point" => (point, point + vec2(30.0, 20.0)),
|
||||||
|
"click" => (point, point),
|
||||||
|
_ => (point - vec2(60.0, 60.0), point - vec2(20.0, 30.0)),
|
||||||
|
};
|
||||||
|
if action == "tool" {
|
||||||
|
app.start("sketch.line");
|
||||||
|
}
|
||||||
|
let before = app.cam;
|
||||||
|
let undo = app.session.undo.len();
|
||||||
|
let modifiers = egui::Modifiers { ctrl: true, command: !cfg!(target_os = "macos"), ..Default::default() };
|
||||||
|
let frame = |app: &mut SolveApp, mut events: Vec<egui::Event>| {
|
||||||
|
events.insert(0, egui::Event::ModifiersChanged(modifiers));
|
||||||
|
hover_frame(app, &ctx, events);
|
||||||
|
};
|
||||||
|
frame(
|
||||||
|
&mut app,
|
||||||
|
vec![
|
||||||
|
egui::Event::PointerMoved(start),
|
||||||
|
egui::Event::PointerButton { pos: start, button: egui::PointerButton::Primary, pressed: true, modifiers },
|
||||||
|
],
|
||||||
|
);
|
||||||
|
frame(&mut app, vec![egui::Event::PointerMoved(end)]);
|
||||||
|
frame(&mut app, vec![]);
|
||||||
|
match action {
|
||||||
|
"orbit" | "tool" => {
|
||||||
|
assert_ne!(app.cam, before, "{preset}: Ctrl+left drag on empty space orbits ({action})");
|
||||||
|
assert!(app.viewport.boxsel.is_none(), "{preset}: no box selection while orbiting");
|
||||||
|
}
|
||||||
|
"point" => {
|
||||||
|
assert_eq!(app.cam, before, "{preset}: Ctrl+drag on sketch geometry is not an orbit");
|
||||||
|
assert!(app.viewport.point_drag.is_some(), "{preset}: Ctrl+drag still drags the point");
|
||||||
|
}
|
||||||
|
_ => assert_eq!(app.cam, before, "{preset}: a Ctrl+click does not orbit"),
|
||||||
|
}
|
||||||
|
frame(&mut app, vec![egui::Event::PointerButton { pos: end, button: egui::PointerButton::Primary, pressed: false, modifiers }]);
|
||||||
|
frame(&mut app, vec![]);
|
||||||
|
match action {
|
||||||
|
"click" => assert!(!app.session.selection.is_empty(), "{preset}: Ctrl+click still selects"),
|
||||||
|
"tool" => assert_eq!(app.session.undo.len(), undo, "{preset}: the orbit drag adds no geometry"),
|
||||||
|
"orbit" => assert!(app.session.selection.is_empty(), "{preset}: the end of the orbit selects nothing"),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn openscad_left_drag_preserves_editing_and_selection() {
|
fn openscad_left_drag_preserves_editing_and_selection() {
|
||||||
for action in ["point", "box", "tool"] {
|
for action in ["point", "box", "tool"] {
|
||||||
|
|||||||
@@ -20,7 +20,8 @@
|
|||||||
trackpad, pinch zooms and two-finger scroll pans (Navigation › Two-finger scroll pans;
|
trackpad, pinch zooms and two-finger scroll pans (Navigation › Two-finger scroll pans;
|
||||||
off: scroll zooms like the wheel); mouse wheels zoom and the thumb wheel pans sideways.
|
off: scroll zooms like the wheel); mouse wheels zoom and the thumb wheel pans sideways.
|
||||||
Shift+middle orbits freely
|
Shift+middle orbits freely
|
||||||
in the default and Inventor presets. Shift+right-drag snaps to the nearest standard or
|
in the default and Inventor presets. In every preset, Ctrl+left drag (Cmd on macOS) that starts
|
||||||
|
on empty space orbits; a Ctrl+click still adds to the selection. Shift+right-drag snaps to the nearest standard or
|
||||||
diagonal view; Shift does not request snapping when the preset already requires it to orbit.
|
diagonal view; Shift does not request snapping when the preset already requires it to orbit.
|
||||||
The axis view shows the world's X/Y/Z directions in SolidCraft's colors. Click a signed
|
The axis view shows the world's X/Y/Z directions in SolidCraft's colors. Click a signed
|
||||||
axis tip to look along that axis; click the aligned tip again to look from the opposite side.
|
axis tip to look along that axis; click the aligned tip again to look from the opposite side.
|
||||||
|
|||||||
Reference in new issue
Block a user