From 873a65fd4d2cf79269446970f3e00285612fb62d Mon Sep 17 00:00:00 2001 From: DuBearte <130190089+DuBearte@users.noreply.github.com> Date: Thu, 15 Oct 2026 02:13:22 -0400 Subject: [PATCH] SpaceMouse: on Linux, default to the driver's object-mode axes (#1292) Without a 3DxWare profile, Linux now uses the driver's object-mode mapping (physical Ry and Rz swapped and reversed), so a tilt rolls and a twist spins. Windows and macOS are unchanged; a profile still takes precedence. Fixes #1172. --- apps/solidcraft/src/space_mouse.rs | 69 +++++++++++++++++++++++++++++- 1 file changed, 67 insertions(+), 2 deletions(-) diff --git a/apps/solidcraft/src/space_mouse.rs b/apps/solidcraft/src/space_mouse.rs index 0880577f..62734b6e 100644 --- a/apps/solidcraft/src/space_mouse.rs +++ b/apps/solidcraft/src/space_mouse.rs @@ -48,6 +48,21 @@ impl Default for DriverMapping { } } +impl DriverMapping { + /// The mapping used when no 3DxWare profile is found. On Linux there is no 3DxWare, so it is + /// what the driver applies by default in object mode (#1172): the physical Ry and Rz swapped + /// and reversed, so a tilt left/right rolls and a twist spins. Elsewhere the driver writes a + /// profile, and the raw axes are used until it does. + fn without_profile() -> Self { + let mut mapping = Self::default(); + if cfg!(target_os = "linux") { + mapping.axes[4] = AxisRule { output: 5, enabled: true, reversed: true, scale: 1.0 }; + mapping.axes[5] = AxisRule { output: 4, enabled: true, reversed: true, scale: 1.0 }; + } + mapping + } +} + #[derive(Clone, Copy, Debug, Default, PartialEq)] struct Motion { translation: [f64; 3], @@ -377,7 +392,12 @@ impl SpaceMouse { if let Err(error) = std::thread::Builder::new().name("solidcraft-3d-mouse".into()).spawn(move || reader(worker_ctx, worker_state)) { log::warn!("3D mouse: reader could not start: {error}"); } - Self { shared, last_frame: now, driver_mapping: load_driver_mapping().unwrap_or_default(), mapping_checked_at: now } + Self { + shared, + last_frame: now, + driver_mapping: load_driver_mapping().unwrap_or_else(DriverMapping::without_profile), + mapping_checked_at: now, + } } pub fn tick(&mut self, app: &mut SolveApp, ctx: &Context) { @@ -385,7 +405,7 @@ impl SpaceMouse { let seconds = now.duration_since(self.last_frame).as_secs_f64().min(0.05); self.last_frame = now; if now.duration_since(self.mapping_checked_at) >= RETRY_AFTER { - let updated = load_driver_mapping().unwrap_or_default(); + let updated = load_driver_mapping().unwrap_or_else(DriverMapping::without_profile); if updated != self.driver_mapping { log::info!("3D mouse: applying updated 3DxWare axis profile"); self.driver_mapping = updated; @@ -580,6 +600,51 @@ mod tests { assert_eq!(motion.rotation, [1.0, -1.0, 0.0]); } + #[test] + fn without_a_profile_linux_uses_the_drivers_object_mode_axes() { + // 3DxWare's object-mode mapping, as a profile (#1172: confirmed on a SpaceMouse Compact). + let object_mode = br#" + + trueHIDMultiAxis_XHIDMultiAxis_Xfalse + trueHIDMultiAxis_YHIDMultiAxis_Yfalse + trueHIDMultiAxis_ZHIDMultiAxis_Zfalse + trueHIDMultiAxis_RxHIDMultiAxis_Rxfalse + trueHIDMultiAxis_RyHIDMultiAxis_Rztrue + trueHIDMultiAxis_RzHIDMultiAxis_Rytrue + + "#; + let profile = parse_driver_mapping(object_mode).expect("valid 3DxWare profile"); + if cfg!(target_os = "linux") { + assert_eq!(DriverMapping::without_profile(), profile); + // Tilting the cap rolls the view and twisting it spins it, as with the driver. + let before = Camera::default(); + let mut tilt = before; + navigate( + &mut tilt, + DriverMapping::without_profile().apply(Axes { translation: [0; 3], rotation: [0, 350, 0] }), + 0.02, + 600.0, + false, + false, + ); + assert_eq!((tilt.yaw, tilt.pitch), (before.yaw, before.pitch)); + assert_ne!(tilt.roll, before.roll); + let mut twist = before; + navigate( + &mut twist, + DriverMapping::without_profile().apply(Axes { translation: [0; 3], rotation: [0, 0, 350] }), + 0.02, + 600.0, + false, + false, + ); + assert_ne!(twist.yaw, before.yaw); + assert_eq!(twist.roll, before.roll); + } else { + assert_eq!(DriverMapping::without_profile(), DriverMapping::default()); + } + } + #[test] fn mapped_roll_and_spin_reach_distinct_camera_rotations() { let profile = br#"