From bf818f8f516e052432ad8f13110a774ba0e45f38 Mon Sep 17 00:00:00 2001 From: Owen McGirr Date: Sun, 11 Oct 2026 17:30:44 +0100 Subject: [PATCH] Mark the chosen point with a crosshair instead of a square The action menu and drag-destination scan now mark the chosen point with four arms in the scan colour around a clear centre, kept on screen at display edges. Closes #1017. Co-Authored-By: Claude Opus 5.5 --- docs/point-scan.md | 2 +- src-tauri/src/point_workflow.rs | 86 +++++++++++++++++++++++++++++---- src-tauri/src/scan_menu.rs | 16 ------ 3 files changed, 77 insertions(+), 27 deletions(-) diff --git a/docs/point-scan.md b/docs/point-scan.md index 32e484e5..92872c1b 100644 --- a/docs/point-scan.md +++ b/docs/point-scan.md @@ -40,7 +40,7 @@ Physical validation should include forward/reverse escape with a real switch, pa ## Actions at a point -Choosing a point opens a native grid beside its marker. The rows are Left click / Right click / Double click; Scroll / Drag / More; New point / Close menu. The shared tree navigator handles row/item selection and row escape. The menu uses the current automatic/manual mode and block interval. After three automatic passes, it retains the target and shows Select to resume. That Select resumes scanning without executing an action. After a scroll or media item the menu stays on that item and keeps scanning; After a selection and Start again from change this for menus. +Choosing a point opens a native grid beside its marker. The marker is a crosshair in the scan colour with a small gap at its centre, so the exact point stays visible; it stays on screen while choosing a drag destination, and arms that would leave the display are cut short at its edge. The rows are Left click / Right click / Double click; Scroll / Drag / More; New point / Close menu. The shared tree navigator handles row/item selection and row escape. The menu uses the current automatic/manual mode and block interval. After three automatic passes, it retains the target and shows Select to resume. That Select resumes scanning without executing an action. After a scroll or media item the menu stays on that item and keeps scanning; After a selection and Start again from change this for menus. Scroll offers Up / Down, Left / Right, and Back to actions. Each selection moves the pointer to the target and sends three wheel steps; the selected direction remains available. Back restores the main menu's Scroll item. Choose another point immediately restarts the configured technique on the original display. Clicks and completed drags return to armed idle, unless After a selection is set to Keep scanning for point scanning. diff --git a/src-tauri/src/point_workflow.rs b/src-tauri/src/point_workflow.rs index 3e459007..8315d825 100644 --- a/src-tauri/src/point_workflow.rs +++ b/src-tauri/src/point_workflow.rs @@ -1,7 +1,7 @@ //! Point selection policy, menu navigation and execution requests. No OS input. use crate::{ point_scan::{Engine, PointSettings}, - scan_menu::{outline, Item, Kind, Menu}, + scan_menu::{Item, Kind, Menu}, scanning::{Action, Frame, FrameLabel, Rect, Technique, UpdateContext}, }; use serde::Serialize; @@ -1369,15 +1369,9 @@ impl Technique for Workflow { } if matches!(self.stage, Stage::Menu | Stage::Destination) && !self.home_open() { let s = self.point.units_per_logical_pixel; - frame.strips.extend(outline( - Rect { - x: self.source.0 as f64 - 5.0 * s, - y: self.source.1 as f64 - 5.0 * s, - width: 10.0 * s, - height: 10.0 * s, - }, - 2.0 * s, - )); + frame + .strips + .extend(crosshair(self.source, self.point.screen, s)); if self.stage == Stage::Destination { frame.label = Some(FrameLabel { text: "Choose drag destination".into(), @@ -1407,6 +1401,43 @@ impl Technique for Workflow { frame } } +/// Four arms around a chosen point, leaving its centre clear so the exact spot +/// stays visible. Arms keep their full thickness on screen, as scan lines do, and +/// are cut short at its edges; one too short to draw is left out. +fn crosshair(point: Point, screen: Rect, s: f64) -> Vec { + let (x, y) = (point.0 as f64, point.1 as f64); + let (gap, reach, t) = (4.0 * s, 16.0 * s, 2.0 * s); + let (right, bottom) = (screen.x + screen.width, screen.y + screen.height); + let across_x = (x - t / 2.0).clamp(screen.x, (right - t).max(screen.x)); + let across_y = (y - t / 2.0).clamp(screen.y, (bottom - t).max(screen.y)); + let horizontal = |from: f64, to: f64| { + let from = from.max(screen.x); + Rect { + x: from, + y: across_y, + width: to.min(right) - from, + height: t, + } + }; + let vertical = |from: f64, to: f64| { + let from = from.max(screen.y); + Rect { + x: across_x, + y: from, + width: t, + height: to.min(bottom) - from, + } + }; + [ + horizontal(x - reach, x - gap), + horizontal(x + gap, x + reach), + vertical(y - reach, y - gap), + vertical(y + gap, y + reach), + ] + .into_iter() + .filter(|arm| arm.valid()) + .collect() +} #[cfg(test)] mod tests { @@ -3152,6 +3183,41 @@ mod tests { } assert_eq!(s.technique.phase(), Phase::Workflow(WorkflowPhase::Menu)); } + #[test] + fn a_chosen_point_is_marked_by_a_crosshair_that_stays_on_screen() { + let mut s = session(false); + open(&mut s); + s.technique.source = (-500, 400); + let rect = |x, y, width, height| Rect { + x, + y, + width, + height, + }; + // The menu draws no strips of its own, so these are the whole marker. + assert_eq!( + s.technique.frame().strips, + vec![ + rect(-516.0, 399.0, 12.0, 2.0), + rect(-496.0, 399.0, 12.0, 2.0), + rect(-501.0, 384.0, 2.0, 12.0), + rect(-501.0, 404.0, 2.0, 12.0), + ] + ); + // At the top-left corner only the arms pointing into the screen are + // drawn, at full thickness along its edges. + s.technique.source = (-1000, 20); + assert_eq!( + s.technique.frame().strips, + vec![ + rect(-996.0, 20.0, 12.0, 2.0), + rect(-1000.0, 24.0, 2.0, 12.0) + ] + ); + // Home marks no point. + s.technique.selected(Item::Home); + assert!(s.technique.frame().strips.is_empty()); + } fn choose(s: &mut Session, row: usize, column: usize) -> Option { for _ in 0..row { s.action(Action::Next); diff --git a/src-tauri/src/scan_menu.rs b/src-tauri/src/scan_menu.rs index 403c7982..4142ed32 100644 --- a/src-tauri/src/scan_menu.rs +++ b/src-tauri/src/scan_menu.rs @@ -256,22 +256,6 @@ impl Menu { frame } } -pub fn outline(r: Rect, t: f64) -> [Rect; 4] { - [ - Rect { height: t, ..r }, - Rect { - y: r.y + r.height - t, - height: t, - ..r - }, - Rect { width: t, ..r }, - Rect { - x: r.x + r.width - t, - width: t, - ..r - }, - ] -} fn place(point: (i32, i32), screen: Rect, width: f64, height: f64, gap: f64) -> Rect { let (x, y) = (point.0 as f64, point.1 as f64); let candidates = [