3D mouse: invert and speed for each of the cap's six axes (#1814)
Thanks @NecroKote.
This commit is contained in:
1 parent
c9b8267b9e
commit
64235ab12a
3 files changed
+58
-2
No files matched your search
@@ -218,7 +218,8 @@ Windows builds: `cargo xwin build --release --target x86_64-pc-windows-msvc -p s
|
||||
On Windows and Linux, SolidCraft detects a connected 3Dconnexion SpaceNavigator or SpaceMouse through its
|
||||
multi-axis HID interface. Move the cap to pan and zoom, and tilt or twist it to rotate the 3D view.
|
||||
While editing a sketch, the cap pans and zooms without changing the sketch view. The Navigation
|
||||
page in Preferences has an on/off switch and a speed control for the 3D mouse. The web app reads
|
||||
page in Preferences has an on/off switch, a speed control and, for each of the cap's six axes, an
|
||||
invert switch and a speed for the 3D mouse. The web app reads
|
||||
it too, through the browser; Firefox needs a 3DxWare setting first (see
|
||||
[Getting started › 3D mouse in the browser](docs/book/src/guide/getting-started.md#3d-mouse-in-the-browser)).
|
||||
|
||||
|
||||
@@ -273,6 +273,11 @@ pub struct Prefs {
|
||||
pub space_mouse_enabled: bool,
|
||||
/// Speed multiplier for 3D mouse navigation.
|
||||
pub space_mouse_speed: f32,
|
||||
/// Per physical axis of the cap (push X, Y, Z, then tilt X, Y, Z), whether it moves the view
|
||||
/// the other way.
|
||||
pub space_mouse_reverse: [bool; 6],
|
||||
/// Per physical axis of the cap, a multiplier on top of `space_mouse_speed`; 0 switches it off.
|
||||
pub space_mouse_axis_speed: [f32; 6],
|
||||
/// Anti-aliasing samples (applies on restart).
|
||||
pub msaa: u32,
|
||||
/// How finely the viewport tessellates curved faces and edges: `coarse`, `normal`, `fine` or
|
||||
@@ -402,6 +407,8 @@ impl Default for Prefs {
|
||||
orbit_cursor: false,
|
||||
space_mouse_enabled: true,
|
||||
space_mouse_speed: 1.0,
|
||||
space_mouse_reverse: [false; 6],
|
||||
space_mouse_axis_speed: [1.0; 6],
|
||||
msaa: 4,
|
||||
display_quality: "normal".into(),
|
||||
supersampling: 1.0,
|
||||
@@ -618,6 +625,8 @@ fn defaults(app: &mut SolveApp, page: usize) {
|
||||
p.orbit_cursor = d.orbit_cursor;
|
||||
p.space_mouse_enabled = d.space_mouse_enabled;
|
||||
p.space_mouse_speed = d.space_mouse_speed;
|
||||
p.space_mouse_reverse = d.space_mouse_reverse;
|
||||
p.space_mouse_axis_speed = d.space_mouse_axis_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;
|
||||
@@ -1214,6 +1223,20 @@ fn page(app: &mut SolveApp, ui: &mut egui::Ui, t: &Tokens) {
|
||||
egui::Slider::new(&mut app.preferences.space_mouse_speed, 0.25..=3.0).step_by(0.05).custom_formatter(|v, _| format!("{v:.2}×")),
|
||||
);
|
||||
ui.end_row();
|
||||
for (axis, name) in SPACE_MOUSE_AXES.iter().enumerate() {
|
||||
ui.label(crate::i18n::tr_label(name));
|
||||
ui.horizontal(|ui| {
|
||||
ui.add_enabled_ui(app.preferences.space_mouse_enabled, |ui| {
|
||||
if let Some(reverse) = app.preferences.space_mouse_reverse.get_mut(axis) {
|
||||
ui.checkbox(reverse, crate::i18n::tr_label("Invert"));
|
||||
}
|
||||
if let Some(speed) = app.preferences.space_mouse_axis_speed.get_mut(axis) {
|
||||
ui.add(egui::Slider::new(speed, 0.0..=3.0).step_by(0.05).custom_formatter(|v, _| format!("{v:.2}×")));
|
||||
}
|
||||
});
|
||||
});
|
||||
ui.end_row();
|
||||
}
|
||||
note(ui, crate::i18n::tr_label("Move and rotate the view together; sketches use pan and zoom only."));
|
||||
}
|
||||
PAGE_DESIGN => {
|
||||
@@ -1453,6 +1476,9 @@ pub fn control(app: &mut SolveApp, p: &serde_json::Value) -> Result<serde_json::
|
||||
|
||||
/// Fold a setting an older release kept in its own field into the one this version reads.
|
||||
/// `orbit_cursor` (`true` = the point under the cursor) became `orbit_around`.
|
||||
/// The cap's physical axes, in the order of `Prefs::space_mouse_reverse`.
|
||||
const SPACE_MOUSE_AXES: [&str; 6] = ["Push left / right", "Push forward / back", "Lift / press", "Tilt forward / back", "Tilt left / right", "Twist"];
|
||||
|
||||
pub fn migrate(mut p: Prefs) -> Prefs {
|
||||
if p.orbit_cursor {
|
||||
p.orbit_around = OrbitAround::Cursor;
|
||||
|
||||
@@ -87,6 +87,16 @@ impl DriverMapping {
|
||||
mapping
|
||||
}
|
||||
|
||||
/// The mapping with the user's per-axis choices folded in: `reverse` flips and `speed` scales
|
||||
/// each physical axis of the cap. A speed that is not a finite number counts as 1.
|
||||
pub fn with_user_axes(mut self, reverse: [bool; 6], speed: [f32; 6]) -> Self {
|
||||
for ((rule, reverse), speed) in self.axes.iter_mut().zip(reverse).zip(speed) {
|
||||
rule.reversed ^= reverse;
|
||||
rule.scale *= if speed.is_finite() { f64::from(speed).clamp(0.0, 3.0) } else { 1.0 };
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn apply(self, axes: Axes) -> Motion {
|
||||
let raw = [
|
||||
normalized(axes.translation[0]),
|
||||
@@ -139,7 +149,10 @@ pub fn navigate_frame(app: &mut SolveApp, ctx: &egui::Context, mapping: DriverMa
|
||||
app.begin_external_navigation();
|
||||
// A long gap (the first frame after a pause) moves as far as a 50 ms one.
|
||||
let step = if seconds.is_finite() { seconds.min(0.05) } else { 0.0 };
|
||||
app.viewport.device_motion = Some(DeviceMotion { motion: mapping.apply(axes), seconds: step * speed.clamp(0.1, 5.0) });
|
||||
app.viewport.device_motion = Some(DeviceMotion {
|
||||
motion: mapping.with_user_axes(app.preferences.space_mouse_reverse, app.preferences.space_mouse_axis_speed).apply(axes),
|
||||
seconds: step * speed.clamp(0.1, 5.0),
|
||||
});
|
||||
ctx.request_repaint_after(Duration::from_millis(16));
|
||||
true
|
||||
}
|
||||
@@ -182,6 +195,22 @@ mod tests {
|
||||
app
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn per_axis_choices_flip_and_scale_one_physical_axis() {
|
||||
let mapping = DriverMapping::default().with_user_axes([false, true, false, false, false, false], [1.0, 1.0, 1.0, 0.5, f32::NAN, 1.0]);
|
||||
let still = Motion::default();
|
||||
assert_eq!(mapping.apply(Axes { translation: [0, 350, 0], rotation: [0; 3] }).translation[1], -raw(PUSH).translation[1]);
|
||||
assert_eq!(
|
||||
mapping.apply(Axes { translation: [350, 0, 0], rotation: [0; 3] }).translation[0],
|
||||
raw(Axes { translation: [350, 0, 0], rotation: [0; 3] }).translation[0]
|
||||
);
|
||||
assert_eq!(mapping.apply(Axes { translation: [0; 3], rotation: [350, 0, 0] }).rotation[0], 0.5);
|
||||
assert_eq!(mapping.apply(Axes { translation: [0; 3], rotation: [0, 350, 0] }).rotation[1], 1.0);
|
||||
assert_eq!(mapping.apply(Axes::default()), still);
|
||||
let off = DriverMapping::default().with_user_axes([false; 6], [0.0; 6]);
|
||||
assert_eq!(off.apply(PUSH), still);
|
||||
}
|
||||
|
||||
const PUSH: Axes = Axes { translation: [0, 350, 0], rotation: [0; 3] };
|
||||
|
||||
fn wheel(dy: f32) -> egui::Event {
|
||||
|
||||
Reference in new issue
Block a user