Files
cadcraft/crates/engine/tests/options_profile.rs
261d46adcf Selection like AutoCAD: colour highlight and the Options Selection tab (#808)
Selecting or hovering an object outlined each filled primitive (TrueType
glyphs, dimension and leader arrowheads, solid hatches, wide polylines)
by the rectangle around its triangles, and dashed everything else. A
selected dimension's text became a row of dashed boxes, one per glyph,
and its closed arrowheads small rectangles.

Selected objects are now redrawn in the selection colour: lines solid,
fills filled half see-through (so the lines and text on a highlighted
hatch stay visible), and the whole object reads as selected, text and
arrowheads included. A wipeout is always outlined, since filling it would
hide the drawing under it.

The Options dialog's Selection tab is laid out as AutoCAD's, with the
settings CADCraft can act on, each one AutoCAD's variable, kept in the
user profile:

- Selection modes: noun/verb selection (PICKFIRST), Shift to add
  (PICKADD), Object grouping (PICKSTYLE bit 1, now saved in the profile).
- Selection effect color (selectionColor, an interface preference; null
  is the theme's).
- Grips: Show grips (GRIPS) and the object selection limit for display of
  grips (GRIPOBJLIMIT, 100; 0 no limit): grips are neither drawn nor
  picked beyond it.
- Selection preview (SELECTIONPREVIEW): the object under the cursor is
  highlighted when a command is active (2) and when none is (1).
- Visual Effect Settings: the selection area effect (SELECTIONAREA,
  WINDOWAREACOLOR 150, CROSSINGAREACOLOR 100, SELECTIONAREAOPACITY 25)
  and the Selection Preview Filter (PREVIEWFILTER 3: 1 objects on locked
  layers, 2 xrefs, 4 tables, 8 multiline text, 16 hatches, 32 groups).

As in AutoCAD these are typed, not on the dialog: HIGHLIGHT (0 shows only
the grips of selected objects; not saved, so every session starts
highlighted), SELECTIONEFFECT (1 the selection colour, 0 dashed lines with
fills outlined along their real boundary) and PREVIEWEFFECT (the preview
dashed 0, thicker 1 or both 2).

Left for later, as CADCraft lacks what they control: lasso, grip tips and
menus, group grips, grips within blocks, associative hatch selection,
command and property preview, PICKDRAG, PICKAUTO and PROPOBJLIMIT.

DisplayList::fill_outlines returns the boundary of the region a filled
primitive covers as closed polylines (edges only one triangle has; the
slab filler's horizontal cuts, which need not line up between trapezoids,
are split at every x on their line and kept where an odd number cover
them), falling back to the bounding box over 600,000 vertices. The GPU
and CPU highlight paths and the drawing comparison overlay share the same
looks.

The new labels have Ukrainian rows (the required catalog); other
languages fall back to English.

Co-authored-by: Cody Vogt <18035288+codyvogt@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-13 06:09:39 -04:00

189 lines
8.4 KiB
Rust

//! OPTIONS: the JSON form reads and sets the user-profile variables without a dialog.
use cadcraft_engine::Session;
use cadcraft_engine::cmd::options::{PROFILE_VARS, profile, restore};
use serde_json::json;
#[test]
fn options_json_reports_sets_and_resets_the_profile() {
let mut s = Session::new();
let all = s.execute("options", &json!({})).unwrap();
for name in PROFILE_VARS {
assert!(all.get(*name).is_some(), "{name} is reported");
}
assert_eq!(all["PICKBOX"], json!(3.0));
// Names in any case; the reply is the whole profile.
let r = s.execute("options", &json!({"pickbox": 6, "GRIPSIZE": 9, "PICKFIRST": 0, "CursorSize": 25})).unwrap();
assert_eq!(
(r["PICKBOX"].clone(), r["GRIPSIZE"].clone(), r["PICKFIRST"].clone(), r["CURSORSIZE"].clone()),
(json!(6.0), json!(9.0), json!(0), json!(25.0))
);
assert_eq!(s.execute("getvar", &json!({"name": "PICKBOX"})).unwrap()["value"], json!(6.0));
// All or nothing: a bad name or value changes nothing.
assert!(s.execute("options", &json!({"PICKBOX": 2, "LUNITS": 4})).is_err(), "drawing variables are not profile settings");
assert!(s.execute("options", &json!({"APERTURE": 20, "SNAPUNIT": "wide"})).is_err());
assert!(s.execute("options", &json!({"reset": "yes"})).is_err());
assert!(s.execute("options", &json!([1, 2])).is_err());
let now = s.execute("options", &json!({})).unwrap();
assert_eq!((now["PICKBOX"].clone(), now["APERTURE"].clone()), (json!(6.0), json!(10.0)));
// Reset, then apply in the same call.
let r = s.execute("options", &json!({"reset": true, "APERTURE": 15})).unwrap();
assert_eq!((r["PICKBOX"].clone(), r["GRIPSIZE"].clone(), r["APERTURE"].clone()), (json!(3.0), json!(5.0), json!(15.0)));
}
#[test]
fn saved_profile_restores_and_hostile_entries_are_skipped() {
let mut a = Session::new();
a.execute("options", &json!({"OSMODE": 515, "ORTHOMODE": 1, "POLARANG": 15, "GRIDUNIT": [2, 2], "DYNMODE": 0, "QPMODE": 1})).unwrap();
let saved = profile(&a);
let mut b = Session::new();
restore(&mut b, &saved);
assert_eq!(profile(&b), saved);
// Unknown names, drawing variables, bad values and non-objects are ignored, never panic.
let mut c = Session::new();
restore(&mut c, &json!({"PICKBOX": "huge", "LUNITS": 4, "NOPE": 1, "POLARANG": -5, "SNAPUNIT": ["x"], "GRIPSIZE": 8}));
restore(&mut c, &json!("not an object"));
restore(&mut c, &json!(null));
let p = profile(&c);
assert_eq!((p["PICKBOX"].clone(), p["GRIPSIZE"].clone()), (json!(3.0), json!(8.0)));
assert_ne!(c.execute("getvar", &json!({"name": "LUNITS"})).ok().map(|v| v["value"].clone()), Some(json!(4)));
}
#[test]
fn typed_options_without_a_dialog_reports_how_to_change_settings() {
let mut s = Session::new();
s.cmdline("OP").unwrap();
assert!(s.log.iter().any(|l| l.contains("SETVAR")), "{:?}", s.log);
}
fn getvar(s: &mut Session, name: &str) -> serde_json::Value {
s.execute("getvar", &json!({"name": name})).unwrap()["value"].clone()
}
#[test]
fn selection_highlight_variables() {
let mut s = Session::new();
// AutoCAD's defaults: highlighted, the colour effect, preview with and without a command,
// the preview dashed and thicker.
for (name, v) in [("HIGHLIGHT", 1), ("SELECTIONEFFECT", 1), ("SELECTIONPREVIEW", 3), ("PREVIEWEFFECT", 2)] {
assert_eq!(getvar(&mut s, name), json!(v), "{name}");
}
s.execute("setvar", &json!({"name": "HIGHLIGHT", "value": 0})).unwrap();
s.execute("options", &json!({"SELECTIONEFFECT": 0, "SELECTIONPREVIEW": 2, "PREVIEWEFFECT": 1})).unwrap();
let saved = profile(&s);
assert_eq!(saved["SELECTIONEFFECT"], json!(0));
// HIGHLIGHT lasts the session only (AutoCAD doesn't save it either): not in the profile, and
// OPTIONS, which sets profile variables, refuses it.
assert!(saved.get("HIGHLIGHT").is_none());
assert!(s.execute("options", &json!({"HIGHLIGHT": 0})).is_err());
for (name, value) in [("SELECTIONEFFECT", json!(2)), ("SELECTIONPREVIEW", json!(4)), ("PREVIEWEFFECT", json!(-1)), ("PREVIEWEFFECT", json!("x"))]
{
assert!(s.execute("setvar", &json!({"name": name, "value": value})).is_err(), "{name} {value}");
}
assert_eq!(getvar(&mut s, "SELECTIONPREVIEW"), json!(2), "a refused value changes nothing");
// A new session restores the profile and starts highlighted, even from a profile saved
// with HIGHLIGHT 0 by a build that kept it.
let mut t = Session::new();
let mut old = saved.clone();
old["HIGHLIGHT"] = json!(0);
restore(&mut t, &old);
assert_eq!((getvar(&mut t, "HIGHLIGHT"), getvar(&mut t, "SELECTIONPREVIEW"), getvar(&mut t, "PREVIEWEFFECT")), (json!(1), json!(2), json!(1)));
}
#[test]
fn selection_preview_follows_its_bits() {
let mut s = Session::new();
s.settings.pickfirst = true;
s.settings.selectionpreview = 3;
assert!(s.previews_selection(), "no command, bit 1");
s.settings.selectionpreview = 2;
assert!(!s.previews_selection(), "bit 1 off");
s.start("erase").unwrap();
assert!(s.picking_objects());
assert!(s.previews_selection(), "ERASE asks for objects, bit 2");
s.settings.selectionpreview = 1;
assert!(!s.previews_selection(), "bit 2 off");
}
#[test]
fn selection_tab_variables_have_autocads_defaults_ranges_and_are_saved() {
let mut s = Session::new();
for (name, v) in [
("PREVIEWFILTER", json!(3)),
("GRIPS", json!(1)),
("GRIPOBJLIMIT", json!(100)),
("SELECTIONAREA", json!(1)),
("WINDOWAREACOLOR", json!(150)),
("CROSSINGAREACOLOR", json!(100)),
("SELECTIONAREAOPACITY", json!(25)),
] {
assert_eq!(getvar(&mut s, name), v, "{name}");
}
s.execute("options", &json!({"PREVIEWFILTER": 20, "GRIPS": 0, "GRIPOBJLIMIT": 0, "SELECTIONAREA": 0, "WINDOWAREACOLOR": 5, "CROSSINGAREACOLOR": 3, "SELECTIONAREAOPACITY": 80, "PICKSTYLE": 0})).unwrap();
for (name, value) in [
("PREVIEWFILTER", json!(64)),
("GRIPS", json!(3)),
("GRIPOBJLIMIT", json!(40_000)),
("WINDOWAREACOLOR", json!(0)),
("CROSSINGAREACOLOR", json!(256)),
("SELECTIONAREAOPACITY", json!(101)),
] {
assert!(s.execute("setvar", &json!({"name": name, "value": value})).is_err(), "{name} {value}");
}
let mut t = Session::new();
restore(&mut t, &profile(&s));
assert_eq!(
(
getvar(&mut t, "PREVIEWFILTER"),
getvar(&mut t, "GRIPS"),
getvar(&mut t, "SELECTIONAREA"),
getvar(&mut t, "WINDOWAREACOLOR"),
getvar(&mut t, "PICKSTYLE")
),
(json!(20), json!(0), json!(0), json!(5), json!(0)),
"saved with the profile"
);
}
#[test]
fn grips_show_up_to_the_object_limit() {
let mut s = Session::new();
assert!(s.shows_grips(100) && !s.shows_grips(101), "GRIPOBJLIMIT 100");
s.settings.gripobjlimit = 0;
assert!(s.shows_grips(100_000), "0: no limit");
s.settings.grips = 0;
assert!(!s.shows_grips(1), "GRIPS 0");
}
#[test]
fn preview_filter_excludes_each_kind_it_names() {
use cadcraft_engine::select::preview_filtered;
let mut s = Session::new();
s.execute("mtext", &json!({"at": [0, 0], "text": "NOTE", "height": 2})).unwrap();
s.execute("rectang", &json!({"p1": [10, 0], "p2": [20, 10]})).unwrap();
s.execute("hatch", &json!({"points": [[15, 5]], "pattern": "SOLID"})).unwrap();
s.execute("line", &json!({"points": [[30, 0], [40, 0]]})).unwrap();
let d = s.doc().unwrap();
let find = |k: &str| d.model.iter().find(|e| e.kind.type_name() == k).unwrap().clone();
let (mtext, hatch, line) = (find("MText"), find("Hatch"), find("Line"));
assert!(preview_filtered(d, &mtext, 8) && !preview_filtered(d, &mtext, 16));
assert!(preview_filtered(d, &hatch, 16) && !preview_filtered(d, &hatch, 8));
assert!(!preview_filtered(d, &line, 63), "a line on an unlocked layer, in no group");
// Locked layers (1) and groups (32).
let d = s.doc_mut().unwrap();
d.ensure_layer("LOCKED");
if let Some(l) = d.layers.iter_mut().find(|l| l.name == "LOCKED") {
l.locked = true;
}
d.model.modify(line.handle, |e| e.common.layer = "LOCKED".into());
d.groups.push(cadcraft_engine::doc::Group { name: "G".into(), description: String::new(), selectable: true, members: vec![mtext.handle] });
let d = s.doc().unwrap();
let line = d.entity(line.handle).unwrap();
assert!(preview_filtered(d, line, 1) && !preview_filtered(d, line, 62));
assert!(preview_filtered(d, &mtext, 32));
}