3D mouse: invert and speed for each of the cap's six axes (#1814)

Thanks @NecroKote.
This commit is contained in:
Mikalai Kukhta authored and GitHub committed 2026-10-17 04:09:55 -04:00
1 parent c9b8267b9e
commit 64235ab12a
3 files changed
+58 -2

No files matched your search

+2 -1
View File
@@ -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)).
+26
View File
@@ -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;
+30 -1
View File
@@ -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 {