diff --git a/src/app/commands.rs b/src/app/commands.rs index 0d21d7329..ed9ae96c5 100644 --- a/src/app/commands.rs +++ b/src/app/commands.rs @@ -29,6 +29,9 @@ impl OpenCADStudio { self.tabs[i].scene.clear_preview_wire(); self.tabs[i].active_cmd = None; } + // Starting any command leaves interactive PAN mode (the PAN arm below + // re-enables it). + self.tabs[i].pan_mode = false; // Reset the last committed point so the first click of the new command // is not constrained by ortho/polar relative to a previous command's endpoint. self.last_point = None; @@ -5148,6 +5151,14 @@ impl OpenCADStudio { self.command_line.push_output("Display regenerated."); } + // Interactive pan: left-drag pans the view until Esc. The only pan + // path when there is no middle mouse button (trackpad / web). + "PAN" | "P" => { + self.tabs[i].pan_mode = true; + self.command_line + .push_output("PAN: drag with the left mouse button. Press Esc to exit."); + } + // ── TABLE cell editing ───────────────────────────────────────────── // TABLE CELL — set text for a cell in the selected Table cmd if cmd.starts_with("TABLE ") => { @@ -5972,7 +5983,8 @@ inventory::submit!(crate::command::CommandRegistration { "LAYER", "LAYERS", "LAYISO", "LAYON", "LAYOUTMANAGER", "LAYOUTPANEL", "LAYOUTTAB", "LAYTHW", "LAYUNISO", "LI", "LINETYPE", "LIST", "LTSCALE", "LWDISPLAY", "MASSPROP", "MLEADERSTYLE", "MLSTYLE", "MS", "MSPACE", "NAVVCUBE", "NEW", "OBJIMPORT", "OPEN", "ORTHO", - "PAGESETUP", "PERF", "PERSP", "PLOT", "PLOTSTYLE", "PLOTSTYLEEDITOR", "PLOTSTYLEPANEL", "PR", + "P", "PAN", "PAGESETUP", "PERF", "PERSP", "PLOT", "PLOTSTYLE", "PLOTSTYLEEDITOR", + "PLOTSTYLEPANEL", "PR", "PRINT", "PROPERTIES", "PROPS", "PSPACE", "PURGE", "QS", "QSAVE", "QSELECT", "QUIT", "REDO", "REDRAW", "REDRWALL", "REGEN", "REGENALL", "RENAME", "REPORT", "SA", "SAVE", "SAVEAS", "SCALETEXT", "SELECTALL", "SELECTSIMILAR", "SELSIM", "SHEETSET", diff --git a/src/app/document.rs b/src/app/document.rs index 3f57eeeee..9fbb2adc8 100644 --- a/src/app/document.rs +++ b/src/app/document.rs @@ -158,6 +158,11 @@ pub(super) struct DocumentTab { /// of the model-space shader. The scene is still constructed so the /// rest of the code can treat it as a normal tab when reading. pub(super) is_start: bool, + /// Interactive PAN mode (the PAN command / tool). While active, a left- + /// button drag pans the view instead of selecting — the only pan path on a + /// device with no middle mouse button (a trackpad / web client). Exited + /// with Esc or by starting another command. + pub(super) pan_mode: bool, /// Per-plugin document state (`plugin::BuiltinPlugin` manifest id → state). #[cfg_attr(target_arch = "wasm32", allow(dead_code))] pub(super) plugin_state: HashMap<&'static str, Box>, @@ -377,6 +382,7 @@ impl DocumentTab { active_mleader_style: "Standard".to_string(), last_synced_camera_gen: 0, is_start: false, + pan_mode: false, plugin_state: HashMap::new(), suspended_cmd: None, } diff --git a/src/app/update.rs b/src/app/update.rs index 2344ac602..4b644d058 100644 --- a/src/app/update.rs +++ b/src/app/update.rs @@ -1835,6 +1835,18 @@ impl OpenCADStudio { self.text_inline_cancel(); return self.post_editor_closed(false); } + // Leave interactive PAN mode (and end any in-flight pan drag). + if self.tabs[self.active_tab].pan_mode { + let i = self.active_tab; + self.tabs[i].pan_mode = false; + { + let mut sel = self.tabs[i].scene.selection.borrow_mut(); + sel.middle_down = false; + sel.middle_last_pos = None; + } + self.command_line.push_output("PAN ended."); + return Task::none(); + } // Grip popup intercepts Escape — dismisses the menu // without doing anything else. if self.grip_popup.take().is_some() { @@ -3217,6 +3229,16 @@ impl OpenCADStudio { }; let (vw, vh) = vp_size; + // PAN mode: a left press begins a pan drag. Reuse the middle- + // button pan path (the move handler pans whenever `middle_down`), + // so no selection/pick logic runs while panning. + if self.tabs[i].pan_mode { + let mut sel = self.tabs[i].scene.selection.borrow_mut(); + sel.middle_down = true; + sel.middle_last_pos = Some(p); + return Task::none(); + } + if vw > 1.0 && vh > 1.0 { let rot = self.tabs[i].scene.active_view_rotation_mat(); // Map the cursor into whichever area owns the cube (active @@ -3382,6 +3404,15 @@ impl OpenCADStudio { Message::ViewportLeftRelease => { let i = self.active_tab; + // PAN mode: end the pan drag but stay in pan mode for the next + // drag (exit is Esc / another command). Mirror of the press. + if self.tabs[i].pan_mode { + let mut sel = self.tabs[i].scene.selection.borrow_mut(); + sel.middle_down = false; + sel.middle_last_pos = None; + return Task::none(); + } + // End an in-flight tile-divider drag. Any tile that fell // below the minimum (viewcube fits comfortably) gets // absorbed into its longest-contact neighbour, so the diff --git a/src/app/view.rs b/src/app/view.rs index 2d36fe35a..dc22ab692 100644 --- a/src/app/view.rs +++ b/src/app/view.rs @@ -263,6 +263,7 @@ impl OpenCADStudio { tab.last_cursor_screen, !is_paper && self.show_viewcube, tile_edges, + tab.pan_mode, ) }; diff --git a/src/ui/overlay.rs b/src/ui/overlay.rs index fcd6fce33..fb283fb97 100644 --- a/src/ui/overlay.rs +++ b/src/ui/overlay.rs @@ -129,6 +129,7 @@ pub fn selection_overlay<'a>( cursor_screen: Point, show_viewcube: bool, tile_edges: Vec, + pan_mode: bool, ) -> Element<'a, Message> { canvas(SelectionCanvas { selection, @@ -140,6 +141,7 @@ pub fn selection_overlay<'a>( cursor_screen, show_viewcube, tile_edges, + pan_mode, }) .width(Length::Fill) .height(Length::Fill) @@ -156,6 +158,9 @@ struct SelectionCanvas { cursor_screen: Point, show_viewcube: bool, tile_edges: Vec, + /// Interactive PAN mode: the crosshair is hidden and the cursor becomes a + /// hand so the viewport reads as a draggable surface. + pan_mode: bool, } impl SelectionCanvas { @@ -207,6 +212,15 @@ impl canvas::Program for SelectionCanvas { bounds: iced::Rectangle, cursor: mouse::Cursor, ) -> mouse::Interaction { + // PAN mode owns the whole viewport: an open hand when hovering, a + // closed hand while dragging. + if self.pan_mode && cursor.is_over(bounds) { + return if self.selection.middle_down { + mouse::Interaction::Grabbing + } else { + mouse::Interaction::Grab + }; + } if self.show_viewcube { if let Some(pos) = cursor.position_in(bounds) { use crate::scene::{VIEWCUBE_DRAW_PX, VIEWCUBE_PAD}; @@ -757,7 +771,8 @@ impl canvas::Program for SelectionCanvas { // arrow (see `mouse_interaction`); drawing the CAD crosshair on // top of it would double up the visual feedback. let over_divider = self.tile_edge_under(cursor, bounds).is_some(); - if !over_viewcube && !over_divider { + // PAN mode replaces the crosshair with a hand cursor. + if !over_viewcube && !over_divider && !self.pan_mode { if let Some(cp) = self.selection.last_move_pos { let color = Color { r: 0.85,