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
@@ -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),@@ -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.