Files
OneCAD-Simulation/crates/ui-egui/src/navigation.rs
T

228 lines
9.2 KiB
Rust

//! Mouse navigation presets: which mouse button, with which keys held, orbits, pans or zooms.
//!
//! The presets after FieldCraft's own follow SolidCraft's `crates/ui-egui/src/prefs.rs`
//! (`NavPreset`, `NavBinding`, `NAV_PRESETS`; MIT OR Apache-2.0), see ATTRIBUTION.md. In every
//! preset a left drag the preset does not bind does what the navigation bar's mode says (orbit
//! by default), and a Ctrl (Cmd) + left drag that starts on empty space orbits, unless the
//! preset binds Ctrl + left itself (SolidCraft #471).
use crate::NavMode;
/// A mouse button that can drag the camera.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum NavButton {
Left,
Middle,
Right,
}
/// Modifier keys a binding needs held (others may be held too).
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub struct NavKeys {
pub shift: bool,
pub ctrl: bool,
pub alt: bool,
}
impl NavKeys {
const NONE: NavKeys = NavKeys { shift: false, ctrl: false, alt: false };
const SHIFT: NavKeys = NavKeys { shift: true, ctrl: false, alt: false };
const CTRL: NavKeys = NavKeys { shift: false, ctrl: true, alt: false };
const SHIFT_ALT: NavKeys = NavKeys { shift: true, ctrl: false, alt: true };
/// All of these keys are held (Ctrl is Cmd on macOS).
fn held(self, m: egui::Modifiers) -> bool {
(!self.shift || m.shift) && (!self.ctrl || m.ctrl || m.command) && (!self.alt || m.alt)
}
}
/// One camera binding: dragging `button` with `keys` held does `mode`.
#[derive(Clone, Copy, Debug)]
pub struct NavBinding {
pub button: NavButton,
pub keys: NavKeys,
pub mode: NavMode,
}
const fn bind(button: NavButton, keys: NavKeys, mode: NavMode) -> NavBinding {
NavBinding { button, keys, mode }
}
/// A mouse mapping: its bindings, most specific first (the first one whose button is dragged
/// with all its keys held wins).
pub struct NavPreset {
pub id: &'static str,
pub label: &'static str,
/// What the mouse does, for the settings menu.
pub help: &'static str,
/// Choosing the preset turns the wheel the other way.
pub reverse_zoom: bool,
pub bindings: &'static [NavBinding],
}
impl NavPreset {
/// What dragging `button` with `mods` held does, if the preset binds it.
pub fn mode(&self, button: NavButton, mods: egui::Modifiers) -> Option<NavMode> {
self.bindings.iter().find(|b| b.button == button && b.keys.held(mods)).map(|b| b.mode)
}
}
use NavButton::{Left, Middle, Right};
use NavMode::{Orbit, Pan, Zoom};
/// The id of the default preset.
pub const DEFAULT_PRESET: &str = "fieldcraft";
/// The mouse mappings. The first is the default.
pub const NAV_PRESETS: [NavPreset; 7] = [
NavPreset {
id: DEFAULT_PRESET,
// OneBSpace (OneCAD Simulation): the preset's label names the fork (its `id`, which
// preferences store, stays upstream).
label: "OneCAD Simulation",
help: "Left drag does the navigation bar's mode (orbit, pan or zoom); right, middle or Shift+left drag pans",
reverse_zoom: false,
bindings: &[bind(Left, NavKeys::SHIFT, Pan), bind(Right, NavKeys::NONE, Pan), bind(Middle, NavKeys::NONE, Pan)],
},
NavPreset {
id: "fusion",
label: "Fusion",
help: "Middle drag pans, Shift+middle orbits, right drag orbits",
reverse_zoom: false,
bindings: &[bind(Middle, NavKeys::SHIFT, Orbit), bind(Middle, NavKeys::NONE, Pan), bind(Right, NavKeys::NONE, Orbit)],
},
NavPreset {
id: "solidworks",
label: "SolidWorks",
help: "Middle drag orbits, Ctrl+middle pans, Shift+middle zooms, right drag orbits",
reverse_zoom: true,
bindings: &[
bind(Middle, NavKeys::CTRL, Pan),
bind(Middle, NavKeys::SHIFT, Zoom),
bind(Middle, NavKeys::NONE, Orbit),
bind(Right, NavKeys::NONE, Orbit),
],
},
NavPreset {
id: "inventor",
label: "Inventor",
help: "Middle drag pans, Shift+middle orbits, Ctrl+middle zooms, right drag orbits",
reverse_zoom: false,
bindings: &[
bind(Middle, NavKeys::SHIFT, Orbit),
bind(Middle, NavKeys::CTRL, Zoom),
bind(Middle, NavKeys::NONE, Pan),
bind(Right, NavKeys::NONE, Orbit),
],
},
NavPreset {
id: "alias",
label: "Alias",
help: "Shift+Alt+left drag orbits, Shift+Alt+middle pans, Shift+Alt+right zooms; middle drag also pans and right drag orbits",
reverse_zoom: false,
bindings: &[
bind(Left, NavKeys::SHIFT_ALT, Orbit),
bind(Middle, NavKeys::SHIFT_ALT, Pan),
bind(Right, NavKeys::SHIFT_ALT, Zoom),
bind(Middle, NavKeys::CTRL, Zoom),
bind(Middle, NavKeys::NONE, Pan),
bind(Right, NavKeys::NONE, Orbit),
],
},
NavPreset {
id: "tinkercad",
label: "Tinkercad",
help: "Right drag orbits, middle or Shift+right drag pans",
reverse_zoom: false,
bindings: &[bind(Right, NavKeys::SHIFT, Pan), bind(Right, NavKeys::NONE, Orbit), bind(Middle, NavKeys::NONE, Pan)],
},
NavPreset {
id: "openscad",
label: "OpenSCAD",
help: "Left drag orbits; right or middle drag pans, Shift+right or Shift+middle drag zooms",
reverse_zoom: false,
bindings: &[
bind(Left, NavKeys::NONE, Orbit),
bind(Right, NavKeys::SHIFT, Zoom),
bind(Middle, NavKeys::SHIFT, Zoom),
bind(Right, NavKeys::NONE, Pan),
bind(Middle, NavKeys::NONE, Pan),
],
},
];
/// The preset with this id (the default for an unknown one).
pub fn preset(id: &str) -> &'static NavPreset {
NAV_PRESETS.iter().find(|p| p.id == id).unwrap_or(&NAV_PRESETS[0])
}
/// What a drag of `button` with `mods` held does under `preset`: the preset's binding; else, for
/// the left button, an orbit when only Ctrl (Cmd) is held and the drag starts off the model,
/// nothing in box selection (the drag draws the box), and the navigation bar's mode `bar`
/// otherwise. `None`: the drag does not move the camera.
pub fn drag_mode(preset: &NavPreset, button: NavButton, mods: egui::Modifiers, bar: NavMode, boxing: bool, on_model: bool) -> Option<NavMode> {
if let Some(m) = preset.mode(button, mods) {
return Some(m);
}
if button != Left {
return None;
}
let ctrl_only = NavKeys::CTRL.held(mods) && !mods.shift && !mods.alt;
if ctrl_only && !on_model {
Some(Orbit)
} else if boxing {
None
} else {
Some(bar)
}
}
#[cfg(test)]
mod tests {
use super::*;
use egui::Modifiers;
const SHIFT: Modifiers = Modifiers::SHIFT;
const CTRL: Modifiers = Modifiers::CTRL;
const NONE: Modifiers = Modifiers::NONE;
/// Each preset maps its buttons and keys as its help says.
#[test]
fn presets_bind_as_listed() {
let shift_alt = Modifiers { shift: true, alt: true, ..NONE };
let cases: [(&str, &[(NavButton, Modifiers, Option<NavMode>)]); 7] = [
("fieldcraft", &[(Left, SHIFT, Some(Pan)), (Right, NONE, Some(Pan)), (Middle, NONE, Some(Pan)), (Left, NONE, None)]),
("fusion", &[(Middle, NONE, Some(Pan)), (Middle, SHIFT, Some(Orbit)), (Right, NONE, Some(Orbit)), (Left, NONE, None)]),
("solidworks", &[(Middle, NONE, Some(Orbit)), (Middle, CTRL, Some(Pan)), (Middle, SHIFT, Some(Zoom)), (Right, NONE, Some(Orbit))]),
("inventor", &[(Middle, NONE, Some(Pan)), (Middle, SHIFT, Some(Orbit)), (Middle, CTRL, Some(Zoom)), (Right, NONE, Some(Orbit))]),
("alias", &[(Left, shift_alt, Some(Orbit)), (Middle, shift_alt, Some(Pan)), (Right, shift_alt, Some(Zoom)), (Middle, NONE, Some(Pan))]),
("tinkercad", &[(Right, NONE, Some(Orbit)), (Right, SHIFT, Some(Pan)), (Middle, NONE, Some(Pan)), (Left, NONE, None)]),
("openscad", &[(Left, NONE, Some(Orbit)), (Right, NONE, Some(Pan)), (Right, SHIFT, Some(Zoom)), (Middle, SHIFT, Some(Zoom))]),
];
for (id, binds) in cases {
let p = preset(id);
assert_eq!(p.id, id);
for &(button, mods, want) in binds {
assert_eq!(p.mode(button, mods), want, "{id}: {button:?} {mods:?}");
}
}
assert_eq!(preset("no such preset").id, DEFAULT_PRESET);
assert!(preset("solidworks").reverse_zoom);
}
/// An unbound left drag does the bar's mode; Ctrl+left off the model orbits in every preset
/// (in box selection too); box selection keeps the plain left drag for the box.
#[test]
fn left_drag_follows_the_bar_and_ctrl_orbits_from_empty_space() {
for p in &NAV_PRESETS {
assert_eq!(drag_mode(p, Left, CTRL, Pan, true, false), Some(Orbit), "{}", p.id);
assert_eq!(drag_mode(p, Left, CTRL, Pan, false, true), Some(p.mode(Left, CTRL).unwrap_or(Pan)), "{}", p.id);
}
let fc = preset(DEFAULT_PRESET);
assert_eq!(drag_mode(fc, Left, NONE, Zoom, false, true), Some(Zoom));
assert_eq!(drag_mode(fc, Left, NONE, Orbit, true, false), None);
assert_eq!(drag_mode(fc, Left, SHIFT, Orbit, false, false), Some(Pan));
assert_eq!(drag_mode(preset("fusion"), Right, CTRL, Orbit, false, false), Some(Orbit));
}
}