Press-and-drag selection windows (PICKDRAG = 2) (#63)
Selection windows only worked click-then-click: the canvas reacts to clicks, and a press that moves before release is a drag, not a click, so a drag was ignored entirely. Now, while objects are being selected (no command running, or a command asking for objects), a drag opens the window where the button went down and letting go closes it, as with AutoCAD's default PICKDRAG = 2. Dragging right is a window, dragging left a crossing window, Shift still removes, and click-click works as before. Drags that start on a grip or while a command asks for a point are left alone. Session::begin_window opens the window at the drag's start, even over an object (a click there would pick the object instead), with an engine test. Claude-Session: https://claude.ai/code/session_019uMCkRAB8DMd8MEmoY9Vyr Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
1dfa3660a7
commit
d87950afe8
|
|
@ -863,6 +863,19 @@ impl Session {
|
|||
|
||||
// ---------------- picking without a command ----------------
|
||||
|
||||
/// Open a selection window with one corner at `corner`, for press-and-drag selection
|
||||
/// (AutoCAD's PICKDRAG = 2: a drag opens the window wherever it starts, even over an object;
|
||||
/// the next point, where the drag ends, closes it). Only while objects are being selected: no
|
||||
/// command running, or a command asking for objects. Returns whether a window was opened.
|
||||
pub fn begin_window(&mut self, corner: Vec2) -> bool {
|
||||
let during_command = self.running.is_some();
|
||||
if !corner.is_finite() || (during_command && !self.current_prompt().is_some_and(|p| p.accept.select)) {
|
||||
return false;
|
||||
}
|
||||
self.pending_window = Some(PendingWindow { corner, during_command });
|
||||
true
|
||||
}
|
||||
|
||||
/// A click with no command running: pick/toggle objects or start a selection window.
|
||||
///
|
||||
/// PICKADD on (the default): picks add to the selection and Shift removes. PICKADD off: each pick
|
||||
|
|
|
|||
|
|
@ -994,3 +994,34 @@ fn pline_width_answers_taper_the_next_segment() {
|
|||
let p = typed_pline("pline 0,0 10,0 w 1 0.2 20,0 u");
|
||||
assert_eq!(widths(&p), (0.0, vec![(0.0, 0.0), (0.0, 0.0)]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn press_and_drag_selection_window() {
|
||||
let mut s = Session::new();
|
||||
s.cmdline("line 0,0 10,0").unwrap();
|
||||
s.cmdline("").unwrap();
|
||||
s.cmdline("circle 50,50 1").unwrap();
|
||||
// No command: a drag from (-1,-1) to (11,1) is a window around the line only.
|
||||
assert!(s.begin_window(Vec2::new(-1.0, -1.0)));
|
||||
s.idle_click(Vec2::new(11.0, 1.0), false).unwrap();
|
||||
assert_eq!(s.selection().len(), 1);
|
||||
assert!(s.pending_window.is_none());
|
||||
// Dragging right to left is a crossing window: it also catches the circle it touches.
|
||||
s.set_selection(Vec::new());
|
||||
assert!(s.begin_window(Vec2::new(60.0, 60.0)));
|
||||
s.idle_click(Vec2::new(50.0, 40.0), false).unwrap();
|
||||
assert_eq!(s.selection().len(), 1);
|
||||
// A command asking for objects: the window feeds its selection.
|
||||
s.set_selection(Vec::new());
|
||||
s.cmdline("erase").unwrap();
|
||||
assert!(s.begin_window(Vec2::new(-1.0, -1.0)));
|
||||
s.input(Input::Point(Vec2::new(11.0, 1.0))).unwrap();
|
||||
s.input(Input::Enter).unwrap();
|
||||
assert_eq!(kinds(&s), vec!["Circle"]);
|
||||
// A drawing command never opens one, and neither does a hostile point.
|
||||
s.cmdline("line").unwrap();
|
||||
assert!(!s.begin_window(Vec2::ZERO));
|
||||
s.cancel();
|
||||
assert!(!s.begin_window(Vec2::new(f64::NAN, 0.0)));
|
||||
assert!(s.pending_window.is_none());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -112,6 +112,8 @@ pub struct CanvasState {
|
|||
hover_at: Option<Pos2>,
|
||||
/// Constraint glyphs for the parametric overlay (cached per drawing revision).
|
||||
pub param: crate::parametric::Cache,
|
||||
/// A selection window opened by a press-and-drag, to close where the button is let go.
|
||||
drag_window: bool,
|
||||
}
|
||||
|
||||
/// Primitives of `list` sorted by entity handle, built on the first highlight after each list
|
||||
|
|
@ -648,6 +650,31 @@ pub fn show(app: &mut CadApp, ui: &mut egui::Ui) {
|
|||
app.session.echo(e.to_string());
|
||||
}
|
||||
}
|
||||
// Press-and-drag selection window (AutoCAD's PICKDRAG = 2; click-click still works): a drag
|
||||
// that starts while objects are being selected opens a window where the button went down,
|
||||
// and letting go closes it. Drags on grips and drags while drawing are left alone.
|
||||
if resp.drag_started_by(egui::PointerButton::Primary)
|
||||
&& !middle_down
|
||||
&& app.canvas.hot_grip.is_none()
|
||||
&& app.session.pending_window.is_none()
|
||||
&& let Some(origin) = ui.input(|i| i.pointer.press_origin())
|
||||
&& rect.contains(origin)
|
||||
&& grip_at(app, &xf, Some(origin)).is_none()
|
||||
{
|
||||
app.canvas.drag_window = app.session.begin_window(xf.to_world(origin));
|
||||
}
|
||||
if app.canvas.drag_window && resp.drag_stopped_by(egui::PointerButton::Primary) {
|
||||
app.canvas.drag_window = false;
|
||||
if app.session.pending_window.is_some()
|
||||
&& let Some(end) = ui.input(|i| i.pointer.interact_pos())
|
||||
{
|
||||
let end = xf.to_world(end);
|
||||
let done = if app.session.running.is_some() { app.session.input(Input::Point(end)) } else { app.session.idle_click(end, mods.shift) };
|
||||
if let Err(e) = done {
|
||||
app.session.echo(e.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
if inside && pressed_secondary {
|
||||
// Right-click acts as Enter while a command runs (the classic CAD default).
|
||||
if app.session.running.is_some() {
|
||||
|
|
|
|||
Loading…
Reference in New Issue