feat(select): PICKADD/PICKDRAG command-line toggles (#226)

PICKADD 0 = click/box/lasso replaces the selection (Shift toggles),
empty click clears; PICKADD 1 (default) keeps today's accumulate.
PICKDRAG 1 = press-drag spans a rectangle marquee through the box
machinery; PICKDRAG 0 (default) keeps the freeform lasso. Both persist
in settings.json. Also re-points the start-page gate test at the
start_allowed fn it moved into (#388/389 refactor left it broken).

Closes #226

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Hakan Seven 2026-07-22 21:35:37 +03:00
commit 0c578b03f3
7 changed files with 250 additions and 17 deletions

View file

@ -620,11 +620,13 @@ mod tests {
// rather than by dispatching: DONATE / REPORT / WEBVERSION shell out to
// a real browser, which a test must not do.
let dispatch_src = include_str!("commands/mod.rs");
// The gate's allow-list lives in the `start_allowed` fn (#388/389);
// its first `}` closes the `matches!` body, past every name.
let gate = dispatch_src
.split("is_start")
.split("pub fn start_allowed")
.nth(1)
.and_then(|s| s.split('{').next())
.expect("the is_start gate moved — re-point this test");
.and_then(|s| s.split('}').next())
.expect("the start_allowed gate moved — re-point this test");
// DONATE/REPORT/WEBVERSION are the welcome page's own buttons; the rest
// are ribbon tools that configure the application, not a drawing.
let standalone = [
@ -654,6 +656,103 @@ mod tests {
}
}
/// PICKADD / PICKDRAG (#226): the command flips the live flag both via the
/// inline form and the two-step ValuePrompt flow.
#[test]
fn lasso_press_drag_selects() {
// Press-drag lasso must select crossed entities — regression probe
// for the #226 PICKDRAG work (both PICKDRAG modes complete through
// the poly path).
use crate::app::Message;
for (add, rect) in [(true, false), (true, true), (false, false), (false, true)] {
let mut app = OpenCADStudio::new_for_test();
app.automation_op(r#"{"op":"new"}"#);
let i = app.active_tab;
app.pick_add = add;
app.pick_drag_rect = rect;
let _ = app.run_command_line("LINE 0,0 10,10");
app.tabs[i].scene.selection.borrow_mut().vp_size = (800.0, 600.0);
let _ = app.run_command_line("ZOOM EXTENTS");
// Both directions. Crossing = a right → left diagonal sweep (the
// freeform path may be degenerate — crossing counts hits).
// Window = a left → right perimeter walk so the freeform ring
// actually ENCLOSES the line (a diagonal has no area).
let path: Vec<(f32, f32)> = if !add && rect {
// Rectangle window: a simple left → right diagonal spans it.
(0..=10)
.map(|k| {
let t = k as f32 / 10.0;
(20.0 + t * 760.0, 20.0 + t * 560.0)
})
.collect()
} else if add {
let (sx, sy, ex, ey) = (780.0f32, 580.0f32, 20.0f32, 20.0f32);
(0..=10)
.map(|k| {
let t = k as f32 / 10.0;
(sx + t * (ex - sx), sy + t * (ey - sy))
})
.collect()
} else {
vec![
(20.0, 20.0),
(400.0, 20.0),
(780.0, 20.0),
(780.0, 300.0),
(780.0, 580.0),
(400.0, 580.0),
(20.0, 580.0),
(20.0, 300.0),
]
};
let _ = app.update(Message::ViewportMove(iced::Point::new(path[0].0, path[0].1)));
let _ = app.update(Message::ViewportLeftPress);
std::thread::sleep(std::time::Duration::from_millis(180));
for &(x, y) in &path {
let _ = app.update(Message::ViewportMove(iced::Point::new(x, y)));
}
{
let sel = app.tabs[i].scene.selection.borrow();
assert!(sel.left_dragging, "drag must start (add={add} rect={rect})");
if rect {
// Rectangle mode drives the box machinery, not the lasso.
assert!(
sel.box_anchor.is_some() && sel.box_current.is_some() && !sel.poly_active,
"rect marquee must arm the box (add={add})"
);
} else {
assert!(sel.poly_active, "lasso must start (add={add})");
assert!(sel.poly_points.len() >= 3, "lasso points (add={add})");
}
}
let _ = app.update(Message::ViewportLeftRelease);
assert!(
!app.tabs[i].scene.selected.is_empty(),
"marquee must select the line (add={add} rect={rect})"
);
}
}
#[test]
fn pickadd_command_flips_flag() {
let mut app = OpenCADStudio::new_for_test();
// The Start tab blocks drawing commands — open a drawing first.
app.automation_op(r#"{"op":"new"}"#);
// The boot path may have restored a persisted value — normalize.
app.pick_add = true;
app.pick_drag_rect = false;
let _ = app.run_command_line("PICKADD 0");
assert!(!app.pick_add, "PICKADD 0 must switch to replace mode");
let _ = app.run_command_line("PICKADD 1");
assert!(app.pick_add);
// Two-step: bare command then the value, like typing 1 + Enter
// (feed_active_cmd is the same path the GUI submit offers first).
let _ = app.run_command_line("PICKDRAG");
assert!(app.tabs[app.active_tab].active_cmd.is_some(), "prompt must open");
app.feed_active_cmd("1");
assert!(app.pick_drag_rect, "PICKDRAG 1 via the prompt must switch");
}
#[test]
fn matchprop_matches_text_style_and_height() {
// MATCHPROP between text objects must carry the text-specific