collective

Browse / github.com/storytold/cadcraft / patch

feat(ui): show pickbox-only cursor at command object-selection prompts

feat(ui): show pickbox-only cursor at command object-selection prompts ### Summary - At command object-selection prompts (Prompt::picks_objects: SELECT, ERASE, MOVE, COPY, TRIM, OFFSET, (entsel), etc.), draw only the pickbox square without the four crosshair arms, matching AutoCAD's selection cursor behavior. - Keep the full crosshair + pickbox when idle with PICKFIRST = 1 (even when SELECTIONPREVIEW = 0), and restore the crosshair lines while picking the opposite corner of a selection window (pending_window) or typed selection mode (W, C, F, WP, CP). - Guarantee a minimal visible pickbox (1.5 px half-side) at command object-selection prompts when PICKBOX = 0 so

Status
open
Score
0.00
Adopters
0 other operators
Author
Torentje (imported from pull request #2070, open)
Base
41d9b5e957b0719c04b9687c4f29088d77153c31
Size
+95 −17 in 2 files
Review
pending
Shared
2026-10-28
Id
c4f34610e98e6205495f9fa9

How it went

No other operator has reported using it yet.

Diff

raw · crates/engine/src/cmd/modify.rs crates/ui-egui/src/canvas.rs

diff --git a/crates/engine/src/cmd/modify.rs b/crates/engine/src/cmd/modify.rs--- a/crates/engine/src/cmd/modify.rs+++ b/crates/engine/src/cmd/modify.rs@@ -512,13 +512,17 @@ impl Interactive for StretchM {         s.echo("Select objects to stretch by crossing-window or crossing-polygon...");         Ok(Step::Continue)     }+    /// Returns the active interactive prompt for the current `STRETCH` phase.     fn prompt(&self, _s: &Session) -> Prompt {         if let (StretchPhase::Select, Some(m)) = (self.phase, &self.mode) {             return m.prompt();         }         match (self.phase, self.corner) {             (StretchPhase::Select, Some(c)) => Prompt::new("Specify opposite corner", Accept::POINT).base(c),-            (StretchPhase::Select, None) => Prompt::new(if self.removing { "Remove objects" } else { "Select objects" }, Accept::POINT),+            (StretchPhase::Select, None) => Prompt {+                picks_one: self.removing,+                ..Prompt::new(if self.removing { "Remove objects" } else { "Select objects" }, Accept::POINT)+            },             (StretchPhase::Base, _) => Prompt::new("Specify base point", Accept::POINT).kw(&["Displacement"]).default("Displacement"),             (StretchPhase::Displacement, _) => Prompt::new("Specify displacement", Accept::POINT).default("0,0"),             (StretchPhase::Second, _) => Prompt::new("Specify second point or <use first point as displacement>", Accept::POINT).base_opt(self.base),diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs--- a/crates/ui-egui/src/canvas.rs+++ b/crates/ui-egui/src/canvas.rs@@ -1742,9 +1742,9 @@ pub fn show(app: &mut CadApp, ui: &mut egui::Ui) {      // Hover highlight (throttled to cursor movement).     // The cursor picks objects (pick box, rollover highlight): "Select objects" and single-object-    // prompts, or no command with PICKFIRST on; SELECTIONPREVIEW says in which of those it shows.-    let selecting = app.session.previews_selection();-    if inside && selecting && app.session.pending_window.is_none() {+    // prompts, or no command with PICKFIRST on; SELECTIONPREVIEW says in which of those the+    // rollover highlight shows.+    if inside && app.session.previews_selection() && app.session.pending_window.is_none() {         if hover_pos != app.canvas.hover_at             && let (Some(w), Ok(st)) = (raw_world, app.session.state())         {@@ -2034,7 +2034,8 @@ pub fn show(app: &mut CadApp, ui: &mut egui::Ui) {             // Use the painted sheet even in MSPACE, where the editing transform follows a             // model viewport. The whole cursor takes one colour from its centre.             let ink = paper_rect.map_or(t.crosshair, |paper| crate::theme::crosshair_color(if paper.contains(hp) { t.paper } else { surround }));-            draw_crosshair(app, &painter, rect, hp, selecting, ink);+            let picking = app.session.picking_objects() && app.session.pending_window.is_none();+            draw_crosshair(app, &painter, rect, hp, picking, ink);             if app.canvas.cycle_badge && app.canvas.cycle.is_none() {                 cycle_badge(&painter, hp);             }@@ -2307,28 +2308,41 @@ fn override_cursor(app: &mut CadApp, raw: Vec2, xf: &Xf) {     app.session.cursor_deferred = app.canvas.snap.and_then(|h| h.deferred); } +/// Half the side of the pick box (`PICKBOX * 1.5` screen points), or `None` when `PICKBOX` is 0. fn pickbox_half(pickbox: f64) -> Option<f32> {     let half = (pickbox * 1.5) as f32;     (half.is_finite() && half >= 1.0).then_some(half) } +/// Draws the canvas cursor at `at`: the full crosshair with pickbox when idle, crosshair lines+/// only at point prompts, and the pickbox square alone at command object-selection prompts. fn draw_crosshair(app: &CadApp, p: &egui::Painter, rect: Rect, at: Pos2, pickbox: bool, ink: Color32) {     let st = Stroke::new(1.0, ink);     let len = (rect.height() * app.session.settings.cursorsize as f32 / 100.0).max(12.0);-    let pb = if pickbox { pickbox_half(app.session.settings.pickbox) } else { None };+    let picking_in_cmd = pickbox && app.session.current_prompt().is_some_and(|pr| pr.picks_objects());+    let pb = if pickbox {+        pickbox_half(app.session.settings.pickbox).or(picking_in_cmd.then_some(1.5))+    } else {+        None+    };     let gap = pb.unwrap_or(0.0);-    // Along the isometric plane's axes in isometric drafting.-    if app.session.settings.isodraft {-        for a in cadcraft_engine::snap::iso::cursor_axes(&app.session.settings) {-            let dir = vec2(a.cos() as f32, -(a.sin() as f32));-            p.line_segment([at - dir * len, at - dir * gap], st);-            p.line_segment([at + dir * gap, at + dir * len], st);+    // At a command prompt that selects objects, AutoCAD shows only the pickbox square (no+    // crosshair lines); when idle or once a selection window corner is picked, the crosshair+    // lines are drawn.+    if !picking_in_cmd {+        // Along the isometric plane's axes in isometric drafting.+        if app.session.settings.isodraft {+            for a in cadcraft_engine::snap::iso::cursor_axes(&app.session.settings) {+                let dir = vec2(a.cos() as f32, -(a.sin() as f32));+                p.line_segment([at - dir * len, at - dir * gap], st);+                p.line_segment([at + dir * gap, at + dir * len], st);+            }+        } else {+            p.line_segment([pos2(at.x - len, at.y), pos2(at.x - gap, at.y)], st);+            p.line_segment([pos2(at.x + gap, at.y), pos2(at.x + len, at.y)], st);+            p.line_segment([pos2(at.x, at.y - len), pos2(at.x, at.y - gap)], st);+            p.line_segment([pos2(at.x, at.y + gap), pos2(at.x, at.y + len)], st);         }-    } else {-        p.line_segment([pos2(at.x - len, at.y), pos2(at.x - gap, at.y)], st);-        p.line_segment([pos2(at.x + gap, at.y), pos2(at.x + len, at.y)], st);-        p.line_segment([pos2(at.x, at.y - len), pos2(at.x, at.y - gap)], st);-        p.line_segment([pos2(at.x, at.y + gap), pos2(at.x, at.y + len)], st);     }     if let Some(pb) = pb {         p.rect_stroke(Rect::from_center_size(at, vec2(pb * 2.0, pb * 2.0)), 0.0, st, egui::StrokeKind::Middle);@@ -2938,6 +2952,66 @@ pub(crate) mod tests {         let text = painted_text(&mut app, &ctx, at);         assert!(text.contains(&format!("   {}  <", arch(c.len()))), "no {} in:\n{text}", arch(c.len()));     }++    /// Verifies that command object-selection prompts draw only the pickbox square while idle and+    /// point prompts draw the crosshair lines.+    #[test]+    fn cursor_shows_pickbox_only_at_command_object_selection_prompts() {+        let mut app = CadApp::new(cadcraft_engine::Session::new(), crate::Services::default());+        let rect = Rect::from_min_size(Pos2::ZERO, vec2(800.0, 600.0));+        let at = pos2(400.0, 300.0);+        let paint = |app: &CadApp| {+            let ctx = egui::Context::default();+            let mut out = ctx.run_ui(egui::RawInput::default(), |ui| {+                let painter = ui.painter_at(rect);+                let picking = app.session.picking_objects() && app.session.pending_window.is_none();+                draw_crosshair(app, &painter, rect, at, picking, Color32::WHITE);+            });+            out.textures_delta.clear();+            let mut lines = 0usize;+            let mut rects = 0usize;+            for clipped in &out.shapes {+                match &clipped.shape {+                    Shape::LineSegment { .. } => lines += 1,+                    Shape::Rect(r) if r.rect.center() == at => rects += 1,+                    _ => {}+                }+            }+            (lines, rects)+        };++        // 1. Idle with PICKFIRST on: crosshair lines (4 segments) + pickbox square (1 rect).+        assert_eq!(paint(&app), (4, 1), "idle with PICKFIRST on");++        // 2. Idle with SELECTIONPREVIEW = 0: pickbox still drawn while PICKFIRST is on.+        app.session.settings.selectionpreview = 0;+        assert_eq!(paint(&app), (4, 1), "idle with SELECTIONPREVIEW = 0");+        app.session.settings.selectionpreview = 3;++        // 3. Command asking for points (LINE): crosshair lines only (4 segments, 0 rects).+        app.session.cmdline("line").unwrap();+        assert_eq!(paint(&app), (4, 0), "point prompt (LINE)");+        app.session.cancel();++        // 4. Command asking to select objects (ERASE): pickbox square only (0 segments, 1 rect).+        app.session.cmdline("erase").unwrap();+        assert_eq!(paint(&app), (0, 1), "object selection prompt (ERASE)");++        // 5. Even when PICKBOX = 0, a command object-selection prompt keeps a minimal visible pickbox.+        app.session.settings.pickbox = 0.0;+        assert_eq!(paint(&app), (0, 1), "object selection prompt with PICKBOX = 0");+        app.session.settings.pickbox = 3.0;++        // 6. While picking the second corner of a selection window, the crosshair lines return.+        app.session.pending_window = Some(cadcraft_engine::PendingWindow { corner: Vec2::ZERO, during_command: true });+        assert_eq!(paint(&app), (4, 0), "selection window second corner");+        app.session.pending_window = None;+        app.session.cancel();++        // 7. STRETCH's "Remove objects" prompt also draws the pickbox-only cursor.+        app.session.cmdline("stretch r").unwrap();+        assert_eq!(paint(&app), (0, 1), "STRETCH Remove objects prompt");+    } }  /// The Block Editor's tint, base point and bar.