From 6a63726effb5c4607e92e6601f6939d5c42f4a1a Mon Sep 17 00:00:00 2001 From: Christopher Overbeck Date: Tue, 15 Sep 2026 12:55:31 -0500 Subject: [PATCH] Add Windows SpaceMouse navigation through 3DxWare --- Cargo.lock | 1 + Cargo.toml | 5 +- assets/icons/ui/spacemouse.svg | 1 + docs/images/spacemouse-menu.png | Bin 0 -> 72947 bytes docs/images/spacemouse-preferences.png | Bin 0 -> 170304 bytes docs/spacemouse.md | 147 ++++ src/app/commands/mod.rs | 20 +- src/app/mod.rs | 32 + src/app/navigation.rs | 699 +++++++++++++++ src/app/properties.rs | 2 +- src/app/settings.rs | 2 + src/app/shortcuts.rs | 33 +- src/app/update/file.rs | 2 + src/app/update/mod.rs | 43 + src/app/update/viewport.rs | 4 +- src/app/view/mod.rs | 30 + src/app/view/modal.rs | 4 + src/input/mod.rs | 2 + src/input/spacemouse/mod.rs | 593 +++++++++++++ src/input/spacemouse/navlib.rs | 1121 ++++++++++++++++++++++++ src/input/spacemouse/pan.rs | 138 +++ src/input/spacemouse/raw_input.rs | 235 +++++ src/input/spacemouse/worker.rs | 122 +++ src/lib.rs | 1 + src/scene/mod.rs | 16 +- src/scene/mspace.rs | 39 + src/scene/view/camera.rs | 2 +- src/ui/statusbar/mod.rs | 7 + src/ui/statusbar/spacemouse.rs | 78 ++ src/ui/statusbar/statusbar_config.rs | 4 + src/ui/window/options.rs | 4 + src/ui/window/options/spacemouse.rs | 135 +++ 32 files changed, 3510 insertions(+), 12 deletions(-) create mode 100644 assets/icons/ui/spacemouse.svg create mode 100644 docs/images/spacemouse-menu.png create mode 100644 docs/images/spacemouse-preferences.png create mode 100644 docs/spacemouse.md create mode 100644 src/app/navigation.rs create mode 100644 src/input/mod.rs create mode 100644 src/input/spacemouse/mod.rs create mode 100644 src/input/spacemouse/navlib.rs create mode 100644 src/input/spacemouse/pan.rs create mode 100644 src/input/spacemouse/raw_input.rs create mode 100644 src/input/spacemouse/worker.rs create mode 100644 src/ui/statusbar/spacemouse.rs create mode 100644 src/ui/window/options/spacemouse.rs diff --git a/Cargo.lock b/Cargo.lock index 76a3880ef..41b20a3f3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -34,6 +34,7 @@ dependencies = [ "image", "inventory", "js-sys", + "libloading", "log", "lyon_tessellation", "meshopt", diff --git a/Cargo.toml b/Cargo.toml index ed9e2120a..f9406bbaa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,6 +32,7 @@ glam = { version = "0.33", features = ["bytemuck"] } rfd = "0.17" clap = { version = "4", features = ["derive", "string"] } env_logger = "0.11" +log = "0.4" acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "568a12c", features = ["serde"] } cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "dcf0840", features = ["acis", "offset"] } cadkernel-constraints = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "dcf0840" } @@ -60,7 +61,8 @@ fluent-syntax = "0.12" naga = { version = "27", features = ["wgsl-in"] } [target.'cfg(target_os = "windows")'.dependencies] -windows-sys = { version = "0.61", features = ["Win32_UI_Shell", "Win32_UI_WindowsAndMessaging", "Win32_System_Com", "Win32_System_Registry", "Win32_Storage_FileSystem", "Win32_Foundation", "Win32_Graphics_Printing"] } +libloading = "0.8" +windows-sys = { version = "0.61", features = ["Win32_UI_Shell", "Win32_UI_WindowsAndMessaging", "Win32_UI_Input", "Win32_System_LibraryLoader", "Win32_System_Threading", "Win32_Security", "Win32_System_Com", "Win32_System_Registry", "Win32_Storage_FileSystem", "Win32_Foundation", "Win32_Graphics_Printing"] } [target.'cfg(target_os = "linux")'.dependencies] ashpd = { version = "0.13.13", default-features = false, features = ["async-io", "wayland"] } @@ -93,7 +95,6 @@ ocs_plugin_api = { path = "crates/ocs_plugin_api" } i18n-embed = { version = "0.16", features = ["web-sys-requester"] } rust-embed = { version = "8", features = ["debug-embed"] } console_error_panic_hook = "0.1" -log = "0.4" getrandom = { version = "0.3", features = ["wasm_js"] } wasm-bindgen = "0.2" wasm-bindgen-futures = "0.4" diff --git a/assets/icons/ui/spacemouse.svg b/assets/icons/ui/spacemouse.svg new file mode 100644 index 000000000..e9e73fc43 --- /dev/null +++ b/assets/icons/ui/spacemouse.svg @@ -0,0 +1 @@ + diff --git a/docs/images/spacemouse-menu.png b/docs/images/spacemouse-menu.png new file mode 100644 index 0000000000000000000000000000000000000000..996ae27c1ca18a24054033d8a9f4db3859034598 GIT binary patch literal 72947 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 IF#o>^0N3sLp#T5? literal 0 HcmV?d00001 diff --git a/docs/images/spacemouse-preferences.png b/docs/images/spacemouse-preferences.png new file mode 100644 index 0000000000000000000000000000000000000000..f6376e4563a5ecd814b422eaa3779afc3bab2776 GIT binary patch literal 170304 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 zđ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 CHmW5Ol1mb2j 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 bool { + is_spacemouse_command(cmd) + || matches!( + cmd, + "ORTHO" | "GRID" | "SNAP" | "POLAR" | "OSNAP" | "DSETTINGS" + ) +} + +fn is_spacemouse_command(cmd: &str) -> bool { matches!( cmd, - "ORTHO" | "GRID" | "SNAP" | "POLAR" | "OSNAP" | "DSETTINGS" + "SPACEMOUSE" + | "SPACEMOUSEPAUSE" + | "SPACEMOUSEPAN" + | "SPACEMOUSEPANZOOM" + | "SPACEMOUSEAUTO" + | "SPACEMOUSE3D" ) } @@ -281,7 +297,7 @@ pub fn is_transparent(cmd: &str) -> bool { /// source of truth: the dispatch gate refuses everything else, and the ribbon /// dims the tools this rejects. pub fn start_allowed(cmd: &str) -> bool { - matches!( + is_spacemouse_command(cmd) || matches!( cmd, "NEW" | "OPEN" diff --git a/src/app/mod.rs b/src/app/mod.rs index 69e09fc03..34a803946 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -18,6 +18,7 @@ pub(crate) mod helpers; mod history; mod layers; mod model_ops; +mod navigation; mod mtext_editor; #[cfg(not(target_arch = "wasm32"))] pub mod plugin_host; @@ -540,6 +541,14 @@ pub(super) struct OpenCADStudio { dyn_input: bool, /// Currently visible page in the application Options dialog. options_tab: crate::ui::window::options::OptionsTab, + spacemouse: crate::input::spacemouse::Service, + spacemouse_preferences: crate::input::spacemouse::Preferences, + spacemouse_paused: bool, + spacemouse_focused: bool, + spacemouse_details: bool, + spacemouse_was_moving: bool, + spacemouse_pivot: Option<(crate::input::spacemouse::Target, glam::DVec3)>, + spacemouse_selection: navigation::SelectionCache, /// Controls whether the TEXTEDIT command repeats automatically (0 = Multiple, 1 = Single). pub texteditmode: bool, /// QDIM extension-origin priority: 0 = endpoints, 1 = intersections. @@ -1882,6 +1891,17 @@ pub enum ArrowKey { #[derive(Debug, Clone)] pub enum Message { + SpaceMouseWake, + SpaceMouseFrame(iced::time::Instant), + SpaceMouseFocus(iced::window::Id, bool), + SpaceMouseEnabled(bool), + SpaceMouseMode(crate::input::spacemouse::NavigationMode), + SpaceMousePanSpeed(u16), + SpaceMousePanReversed(bool), + SpaceMousePause, + SpaceMousePreferences, + SpaceMouseDriverSettings, + SpaceMouseDetails, ControlRequest(control::Envelope), PollWebControl, ControlStep(String, Box), @@ -3650,6 +3670,18 @@ impl OpenCADStudio { show_grid: false, dyn_input: true, options_tab: crate::ui::window::options::OptionsTab::General, + spacemouse: { + let service = crate::input::spacemouse::Service::default(); + service.set_actions(navigation::actions()); + service + }, + spacemouse_preferences: crate::input::spacemouse::Preferences::default(), + spacemouse_paused: false, + spacemouse_focused: false, + spacemouse_details: false, + spacemouse_was_moving: false, + spacemouse_pivot: None, + spacemouse_selection: navigation::SelectionCache::default(), texteditmode: false, quick_dimension_snap_priority: 0, dimension_continue_mode: 1, diff --git a/src/app/navigation.rs b/src/app/navigation.rs new file mode 100644 index 000000000..5a9575230 --- /dev/null +++ b/src/app/navigation.rs @@ -0,0 +1,699 @@ +//! Application policy for device navigation. All scene writes run on the UI +//! thread, preserving active commands and the scene's viewport locks. +use super::{Message, ModalKind, OpenCADStudio}; +use crate::input::spacemouse::{Action, NavigationMode, Target, View}; +use crate::scene::view::camera::Projection; +use iced::Task; + +#[derive(Default)] +pub(super) struct SelectionCache( + std::cell::RefCell)>>, +); +impl SelectionCache { + fn bounds( + &self, + scene: &crate::scene::Scene, + target: &Target, + ) -> Option<(glam::DVec3, glam::DVec3)> { + let mut cache = self.0.borrow_mut(); + if let Some((stamp, geometry, selection, bounds)) = cache.as_ref() { + if stamp.same_context(target) + && *geometry == scene.geometry_epoch + && *selection == scene.selection_generation + { + return *bounds; + } + } + let bounds = scene.navigation_selection_bounds(); + *cache = Some(( + target.clone(), + scene.geometry_epoch, + scene.selection_generation, + bounds, + )); + bounds + } +} + +inventory::submit!(crate::command::CommandRegistration { + names: &[ + "SPACEMOUSE", + "SPACEMOUSEPAUSE", + "SPACEMOUSEPAN", + "SPACEMOUSEPANZOOM", + "SPACEMOUSEAUTO", + "SPACEMOUSE3D" + ], +}); + +impl OpenCADStudio { + fn navigation_target(&self) -> Target { + let tab = &self.tabs[self.active_tab]; + Target { + tab: tab.id, + layout: format!("{}:{:?}", tab.scene.current_layout, tab.active_block_edit), + viewport: tab.scene.active_viewport.map(|h| h.value()), + tile: tab.scene.active_model_tile.get(), + generation: tab.scene.camera_generation, + } + } + pub(super) fn spacemouse_sheet(&self) -> bool { + let scene = &self.tabs[self.active_tab].scene; + scene.current_layout != "Model" && scene.active_viewport.is_none() + } + pub(super) fn spacemouse_label(&self) -> String { + if !self.spacemouse_preferences.enabled { + return crate::t!("Disabled").into_owned(); + } + if self.spacemouse_paused { + return crate::t!("Paused").into_owned(); + } + if self.tabs[self.active_tab].scene.navigation_locked() { + return crate::t!("Viewport locked").into_owned(); + } + if self.spacemouse.status() != crate::input::spacemouse::Status::Ready { + return crate::t!(self.spacemouse.status().label()).into_owned(); + } + let mode = self.spacemouse_preferences.mode; + let effective = if self.spacemouse_sheet() && mode.allows_zoom() { + NavigationMode::PanZoom + } else if mode == NavigationMode::Auto { + NavigationMode::Full3D + } else { + mode + }; + crate::t!(effective.label()).into_owned() + } + pub(super) fn sync_spacemouse(&self) { + let tab = &self.tabs[self.active_tab]; + let scene = &tab.scene; + let available = self.spacemouse_preferences.enabled + && !tab.is_start + && self.opening.is_none() + && self.spacemouse.status() == crate::input::spacemouse::Status::Ready; + if !available { + self.spacemouse.sync(None, false); + return; + } + // The driver's settings dialog must still be able to query this view + // after it takes keyboard focus. Gate input separately from its metadata. + let allowed = self.spacemouse_focused && self.active_modal.is_none(); + let camera = scene.navigation_camera(); + let (w, h) = scene.selection.borrow().vp_size; + let bounds = scene.active_model_tile_bounds(w, h); + let aspect = scene + .active_viewport + .and_then(|handle| match scene.document.get_entity(handle) { + Some(acadrust::EntityType::Viewport(vp)) => Some(vp.width / vp.height.max(1e-6)), + _ => None, + }) + .unwrap_or((bounds.width / bounds.height.max(1.)).max(0.01) as f64); + let center = camera.target; + let span = glam::DVec3::splat(camera.distance.max(1.) as f64); + let (min, max) = camera + .model_bounds + .or_else(|| { + scene + .model_space_extents() + .map(|(min, max)| (min.as_dvec3(), max.as_dvec3())) + }) + .unwrap_or((center - span, center + span)); + let target = self.navigation_target(); + let selected = self.spacemouse_selection.bounds(scene, &target); + let pivot = selected + .map(|(min, max)| (min + max) * 0.5) + .or_else(|| { + self.spacemouse_pivot + .as_ref() + .filter(|(stamp, _)| stamp.same_context(&target)) + .map(|(_, point)| *point) + }) + .unwrap_or(center); + let selection_bounds = + selected.map(|(min, max)| [min.x, min.y, min.z, max.x, max.y, max.z]); + self.spacemouse.sync( + Some(View { + target, + eye: camera.eye(), + rotation: camera.rotation.as_dquat().normalize(), + distance: camera.distance as f64, + fov: camera.fov_y as f64, + perspective: camera.projection == Projection::Perspective, + aspect, + model_bounds: [min.x, min.y, min.z, max.x, max.y, max.z], + selection_bounds, + pointer: tab.last_cursor_world, + pivot, + pivot_visible: self.spacemouse_pivot.is_some(), + rotate: self + .spacemouse_preferences + .mode + .allows_rotation(self.spacemouse_sheet()), + zoom: self.spacemouse_preferences.mode.allows_zoom(), + motion_enabled: !self.spacemouse_paused && !scene.navigation_locked(), + units_to_meters: super::properties::insunits_to_mm( + scene.document.header.insertion_units, + ) + .unwrap_or(1000.) + / 1000., + pan_speed: self.spacemouse_preferences.pan_speed.clamp(10, 300) as f64 / 100. + * if self.spacemouse_preferences.pan_reversed { + -1. + } else { + 1. + }, + }), + allowed, + ); + } + pub(super) fn on_spacemouse_wake(&mut self) -> Task { + let (view, commands) = self.spacemouse.drain(); + if !self.spacemouse_focused + || self.active_modal.is_some() + || !self.spacemouse_preferences.enabled + || self.tabs[self.active_tab].is_start + { + self.spacemouse_was_moving = false; + self.spacemouse_pivot = None; + return Task::none(); + } + let target = self.navigation_target(); + let mut tasks = Vec::new(); + let moving = self.spacemouse.moving() && !self.spacemouse_paused; + if moving && !self.spacemouse_was_moving { + self.tabs[self.active_tab].scene.remember_current_view(); + self.clear_navigation_hover(self.active_tab); + } + if !moving && self.spacemouse_was_moving { + self.arm_hover_after_navigation(self.active_tab); + } + self.spacemouse_was_moving = moving; + if !moving { + self.spacemouse_pivot = None; + } + if let Some(view) = view.filter(|view| view.target == target && !self.spacemouse_paused) { + let i = self.active_tab; + let mut camera = self.tabs[i].scene.navigation_camera(); + if view.rotate { + camera.rotation = view.rotation.as_quat(); + camera.sync_yaw_pitch(); + } + if view.zoom { + camera.distance = view.distance as f32; + } + // Use the same f32 basis/distance as Camera::eye so a callback + // that only updates the pivot cannot introduce camera drift. + camera.target = + view.eye - (camera.rotation * glam::Vec3::Z).as_dvec3() * camera.distance as f64; + let started = iced::time::Instant::now(); + if self.tabs[i].scene.apply_navigation_camera(camera) { + self.spacemouse + .acknowledge(&view.target, &self.navigation_target()); + self.spacemouse_pivot = (view.pivot_visible && moving) + .then_some((self.navigation_target(), view.pivot)); + let (w, h) = self.tabs[i].scene.selection.borrow().vp_size; + self.reproject_box_anchor(i, w, h); + // The pointer may be stationary while the left hand navigates. + // Refresh the active command's snap/preview against the new + // camera, using the existing pointer path without a click. + let cursor = { + let selection = self.tabs[i].scene.selection.borrow(); + (!selection.left_down + && !selection.middle_down + && self.tabs[i].active_cmd.is_some()) + .then_some(selection.last_move_pos) + .flatten() + }; + if let Some(cursor) = cursor { + tasks.push(self.on_viewport_move(cursor)); + } + self.tabs[i] + .scene + .record_nav_perf(crate::scene::NavPerfOp::Pan, started); + } + } + for (stamp, command) in commands { + if stamp.same_context(&self.navigation_target()) && self.active_modal.is_none() { + tasks.push(self.run_action(&command)); + } + } + Task::batch(tasks) + } + + pub(super) fn spacemouse_pivot_overlay(&self) -> Option> { + let (target, point) = self.spacemouse_pivot.as_ref()?; + if target != &self.navigation_target() || self.active_modal.is_some() { + return None; + } + let scene = &self.tabs[self.active_tab].scene; + let (w, h) = scene.selection.borrow().vp_size; + let rect = scene + .active_viewport + .and_then(|handle| scene.viewport_screen_rect(handle, (w, h))) + .unwrap_or_else(|| scene.active_model_tile_bounds(w, h)); + let p = scene.navigation_camera().project(*point, rect)?; + if p.x < 0. || p.y < 0. || p.x > rect.width || p.y > rect.height { + return None; + } + let marker = iced::widget::container(iced::widget::Space::new()) + .width(10) + .height(10) + .style(|theme: &iced::Theme| iced::widget::container::Style { + border: iced::Border { + color: theme.palette().primary.base.color, + width: 2., + radius: 5.0.into(), + }, + ..Default::default() + }); + Some( + iced::widget::pin(marker) + .position(iced::Point::new(rect.x + p.x - 5., rect.y + p.y - 5.)) + .into(), + ) + } + pub(super) fn open_spacemouse_preferences(&mut self) { + self.options_tab = crate::ui::window::options::OptionsTab::UserPreferences; + self.active_modal = Some(ModalKind::Options); + } + pub(super) fn open_spacemouse_driver_settings(&self) -> Task { + #[cfg(windows)] + { + if let Some(program_files) = + std::env::var_os("ProgramW6432").or_else(|| std::env::var_os("ProgramFiles")) + { + let path = std::path::PathBuf::from(program_files) + .join("3Dconnexion/3DxWare/3DxWinCore/3DxSmartUi.exe"); + if path.is_file() { + return Task::perform( + async move { + let _ = std::process::Command::new(path).spawn(); + }, + |_| Message::SpaceMouseWake, + ); + } + } + } + crate::sys::open_url("https://3dconnexion.com/drivers/", self.main_window) + } +} + +pub(super) fn actions() -> Vec { + use crate::modules::{IconKind, ModuleEvent, RibbonItem, ToolDef}; + use std::collections::BTreeMap; + let mut names = crate::command::all_registered_command_names(); + names.extend([ + "UNDO", + "REDO", + "CANCEL", + "QSAVE", + "OPEN", + "ZOOM", + "PLAN", + "SPACEMOUSEFIT", + "SPACEMOUSETOP", + ]); + names.sort_unstable(); + names.dedup(); + let mut actions: BTreeMap = names + .into_iter() + .map(|name| { + let label = match name { + "SPACEMOUSE" => "SpaceMouse preferences", + "SPACEMOUSEPAUSE" => "Pause / resume SpaceMouse", + "SPACEMOUSEPAN" => "SpaceMouse: pan only", + "SPACEMOUSEPANZOOM" => "SpaceMouse: pan and zoom", + "SPACEMOUSEAUTO" => "SpaceMouse: follow context", + "SPACEMOUSE3D" => "SpaceMouse: 3D navigation", + "SPACEMOUSEFIT" => "Fit view", + "SPACEMOUSETOP" => "Top view", + _ => name, + }; + ( + name.into(), + Action { + id: name.into(), + label: crate::t!(label).into_owned(), + description: format!("{} ({name})", crate::t!(label)), + icon: name + .starts_with("SPACEMOUSE") + .then_some(crate::ui::window::options::spacemouse::ICON), + }, + ) + }) + .collect(); + fn describe( + actions: &mut BTreeMap, + command: &str, + label: &str, + icon: IconKind, + ) { + if let Some(action) = actions.get_mut(command) { + action.label = crate::t!(label).into_owned(); + action.description = format!("{} ({command})", action.label); + action.icon = match icon { + IconKind::Svg(bytes) => Some(bytes), + _ => None, + }; + } + } + fn tool(actions: &mut BTreeMap, tool: &ToolDef) { + if let ModuleEvent::Command(command) = &tool.event { + describe(actions, command, tool.label, tool.icon); + } + } + for module in crate::modules::registry::all_modules() { + for group in module.ribbon_groups() { + for item in &group.tools { + match item { + RibbonItem::Tool(t) | RibbonItem::LabeledTool(t) | RibbonItem::LargeTool(t) => { + tool(&mut actions, t) + } + RibbonItem::Dropdown { items, .. } + | RibbonItem::LabeledDropdown { items, .. } + | RibbonItem::LargeDropdown { items, .. } => { + for (label, command, icon) in items { + describe(&mut actions, command, label, *icon); + } + } + RibbonItem::ToolGrid { columns } + | RibbonItem::StyleComboGroup { rows: columns, .. } => { + for t in columns.iter().flatten() { + tool(&mut actions, t); + } + } + RibbonItem::LayerComboGroup { row2, row3 } => { + for t in row2.iter().chain(row3) { + tool(&mut actions, t); + } + } + RibbonItem::PropertiesGroup { match_prop } => tool(&mut actions, match_prop), + } + } + } + } + actions.into_values().collect() +} + +#[cfg(test)] +mod tests { + use super::*; + use glam::DVec3; + + fn app() -> OpenCADStudio { + let mut app = OpenCADStudio::new_for_test(); + app.automation_op(r#"{"op":"new"}"#); + app.tabs[app.active_tab] + .scene + .selection + .borrow_mut() + .vp_size = (800., 600.); + app.spacemouse_focused = true; + app.spacemouse.test_connect(); + app.sync_spacemouse(); + app + } + + #[test] + fn spacemouse_navigation_and_buttons_preserve_a_live_polyline() { + let mut app = app(); + let _ = app.dispatch_command("PLINE"); + for point in [ + DVec3::ZERO, + DVec3::new(10., 0., 0.), + DVec3::new(10., 10., 0.), + ] { + let _ = app.feed_command(crate::command::StepInput::Point(point)); + } + let i = app.active_tab; + let before = app.tabs[i].scene.navigation_camera(); + let epoch = app.tabs[i].scene.geometry_epoch; + app.sync_spacemouse(); + app.spacemouse.test_propose(|v| v.eye.x += 3.); + let _ = app.on_spacemouse_wake(); + assert_eq!( + app.tabs[i].scene.navigation_camera().target.x, + before.target.x + 3. + ); + assert_eq!(app.tabs[i].scene.geometry_epoch, epoch); + assert!(app.tabs[i].active_cmd.is_some()); + let _ = app.run_action("UNDO"); + assert!(app.tabs[i].active_cmd.is_some()); + // Finish after Undo: the last point was removed, leaving one segment. + let _ = app.feed_command(crate::command::StepInput::Enter); + let polyline = app.tabs[i] + .scene + .document + .entities() + .find_map(|e| { + if let acadrust::EntityType::LwPolyline(p) = e { + Some(p) + } else { + None + } + }) + .expect("finished polyline"); + assert_eq!(polyline.vertices.len(), 2); + } + + #[test] + fn spacemouse_refreshes_a_live_command_at_a_stationary_pointer() { + let mut app = app(); + app.snapper.snap_enabled = false; + app.snapper.grid_snap_on = false; + app.snapper.otrack_enabled = false; + app.ortho_mode = false; + app.polar_mode = false; + let _ = app.dispatch_command("LINE"); + let _ = app.feed_command(crate::command::StepInput::Point(DVec3::ZERO)); + let _ = app.on_viewport_move(iced::Point::new(480., 300.)); + let i = app.active_tab; + let before = app.tabs[i].last_cursor_world; + let count = app.tabs[i].scene.document.entities().count(); + app.sync_spacemouse(); + app.spacemouse.test_propose(|v| v.eye.x += 3.); + let _ = app.on_spacemouse_wake(); + assert!((app.tabs[i].last_cursor_world.x - before.x - 3.).abs() < 1e-4); + assert!(app.tabs[i].active_cmd.is_some()); + assert_eq!(app.tabs[i].scene.document.entities().count(), count); + } + + #[test] + fn spacemouse_stale_camera_modal_and_pause_block_pending_motion() { + for guard in ["camera", "modal", "pause", "focus"] { + let mut app = app(); + let before = app.tabs[app.active_tab].scene.navigation_camera().target; + app.spacemouse.test_propose(|v| v.eye.x += 30.); + match guard { + "camera" => app.tabs[app.active_tab].scene.camera_generation += 1, + "modal" => app.active_modal = Some(ModalKind::Options), + "pause" => app.spacemouse_paused = true, + _ => app.spacemouse_focused = false, + } + let _ = app.on_spacemouse_wake(); + assert_eq!( + app.tabs[app.active_tab].scene.navigation_camera().target, + before, + "{guard}" + ); + } + } + + #[test] + fn spacemouse_settings_dialog_retains_view_metadata_without_accepting_motion() { + for modal in [false, true] { + let mut app = app(); + app.spacemouse_preferences.mode = NavigationMode::Full3D; + app.sync_spacemouse(); + let before = app.tabs[app.active_tab].scene.navigation_camera(); + if modal { + app.active_modal = Some(ModalKind::Options); + } else { + app.spacemouse_focused = false; + } + app.sync_spacemouse(); + let snapshot = app + .spacemouse + .test_view() + .expect("driver can describe the inactive view"); + assert!(snapshot.rotate); + assert_eq!(snapshot.distance, before.distance as f64); + app.spacemouse.test_propose(|v| v.eye.x += 10.); + let _ = app.on_spacemouse_wake(); + assert_eq!( + app.tabs[app.active_tab].scene.navigation_camera().target, + before.target + ); + } + } + + #[test] + fn spacemouse_floating_camera_round_trip_and_lock() { + let mut app = app(); + let scene = &mut app.tabs[app.active_tab].scene; + let paper_camera = scene.camera.borrow().clone(); + let mut vp = acadrust::entities::Viewport::default(); + vp.width = 200.; + vp.height = 100.; + vp.view_height = 50.; + let handle = scene.add_entity(acadrust::EntityType::Viewport(vp)); + scene.active_viewport = Some(handle); + let mut camera = scene.navigation_camera(); + camera.target = DVec3::new(120., 30., 8.); + camera.rotation = glam::Quat::from_euler(glam::EulerRot::XYZ, 0.7, 0.3, 0.2); + camera.distance = 80.; + assert!(scene.apply_navigation_camera(camera.clone())); + let round_trip = scene.navigation_camera(); + assert!(round_trip.target.distance(camera.target) < 1e-6); + assert!(round_trip.rotation.abs_diff_eq(camera.rotation, 1e-5)); + assert!((round_trip.distance - camera.distance).abs() < 1e-4); + assert_eq!(scene.camera.borrow().target, paper_camera.target); + assert_eq!(scene.camera.borrow().distance, paper_camera.distance); + if let Some(acadrust::EntityType::Viewport(vp)) = scene.document.get_entity_mut(handle) { + vp.status.locked = true; + } + camera.target.x += 100.; + assert!(!scene.apply_navigation_camera(camera)); + assert_eq!(scene.navigation_camera().target, round_trip.target); + } + + #[test] + fn spacemouse_push_and_tilt_move_the_drawing_in_screen_space_continuously() { + for mode in [NavigationMode::PanOnly, NavigationMode::PanZoom] { + for projection in [Projection::Orthographic, Projection::Perspective] { + for reversed in [false, true] { + for (axes, direction) in [ + ([350, 0, 0, 0, 0, 0], glam::Vec2::X), + ([0, 0, 0, 0, -350, 0], glam::Vec2::X), + ([-350, 0, 0, 0, 0, 0], -glam::Vec2::X), + ([0, 0, 0, 0, 350, 0], -glam::Vec2::X), + ([0, -350, 0, 0, 0, 0], -glam::Vec2::Y), + ([0, 0, 0, -350, 0, 0], -glam::Vec2::Y), + ([0, 350, 0, 0, 0, 0], glam::Vec2::Y), + ([0, 0, 0, 350, 0, 0], glam::Vec2::Y), + ] { + let mut app = app(); + app.spacemouse_preferences.mode = mode; + app.spacemouse_preferences.pan_reversed = reversed; + let i = app.active_tab; + { + let mut camera = app.tabs[i].scene.camera.borrow_mut(); + camera.rotation = + glam::Quat::from_euler(glam::EulerRot::XYZ, 0.7, 0.4, 0.2); + camera.projection = projection; + } + app.sync_spacemouse(); + let before = app.tabs[i].scene.navigation_camera(); + let bounds = iced::Rectangle::with_size(iced::Size::new(800., 600.)); + let point = before.target; + let screen = before.project(point, bounds).unwrap(); + let direction = direction * if reversed { -1. } else { 1. }; + let mut last = glam::Vec2::new(screen.x, screen.y); + for frame in 0..20 { + app.spacemouse.test_pan_report(axes); + app.spacemouse.frame(frame as f64 * 16.); + let _ = app.on_spacemouse_wake(); + app.sync_spacemouse(); + let camera = app.tabs[i].scene.navigation_camera(); + let moved = camera.project(point, bounds).unwrap(); + let moved = glam::Vec2::new(moved.x, moved.y); + let displacement = moved - last; + assert!(displacement.dot(direction) > 0., "{mode:?}: {axes:?}"); + assert!(displacement.perp_dot(direction).abs() < 0.001); + assert_eq!(camera.rotation, before.rotation); + assert_eq!(camera.distance, before.distance); + last = moved; + } + let stopped = app.tabs[i].scene.navigation_camera().target; + app.spacemouse.test_pan_report([0, 0, 0, 0, 0, 350]); + app.spacemouse.frame(400.); + let _ = app.on_spacemouse_wake(); + assert_eq!(app.tabs[i].scene.navigation_camera().target, stopped); + } + } + } + } + } + + #[test] + fn spacemouse_panzoom_lift_and_pressure_zoom_about_the_center_and_release_stops() { + for projection in [Projection::Orthographic, Projection::Perspective] { + for mode in [NavigationMode::PanOnly, NavigationMode::PanZoom] { + for pressure in [-350, 350] { + let mut app = app(); + app.spacemouse_preferences.mode = mode; + let i = app.active_tab; + { + let mut camera = app.tabs[i].scene.camera.borrow_mut(); + camera.rotation = + glam::Quat::from_euler(glam::EulerRot::XYZ, 0.7, 0.4, 0.2); + camera.projection = projection; + } + app.sync_spacemouse(); + let before = app.tabs[i].scene.navigation_camera(); + let mut distance = before.distance; + for frame in 0..20 { + app.spacemouse.test_pan_report([0, 0, pressure, 0, 0, 0]); + app.spacemouse.frame(frame as f64 * 16.); + let _ = app.on_spacemouse_wake(); + app.sync_spacemouse(); + let camera = app.tabs[i].scene.navigation_camera(); + assert!(camera.target.distance(before.target) < 1e-4); + assert_eq!(camera.rotation, before.rotation); + if mode == NavigationMode::PanOnly { + assert_eq!(camera.distance, distance); + } else { + assert!((camera.distance - distance) * pressure as f32 > 0.); + } + distance = camera.distance; + } + app.spacemouse.test_pan_report([0; 6]); + app.spacemouse.frame(400.); + let _ = app.on_spacemouse_wake(); + assert_eq!(app.tabs[i].scene.navigation_camera().distance, distance); + assert!(!app.spacemouse.moving()); + } + } + } + } + + #[test] + fn spacemouse_preferences_round_trip_and_old_settings_default() { + let old: super::super::settings::UserSettings = serde_json::from_str("{}").unwrap(); + assert_eq!( + old.spacemouse, + crate::input::spacemouse::Preferences::default() + ); + let mut app = app(); + app.spacemouse_preferences.mode = NavigationMode::PanOnly; + app.spacemouse_preferences.pan_speed = 170; + app.spacemouse_preferences.pan_reversed = true; + let encoded = serde_json::to_string(&app.current_settings()).unwrap(); + let restored: super::super::settings::UserSettings = + serde_json::from_str(&encoded).unwrap(); + app.spacemouse_preferences.mode = NavigationMode::Auto; + app.apply_settings(&restored); + assert_eq!(app.spacemouse_preferences.mode, NavigationMode::PanOnly); + assert_eq!(app.spacemouse_preferences.pan_speed, 170); + assert!(app.spacemouse_preferences.pan_reversed); + } + + #[test] + fn spacemouse_action_catalog_has_stable_unique_ids_and_ui_icons() { + let actions = actions(); + let ids: std::collections::HashSet<_> = actions.iter().map(|a| &a.id).collect(); + assert_eq!(ids.len(), actions.len()); + for id in [ + "LINE", + "UNDO", + "SPACEMOUSEPAN", + "SPACEMOUSEPAUSE", + "SPACEMOUSEFIT", + ] { + assert!(actions.iter().any(|a| a.id == id && !a.label.is_empty())); + } + assert!(actions.iter().any(|a| a.id == "LINE" && a.icon.is_some())); + assert!(crate::app::commands::is_transparent("SPACEMOUSEPAN")); + assert!(crate::app::commands::start_allowed("SPACEMOUSE")); + } +} diff --git a/src/app/properties.rs b/src/app/properties.rs index a942c5ca6..415a10483 100644 --- a/src/app/properties.rs +++ b/src/app/properties.rs @@ -3806,7 +3806,7 @@ fn format_unit_factor(factor: f64) -> String { } /// Convert INSUNITS (DXF group 70) to millimetres. -fn insunits_to_mm(code: i16) -> Option { +pub(super) fn insunits_to_mm(code: i16) -> Option { Some(match code { 1 => 25.4, // Inches 2 => 304.8, // Feet diff --git a/src/app/settings.rs b/src/app/settings.rs index 9c2757542..b19d6daab 100644 --- a/src/app/settings.rs +++ b/src/app/settings.rs @@ -289,6 +289,7 @@ pub const DEFAULT_GRIP_OBJECT_LIMIT: i32 = 100; #[derive(Clone, PartialEq, Serialize, Deserialize)] #[serde(default)] pub struct UserSettings { + pub spacemouse: crate::input::spacemouse::Preferences, pub dyn_input: bool, pub polar: bool, pub polar_increment_deg: f32, @@ -497,6 +498,7 @@ pub fn clamp_clipromptlines(v: i32) -> i32 { impl Default for UserSettings { fn default() -> Self { Self { + spacemouse: crate::input::spacemouse::Preferences::default(), dyn_input: true, polar: false, polar_increment_deg: 45.0, diff --git a/src/app/shortcuts.rs b/src/app/shortcuts.rs index 3682bbbe3..8df20b273 100644 --- a/src/app/shortcuts.rs +++ b/src/app/shortcuts.rs @@ -145,6 +145,8 @@ fn is_named_key(key: &str) -> bool { /// the command dispatcher — valid shortcut commands that never appear in the /// command registry. Used to validate the shortcut editor's command column. pub(super) const INPUT_ACTIONS: &[&str] = &[ + "SPACEMOUSEFIT", + "SPACEMOUSETOP", "FINALIZE", "COMMANDSPACE", "CANCEL", @@ -270,7 +272,36 @@ impl OpenCADStudio { let Some(action) = action.cloned() else { return Task::none(); }; - let message = match action.as_str() { + self.run_action(&action) + } + + /// Shared by keyboard bindings and exported device actions. In particular, + /// Undo retains its command-local behavior during PLINE and SPLINE. + pub(super) fn run_action(&mut self, action: &str) -> Task { + let message = match action { + "SPACEMOUSEFIT" => { + let i = self.active_tab; + self.clear_navigation_hover(i); + self.tabs[i].scene.remember_current_view(); + self.tabs[i].scene.fit_all(); + self.arm_hover_after_navigation(i); + return Task::none(); + } + "SPACEMOUSETOP" => Message::ViewCubeHome, + "SPACEMOUSE" => Message::SpaceMousePreferences, + "SPACEMOUSEPAUSE" => Message::SpaceMousePause, + "SPACEMOUSEPAN" => { + Message::SpaceMouseMode(crate::input::spacemouse::NavigationMode::PanOnly) + } + "SPACEMOUSEPANZOOM" => { + Message::SpaceMouseMode(crate::input::spacemouse::NavigationMode::PanZoom) + } + "SPACEMOUSEAUTO" => { + Message::SpaceMouseMode(crate::input::spacemouse::NavigationMode::Auto) + } + "SPACEMOUSE3D" => { + Message::SpaceMouseMode(crate::input::spacemouse::NavigationMode::Full3D) + } "FINALIZE" => Message::CommandFinalize, "COMMANDSPACE" => Message::CommandSpace, "CANCEL" => Message::CommandEscape, diff --git a/src/app/update/file.rs b/src/app/update/file.rs index 8f7ca88f2..a2dc7be0f 100644 --- a/src/app/update/file.rs +++ b/src/app/update/file.rs @@ -382,6 +382,7 @@ impl OpenCADStudio { /// Snapshot the persisted UI preferences from live state. pub(in crate::app) fn current_settings(&self) -> crate::app::settings::UserSettings { crate::app::settings::UserSettings { + spacemouse: self.spacemouse_preferences, dyn_input: self.dyn_input, polar: self.polar_mode, polar_increment_deg: self.polar_increment_deg, @@ -457,6 +458,7 @@ impl OpenCADStudio { self.cursor_size = s.cursor_size.clamp(1, 100); self.pick_box = s.pick_box.clamp(0, 50); self.options_tab = s.options_tab; + self.spacemouse_preferences = s.spacemouse; // These four drive real features with commands and status-bar pills, // but they lived only on the app struct: turning the ViewCube off and // restarting brought it straight back. diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index 1f999afd0..2fb5af259 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -354,6 +354,7 @@ impl OpenCADStudio { self.block_palette.placing = None; } self.control_settle(); + self.sync_spacemouse(); task } @@ -369,6 +370,48 @@ impl OpenCADStudio { fn update_inner(&mut self, msg: Message) -> Task { match msg { + Message::SpaceMouseWake => self.on_spacemouse_wake(), + Message::SpaceMouseFrame(time) => { + self.spacemouse.frame( + time.saturating_duration_since(self.start).as_secs_f64() * 1000., + ); + Task::none() + } + Message::SpaceMouseFocus(id, focused) => { + if Some(id) == self.main_window { + self.spacemouse_focused = focused; + } + Task::none() + } + Message::SpaceMouseEnabled(enabled) => { + self.spacemouse_preferences.enabled = enabled; + Task::none() + } + Message::SpaceMouseMode(mode) => { + self.spacemouse_preferences.mode = mode; + Task::none() + } + Message::SpaceMousePanSpeed(speed) => { + self.spacemouse_preferences.pan_speed = speed.clamp(10, 300); + Task::none() + } + Message::SpaceMousePanReversed(reversed) => { + self.spacemouse_preferences.pan_reversed = reversed; + Task::none() + } + Message::SpaceMousePause => { + self.spacemouse_paused = !self.spacemouse_paused; + Task::none() + } + Message::SpaceMousePreferences => { + self.open_spacemouse_preferences(); + Task::none() + } + Message::SpaceMouseDriverSettings => self.open_spacemouse_driver_settings(), + Message::SpaceMouseDetails => { + self.spacemouse_details = !self.spacemouse_details; + Task::none() + } Message::ControlRequest(envelope) => { let (response, task) = self.control_request(envelope.request); envelope.reply.send(response); diff --git a/src/app/update/viewport.rs b/src/app/update/viewport.rs index 958df1fd6..02f0c8d2a 100644 --- a/src/app/update/viewport.rs +++ b/src/app/update/viewport.rs @@ -922,7 +922,7 @@ impl OpenCADStudio { Task::none() } - pub(super) fn on_viewport_move(&mut self, p: Point) -> Task { + pub(in crate::app) fn on_viewport_move(&mut self, p: Point) -> Task { // A ribbon dropdown is open over the viewport. Its backdrop // cannot swallow cursor motion — in iced 0.14 mouse_area/opaque // capture only button presses, never CursorMoved — so the move @@ -5324,7 +5324,7 @@ properties={:.1}ms picked={}", /// model-space view moves (zoom/pan), so the selection rectangle tracks the /// drawing instead of staying frozen at its original pixel. No-op when no /// box is in progress. (#234) - pub(super) fn reproject_box_anchor(&mut self, i: usize, vw: f32, vh: f32) { + pub(in crate::app) fn reproject_box_anchor(&mut self, i: usize, vw: f32, vh: f32) { let world = self.tabs[i].scene.selection.borrow().box_anchor_world; let Some(world) = world else { return }; let tile_b = self.tabs[i].scene.active_model_tile_bounds(vw, vh); diff --git a/src/app/view/mod.rs b/src/app/view/mod.rs index 0e3c7fdae..f58533c92 100644 --- a/src/app/view/mod.rs +++ b/src/app/view/mod.rs @@ -1200,6 +1200,9 @@ bg={bg_ms:.1}ms n={view_count}" }; if !thumbnail_capture_clean && !self.layout_settling { + if let Some(pivot) = self.spacemouse_pivot_overlay() { + viewport_stack = viewport_stack.push(pivot); + } // Per-pane input pane_grid goes ABOVE the crosshair overlay so it // receives mouse events (the overlay's `Hidden` cursor would otherwise // starve any layer beneath it). The controls bar is pushed on top of it. @@ -2199,6 +2202,18 @@ bg={bg_ms:.1}ms n={view_count}" .map(|s| s.conflicts.len()) .unwrap_or(0), &self.gpu_status, + (self.spacemouse.visible() + || self.spacemouse_preferences.mode + != crate::input::spacemouse::NavigationMode::Auto) + .then(|| { + crate::ui::statusbar::spacemouse::view( + self.spacemouse_preferences, + self.spacemouse.status(), + self.spacemouse_paused, + self.spacemouse_label(), + self.spacemouse_sheet(), + ) + }), ) }) .width(Fill) @@ -2658,6 +2673,21 @@ impl OpenCADStudio { let control = iced::time::every(std::time::Duration::from_millis(50)) .map(|_| Message::PollWebControl); iced::Subscription::batch([ + if self.spacemouse.moving() && self.spacemouse_focused && !self.spacemouse_paused { + window::frames().map(Message::SpaceMouseFrame) + } else { + Subscription::none() + }, + self.spacemouse.subscription().map(|_| Message::SpaceMouseWake), + event::listen_with(|event, _, id| match event { + iced::Event::Window(window::Event::Focused) => { + Some(Message::SpaceMouseFocus(id, true)) + } + iced::Event::Window(window::Event::Unfocused) => { + Some(Message::SpaceMouseFocus(id, false)) + } + _ => None, + }), control, frames, history_tick, diff --git a/src/app/view/modal.rs b/src/app/view/modal.rs index 2291d8654..9d6787487 100644 --- a/src/app/view/modal.rs +++ b/src/app/view/modal.rs @@ -232,6 +232,10 @@ impl OpenCADStudio { show_ucs_icon: self.show_ucs_icon, ucs_icon_at_origin: self.ucs_icon_at_origin, }, + crate::ui::window::options::spacemouse::view( + self.spacemouse_preferences, self.spacemouse.status(), + self.spacemouse_paused, self.spacemouse_details, + ), &self.snap_angle_input, { let header = self diff --git a/src/input/mod.rs b/src/input/mod.rs new file mode 100644 index 000000000..c1131bdeb --- /dev/null +++ b/src/input/mod.rs @@ -0,0 +1,2 @@ +//! Device integrations. Camera ownership and CAD commands stay in the app. +pub(crate) mod spacemouse; diff --git a/src/input/spacemouse/mod.rs b/src/input/spacemouse/mod.rs new file mode 100644 index 000000000..392ae591d --- /dev/null +++ b/src/input/spacemouse/mod.rs @@ -0,0 +1,593 @@ +//! SpaceMouse navigation data and the thread-safe bridge to the platform driver. +//! +//! No SDK types escape this module. Driver callbacks operate on a snapshot; +//! only the app thread applies a proposed view to a drawing. + +// Snapshot/ABI support is intentionally dormant until another platform has an adapter. +#![cfg_attr(not(windows), allow(dead_code))] + +use glam::{DMat4, DQuat, DVec3}; +use serde::{Deserialize, Serialize}; +use std::collections::VecDeque; +use std::hash::{Hash, Hasher}; +use std::sync::{Arc, Mutex}; + +#[cfg(windows)] +mod navlib; +mod pan; +#[cfg(windows)] +mod raw_input; +#[cfg(windows)] +mod worker; + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum NavigationMode { + #[default] + Auto, + PanOnly, + PanZoom, + Full3D, +} + +impl NavigationMode { + pub const ALL: [Self; 4] = [Self::Auto, Self::PanOnly, Self::PanZoom, Self::Full3D]; + pub fn label(self) -> &'static str { + match self { + Self::Auto => "Follow context", + Self::PanOnly => "Pan only", + Self::PanZoom => "Pan and zoom", + Self::Full3D => "3D navigation", + } + } + pub fn description(self) -> &'static str { + match self { + Self::Auto => "Pan and zoom on paper sheets; full navigation in model views.", + Self::PanOnly => { + "Push or tilt to pan. Twist and vertical pressure are ignored; zoom and rotation stay fixed." + } + Self::PanZoom => { + "Push or tilt to pan; lift or press to zoom. Twist is ignored and rotation stays fixed." + } + Self::Full3D => "Pan, zoom, and orbit model views. Paper sheets keep rotation locked.", + } + } + pub fn allows_rotation(self, sheet: bool) -> bool { + !sheet && matches!(self, Self::Auto | Self::Full3D) + } + pub fn allows_zoom(self) -> bool { + self != Self::PanOnly + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(default)] +pub struct Preferences { + pub enabled: bool, + pub mode: NavigationMode, + pub pan_speed: u16, + pub pan_reversed: bool, +} +impl Default for Preferences { + fn default() -> Self { + Self { + enabled: true, + mode: NavigationMode::Auto, + pan_speed: 100, + pan_reversed: false, + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum Status { + Starting, + Ready, + Disconnected, + Unavailable(String), + Unsupported, +} +impl Status { + pub fn label(&self) -> &str { + match self { + Self::Starting => "Connecting…", + Self::Ready => "Ready", + Self::Disconnected => "No SpaceMouse connected", + Self::Unavailable(_) => "3DxWare unavailable", + Self::Unsupported => "Available in the Windows desktop app", + } + } +} + +/// Stable target plus camera revision. Late callbacks must never move another +/// document, pane, layout, or a camera already changed by another input source. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Target { + pub tab: u64, + pub layout: String, + pub viewport: Option, + pub tile: usize, + pub generation: u64, +} +impl Target { + pub fn same_context(&self, other: &Self) -> bool { + self.tab == other.tab + && self.layout == other.layout + && self.viewport == other.viewport + && self.tile == other.tile + } +} + +#[derive(Clone, Debug)] +pub struct View { + pub target: Target, + pub eye: DVec3, + pub rotation: DQuat, + pub distance: f64, + pub fov: f64, + pub perspective: bool, + pub aspect: f64, + pub model_bounds: [f64; 6], + pub selection_bounds: Option<[f64; 6]>, + pub pointer: DVec3, + pub pivot: DVec3, + pub pivot_visible: bool, + pub rotate: bool, + pub zoom: bool, + pub motion_enabled: bool, + pub units_to_meters: f64, + pub pan_speed: f64, +} +impl View { + fn planar(&self) -> bool { + !self.rotate + } + pub fn affine(&self) -> [f64; 16] { + DMat4::from_rotation_translation(self.rotation, self.eye).to_cols_array() + } + pub fn half_height(&self) -> f64 { + self.distance * (self.fov * 0.5).tan() + } + pub fn extents(&self) -> [f64; 6] { + let h = self.half_height(); + let w = h * self.aspect; + [ + -w, + -h, + -self.distance * 1000.0, + w, + h, + self.distance * 1000.0, + ] + } + /// Validate before accepting driver data. A pan-only view projects the + /// translation onto its original screen plane, including in perspective. + pub fn set_affine(&mut self, values: [f64; 16]) -> bool { + if !values.iter().all(|v| v.is_finite()) { + return false; + } + let matrix = DMat4::from_cols_array(&values); + let x = matrix.x_axis.truncate(); + let y = matrix.y_axis.truncate(); + let z = matrix.z_axis.truncate(); + if (x.length_squared() - 1.0).abs() > 0.01 + || (y.length_squared() - 1.0).abs() > 0.01 + || (z.length_squared() - 1.0).abs() > 0.01 + || x.dot(y).abs() > 0.01 + || x.cross(y).dot(z) < 0.99 + || matrix.w_axis.w != 1.0 + || matrix.x_axis.w != 0.0 + || matrix.y_axis.w != 0.0 + || matrix.z_axis.w != 0.0 + { + return false; + } + let proposed_eye = matrix.w_axis.truncate(); + if self.zoom { + self.eye = proposed_eye; + } else { + let normal = self.rotation * DVec3::Z; + let delta = proposed_eye - self.eye; + self.eye += delta - normal * delta.dot(normal); + } + if self.rotate { + self.rotation = DQuat::from_mat4(&matrix).normalize(); + } + true + } + pub fn set_extents(&mut self, extents: [f64; 6]) -> bool { + if !extents.iter().all(|v| v.is_finite()) { + return false; + } + let distance = (extents[4] - extents[1]) * 0.5 / (self.fov * 0.5).tan(); + if !(1e-6..=1e15).contains(&distance) { + return false; + } + if self.zoom { + // Orthographic zoom changes scale while retaining the view center. + self.eye += self.rotation * DVec3::Z * (distance - self.distance); + self.distance = distance; + } + true + } + + /// Honor the driver's Lock Horizon option in our Z-up world. Keep the + /// proposed eye and viewing direction away from the poles. A pitch through + /// a pole must stop there: rebuilding world-up on the far side would reverse + /// screen-right and turn the view through 180 degrees. + fn lock_horizon(&mut self, previous: DQuat, orbit_about_pivot: bool) { + if !self.rotate { + return; + } + // Leave enough room for the app camera's f32 quaternion round-trip. + const POLE_MARGIN: f64 = 1e-4; + let proposed_normal = self.rotation * DVec3::Z; + let mut normal = proposed_normal; + let previous_right = (previous * DVec3::X) + .truncate() + .extend(0.) + .try_normalize() + .unwrap_or(DVec3::X); + let right = DVec3::Z.cross(normal); + let crossed_pole = + right.dot(previous_right) < 0. && (self.rotation * DVec3::X).dot(previous_right) > 0.; + let right = if crossed_pole || right.length_squared() < POLE_MARGIN.powi(2) { + let horizontal_normal = previous_right.cross(DVec3::Z); + normal = horizontal_normal * POLE_MARGIN + + DVec3::Z * normal.z.signum() * (1. - POLE_MARGIN.powi(2)).sqrt(); + // Apply the rejected pitch to the eye around the SDK's orbit pivot + // too. Correcting orientation alone lets a held orbit carry the eye + // over the pole and shifts the object across the screen. + if orbit_about_pivot { + let correction = DQuat::from_rotation_arc(proposed_normal, normal); + self.eye = self.pivot + correction * (self.eye - self.pivot); + } + previous_right + } else { + right.normalize() + }; + self.rotation = + DQuat::from_mat3(&glam::DMat3::from_cols(right, normal.cross(right), normal)) + .normalize(); + } +} + +#[derive(Clone, Debug)] +pub struct Action { + pub id: String, + pub label: String, + pub description: String, + pub icon: Option<&'static [u8]>, +} + +#[derive(Default)] +struct Bridge { + view: Option, + pending_view: Option, + actions: Vec, + commands: VecDeque<(Target, String)>, + focus: bool, + moving: bool, + in_transaction: bool, + frame_time: Option, + reset_motion: bool, + driver_settings_changed: bool, + lock_horizon: bool, + orbit_about_pivot: bool, + pan: pan::Pan, + status: Option, + ever_connected: bool, + wake: Option>, + #[cfg(windows)] + control: Option, +} +impl Bridge { + fn wake(&mut self) { + if let Some(wake) = &mut self.wake { + let _ = wake.try_send(()); + } + } +} + +#[derive(Clone, Default)] +pub struct Service(Arc>); +impl Hash for Service { + fn hash(&self, state: &mut H) { + Arc::as_ptr(&self.0).hash(state); + } +} +impl Service { + pub fn status(&self) -> Status { + if !cfg!(windows) { + return Status::Unsupported; + } + self.0 + .lock() + .unwrap() + .status + .clone() + .unwrap_or(Status::Starting) + } + pub fn visible(&self) -> bool { + self.0.lock().unwrap().ever_connected + } + pub fn moving(&self) -> bool { + let bridge = self.0.lock().unwrap(); + if let Some(view) = bridge.view.as_ref().filter(|v| v.planar()) { + bridge.pan.moving(view.zoom) + } else { + bridge.moving + } + } + pub fn frame(&self, milliseconds: f64) { + let mut bridge = self.0.lock().unwrap(); + if bridge.focus + && bridge + .view + .as_ref() + .is_some_and(|v| v.planar() && v.motion_enabled) + { + let zoom = bridge.view.as_ref().unwrap().zoom; + let delta = bridge.pan.frame(milliseconds, zoom); + if delta != DVec3::ZERO { + let view = bridge.view.as_mut().unwrap(); + view.eye -= view.rotation + * delta.truncate().extend(0.) + * view.half_height() + * 2. + * view.pan_speed; + // Lift/pressure owns zoom in both projections. Keep the center + // fixed and leave push/tilt exclusively responsible for pan. + let distance = (view.distance * (delta.z * 2.).exp()).clamp(1e-6, 1e15); + view.eye += view.rotation * DVec3::Z * (distance - view.distance); + view.distance = distance; + bridge.pending_view = bridge.view.clone(); + } + // Also wake after a stale report stops motion, so rendering can idle. + bridge.wake(); + return; + } + if bridge.focus && bridge.moving { + bridge.frame_time = Some(milliseconds); + #[cfg(windows)] + if let Some(wake) = &bridge.control { + wake.signal(); + } + } + } + #[cfg(test)] + pub fn test_connect(&self) { + self.0.lock().unwrap().status = Some(Status::Ready); + } + #[cfg(test)] + pub fn test_view(&self) -> Option { + self.0.lock().unwrap().view.clone() + } + #[cfg(test)] + pub fn test_pan_report(&self, axes: [i16; 6]) { + let report: Vec<_> = std::iter::once(1) + .chain(axes.into_iter().flat_map(i16::to_le_bytes)) + .collect(); + self.0.lock().unwrap().pan.report(&report); + } + #[cfg(test)] + pub fn test_propose(&self, change: impl FnOnce(&mut View)) { + let mut bridge = self.0.lock().unwrap(); + let mut view = bridge.view.clone().expect("focused drawing snapshot"); + change(&mut view); + bridge.pending_view = Some(view); + bridge.moving = true; + } + pub fn set_actions(&self, actions: Vec) { + self.0.lock().unwrap().actions = actions; + } + /// Rebase callbacks that arrived while the UI applied the previous frame. + /// They still belong to the same gesture, despite the scene revision bump. + pub fn acknowledge(&self, previous: &Target, applied: &Target) { + let mut bridge = self.0.lock().unwrap(); + if previous.same_context(applied) { + let Bridge { + view, pending_view, .. + } = &mut *bridge; + for view in [view, pending_view].into_iter().flatten() { + if &view.target == previous { + view.target = applied.clone(); + } + } + } + } + pub fn sync(&self, view: Option, focus: bool) { + let mut bridge = self.0.lock().unwrap(); + let context_changed = match (&bridge.view, &view) { + (Some(old), Some(new)) => !old.target.same_context(&new.target), + (None, None) => false, + _ => true, + }; + let changed = bridge.view.as_ref().map(|v| &v.target) != view.as_ref().map(|v| &v.target) + || bridge.focus != focus + || bridge + .view + .as_ref() + .map(|v| (v.rotate, v.zoom, v.motion_enabled)) + != view.as_ref().map(|v| (v.rotate, v.zoom, v.motion_enabled)); + if changed { + bridge.reset_motion = true; + bridge.frame_time = None; + bridge.pan.clear(); + bridge.pending_view = None; + if context_changed || bridge.focus != focus { + bridge.commands.clear(); + bridge.moving = false; + } + bridge.in_transaction = false; + bridge.view = view; + } else if bridge.pending_view.is_none() { + bridge.view = view; + } + bridge.focus = focus; + if changed { + #[cfg(windows)] + if let Some(wake) = &bridge.control { + wake.signal(); + } + } + } + pub fn drain(&self) -> (Option, Vec<(Target, String)>) { + let mut bridge = self.0.lock().unwrap(); + let view = if bridge.in_transaction { + None + } else { + bridge.pending_view.take() + }; + (view, bridge.commands.drain(..).collect()) + } + pub fn subscription(&self) -> iced::Subscription<()> { + #[cfg(windows)] + { + iced::Subscription::run_with(self.clone(), navlib::subscription) + } + #[cfg(not(windows))] + { + iced::Subscription::none() + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + pub(super) fn view() -> View { + View { + target: Target { + tab: 1, + layout: "Model".into(), + viewport: None, + tile: 0, + generation: 0, + }, + eye: DVec3::new(0., 0., 10.), + rotation: DQuat::IDENTITY, + distance: 10., + fov: 1., + perspective: false, + aspect: 1., + model_bounds: [-1., -1., -1., 1., 1., 1.], + selection_bounds: None, + pointer: DVec3::ZERO, + pivot: DVec3::ZERO, + pivot_visible: false, + rotate: false, + zoom: false, + motion_enabled: true, + units_to_meters: 1., + pan_speed: 1., + } + } + #[test] + fn pan_only_preserves_depth_rotation_and_zoom() { + let mut v = view(); + v.rotation = DQuat::from_rotation_x(0.7); + let initial = v.clone(); + let normal = initial.rotation * DVec3::Z; + let proposed = DMat4::from_rotation_translation( + DQuat::from_rotation_y(0.4), + v.eye + DVec3::new(4., 5., 6.), + ); + assert!(v.set_affine(proposed.to_cols_array())); + assert!((v.eye - initial.eye).dot(normal).abs() < 1e-10); + assert_eq!(v.rotation, initial.rotation); + assert!(v.set_extents([-20., -20., -20., 20., 20., 20.])); + assert_eq!(v.distance, initial.distance); + } + #[test] + fn invalid_driver_matrices_do_not_corrupt_the_camera() { + let mut v = view(); + let eye = v.eye; + assert!(!v.set_affine([f64::NAN; 16])); + assert!(!v.set_affine([0.; 16])); + assert!(!v.set_extents([0.; 6])); + assert_eq!(v.eye, eye); + } + #[test] + fn context_and_focus_changes_discard_queued_input() { + let service = Service::default(); + let v = view(); + service.sync(Some(v.clone()), true); + service.0.lock().unwrap().pending_view = Some(v.clone()); + service.sync(Some(v.clone()), false); + assert!(service.drain().0.is_none()); + service.0.lock().unwrap().pending_view = Some(v.clone()); + let mut other = v; + other.target.tab = 2; + service.sync(Some(other), true); + assert!(service.drain().0.is_none()); + } + #[test] + fn paper_cannot_rotate_in_any_mode() { + for mode in NavigationMode::ALL { + assert!(!mode.allows_rotation(true)); + } + assert!(NavigationMode::Auto.allows_rotation(false)); + assert!(!NavigationMode::PanOnly.allows_rotation(false)); + } + #[test] + fn navigation_frames_are_drained_atomically() { + let service = Service::default(); + service.sync(Some(view()), true); + service.test_propose(|v| v.eye.x += 2.); + service.0.lock().unwrap().in_transaction = true; + assert!(service.drain().0.is_none()); + service.0.lock().unwrap().in_transaction = false; + assert_eq!(service.drain().0.unwrap().eye.x, 2.); + assert!(service.drain().0.is_none()); + } + #[test] + fn camera_updates_retain_buttons_but_document_changes_drop_them() { + let service = Service::default(); + let mut v = view(); + service.sync(Some(v.clone()), true); + service + .0 + .lock() + .unwrap() + .commands + .push_back((v.target.clone(), "UNDO".into())); + v.target.generation += 1; + service.sync(Some(v.clone()), true); + assert_eq!(service.drain().1.len(), 1); + service + .0 + .lock() + .unwrap() + .commands + .push_back((v.target.clone(), "UNDO".into())); + v.target.viewport = Some(123); + service.sync(Some(v), true); + assert!(service.drain().1.is_empty()); + } + #[test] + fn applying_a_frame_retains_newer_callbacks_in_the_same_gesture() { + let service = Service::default(); + let mut applied = view(); + service.sync(Some(applied.clone()), true); + service.0.lock().unwrap().reset_motion = false; + service.test_propose(|v| v.eye.x = 1.); + let first = service.drain().0.unwrap(); + service.test_propose(|v| v.eye.x = 2.); + service.0.lock().unwrap().in_transaction = true; + applied.eye = first.eye; + applied.target.generation += 1; + service.acknowledge(&first.target, &applied.target); + service.sync(Some(applied.clone()), true); + assert!(service.drain().0.is_none()); + assert!(!service.0.lock().unwrap().reset_motion); + service.0.lock().unwrap().in_transaction = false; + let second = service.drain().0.unwrap(); + assert_eq!(second.eye.x, 2.); + assert_eq!(second.target, applied.target); + // A subsequent wheel/mouse camera revision must reset the SDK model. + applied.target.generation += 1; + service.sync(Some(applied), true); + assert!(service.0.lock().unwrap().reset_motion); + } +} diff --git a/src/input/spacemouse/navlib.rs b/src/input/spacemouse/navlib.rs new file mode 100644 index 000000000..2f57623db --- /dev/null +++ b/src/input/spacemouse/navlib.rs @@ -0,0 +1,1121 @@ +//! Windows NavLib adapter. Loads the user's installed 3DxWare runtime; neither +//! SDK binaries nor SDK source files are bundled with Open CAD Studio. +//! +//! ABI reference: 3DxWare SDK v4 navlib.h, navlib_types.h, siappcmd_types.h. +//! All callbacks own only a mutex-protected snapshot. NlClose runs before their +//! storage is released. No mutex is held while calling into the SDK. + +use super::*; +use iced::futures::{channel::mpsc::Sender, Stream}; +use std::ffi::{c_char, c_long, CStr, CString}; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::time::Duration; + +type Get = unsafe extern "C" fn(u64, *const c_char, *mut Value) -> c_long; +type Set = unsafe extern "C" fn(u64, *const c_char, *const Value) -> c_long; +#[repr(C)] +struct Accessor { + name: *const c_char, + get: Option, + set: Option, + param: u64, +} +#[repr(C)] +struct Options { + size: u32, + multithreaded: u8, + flags: u32, +} +#[repr(C)] +#[derive(Clone, Copy)] +struct Array { + data: *const u8, + len: usize, +} +#[repr(C)] +#[derive(Clone, Copy)] +union Data { + boolean: u32, + long: c_long, + number: f64, + point: [f64; 3], + bounds: [f64; 6], + matrix: [f64; 16], + array: Array, + ptr: *const Node, +} +#[repr(C)] +#[derive(Clone, Copy)] +struct Value { + kind: i32, + data: Data, +} +impl Value { + fn boolean(v: bool) -> Self { + Self { + kind: 1, + data: Data { boolean: v.into() }, + } + } + fn number(v: f64) -> Self { + Self { + kind: 4, + data: Data { number: v }, + } + } + fn values(kind: i32, v: &[f64]) -> Self { + let mut matrix = [0.; 16]; + matrix[..v.len()].copy_from_slice(v); + Self { + kind, + data: Data { matrix }, + } + } + fn string(v: &CStr) -> Self { + Self { + kind: 13, + data: Data { + array: Array { + data: v.as_ptr().cast(), + len: v.to_bytes().len(), + }, + }, + } + } +} + +type Create = + unsafe extern "C" fn(*mut u64, *const c_char, *const Accessor, usize, *const Options) -> c_long; +type Close = unsafe extern "C" fn(u64) -> c_long; +type Read = unsafe extern "C" fn(u64, *const c_char, *mut Value) -> c_long; +type Write = unsafe extern "C" fn(u64, *const c_char, *const Value) -> c_long; +struct Api { + _library: libloading::Library, + create: Create, + close: Close, + read: Read, + write: Write, +} +impl Api { + fn load() -> Result { + // Search only the Windows system directory, never a drawing's folder. + let library: libloading::Library = unsafe { + libloading::os::windows::Library::load_with_flags("TDxNavLib.dll", 0x00000800) + } + .map_err(|_| "Install or start 3DxWare, then reconnect your SpaceMouse.".to_owned())? + .into(); + unsafe { + Ok(Self { + create: *library.get(b"NlCreate\0").map_err(|e| e.to_string())?, + close: *library.get(b"NlClose\0").map_err(|e| e.to_string())?, + read: *library.get(b"NlReadValue\0").map_err(|e| e.to_string())?, + write: *library.get(b"NlWriteValue\0").map_err(|e| e.to_string())?, + _library: library, + }) + } + } + fn write(&self, handle: u64, name: &CStr, value: Value) -> c_long { + unsafe { (self.write)(handle, name.as_ptr(), &value) } + } +} + +const NO_DATA: c_long = 0x80040078_u32 as c_long; +const INVALID: c_long = 0x80040016_u32 as c_long; + +unsafe extern "C" fn get(param: u64, name: *const c_char, out: *mut Value) -> c_long { + if name.is_null() || out.is_null() { + return INVALID; + } + let name = unsafe { CStr::from_ptr(name) }.to_bytes(); + // NavLib asks for the coordinate system during NlCreate, before any + // document snapshot or window-focus event is available. + let constant = match name { + b"coordinateSystem" => Some(DMat4::from_rotation_x(-std::f64::consts::FRAC_PI_2)), + b"views.front" => Some(DMat4::from_rotation_x(std::f64::consts::FRAC_PI_2)), + _ => None, + }; + if let Some(matrix) = constant { + unsafe { + *out = Value::values(7, &matrix.to_cols_array()); + } + return 0; + } + // This Arc's allocation is retained by Connection until NlClose completes. + let shared = unsafe { &*(param as *const Mutex) }; + let Ok(bridge) = shared.lock() else { + return NO_DATA; + }; + let Some(v) = &bridge.view else { + return NO_DATA; + }; + let value = match name { + b"view.affine" => Value::values(7, &v.affine()), + b"view.extents" => Value::values(11, &v.extents()), + b"view.perspective" => Value::boolean(v.perspective), + b"view.rotatable" => Value::boolean(v.rotate), + b"view.fov" => Value::number(v.fov), + b"view.focusDistance" => Value::number(v.distance), + // This SDK property enables its separate target-camera model. OCS's + // camera.target is only a representation of a free camera's center; + // advertising it here prevents normal continuous camera navigation. + // https://forum.3dconnexion.com/viewtopic.php?t=53430 + b"view.target" => return NO_DATA, + b"view.frustum" => { + let near = v.distance * 0.001; + let h = near * (v.fov * 0.5).tan(); + Value::values( + 12, + &[-h * v.aspect, h * v.aspect, -h, h, near, v.distance * 1000.], + ) + } + b"model.extents" => Value::values(11, &v.model_bounds), + b"model.unitsToMeters" => Value::number(v.units_to_meters), + b"selection.empty" => Value::boolean(v.selection_bounds.is_none()), + b"selection.extents" => match v.selection_bounds { + Some(b) => Value::values(11, &b), + None => return NO_DATA, + }, + b"pivot.position" => Value::values(5, &v.pivot.to_array()), + b"pivot.user" => Value::boolean(v.selection_bounds.is_some()), + b"pivot.visible" => Value::boolean(v.pivot_visible), + b"pointer.position" => Value::values(5, &v.pointer.to_array()), + _ => return NO_DATA, + }; + unsafe { + *out = value; + } + 0 +} + +unsafe extern "C" fn set(param: u64, name: *const c_char, value: *const Value) -> c_long { + if name.is_null() || value.is_null() { + return INVALID; + } + let shared = unsafe { &*(param as *const Mutex) }; + let Ok(mut bridge) = shared.lock() else { + return NO_DATA; + }; + let name = unsafe { CStr::from_ptr(name) }.to_bytes(); + let value = unsafe { &*value }; + // Profile changes can arrive while the settings window owns focus and + // there is no drawing snapshot. Query the SDK later, outside this mutex. + if name == b"settings.changed" && value.kind == 2 { + bridge.driver_settings_changed = true; + log::debug!("SpaceMouse: driver settings revision {}", unsafe { + value.data.long + }); + if let Some(wake) = &bridge.control { + wake.signal(); + } + return 0; + } + if name == b"motion" && value.kind == 1 { + if bridge.view.as_ref().is_some_and(View::planar) { + return 0; + } + bridge.moving = unsafe { value.data.boolean } != 0; + bridge.wake(); + return 0; + } + if name == b"transaction" && value.kind == 2 { + if bridge.view.as_ref().is_some_and(View::planar) { + return 0; + } + bridge.in_transaction = unsafe { value.data.long } != 0; + if !bridge.in_transaction { + bridge.wake(); + } + return 0; + } + if !bridge.focus { + return NO_DATA; + } + if name == b"commands.activeCommand" { + if !matches!(value.kind, 8 | 13) { + return INVALID; + } + let array = unsafe { value.data.array }; + if array.data.is_null() || array.len > 1024 { + return INVALID; + } + let bytes = unsafe { std::slice::from_raw_parts(array.data, array.len) }; + let Ok(id) = std::str::from_utf8(bytes) else { + return INVALID; + }; + let id = id.trim_end_matches('\0'); + if bridge.commands.len() < 32 && bridge.actions.iter().any(|a| a.id == id) { + if let Some(target) = bridge.view.as_ref().map(|v| v.target.clone()) { + bridge.commands.push_back((target, id.to_owned())); + bridge.wake(); + } + } + return 0; + } + let lock_horizon = bridge.lock_horizon; + let orbit_about_pivot = bridge.orbit_about_pivot; + let Some(view) = &mut bridge.view else { + return NO_DATA; + }; + if !view.motion_enabled { + return NO_DATA; + } + // Planar navigation combines physical pushes and tilts before NavLib turns them + // into orbit/zoom camera changes. Buttons still use the SDK connection. + if view.planar() { + return 0; + } + let accepted = match (name, value.kind) { + (b"view.affine", 7) => { + let previous = view.rotation; + let accepted = view.set_affine(unsafe { value.data.matrix }); + if accepted && lock_horizon { + let proposed = view.rotation; + view.lock_horizon(previous, orbit_about_pivot); + if log::log_enabled!(log::Level::Trace) && (proposed * DVec3::Z).z.abs() > 0.99 { + log::trace!( + "SpaceMouse: pole previous={previous:?} driver={proposed:?} accepted={:?}", + view.rotation + ); + } + } + accepted + } + (b"view.extents", 11) => view.set_extents(unsafe { value.data.bounds }), + (b"pivot.position", 5) => { + let p = unsafe { value.data.point }; + let point = DVec3::new(p[0], p[1], p[2]); + if !point.is_finite() { + false + } else { + view.pivot = point; + true + } + } + (b"pivot.visible", 1) => { + view.pivot_visible = unsafe { value.data.boolean } != 0; + true + } + _ => return NO_DATA, + }; + if !accepted { + return INVALID; + } + bridge.pending_view = bridge.view.clone(); + if !bridge.in_transaction { + bridge.wake(); + } + 0 +} + +#[repr(C)] +struct Node { + size: u32, + kind: i32, + next: *const Node, + children: *const Node, + id: *const c_char, + label: *const c_char, + description: *const c_char, +} +#[repr(C)] +#[derive(Clone, Copy)] +struct ImageData { + data: *const u8, + size: usize, + index: u32, +} +#[repr(C)] +union ImageUnion { + image: ImageData, + padding: [usize; 4], +} +#[repr(C)] +struct Image { + size: u32, + kind: i32, + id: *const c_char, + data: ImageUnion, +} + +/// Heap storage survives the entire connection, including deferred SDK reads. +struct Commands { + _strings: Vec, + nodes: Box<[Node]>, + _pngs: Vec>, + images: Vec, +} +impl Commands { + fn new(actions: &[Action]) -> Self { + let mut strings = vec![ + CString::new("Default").unwrap(), + CString::new("Open CAD Studio").unwrap(), + CString::new("").unwrap(), + ]; + let mut nodes = vec![Node { + size: std::mem::size_of::() as u32, + kind: 0, + next: std::ptr::null(), + children: std::ptr::null(), + id: strings[0].as_ptr(), + label: strings[1].as_ptr(), + description: strings[2].as_ptr(), + }]; + let mut pngs = Vec::new(); + let mut images = Vec::new(); + for action in actions { + let start = strings.len(); + for text in [&action.id, &action.label, &action.description] { + strings.push(CString::new(text.replace('\0', "")).unwrap()); + } + nodes.push(Node { + size: std::mem::size_of::() as u32, + kind: 2, + next: std::ptr::null(), + children: std::ptr::null(), + id: strings[start].as_ptr(), + label: strings[start + 1].as_ptr(), + description: strings[start + 2].as_ptr(), + }); + if let Some(svg) = action.icon { + if let Some(png) = icon_png(svg) { + pngs.push(png); + let png = pngs.last().unwrap(); + images.push(Image { + size: std::mem::size_of::() as u32, + kind: 3, + id: strings[start].as_ptr(), + data: ImageUnion { + image: ImageData { + data: png.as_ptr(), + size: png.len(), + index: 0, + }, + }, + }); + } + } + } + let mut nodes = nodes.into_boxed_slice(); + for i in 1..nodes.len().saturating_sub(1) { + nodes[i].next = &nodes[i + 1]; + } + if nodes.len() > 1 { + nodes[0].children = &nodes[1]; + } + Self { + _strings: strings, + nodes, + _pngs: pngs, + images, + } + } +} +fn icon_png(svg: &[u8]) -> Option> { + let tree = resvg::usvg::Tree::from_data(svg, &resvg::usvg::Options::default()).ok()?; + let mut pixmap = resvg::tiny_skia::Pixmap::new(32, 32)?; + let scale = 32.0 / tree.size().width().max(tree.size().height()); + resvg::render( + &tree, + resvg::tiny_skia::Transform::from_scale(scale, scale), + &mut pixmap.as_mut(), + ); + pixmap.encode_png().ok() +} + +struct Connection { + api: Api, + handle: u64, + _accessors: Vec, + _commands: Commands, + _shared: Arc>, + policy: Option<(bool, bool, bool, bool)>, + target: Option, +} +impl Connection { + fn open(shared: Arc>) -> Result { + let api = Api::load()?; + let param = Arc::as_ptr(&shared) as u64; + let reads: &[&CStr] = &[ + c"coordinateSystem", + c"view.affine", + c"view.extents", + c"view.perspective", + c"view.rotatable", + c"view.fov", + c"view.focusDistance", + c"view.target", + c"view.frustum", + c"views.front", + c"model.extents", + c"model.unitsToMeters", + c"selection.empty", + c"selection.extents", + c"pivot.position", + c"pivot.user", + c"pivot.visible", + c"pointer.position", + ]; + let writes: &[&CStr] = &[ + c"view.affine", + c"view.extents", + c"pivot.position", + c"pivot.visible", + ]; + let mut accessors: Vec = reads + .iter() + .map(|name| Accessor { + name: name.as_ptr(), + get: Some(get), + set: writes.contains(name).then_some(set as Set), + param, + }) + .collect(); + for name in [ + c"commands.activeCommand", + c"transaction", + c"motion", + c"settings.changed", + ] { + accessors.push(Accessor { + name: name.as_ptr(), + get: None, + set: Some(set), + param, + }); + } + let options = Options { + size: std::mem::size_of::() as u32, + multithreaded: 1, + flags: 4, + }; + let commands = Commands::new(&shared.lock().unwrap().actions); + let mut handle = 0; + let result = unsafe { + (api.create)( + &mut handle, + c"Open CAD Studio".as_ptr(), + accessors.as_ptr(), + accessors.len(), + &options, + ) + }; + if result != 0 { + return Err(format!( + "3DxWare connection failed ({result:#x}). Start or update 3DxWare." + )); + } + let connection = Self { + api, + handle, + _accessors: accessors, + _commands: commands, + _shared: shared, + policy: None, + target: None, + }; + connection.refresh_settings(); + // Iced requests animation frames only while motion is active and + // supplies their monotonic timestamps. A held puck must keep moving + // even when it sends no new axis values. + connection.write_checked( + c"frame.timingSource", + Value { + kind: 2, + data: Data { long: 1 }, + }, + )?; + connection.write_checked( + c"commands.tree", + Value { + kind: 9, + data: Data { + ptr: connection._commands.nodes.as_ptr(), + }, + }, + )?; + connection.write_checked(c"commands.activeSet", Value::string(c"Default"))?; + if !connection._commands.images.is_empty() { + connection.write_checked( + c"images", + Value { + kind: 14, + data: Data { + array: Array { + data: connection._commands.images.as_ptr().cast(), + len: connection._commands.images.len(), + }, + }, + }, + )?; + } + Ok(connection) + } + fn write_checked(&self, name: &CStr, value: Value) -> Result<(), String> { + let code = self.api.write(self.handle, name, value); + if code == 0 { + Ok(()) + } else { + Err(format!( + "3DxWare rejected {} ({code:#x}). Update 3DxWare.", + name.to_string_lossy() + )) + } + } + fn present(&self) -> Result { + let mut value = Value::boolean(false); + let result = + unsafe { (self.api.read)(self.handle, c"device.present".as_ptr(), &mut value) }; + if result == 0 && value.kind == 1 { + Ok(unsafe { value.data.boolean } != 0) + } else { + Err(result) + } + } + fn string_setting(&self, name: &CStr) -> Option { + // settings.* properties use caller-owned UTF-8 string storage. + let mut buffer = [0_u8; 32]; + let mut value = Value { + kind: 8, + data: Data { + array: Array { + data: buffer.as_mut_ptr(), + len: buffer.len(), + }, + }, + }; + let result = unsafe { (self.api.read)(self.handle, name.as_ptr(), &mut value) }; + if result != 0 { + return None; + } + let end = buffer.iter().position(|b| *b == 0).unwrap_or(buffer.len()); + Some(std::str::from_utf8(&buffer[..end]).ok()?.trim().to_owned()) + } + fn bool_setting(&self, name: &CStr) -> Option { + match self.string_setting(name)?.to_ascii_lowercase().as_str() { + "true" | "1" => Some(true), + "false" | "0" => Some(false), + _ => None, + } + } + fn refresh_settings(&self) { + let value = self.bool_setting(c"settings.LockHorizon"); + let model = self.string_setting(c"settings.MotionModel"); + log::debug!("SpaceMouse: driver Lock Horizon = {value:?}, motion model = {model:?}"); + let mut bridge = self._shared.lock().unwrap(); + if let Some(enabled) = value { + bridge.lock_horizon = enabled; + } + if let Some(model) = model { + // Object mode rotates about the SDK pivot; Camera/Helicopter modes + // keep their proposed eye when pitch is constrained. + bridge.orbit_about_pivot = model == "Examine"; + } + } + fn sync(&mut self) { + let (focus, frame_time, reset_motion, settings_changed, view) = { + let mut bridge = self._shared.lock().unwrap(); + ( + bridge.focus, + bridge.frame_time.take(), + std::mem::take(&mut bridge.reset_motion), + std::mem::take(&mut bridge.driver_settings_changed), + bridge.view.clone(), + ) + }; + let policy = ( + focus, + view.as_ref().is_some_and(|v| v.rotate), + view.as_ref().is_some_and(|v| v.zoom), + view.as_ref().is_some_and(|v| v.motion_enabled), + ); + let context_changed = match (&self.target, &view) { + (Some(old), Some(view)) => !old.same_context(&view.target), + (None, None) => false, + _ => true, + }; + if settings_changed || (focus && self.policy.is_none_or(|p| !p.0)) { + self.refresh_settings(); + } + if self.policy != Some(policy) || context_changed || reset_motion { + self.api + .write(self.handle, c"motion", Value::boolean(false)); + // Native 3DxWare selects the foreground process itself. `focus` + // is for clients hosted through NLServer; releasing it when the + // settings UI takes focus can select another application's profile. + self.api + .write(self.handle, c"active", Value::boolean(view.is_some())); + self.api + .write(self.handle, c"view.rotatable", Value::boolean(policy.1)); + self.policy = Some(policy); + } + if let Some(view) = view { + self.target = Some(view.target); + } else { + self.target = None; + } + // Do not echo driver-originated camera changes back through Write: + // those notifications reset NavLib's current navigation model. + if focus { + if let Some(time) = frame_time { + self.api + .write(self.handle, c"frame.time", Value::number(time)); + } + } + } +} +impl Drop for Connection { + fn drop(&mut self) { + self.api + .write(self.handle, c"active", Value::boolean(false)); + unsafe { + (self.api.close)(self.handle); + } + log::debug!("SpaceMouse: navigation connection closed"); + } +} + +struct Stop( + Arc, + super::worker::Wake, + Option>, +); +impl Drop for Stop { + fn drop(&mut self) { + self.0.store(true, Ordering::Release); + self.1.signal(); + // NlClose persists the driver's per-application settings. Do not leave + // a detached worker racing process exit after the subscription drops. + if let Some(thread) = self.2.take() { + let _ = thread.join(); + } + } +} + +pub(super) fn subscription(service: &Service) -> impl Stream + use<> { + let shared = service.0.clone(); + iced::stream::channel(1, move |output: Sender<()>| async move { + let wake = match super::worker::Wake::new() { + Ok(wake) => wake, + Err(error) => { + publish_status( + &shared, + Status::Unavailable(format!("SpaceMouse worker: {error}")), + ); + return; + } + }; + let stop = Arc::new(AtomicBool::new(false)); + let mut stop_guard = Stop(stop.clone(), wake.clone(), None); + let _raw_input = super::raw_input::Listener::start(shared.clone()); + { + let mut bridge = shared.lock().unwrap(); + bridge.wake = Some(output); + bridge.control = Some(wake.clone()); + } + stop_guard.2 = Some( + std::thread::Builder::new() + .name("spacemouse".into()) + .spawn(move || { + while !stop.load(Ordering::Acquire) { + match Connection::open(shared.clone()) { + Ok(mut connection) => { + let mut last_check = None; + loop { + if stop.load(Ordering::Acquire) { + break; + } + connection.sync(); + if last_check.is_none_or(|t: std::time::Instant| { + t.elapsed() >= Duration::from_secs(1) + }) { + last_check = Some(std::time::Instant::now()); + let status = match connection.present() { + Ok(true) => match super::raw_input::connected() { + Ok(true) => Status::Ready, + Ok(false) => Status::Disconnected, + // Device enumeration can race hotplug. Keep the SDK's + // answer until the next hardware poll in that case. + Err(_) => Status::Ready, + }, + Ok(false) => Status::Disconnected, + Err(code) => Status::Unavailable(format!( + "3DxWare stopped responding ({code:#x})." + )), + }; + let failed = matches!(status, Status::Unavailable(_)); + publish_status(&shared, status); + if failed { + break; + } + } + wake.wait(Duration::from_secs(1)); + } + } + Err(reason) => publish_status(&shared, Status::Unavailable(reason)), + } + if !stop.load(Ordering::Acquire) { + wake.wait(Duration::from_secs(5)); + } + } + }) + .expect("SpaceMouse worker thread"), + ); + std::future::pending::<()>().await; + }) +} +fn publish_status(shared: &Mutex, status: Status) { + let mut bridge = shared.lock().unwrap(); + bridge.ever_connected |= status == Status::Ready; + if bridge.status.as_ref() != Some(&status) { + if status != Status::Ready { + bridge.pending_view = None; + bridge.commands.clear(); + bridge.moving = false; + bridge.in_transaction = false; + bridge.pan.clear(); + } + bridge.status = Some(status); + bridge.wake(); + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn sdk_abi_layout() { + assert_eq!(std::mem::size_of::(), 136); + assert_eq!(std::mem::offset_of!(Value, data), 8); + assert_eq!(std::mem::size_of::(), 12); + assert_eq!(std::mem::offset_of!(Options, flags), 8); + if cfg!(target_pointer_width = "64") { + assert_eq!(std::mem::size_of::(), 32); + assert_eq!(std::mem::size_of::(), 48); + assert_eq!(std::mem::size_of::(), 48); + } + } + #[test] + #[ignore = "requires an installed, running 3DxWare driver"] + fn installed_driver_connects_and_reports_device_presence() { + let shared = Arc::new(Mutex::new(Bridge { + actions: vec![Action { + id: "UNDO".into(), + label: "Undo".into(), + description: "Undo last step".into(), + icon: Some(crate::ui::window::options::spacemouse::ICON), + }], + ..Bridge::default() + })); + let connection = Connection::open(shared).unwrap(); + eprintln!( + "3DxWare connected; device present: {}", + connection.present().unwrap() + ); + let horizon = connection.bool_setting(c"settings.LockHorizon"); + eprintln!("3DxWare Lock Horizon: {horizon:?}"); + assert!( + horizon.is_some(), + "read Lock Horizon through the installed SDK" + ); + assert!(connection.string_setting(c"settings.MotionModel").is_some()); + } + #[test] + fn settings_changes_are_received_while_the_driver_dialog_has_focus() { + let service = Service::default(); + let revision = Value { + kind: 2, + data: Data { long: 1 }, + }; + assert_eq!( + unsafe { + set( + Arc::as_ptr(&service.0) as u64, + c"settings.changed".as_ptr(), + &revision, + ) + }, + 0 + ); + assert!(service.0.lock().unwrap().driver_settings_changed); + } + #[test] + fn orbit_respects_the_driver_horizon_setting_and_unlock_restores_roll() { + let service = Service::default(); + let mut view = super::super::tests::view(); + view.rotate = true; + view.zoom = true; + service.sync(Some(view.clone()), true); + let param = Arc::as_ptr(&service.0) as u64; + for locked in [false, true, false] { + service.0.lock().unwrap().lock_horizon = locked; + for pitch in [0.4, 1.1, 0., std::f64::consts::PI] { + let rotation = DQuat::from_rotation_z(0.6) + * DQuat::from_rotation_x(pitch) + * DQuat::from_rotation_z(0.3); + let matrix = DMat4::from_rotation_translation(rotation, view.eye); + assert_eq!( + unsafe { + set( + param, + c"view.affine".as_ptr(), + &Value::values(7, &matrix.to_cols_array()), + ) + }, + 0 + ); + let result = service.drain().0.unwrap(); + assert!(result.rotation.is_finite()); + let at_pole = pitch == 0. || pitch == std::f64::consts::PI; + if locked && at_pole { + assert!((result.rotation * DVec3::Z).abs_diff_eq(rotation * DVec3::Z, 1.1e-4)); + assert!( + (result.eye.distance(view.pivot) - view.eye.distance(view.pivot)).abs() + < 1e-10 + ); + } else { + assert_eq!(result.eye, view.eye); + assert!((result.rotation * DVec3::Z).abs_diff_eq(rotation * DVec3::Z, 1e-10)); + } + if locked { + assert!((result.rotation * DVec3::X).z.abs() < 1e-10); + } else { + assert!(result.rotation.abs_diff_eq(rotation, 1e-10)); + } + } + } + } + #[test] + fn horizon_locked_orbit_stops_at_both_poles_and_reverses_without_reflection() { + for perspective in [false, true] { + for bottom in [false, true] { + for yaw in [0., 0.7, -2.4] { + let service = Service::default(); + let mut view = super::super::tests::view(); + view.rotate = true; + view.zoom = true; + view.perspective = perspective; + view.pivot = DVec3::new(13., -27., 5.); + let pole = if bottom { std::f64::consts::PI } else { 0. }; + let inward = if bottom { -1. } else { 1. }; + let heading = DQuat::from_rotation_z(yaw); + view.rotation = heading * DQuat::from_rotation_x(pole + inward * 0.03); + view.eye = view.pivot + view.rotation * DVec3::Z * view.distance; + service.sync(Some(view.clone()), true); + service.0.lock().unwrap().lock_horizon = true; + service.0.lock().unwrap().orbit_about_pivot = true; + let param = Arc::as_ptr(&service.0) as u64; + // Approach, cross, hold beyond, then reverse away from each + // pole, including the app camera's f32 quaternion precision. + for offset in [0.02, 0.01, 0., -0.01, -0.03, -0.03, -0.03, 0.01, 0.02] { + let proposed = heading * DQuat::from_rotation_x(pole + inward * offset); + let eye = view.pivot + proposed * DVec3::Z * view.distance; + let matrix = DMat4::from_rotation_translation(proposed, eye); + assert_eq!( + unsafe { + set( + param, + c"view.affine".as_ptr(), + &Value::values(7, &matrix.to_cols_array()), + ) + }, + 0 + ); + let mut result = service.drain().0.unwrap(); + result.rotation = result.rotation.as_quat().as_dquat().normalize(); + let right = result.rotation * DVec3::X; + let up = result.rotation * DVec3::Y; + let normal = result.rotation * DVec3::Z; + assert!( + right.dot(heading * DVec3::X) > 0.99999, + "screen-right flipped" + ); + assert!(right.z.abs() < 1e-7, "horizon rolled"); + assert!(up.z >= 0., "camera passed the pole"); + assert!( + (result.eye - normal * result.distance).distance(view.pivot) < 1e-5, + "orbit pivot moved on screen" + ); + if offset > 0. { + assert!( + normal.abs_diff_eq(proposed * DVec3::Z, 1e-7), + "cannot reverse away from pole" + ); + } else { + assert!(normal.z.abs() > 0.9999999, "pitch should stop at the pole"); + } + service.sync(Some(result), true); + } + } + } + } + } + #[test] + fn horizon_locked_camera_mode_preserves_the_eye_at_a_pole() { + let service = Service::default(); + let mut view = super::super::tests::view(); + view.rotate = true; + view.zoom = true; + view.perspective = true; + view.rotation = DQuat::from_rotation_x(0.01); + service.sync(Some(view.clone()), true); + service.0.lock().unwrap().lock_horizon = true; + let param = Arc::as_ptr(&service.0) as u64; + for tilt in [-0.01, -0.02, 0.01] { + let rotation = DQuat::from_rotation_x(tilt); + let eye = view.eye + DVec3::new(1., 2., 3.); + let matrix = DMat4::from_rotation_translation(rotation, eye); + assert_eq!( + unsafe { + set( + param, + c"view.affine".as_ptr(), + &Value::values(7, &matrix.to_cols_array()), + ) + }, + 0 + ); + let result = service.drain().0.unwrap(); + assert_eq!( + result.eye, eye, + "pitch limits must preserve camera-mode translation" + ); + assert!((result.rotation * DVec3::Y).z > 0.); + } + } + #[test] + fn horizon_lock_allows_yaw_around_the_full_circle() { + let service = Service::default(); + let mut view = super::super::tests::view(); + view.rotate = true; + view.zoom = true; + view.rotation = DQuat::from_rotation_x(0.01); + service.sync(Some(view.clone()), true); + service.0.lock().unwrap().lock_horizon = true; + let param = Arc::as_ptr(&service.0) as u64; + // A large yaw change is not a pitch through the pole either. + for yaw in (0..=72).map(|i| i as f64 * 0.1).chain([10., 13.]) { + let rotation = DQuat::from_rotation_z(yaw) * DQuat::from_rotation_x(0.01); + let matrix = DMat4::from_rotation_translation(rotation, view.eye); + assert_eq!( + unsafe { + set( + param, + c"view.affine".as_ptr(), + &Value::values(7, &matrix.to_cols_array()), + ) + }, + 0 + ); + let result = service.drain().0.unwrap(); + assert!(result.rotation.dot(rotation).abs() > 1. - 1e-10); + assert_eq!(result.eye, view.eye); + } + } + #[test] + fn sdk_motion_cannot_double_apply_planar_push_tilt_or_zoom() { + for zoom in [false, true] { + let service = Service::default(); + let mut view = super::super::tests::view(); + view.zoom = zoom; + service.sync(Some(view), true); + let param = Arc::as_ptr(&service.0) as u64; + for (name, value) in [ + (c"motion", Value::boolean(true)), + ( + c"view.affine", + Value::values(7, &DMat4::IDENTITY.to_cols_array()), + ), + ( + c"view.extents", + Value::values(11, &[-2., -2., -2., 2., 2., 2.]), + ), + ] { + assert_eq!(unsafe { set(param, name.as_ptr(), &value) }, 0); + } + assert!(!service.moving()); + assert!(service.drain().0.is_none()); + } + } + #[test] + fn coordinate_system_is_available_before_a_drawing_exists() { + let bridge = Mutex::new(Bridge::default()); + let mut value = Value::boolean(false); + assert_eq!( + unsafe { + get( + &bridge as *const _ as u64, + c"coordinateSystem".as_ptr(), + &mut value, + ) + }, + 0 + ); + let matrix = DMat4::from_cols_array(&unsafe { value.data.matrix }); + assert!((matrix.transform_vector3(DVec3::Z) - DVec3::Y).length() < 1e-10); + } + #[test] + fn free_camera_does_not_advertise_the_sdk_target_camera_model() { + let service = Service::default(); + service.sync(Some(super::super::tests::view()), true); + let mut value = Value::values(5, &[0., 0., 0.]); + assert_eq!( + unsafe { + get( + Arc::as_ptr(&service.0) as u64, + c"view.target".as_ptr(), + &mut value, + ) + }, + NO_DATA + ); + } + #[test] + fn paused_motion_is_rejected_but_resume_action_is_queued() { + let service = Service::default(); + let mut view = super::super::tests::view(); + view.motion_enabled = false; + service.sync(Some(view), true); + service.set_actions(vec![Action { + id: "SPACEMOUSEPAUSE".into(), + label: "Resume".into(), + description: String::new(), + icon: None, + }]); + let param = Arc::as_ptr(&service.0) as u64; + assert_eq!( + unsafe { + set( + param, + c"view.affine".as_ptr(), + &Value::values(7, &DMat4::IDENTITY.to_cols_array()), + ) + }, + NO_DATA + ); + assert_eq!( + unsafe { + set( + param, + c"commands.activeCommand".as_ptr(), + &Value::string(c"SPACEMOUSEPAUSE"), + ) + }, + 0 + ); + let (view, commands) = service.drain(); + assert!(view.is_none()); + assert_eq!(commands[0].1, "SPACEMOUSEPAUSE"); + service.sync(None, false); + assert_eq!( + unsafe { + set( + param, + c"commands.activeCommand".as_ptr(), + &Value::string(c"SPACEMOUSEPAUSE"), + ) + }, + NO_DATA + ); + assert!(service.drain().1.is_empty()); + } +} diff --git a/src/input/spacemouse/pan.rs b/src/input/spacemouse/pan.rs new file mode 100644 index 000000000..9fc435aaa --- /dev/null +++ b/src/input/spacemouse/pan.rs @@ -0,0 +1,138 @@ +//! Planar input in device space: pushes and tilts have the same meaning. +//! The SDK's camera matrices have already combined orbit, pan, and zoom, so +//! they cannot reliably recover these independent physical inputs. +use glam::{DVec2, DVec3}; +use std::time::{Duration, Instant}; + +#[derive(Default)] +pub(super) struct Pan { + axes: [i16; 6], + received: Option, + frame_time: Option, +} +impl Pan { + pub fn clear(&mut self) { + *self = Self::default(); + } + + /// SpaceMouse USB uses split translation/rotation reports; Wireless also + /// sends a combined report. Ignore buttons, battery, and truncated reports. + pub fn report(&mut self, report: &[u8]) -> bool { + let (start, count) = match (report.first(), report.len()) { + (Some(1), 13..) => (0, 6), + (Some(1), 7) => (0, 3), + (Some(2), 7..) => (3, 3), + _ => return false, + }; + for i in 0..count { + self.axes[start + i] = i16::from_le_bytes([report[1 + i * 2], report[2 + i * 2]]); + } + self.received = Some(Instant::now()); + if !self.moving(true) { + self.frame_time = None; + } + true + } + pub fn moving(&self, zoom: bool) -> bool { + direction(self.axes) != DVec2::ZERO || (zoom && response(self.axes[2] as f64) != 0.) + } + + pub fn frame(&mut self, milliseconds: f64, zoom: bool) -> DVec3 { + if !milliseconds.is_finite() + || self + .received + .is_none_or(|t| t.elapsed() > Duration::from_millis(350)) + { + self.clear(); + return DVec3::ZERO; + } + let dt = self.frame_time.map_or(1. / 60., |old| { + ((milliseconds - old) / 1000.).clamp(0., 0.05) + }); + self.frame_time = Some(milliseconds); + direction(self.axes).extend(if zoom { + response(self.axes[2] as f64) + } else { + 0. + }) * dt + } +} + +fn response(v: f64) -> f64 { + let n = (v.abs() / 350.).min(1.); + if n <= 0.08 { + 0. + } else { + v.signum() * ((n - 0.08) / 0.92).powf(1.5) + } +} + +fn direction(axes: [i16; 6]) -> DVec2 { + fn strongest(push: f64, tilt: f64) -> f64 { + // A hand often pushes and tilts together. Do not double the speed. + if push.abs() >= tilt.abs() { + push + } else { + tilt + } + } + let right = strongest(response(axes[0] as f64), response(-(axes[4] as f64))); + let up = strongest(response(-(axes[1] as f64)), response(-(axes[3] as f64))); + DVec2::new(right, up).clamp_length_max(1.) +} + +#[cfg(test)] +mod tests { + use super::*; + fn report(axes: [i16; 6]) -> Vec { + std::iter::once(1) + .chain(axes.into_iter().flat_map(i16::to_le_bytes)) + .collect() + } + #[test] + fn pushes_and_tilts_pan_in_the_same_screen_directions() { + for (push, tilt, expected) in [ + ([350, 0, 0, 0, 0, 0], [0, 0, 0, 0, -350, 0], DVec2::X), + ([-350, 0, 0, 0, 0, 0], [0, 0, 0, 0, 350, 0], -DVec2::X), + ([0, -350, 0, 0, 0, 0], [0, 0, 0, -350, 0, 0], DVec2::Y), + ([0, 350, 0, 0, 0, 0], [0, 0, 0, 350, 0, 0], -DVec2::Y), + ] { + assert_eq!(direction(push), expected); + assert_eq!(direction(tilt), expected); + } + assert_eq!(direction([350, 0, 0, 0, -350, 0]), DVec2::X); + } + #[test] + fn twist_vertical_pressure_and_sensor_noise_do_not_pan() { + assert_eq!(direction([0, 0, 350, 0, 0, -350]), DVec2::ZERO); + assert_eq!(direction([10, -10, 350, 20, -20, 350]), DVec2::ZERO); + } + #[test] + fn held_input_continues_and_release_or_disconnect_stops_it() { + let mut pan = Pan::default(); + assert!(pan.report(&report([350, 0, 0, 0, 0, 0]))); + for frame in 0..20 { + assert!(pan.frame(frame as f64 * 16., false).x > 0.); + } + assert!(pan.report(&report([0; 6]))); + assert!(!pan.moving(false)); + assert_eq!(pan.frame(400., false), DVec3::ZERO); + pan.report(&report([350, 0, 0, 0, 0, 0])); + pan.received = Some(Instant::now() - Duration::from_secs(1)); + assert_eq!(pan.frame(500., false), DVec3::ZERO); + assert!(!pan.moving(false)); + } + #[test] + fn split_and_combined_reports_agree_and_other_reports_are_ignored() { + let mut pan = Pan::default(); + let full = report([1, 2, 3, 4, 5, 6]); + assert!(pan.report(&full[..7])); + let mut rotation = vec![2]; + rotation.extend_from_slice(&full[7..]); + assert!(pan.report(&rotation)); + assert_eq!(pan.axes, [1, 2, 3, 4, 5, 6]); + assert!(!pan.report(&[1, 2, 3])); + assert!(!pan.report(&[23; 13])); + assert_eq!(pan.axes, [1, 2, 3, 4, 5, 6]); + } +} diff --git a/src/input/spacemouse/raw_input.rs b/src/input/spacemouse/raw_input.rs new file mode 100644 index 000000000..0947e52ed --- /dev/null +++ b/src/input/spacemouse/raw_input.rs @@ -0,0 +1,235 @@ +//! Windows multi-axis HID receiver for push-or-tilt planar navigation. +//! Registers only Generic Desktop / Multi-axis Controller, never a keyboard +//! or ordinary mouse. The driver continues to own buttons and 3D navigation. +use super::*; +use std::sync::atomic::{AtomicIsize, Ordering}; +use windows_sys::Win32::{ + Foundation::{HANDLE, HWND, LPARAM, LRESULT, WPARAM}, + System::LibraryLoader::GetModuleHandleW, + UI::{Input::*, WindowsAndMessaging::*}, +}; + +pub(super) struct Listener { + window: Arc, + thread: Option>, +} +impl Listener { + pub fn start(shared: Arc>) -> Self { + let window = Arc::new(AtomicIsize::new(0)); + let worker_window = window.clone(); + let thread = std::thread::Builder::new() + .name("spacemouse-pan".into()) + .spawn(move || { + if let Err(error) = run(shared, &worker_window) { + eprintln!("SpaceMouse pan input: {error}"); + } + }) + .ok(); + Self { window, thread } + } +} +impl Drop for Listener { + fn drop(&mut self) { + let hwnd = self.window.swap(-1, Ordering::AcqRel); + if hwnd > 0 { + unsafe { + PostMessageW(hwnd as HWND, WM_CLOSE, 0, 0); + } + } + if let Some(thread) = self.thread.take() { + let _ = thread.join(); + } + } +} + +fn run(shared: Arc>, window: &AtomicIsize) -> Result<(), String> { + let class: Vec = "OpenCADStudio.SpaceMouse.Pan\0".encode_utf16().collect(); + unsafe { + let instance = GetModuleHandleW(std::ptr::null()); + let definition = WNDCLASSW { + lpfnWndProc: Some(procedure), + hInstance: instance, + lpszClassName: class.as_ptr(), + ..Default::default() + }; + // Another subscription may have registered the class earlier. + if RegisterClassW(&definition) == 0 { + let mut existing = WNDCLASSW::default(); + if GetClassInfoW(instance, class.as_ptr(), &mut existing) == 0 { + return Err(std::io::Error::last_os_error().to_string()); + } + } + let hwnd = CreateWindowExW( + 0, + class.as_ptr(), + std::ptr::null(), + 0, + 0, + 0, + 0, + 0, + HWND_MESSAGE, + std::ptr::null_mut(), + instance, + std::ptr::null(), + ); + if hwnd.is_null() { + return Err(std::io::Error::last_os_error().to_string()); + } + SetWindowLongPtrW(hwnd, GWLP_USERDATA, Arc::as_ptr(&shared) as isize); + if window + .compare_exchange(0, hwnd as isize, Ordering::AcqRel, Ordering::Acquire) + .is_err() + { + DestroyWindow(hwnd); + return Ok(()); + } + let device = RAWINPUTDEVICE { + usUsagePage: 1, + usUsage: 8, + dwFlags: RIDEV_INPUTSINK | RIDEV_DEVNOTIFY, + hwndTarget: hwnd, + }; + if RegisterRawInputDevices(&device, 1, std::mem::size_of::() as u32) == 0 { + window.store(0, Ordering::Release); + DestroyWindow(hwnd); + return Err(std::io::Error::last_os_error().to_string()); + } + let mut message = MSG::default(); + while GetMessageW(&mut message, std::ptr::null_mut(), 0, 0) > 0 { + TranslateMessage(&message); + DispatchMessageW(&message); + } + let remove = RAWINPUTDEVICE { + dwFlags: RIDEV_REMOVE, + hwndTarget: std::ptr::null_mut(), + ..device + }; + RegisterRawInputDevices(&remove, 1, std::mem::size_of::() as u32); + window.store(0, Ordering::Release); + } + Ok(()) +} + +unsafe extern "system" fn procedure(hwnd: HWND, message: u32, wp: WPARAM, lp: LPARAM) -> LRESULT { + if message == WM_DESTROY { + unsafe { + PostQuitMessage(0); + } + return 0; + } + let pointer = unsafe { GetWindowLongPtrW(hwnd, GWLP_USERDATA) } as *const Mutex; + if !pointer.is_null() { + let shared = unsafe { &*pointer }; + if message == WM_INPUT { + input(shared, lp as HRAWINPUT); + } else if message == WM_INPUT_DEVICE_CHANGE && wp == GIDC_REMOVAL as usize { + if let Ok(mut bridge) = shared.lock() { + bridge.pan.clear(); + bridge.wake(); + } + } + } + unsafe { DefWindowProcW(hwnd, message, wp, lp) } +} + +fn is_spacemouse(device: HANDLE) -> bool { + let mut info = RID_DEVICE_INFO { + cbSize: std::mem::size_of::() as u32, + ..Default::default() + }; + let mut size = info.cbSize; + unsafe { + if GetRawInputDeviceInfoW( + device, + RIDI_DEVICEINFO, + (&mut info as *mut RID_DEVICE_INFO).cast(), + &mut size, + ) == u32::MAX + || info.dwType != RIM_TYPEHID + { + return false; + } + let hid = info.Anonymous.hid; + // Logitech's legacy SpaceMouse IDs and current 3Dconnexion vendor ID. + hid.usUsagePage == 1 + && hid.usUsage == 8 + && (hid.dwVendorId == 0x256f + || (hid.dwVendorId == 0x046d && (0xc603..=0xc631).contains(&hid.dwProductId))) + } +} + +/// Older NavLib versions report device.present as always true. Enumerate the +/// registered multi-axis HID devices as well, so availability reflects hardware. +pub(super) fn connected() -> Result { + let element_size = std::mem::size_of::() as u32; + let mut count = 0; + unsafe { + if GetRawInputDeviceList(std::ptr::null_mut(), &mut count, element_size) == u32::MAX { + return Err(std::io::Error::last_os_error().to_string()); + } + let mut devices = vec![RAWINPUTDEVICELIST::default(); count as usize]; + let read = GetRawInputDeviceList(devices.as_mut_ptr(), &mut count, element_size); + if read == u32::MAX { + return Err(std::io::Error::last_os_error().to_string()); + } + Ok(devices + .iter() + .take(read as usize) + .any(|device| device.dwType == RIM_TYPEHID && is_spacemouse(device.hDevice))) + } +} + +fn input(shared: &Mutex, handle: HRAWINPUT) { + // u64 storage provides the alignment required by RAWINPUTHEADER. + let mut buffer = [0_u64; 128]; + let mut size = std::mem::size_of_val(&buffer) as u32; + let header_size = std::mem::size_of::(); + let read = unsafe { + GetRawInputData( + handle, + RID_INPUT, + buffer.as_mut_ptr().cast(), + &mut size, + header_size as u32, + ) + }; + if read == u32::MAX + || read < (header_size + 8) as u32 + || read as usize > std::mem::size_of_val(&buffer) + { + return; + } + let header = unsafe { &*buffer.as_ptr().cast::() }; + if header.dwType != RIM_TYPEHID || !is_spacemouse(header.hDevice) { + return; + } + let bytes = unsafe { std::slice::from_raw_parts(buffer.as_ptr().cast::(), read as usize) }; + let width = + u32::from_le_bytes(bytes[header_size..header_size + 4].try_into().unwrap()) as usize; + let count = + u32::from_le_bytes(bytes[header_size + 4..header_size + 8].try_into().unwrap()) as usize; + if width == 0 || count > (bytes.len() - header_size - 8) / width { + return; + } + let Ok(mut bridge) = shared.lock() else { + return; + }; + if !bridge.focus + || !bridge + .view + .as_ref() + .is_some_and(|v| v.planar() && v.motion_enabled) + { + return; + } + let zoom = bridge.view.as_ref().unwrap().zoom; + let was_moving = bridge.pan.moving(zoom); + let mut accepted = false; + for report in bytes[header_size + 8..].chunks_exact(width).take(count) { + accepted |= bridge.pan.report(report); + } + if accepted && (was_moving || bridge.pan.moving(zoom)) { + bridge.wake(); + } +} diff --git a/src/input/spacemouse/worker.rs b/src/input/spacemouse/worker.rs new file mode 100644 index 000000000..653b0a0fc --- /dev/null +++ b/src/input/spacemouse/worker.rs @@ -0,0 +1,122 @@ +//! Wait for app work and dispatch messages to windows owned by the SDK thread. +//! A channel-only wait leaves Windows-delivered driver settings unprocessed. +use std::{sync::Arc, time::Duration}; +use windows_sys::Win32::{ + Foundation::{CloseHandle, HANDLE}, + System::Threading::{CreateEventW, SetEvent}, + UI::WindowsAndMessaging::*, +}; + +struct Event(isize); +impl Drop for Event { + fn drop(&mut self) { + unsafe { + CloseHandle(self.0 as HANDLE); + } + } +} + +#[derive(Clone)] +pub(super) struct Wake(Arc); +impl Wake { + pub fn new() -> Result { + let event = unsafe { CreateEventW(std::ptr::null(), 0, 0, std::ptr::null()) }; + if event.is_null() { + Err(std::io::Error::last_os_error()) + } else { + Ok(Self(Arc::new(Event(event as isize)))) + } + } + pub fn signal(&self) { + unsafe { + SetEvent(self.0 .0 as HANDLE); + } + } + pub fn wait(&self, timeout: Duration) { + let handle = self.0 .0 as HANDLE; + unsafe { + MsgWaitForMultipleObjectsEx( + 1, + &handle, + timeout.as_millis().min(u32::MAX as u128) as u32, + QS_ALLINPUT, + MWMO_INPUTAVAILABLE, + ); + let mut message = MSG::default(); + // Bound each batch so an SDK message burst cannot starve shutdown + // or app frames. The next wait returns immediately if more remain. + for _ in 0..256 { + if PeekMessageW(&mut message, std::ptr::null_mut(), 0, 0, PM_REMOVE) == 0 { + break; + } + TranslateMessage(&message); + DispatchMessageW(&message); + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::sync::atomic::{AtomicUsize, Ordering}; + use windows_sys::Win32::{Foundation::*, System::LibraryLoader::GetModuleHandleW}; + + static DELIVERED: AtomicUsize = AtomicUsize::new(0); + unsafe extern "system" fn procedure( + window: HWND, + message: u32, + wp: WPARAM, + lp: LPARAM, + ) -> LRESULT { + if message == WM_APP { + DELIVERED.fetch_add(1, Ordering::SeqCst); + return 0; + } + unsafe { DefWindowProcW(window, message, wp, lp) } + } + #[test] + fn sdk_worker_dispatches_window_messages_and_app_wakes() { + std::thread::spawn(|| unsafe { + let wake = Wake::new().unwrap(); + let class: Vec = "OpenCADStudio.SpaceMouse.Worker.Test\0" + .encode_utf16() + .collect(); + let instance = GetModuleHandleW(std::ptr::null()); + let definition = WNDCLASSW { + lpfnWndProc: Some(procedure), + hInstance: instance, + lpszClassName: class.as_ptr(), + ..Default::default() + }; + assert_ne!(RegisterClassW(&definition), 0); + let window = CreateWindowExW( + 0, + class.as_ptr(), + std::ptr::null(), + 0, + 0, + 0, + 0, + 0, + HWND_MESSAGE, + std::ptr::null_mut(), + instance, + std::ptr::null(), + ); + assert!(!window.is_null()); + assert_ne!(PostMessageW(window, WM_APP, 0, 0), 0); + wake.wait(Duration::from_secs(1)); + assert_eq!(DELIVERED.load(Ordering::SeqCst), 1); + let other = wake.clone(); + std::thread::spawn(move || other.signal()).join().unwrap(); + let start = std::time::Instant::now(); + wake.wait(Duration::from_secs(5)); + assert!(start.elapsed() < Duration::from_secs(1)); + DestroyWindow(window); + UnregisterClassW(class.as_ptr(), instance); + }) + .join() + .unwrap(); + } +} diff --git a/src/lib.rs b/src/lib.rs index a02cd5bcb..073c04980 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,6 +9,7 @@ pub mod config; pub mod discussions; pub mod entities; pub mod i18n; +pub(crate) mod input; pub mod io; #[cfg(not(target_arch = "wasm32"))] pub mod mcp; diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 59eb787f3..59dbe8f40 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -10855,11 +10855,21 @@ vis_index={:.1} visible_probe={:.1}", /// when something is selected. `None` keeps the current camera target as /// the orbit centre. (#229) pub fn orbit_pivot(&self) -> Option { + self.navigation_selection_bounds() + .map(|(min, max)| (min + max) * 0.5) + } + + pub(crate) fn navigation_selection_bounds(&self) -> Option<(glam::DVec3, glam::DVec3)> { if self.selected.is_empty() { return None; } - let block = self.current_layout_block_handle(); - let scale = if self.current_layout == "Model" { + let model_view = self.current_layout == "Model" || self.active_viewport.is_some(); + let block = if self.active_viewport.is_some() { + self.model_space_block_handle() + } else { + self.current_layout_block_handle() + }; + let scale = if model_view { crate::scene::annotative::scale_handle_by_name( &self.document, &self.document.header.current_annotation_scale, @@ -10916,7 +10926,7 @@ vis_index={:.1} visible_probe={:.1}", } } if any { - Some((min + max) * 0.5) + Some((min, max)) } else { None } diff --git a/src/scene/mspace.rs b/src/scene/mspace.rs index 6cad6e5c5..a6c014aa0 100644 --- a/src/scene/mspace.rs +++ b/src/scene/mspace.rs @@ -446,6 +446,45 @@ impl Scene { self.camera.borrow().rotation } + /// Camera owning navigation, including an entered paper-space viewport. + pub(crate) fn navigation_camera(&self) -> view::camera::Camera { + self.active_viewport + .and_then(|h| self.camera_for_viewport(h)) + .unwrap_or_else(|| self.camera.borrow().clone()) + } + + pub(crate) fn navigation_locked(&self) -> bool { + self.active_viewport.is_some_and(|h| { + !matches!(self.document.get_entity(h), Some(acadrust::EntityType::Viewport(v)) if !v.status.locked) + }) + } + + /// Commit an externally navigated camera through the same floating-view + /// encoding as the ViewCube. Camera coordinates already include view_center. + pub(crate) fn apply_navigation_camera(&mut self, camera: view::camera::Camera) -> bool { + if self.navigation_locked() { + return false; + } + if let Some(handle) = self.active_viewport { + if !self.mutate_active_viewport_camera(|c| *c = camera.clone()) { + return false; + } + if let Some(acadrust::EntityType::Viewport(vp)) = self.document.get_entity_mut(handle) { + vp.view_target.x = camera.target.x; + vp.view_target.y = camera.target.y; + vp.view_target.z = camera.target.z; + vp.view_center.x = 0.; + vp.view_center.y = 0.; + vp.view_height = camera.ortho_size() as f64 * 2.; + vp.custom_scale = vp.height / vp.view_height; + } + } else { + *self.camera.borrow_mut() = camera; + } + self.camera_generation += 1; + true + } + pub fn active_camera_projection(&self) -> view::camera::Projection { if let Some(handle) = self.active_viewport { if let Some(camera) = self.camera_for_viewport(handle) { diff --git a/src/scene/view/camera.rs b/src/scene/view/camera.rs index 7b87a2a8d..6f0016f06 100644 --- a/src/scene/view/camera.rs +++ b/src/scene/view/camera.rs @@ -761,7 +761,7 @@ impl Camera { // ── Internal helpers ─────────────────────────────────────────────────── /// Derive yaw and pitch from the current quaternion. - fn sync_yaw_pitch(&mut self) { + pub(crate) fn sync_yaw_pitch(&mut self) { let eye_dir = self.rotation * Vec3::Z; self.pitch = eye_dir.z.clamp(-1.0, 1.0).asin(); self.yaw = if eye_dir.x.abs() < 1e-6 && eye_dir.y.abs() < 1e-6 { diff --git a/src/ui/statusbar/mod.rs b/src/ui/statusbar/mod.rs index 52ec2916e..ea546a0cf 100644 --- a/src/ui/statusbar/mod.rs +++ b/src/ui/statusbar/mod.rs @@ -1,6 +1,7 @@ //! Bottom status bar — Model/Layout tabs + OSNAP toggle + status info pub mod status_menu; +pub(crate) mod spacemouse; pub mod statusbar_config; pub mod statusbar_menu; @@ -135,6 +136,7 @@ impl StatusBar { parametric_conflicts: usize, // What is drawing the scene. Only a degraded verdict shows anything. gpu_status: &'a crate::scene::pipeline::GpuStatus, + spacemouse: Option>, ) -> Element<'a, Message> { let StatusMenuData { layout_names, @@ -241,6 +243,11 @@ impl StatusBar { // when the width can't hold them all on one line. let vis = |p: StatusPill| config.is_visible(p); let mut pills: Vec> = Vec::new(); + if vis(StatusPill::SpaceMouse) { + if let Some(pill) = spacemouse { + pills.push(pill); + } + } // Not a `StatusPill` and not hideable: the scene is on a software // rasterizer or not drawn at all, and the popup that said so has been // dismissed. This stays for the session and reopens it. Many people diff --git a/src/ui/statusbar/spacemouse.rs b/src/ui/statusbar/spacemouse.rs new file mode 100644 index 000000000..b3b78025f --- /dev/null +++ b/src/ui/statusbar/spacemouse.rs @@ -0,0 +1,78 @@ +use super::status_menu::{self, Entry}; +use crate::app::Message; +use crate::input::spacemouse::{NavigationMode, Preferences, Status}; +use iced::widget::{button, row, text}; +use iced::{Element, Fill}; + +pub(crate) fn view( + preferences: Preferences, + status: Status, + paused: bool, + label: String, + sheet: bool, +) -> Element<'static, Message> { + let root = button( + row![ + crate::ui::icons::themed(crate::ui::window::options::spacemouse::ICON, 15.), + text(label).size(11), + crate::ui::icons::themed_arrow_toggle(false, 8.) + ] + .spacing(5) + .align_y(iced::Center), + ) + .padding([3, 6]) + .style(button::subtle); + let mut entries = vec![Entry::stay( + text(format!("SpaceMouse · {}", crate::t!(status.label()))).size(11), + )]; + for mode in NavigationMode::ALL { + let label = format!( + "{} {}", + if mode == preferences.mode { + "●" + } else { + "○" + }, + crate::t!(mode.label()) + ); + entries.push(Entry::close( + button(text(label).size(12)) + .width(Fill) + .padding([7, 10]) + .style(button::text) + .on_press(Message::SpaceMouseMode(mode)), + )); + } + if sheet { + entries.push(Entry::stay( + text(crate::t!("Paper sheets keep rotation locked.")).size(11), + )); + } + entries.push(Entry::close( + button( + text(crate::t!(if paused { + "Resume SpaceMouse" + } else { + "Pause SpaceMouse" + })) + .size(12), + ) + .on_press(Message::SpaceMousePause) + .style(button::text) + .width(Fill) + .padding([7, 10]), + )); + for (label, message) in [ + ("SpaceMouse preferences…", Message::SpaceMousePreferences), + ("3Dconnexion settings…", Message::SpaceMouseDriverSettings), + ] { + entries.push(Entry::close( + button(text(crate::t!(label)).size(12)) + .on_press(message) + .style(button::text) + .width(Fill) + .padding([7, 10]), + )); + } + status_menu::menu_bar(root, entries, 260.) +} diff --git a/src/ui/statusbar/statusbar_config.rs b/src/ui/statusbar/statusbar_config.rs index 84dee3ca8..b0faadbf9 100644 --- a/src/ui/statusbar/statusbar_config.rs +++ b/src/ui/statusbar/statusbar_config.rs @@ -11,6 +11,7 @@ use serde::{Deserialize, Serialize}; /// Identifies a toggleable status-bar pill. #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Serialize, Deserialize)] pub enum StatusPill { + SpaceMouse, Coords, Ortho, Lwt, @@ -37,6 +38,7 @@ impl StatusPill { /// Every pill, in status-bar display order. Drives both the bar layout and /// the customization menu. pub const ALL: &'static [StatusPill] = &[ + StatusPill::SpaceMouse, StatusPill::Coords, StatusPill::Ortho, StatusPill::Lwt, @@ -62,6 +64,7 @@ impl StatusPill { /// Stable identifier used for persistence. pub fn id(self) -> &'static str { match self { + StatusPill::SpaceMouse => "spacemouse", StatusPill::Coords => "coords", StatusPill::Ortho => "ortho", StatusPill::Lwt => "lwt", @@ -88,6 +91,7 @@ impl StatusPill { /// Label shown in the customization menu. pub fn label(self) -> &'static str { match self { + StatusPill::SpaceMouse => "SpaceMouse", StatusPill::Coords => "Coordinates", StatusPill::Ortho => "Ortho Mode", StatusPill::Lwt => "Show Lineweight", diff --git a/src/ui/window/options.rs b/src/ui/window/options.rs index 3c7f168c3..c3e1c0d8e 100644 --- a/src/ui/window/options.rs +++ b/src/ui/window/options.rs @@ -1,3 +1,4 @@ +pub(crate) mod spacemouse; use crate::app::config::UiThemeConfig; use crate::app::settings::CursorType; use crate::app::Message; @@ -153,6 +154,7 @@ pub fn view_window<'a>( cursor_size: i32, selection: SelectionPrefs, prefs: AppPrefs, + spacemouse: Element<'a, Message>, snap_angle_input: &'a str, drawing_prefs: DrawingPrefs, folders: Folders, @@ -1135,6 +1137,8 @@ pub fn view_window<'a>( let user_prefs = column![ text(crate::t!("User Preferences")).size(15), Space::new().height(10), + spacemouse, + Space::new().height(12), text(crate::t!("Zoom")).size(15), Space::new().height(10), row![ diff --git a/src/ui/window/options/spacemouse.rs b/src/ui/window/options/spacemouse.rs new file mode 100644 index 000000000..dea7fca9d --- /dev/null +++ b/src/ui/window/options/spacemouse.rs @@ -0,0 +1,135 @@ +use crate::app::Message; +use crate::input::spacemouse::{NavigationMode, Preferences, Status}; +use iced::widget::{button, checkbox, column, container, pick_list, row, slider, text, Space}; +use iced::{Element, Fill}; + +pub(crate) const ICON: &[u8] = include_bytes!("../../../../assets/icons/ui/spacemouse.svg"); + +pub(crate) fn view( + preferences: Preferences, + status: Status, + paused: bool, + details: bool, +) -> Element<'static, Message> { + let supported = cfg!(windows); + let mut enabled = checkbox(preferences.enabled).size(15); + if supported { + enabled = enabled.on_toggle(Message::SpaceMouseEnabled); + } + let mode = pick_list(Some(preferences.mode), NavigationMode::ALL, |mode| { + crate::t!(mode.label()).into_owned() + }) + .width(220); + let mode = if supported { + mode.on_select(Message::SpaceMouseMode) + } else { + mode + }; + let status_text = if !preferences.enabled { + crate::t!("Disabled").into_owned() + } else if paused { + crate::t!("Paused").into_owned() + } else { + crate::t!(status.label()).into_owned() + }; + let mut content = column![ + row![ + crate::ui::icons::themed(ICON, 22.), + text(crate::t!("SpaceMouse")).size(15) + ] + .spacing(8) + .align_y(iced::Center), + row![enabled, text(crate::t!("Enable SpaceMouse")).size(12)] + .spacing(8) + .align_y(iced::Center), + row![ + text(crate::t!("Status")).size(12).width(150), + text(status_text).size(12) + ] + .spacing(10), + row![text(crate::t!("Navigation")).size(12).width(150), mode] + .spacing(10) + .align_y(iced::Center), + text(crate::t!(preferences.mode.description()).into_owned()) + .size(11) + .width(Fill), + ] + .spacing(12); + if preferences.mode != NavigationMode::Full3D && supported { + content = content.push( + row![ + checkbox(preferences.pan_reversed) + .size(15) + .on_toggle(Message::SpaceMousePanReversed), + text(crate::t!("Reverse pan direction")).size(12), + ] + .spacing(8) + .align_y(iced::Center), + ); + content = content.push( + row![ + text(crate::t!("Pan speed")).size(12).width(150), + slider( + 10..=300, + preferences.pan_speed.clamp(10, 300), + Message::SpaceMousePanSpeed + ) + .step(10) + .width(160), + text(format!("{}%", preferences.pan_speed.clamp(10, 300))).size(11), + ] + .spacing(10) + .align_y(iced::Center), + ); + } + if paused { + content = content.push( + button(text(crate::t!("Resume SpaceMouse")).size(11)) + .on_press(Message::SpaceMousePause) + .style(button::secondary), + ); + } + content = content.push( + button( + text(crate::t!(if supported { + "Open 3Dconnexion settings…" + } else { + "SpaceMouse information…" + })) + .size(11), + ) + .on_press(Message::SpaceMouseDriverSettings) + .style(button::secondary) + .padding([5, 10]), + ); + content = content.push( + text(crate::t!( + "Configure 3D navigation, buttons, and radial menus in 3Dconnexion settings. Pan only and Pan and zoom share the push/tilt mapping and pan settings shown here." + )) + .size(11) + .width(Fill), + ); + content = content.push( + button( + row![ + crate::ui::icons::themed_arrow_toggle(details, 9.), + text(crate::t!("Connection details")).size(11) + ] + .spacing(6), + ) + .on_press(Message::SpaceMouseDetails) + .style(button::text), + ); + if details { + let detail = match status { + Status::Unavailable(reason) => reason, + Status::Unsupported => "This version supports SpaceMouse through 3DxWare on Windows. Other platforms do not yet have a device adapter.".into(), + Status::Disconnected => "3DxWare is running. Connect a SpaceMouse; Open CAD Studio reconnects automatically.".into(), + _ => "Uses the installed 3DxWare driver. Navigation pauses while a dialog is open. Your selected navigation mode is saved across drawings and sessions.".into(), + }; + content = content.push(text(crate::t!(&detail).into_owned()).size(11).width(Fill)); + } + container(content.push(Space::new().height(8))) + .width(Fill) + .into() +}