From d05fe3d4de476fec2da195b5732ee83b026801a2 Mon Sep 17 00:00:00 2001 From: DuBearte <130190089+DuBearte@users.noreply.github.com> Date: Thu, 15 Oct 2026 08:22:46 -0400 Subject: [PATCH] Navigation: a Blender preset (#1356) Middle drag orbits, Shift+middle pans, Ctrl+middle zooms; right drag leaves the camera alone. Opt-in preset; the default is unchanged. Fixes #1090. --- crates/ui-egui/src/prefs.rs | 34 ++++++++++++++++++++++++++++++++-- docs/book/src/guide/ui-tour.md | 2 +- docs/control-protocol.md | 2 +- 3 files changed, 34 insertions(+), 4 deletions(-) diff --git a/crates/ui-egui/src/prefs.rs b/crates/ui-egui/src/prefs.rs index 33298cd6..3960fa22 100644 --- a/crates/ui-egui/src/prefs.rs +++ b/crates/ui-egui/src/prefs.rs @@ -94,7 +94,7 @@ use NavButton::{Left, Middle, Right}; use NavMode::{Orbit, Pan, Zoom}; /// The mouse mappings. The first is the default. -pub const NAV_PRESETS: [NavPreset; 7] = [ +pub const NAV_PRESETS: [NavPreset; 8] = [ NavPreset { id: "fusion", label: "Fusion", @@ -140,6 +140,13 @@ pub const NAV_PRESETS: [NavPreset; 7] = [ bind(Right, NavKeys::NONE, Orbit), ], }, + NavPreset { + id: "blender", + label: "Blender", + help: "Middle drag orbits, Shift+middle pans, Ctrl+middle zooms; right drag leaves the camera alone", + reverse_zoom: false, + bindings: &[bind(Middle, NavKeys::SHIFT, Pan), bind(Middle, NavKeys::CTRL, Zoom), bind(Middle, NavKeys::NONE, Orbit)], + }, NavPreset { id: "tinkercad", label: "Tinkercad", @@ -1659,6 +1666,29 @@ mod tests { assert!(!app.preferences.trackpad_pan); } + #[test] + fn blender_preset_moves_the_camera_with_the_middle_button_only() { + // #1090: orbit, pan and zoom all on the middle button; no left or right drag binding, so + // those stay selection and the context menu (Ctrl+left on empty space orbits in every + // preset, outside the bindings). + let mut app = SolveApp::new(solidcraft_engine::Session::default(), Default::default()); + set(&mut app, &json!({"nav": "blender"})).unwrap(); + let none = Modifiers::default(); + let shift = Modifiers { shift: true, ..Default::default() }; + let ctrl = Modifiers { ctrl: true, ..Default::default() }; + assert_eq!(nav_mode(&app, NavButton::Middle, none), Some(NavMode::Orbit)); + assert_eq!(nav_mode(&app, NavButton::Middle, shift), Some(NavMode::Pan)); + assert_eq!(nav_mode(&app, NavButton::Middle, ctrl), Some(NavMode::Zoom)); + for mods in [none, shift, ctrl] { + assert_eq!(nav_mode(&app, NavButton::Left, mods), None); + assert_eq!(nav_mode(&app, NavButton::Right, mods), None); + } + assert_eq!(zoom_sign(&app), 1.0, "the wheel turns the usual way"); + let mut other = SolveApp::new(solidcraft_engine::Session::default(), Default::default()); + other.load_prefs(&app.prefs()); + assert_eq!(other.preferences.nav, "blender"); + } + #[test] fn presets_map_the_mouse_and_prefs_round_trip() { let mut app = SolveApp::new(solidcraft_engine::Session::default(), Default::default()); @@ -1684,7 +1714,7 @@ mod tests { assert_eq!(nav_mode(&app, NavButton::Left, none), None, "a plain left drag still selects"); assert_eq!(nav_mode(&app, NavButton::Left, shift), None); assert_eq!(nav_mode(&app, NavButton::Right, none), Some(NavMode::Orbit)); - assert!(set(&mut app, &json!({"nav": "blender"})).is_err()); + assert!(set(&mut app, &json!({"nav": "no-such-preset"})).is_err()); assert!(set(&mut app, &json!({"default_units": "furlong"})).is_err()); assert!(set(&mut app, &json!({"nope": 1})).is_err()); // Kept between runs. diff --git a/docs/book/src/guide/ui-tour.md b/docs/book/src/guide/ui-tour.md index 99559fbb..3e29cbe7 100644 --- a/docs/book/src/guide/ui-tour.md +++ b/docs/book/src/guide/ui-tour.md @@ -25,7 +25,7 @@ Esc or a click elsewhere closes the list. Clicking again on the same spot, with nothing changed in between, selects the next thing in that list. The wheel zooms at the cursor, the middle button pans and right-drag orbits freely (the mouse - preset in Preferences switches to SolidWorks, Inventor, Alias, Tinkercad, OpenSCAD or Touchpad style). On a + preset in Preferences switches to SolidWorks, Inventor, Alias, Blender, Tinkercad, OpenSCAD or Touchpad style). On a 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. Shift+middle orbits freely diff --git a/docs/control-protocol.md b/docs/control-protocol.md index 2524a9a2..e1f815d1 100644 --- a/docs/control-protocol.md +++ b/docs/control-protocol.md @@ -132,7 +132,7 @@ unit regardless of the design, pass an expression with one: `"value": "50 mm"` o | `ui.home` | `open?`: bool; `sample?`: index | the start page: show or hide it, open a built-in sample (once built); returns `open` and the recent designs | | `ui.shortcut` | `command`, `key?`, `replace?` | a command's key, or rebinds it (`""` clears; a key another command uses is refused unless `replace`) | | `ui.help` | `item?`: `about`, `shortcuts` (read only), `report` (opens a report draft for review), `crashes` (the Crash Reports window) | the version, commit, build date and diagnostic text | -| `ui.prefs` | any of `default_units`, `nav` (`fusion`, `solidworks`, `inventor`, `alias`, `tinkercad`, `openscad`, `touchpad`), `zoom_reverse`, `zoom_to_cursor`, `smooth_wheel_zoom` (default true), `trackpad_pan` (default true: two-finger scroll pans, except Touchpad always zooms; pinch zooms), `orbit_cursor`, `look_at_anim`, `look_at_speed`, `middle_double_click` (`home` (default), `fit`, `off`), `msaa`, `length_decimals`, `angle_decimals`, `theme` (`dark`, `light`, `system`), `language` (`en`, `ja`, `de`, `fr`, `ko`, `ru`, `uk`, `zh`; see [localization](localization.md)), `ui_scale`, `up_axis` (`z`, `y`), `offer_stand_up`, `color_vision` (`off` (default), `protan`, `deutan`, `tritan`: colour vision modes, #778), `check_updates` (default true; [updates](book/src/guide/updates.md)); `open` (a page: General, Navigation, Keyboard Shortcuts, Design, Graphics, Sketch, Selection) opens the Preferences dialog there; `button`: `ok`, `cancel`, `apply`, `defaults`; `stand_up: true` stands up a just-opened Y-up file | sets preferences (kept between runs), returns them all plus `dialog` (the open page or null), `dark` and `stand_up_offer` | +| `ui.prefs` | any of `default_units`, `nav` (`fusion`, `solidworks`, `inventor`, `alias`, `blender`, `tinkercad`, `openscad`, `touchpad`), `zoom_reverse`, `zoom_to_cursor`, `smooth_wheel_zoom` (default true), `trackpad_pan` (default true: two-finger scroll pans, except Touchpad always zooms; pinch zooms), `orbit_cursor`, `look_at_anim`, `look_at_speed`, `middle_double_click` (`home` (default), `fit`, `off`), `msaa`, `length_decimals`, `angle_decimals`, `theme` (`dark`, `light`, `system`), `language` (`en`, `ja`, `de`, `fr`, `ko`, `ru`, `uk`, `zh`; see [localization](localization.md)), `ui_scale`, `up_axis` (`z`, `y`), `offer_stand_up`, `color_vision` (`off` (default), `protan`, `deutan`, `tritan`: colour vision modes, #778), `check_updates` (default true; [updates](book/src/guide/updates.md)); `open` (a page: General, Navigation, Keyboard Shortcuts, Design, Graphics, Sketch, Selection) opens the Preferences dialog there; `button`: `ok`, `cancel`, `apply`, `defaults`; `stand_up: true` stands up a just-opened Y-up file | sets preferences (kept between runs), returns them all plus `dialog` (the open page or null), `dark` and `stand_up_offer` | | `ui.capture` | `action?`: `capture`, `revert`, `cancel` | answers the Capture Position question (asked when a command starts or the design is saved while components were moved without capturing); without `action`, what waits | | `ui.window` | `action`: minimize, maximize, restore, toggle, close | what the title bar's caption buttons do (on Windows and Linux the application bar is the title bar); close asks about unsaved designs first | | `ui.agent_cursor` | `show` (bool), `speed`: a number from 1 to 5 (times the normal speed) or `"instant"` (no animation: the command runs at once and the pointer jumps to where it acted), `follow_camera` (bool); all optional | the settings. While the cursor is shown, `engine.execute` first glides to the command's toolbar button and to each pick, then runs and replies; later requests wait their turn. `"animate": false` on a call skips it |