mirror of
https://github.com/storytold/cadcraft.git
synced 2026-10-26 03:46:53 -06:00
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>
189 lines
8.4 KiB
Rust
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));
|
|
}
|