Files
OneCAD/crates/ui-egui/src/select_other_tests.rs
T
Alan16888 9f6fb735dd fix(ui): Select Other keeps its list readable on dense meshes (#636)
Select Other keeps at most 20 rows per kind, nearest first, so a dense spot (a modelled thread, an imported mesh) no longer lists thousands of rows. The title shows how many were under the cursor in all, and ui.selectOther reports it as total.
2026-10-13 06:41:18 -04:00

197 lines
9.2 KiB
Rust

//! Select Other: press and hold, the viewport menu's entry, click cycling and dialog inputs.
use serde_json::{Value, json};
use solidcraft_engine::Sel;
use crate::scenario::Harness;
/// A sketch with two lines drawn on top of each other (l1, then l2), off the origin axes.
fn two_lines() -> (Harness, Value) {
let mut h = Harness::new();
h.app.ui.show_origin = false;
h.app.run("sketch.create", json!({"plane":"XY"})).unwrap();
for _ in 0..2 {
h.app.run("sketch.line", json!({"points":[[0,10],[20,10]],"infer":false,"connect":false})).unwrap();
}
h.app.cam.distance = 60.0;
h.settle();
let at = h.call("ui.at", json!({"sketch":[10,10]}))["result"].clone();
(h, at)
}
fn labels(h: &Harness) -> Vec<String> {
crate::select_other::describe(&h.app)["rows"].as_array().into_iter().flatten().filter_map(|r| r["label"].as_str().map(String::from)).collect()
}
fn curves(h: &Harness) -> Vec<String> {
h.app.session.selection.iter().filter_map(|s| if let Sel::SketchCurve { id } = s { Some(id.clone()) } else { None }).collect()
}
#[test]
fn holding_still_lists_both_coincident_lines_and_picks_the_one_underneath() {
let (mut h, at) = two_lines();
let before = h.app.session.doc.to_json();
h.step(&json!({"hold": at})).unwrap();
assert!(crate::select_other::is_open(&h.app), "a held press opens Select Other");
let rows = labels(&h);
assert!(rows.len() >= 2, "{rows:?}");
assert_eq!(&rows[..2], ["Line 1", "Line 2"], "the sketch's own curves come first");
// Letting go of the hold neither selected nor dragged anything.
assert!(h.app.session.selection.is_empty(), "{:?}", h.app.session.selection);
assert_eq!(h.app.session.doc.to_json(), before);
// Hovering a row lights it up in the viewport.
h.step(&json!({"move": {"handle": "select_other:1"}})).unwrap();
assert!(matches!(&h.app.viewport.hover, Some(crate::viewport::Hit::SketchCurve { id, .. }) if id == "l2"), "{:?}", h.app.viewport.hover);
h.step(&json!({"click": {"handle": "select_other:1"}})).unwrap();
assert!(!crate::select_other::is_open(&h.app));
assert_eq!(curves(&h), ["l2"]);
// Shift-picking from the list adds, like a Shift-click.
h.step(&json!({"hold": at})).unwrap();
h.step(&json!({"click": {"handle": "select_other:0"}, "shift": true})).unwrap();
let mut got = curves(&h);
got.sort();
assert_eq!(got, ["l1", "l2"]);
}
#[test]
fn esc_and_a_click_elsewhere_close_the_list_without_selecting() {
let (mut h, at) = two_lines();
h.step(&json!({"hold": at})).unwrap();
assert!(crate::select_other::is_open(&h.app));
h.step(&json!({"key": "Escape"})).unwrap();
assert!(!crate::select_other::is_open(&h.app));
assert!(h.app.session.active_sketch.is_some(), "Esc only closed the list");
h.step(&json!({"hold": at})).unwrap();
h.step(&json!({"click": [at[0].as_f64().unwrap() + 200.0, at[1].as_f64().unwrap() + 150.0]})).unwrap();
assert!(!crate::select_other::is_open(&h.app));
assert!(h.app.session.selection.is_empty(), "{:?}", h.app.session.selection);
}
#[test]
fn clicking_again_on_the_same_spot_selects_the_next_thing_under_it() {
let (mut h, at) = two_lines();
h.step(&json!({"click": at})).unwrap();
assert_eq!(curves(&h), ["l1"]);
// A deliberate second click (not part of a double-click).
h.frames(30);
h.step(&json!({"click": at})).unwrap();
assert_eq!(curves(&h), ["l2"]);
h.frames(30);
h.step(&json!({"click": at})).unwrap();
assert_eq!(curves(&h), ["l1"], "it wraps around");
// A click somewhere else starts over.
let x = at[0].as_f64().unwrap();
let y = at[1].as_f64().unwrap();
h.frames(30);
h.step(&json!({"click": [x + 40.0, y]})).unwrap();
h.frames(30);
h.step(&json!({"click": at})).unwrap();
assert_eq!(curves(&h), ["l1"]);
}
#[test]
fn a_double_click_does_not_cycle() {
let (mut h, at) = two_lines();
h.step(&json!({"click": at, "double": true})).unwrap();
// The double-click's chain starts from the line under the cursor.
assert!(curves(&h).contains(&"l1".to_string()), "{:?}", h.app.session.selection);
}
#[test]
fn the_viewport_menu_offers_select_other() {
let (mut h, at) = two_lines();
h.step(&json!({"click": at, "button": "right"})).unwrap();
let menu = h.call("ui.menu", json!({}))["result"].clone();
let items: Vec<&str> = menu["items"].as_array().into_iter().flatten().filter_map(|i| i["id"].as_str()).collect();
assert!(items.contains(&"ui.selectOther"), "{items:?}");
let r = h.call("ui.menuPick", json!({"item": "ui.selectOther"}));
assert_eq!(r["ok"], true, "{r}");
assert!(labels(&h).starts_with(&["Line 1".to_string(), "Line 2".to_string()]), "{:?}", labels(&h));
// The control channel picks rows too.
let r = h.call("ui.selectOther", json!({"pick": 1}));
assert_eq!(r["ok"], true, "{r}");
assert_eq!(curves(&h), ["l2"]);
// Over empty space there is nothing to offer.
h.step(&json!({"click": [40, 400], "button": "right"})).unwrap();
let menu = h.call("ui.menu", json!({}))["result"].clone();
assert!(!menu["items"].as_array().into_iter().flatten().any(|i| i["id"] == "ui.selectOther"), "{menu}");
}
#[test]
fn overlapping_bodies_list_both_and_the_hidden_one_can_be_picked() {
let mut h = Harness::new();
h.app.ui.show_origin = false;
h.app.run("solid.box", json!({"length":10,"width":10,"height":10,"body_name":"Inner"})).unwrap();
h.app.run("solid.box", json!({"length":10,"width":10,"height":10,"corner":[5,5,5],"body_name":"Outer"})).unwrap();
h.call("ui.view", json!({"view":"top"}));
h.call("ui.view", json!({"view":"fit"}));
h.step(&json!({"hold": {"world": [7, 7, 15]}})).unwrap();
let rows = labels(&h);
assert!(rows.iter().any(|r| r == "Body · Outer") && rows.iter().any(|r| r == "Body · Inner"), "{rows:?}");
let faces = rows.iter().filter(|r| r.starts_with("Face ")).count();
assert_eq!(faces, 2, "the nearest face of each body: {rows:?}");
let i = rows.iter().position(|r| r == "Body · Inner").unwrap();
h.call("ui.selectOther", json!({"pick": i}));
assert_eq!(h.app.session.selection, [Sel::Body { name: "Inner".into() }]);
}
#[test]
fn a_dialog_selection_input_takes_its_pick_from_the_list() {
let mut h = Harness::new();
h.app.ui.show_origin = false;
h.app.run("solid.box", json!({"length":10,"width":10,"height":10,"body_name":"Block"})).unwrap();
h.call("ui.view", json!({"view":"home"}));
h.call("ui.view", json!({"view":"fit"}));
h.step(&json!({"start": "solid.fillet"})).unwrap();
h.frames(2);
h.step(&json!({"hold": {"world": [5, 0, 10]}})).unwrap();
let rows = crate::select_other::describe(&h.app)["rows"].clone();
let sels: Vec<&Value> = rows.as_array().into_iter().flatten().map(|r| &r["sel"]).collect();
assert!(!sels.is_empty(), "{rows}");
// The edge in front, then the face behind it; no bodies, which the fillet doesn't take.
assert_eq!(sels[0]["type"], "edge", "{rows}");
assert!(sels.iter().all(|s| s["type"] == "edge" || s["type"] == "face"), "only what the fillet's input accepts: {rows}");
h.call("ui.selectOther", json!({"pick": 0}));
let d = h.app.dialog.as_ref().expect("the fillet dialog stays open");
assert!(d.inputs.iter().any(|i| i.items.iter().any(|x| matches!(x, Sel::Edge { .. }))), "{:?}", d.inputs);
assert!(h.app.session.selection.is_empty(), "the pick went to the dialog, not the selection");
}
#[test]
fn rows_name_points_by_their_curves() {
let mut h = Harness::new();
h.app.ui.show_origin = false;
h.app.run("sketch.create", json!({"plane":"XY"})).unwrap();
h.app.run("sketch.circle.center", json!({"center":[10,10],"radius":3})).unwrap();
h.app.cam.distance = 60.0;
h.settle();
let at = h.call("ui.at", json!({"sketch":[10,10]}))["result"].clone();
h.call("ui.selectOther", json!({"x": at[0], "y": at[1]}));
let rows = labels(&h);
assert!(rows.iter().any(|r| r.starts_with("Point (") && r.contains("centre")), "{rows:?}");
}
#[test]
fn a_dense_spot_is_capped_per_kind_and_reports_the_total() {
let mut h = Harness::new();
h.app.ui.show_origin = false;
h.app.run("sketch.create", json!({"plane":"XY"})).unwrap();
// Thirty lines drawn on top of each other: one kind of thing, far beyond the per-kind cap.
for _ in 0..30 {
h.app.run("sketch.line", json!({"points":[[0,10],[20,10]],"infer":false,"connect":false})).unwrap();
}
h.app.cam.distance = 60.0;
h.settle();
let at = h.call("ui.at", json!({"sketch":[10,10]}))["result"].clone();
h.step(&json!({"hold": at})).unwrap();
assert!(crate::select_other::is_open(&h.app));
let d = crate::select_other::describe(&h.app);
let rows = d["rows"].as_array().expect("rows");
let lines = rows.iter().filter(|r| r["label"].as_str().is_some_and(|l| l.starts_with("Line "))).count();
assert_eq!(lines, 20, "at most 20 rows of one kind, nearest first: {rows:?}");
assert_eq!(rows[0]["label"], json!("Line 1"), "the nearest still leads");
let total = d["total"].as_u64().expect("total");
assert!(total >= 30, "the total counts every candidate, not just the rows: {total}");
assert!((total as usize) < rows.len() + 30, "the list is truncated: {total} total, {} rows", rows.len());
}