mirror of
https://github.com/storytold/cadcraft.git
synced 2026-11-03 00:42:46 -06:00
* -ATTDEF at the command line: attribute modes, AFLAGS, and the text's prompts
-ATTDEF asked for the tag first. A script's mode answer ("_V", "_C",
"_P") became the tag; the attribute kept no mode; AFLAGS never changed.
The definition was made at the start point, so the height and rotation a
script gives next ran as commands. It now asks as AutoCAD 2024 does:
- "Current attribute modes:" from AFLAGS, then "Enter an option to
change [Invisible/Constant/Verify/Preset/Lock position/Annotative/
Multiple lines] <done>", each answer toggling a mode, kept in AFLAGS.
- The tag (spaces taken, upper-cased; an empty one gets "Tag cannot be
null"), the prompt and default value, or a constant attribute's value.
- "Current text style: ...", the point with Justify and Style, the
height and the rotation.
The definition takes Invisible, Constant, Verify, Preset and Lock
position from the modes; a constant attribute stays in its block.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* -ATTDEF: a typed height is a distance, as TEXT takes it
The height was read with str::parse, so 1'6" (or any distance TEXT
takes) was refused as "Requires a positive height." It now goes through
the shared distance parser, `number`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* -ATTDEF: a wrong answer at Justify is asked again
A wrong answer at "Enter an option [Left/Center/...]", or Align or Fit
(not available here), went on to the point prompt with the justification
unchanged, so a script's next answers landed one prompt off. Only a
justification goes on to its point now; anything else is asked again.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* -ATTDEF test: the modes start from AFLAGS, not from the default
The other cases start from AFLAGS 16, which is also the default, so
reading the modes from the drawing went untested. This starts from 0.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
187 lines
8.9 KiB
Rust
187 lines
8.9 KiB
Rust
//! -ATTDEF at the command line, as AutoCAD 2024 runs it (accoreconsole: these scripts with
|
|
//! TEXTSIZE 2.5 and AFLAGS 16, the drawing and AFLAGS read back afterwards).
|
|
|
|
use cadcraft_engine::doc::{Attrib, EntityKind, HAlign, VAlign};
|
|
use cadcraft_engine::{Session, sysvars};
|
|
use serde_json::json;
|
|
|
|
fn session() -> Session {
|
|
let mut s = Session::new();
|
|
s.execute("setvar", &json!({ "name": "TEXTSIZE", "value": 2.5 })).unwrap();
|
|
s.execute("setvar", &json!({ "name": "AFLAGS", "value": 16 })).unwrap();
|
|
s
|
|
}
|
|
|
|
fn attdefs(s: &Session) -> Vec<Attrib> {
|
|
s.doc().unwrap().model.iter().filter_map(|e| if let EntityKind::AttDef(a) = &e.kind { Some(a.clone()) } else { None }).collect()
|
|
}
|
|
|
|
fn aflags(s: &Session) -> i64 {
|
|
sysvars::get(s, "AFLAGS").and_then(|v| v.as_f64()).unwrap() as i64
|
|
}
|
|
|
|
fn run(script: &str) -> Session {
|
|
let mut s = session();
|
|
s.cmdline(script).unwrap();
|
|
assert!(s.running.is_none(), "{script}: {}", s.prompt_text());
|
|
s
|
|
}
|
|
|
|
#[test]
|
|
fn modes_are_toggled_kept_in_aflags_and_stored_on_the_definition() {
|
|
// (script, AFLAGS after, invisible, constant, verify, preset, lock position, tag, prompt, value)
|
|
type Case = (&'static str, i64, [bool; 5], &'static str, &'static str, &'static str);
|
|
let cases: [Case; 5] = [
|
|
// ATTDEF-000: the modes as they are.
|
|
(
|
|
"(command \"_.-ATTDEF\" \"\" \"T1\" \"Nhap ten\" \"MAC\" '(5.0 10.0) 2.5 0)",
|
|
16,
|
|
[false, false, false, false, true],
|
|
"T1",
|
|
"Nhap ten",
|
|
"MAC",
|
|
),
|
|
// ATTDEF-001: Invisible and Verify.
|
|
(
|
|
"(command \"_.-ATTDEF\" \"_I\" \"_V\" \"\" \"T2\" \"Hoi hai\" \"D2\" '(5.0 10.0) 3.0 30)",
|
|
21,
|
|
[true, false, true, false, true],
|
|
"T2",
|
|
"Hoi hai",
|
|
"D2",
|
|
),
|
|
// ATTDEF-002: Constant asks for the value, not a prompt and a default.
|
|
("(command \"_.-ATTDEF\" \"_C\" \"\" \"T3\" \"GIATRI\" '(5.0 10.0) 2.5 0)", 18, [false, true, false, false, true], "T3", "", "GIATRI"),
|
|
// ATTDEF-003: Preset.
|
|
(
|
|
"(command \"_.-ATTDEF\" \"_P\" \"\" \"T4\" \"Hoi bon\" \"D4\" '(612350.678 2398775.432) 2.5 0)",
|
|
24,
|
|
[false, false, false, true, true],
|
|
"T4",
|
|
"Hoi bon",
|
|
"D4",
|
|
),
|
|
// ATTDEF-004: Lock position off.
|
|
(
|
|
"(command \"_.-ATTDEF\" \"_L\" \"\" \"T5\" \"Hoi nam\" \"D5\" '(5.0 10.0) 2.5 0)",
|
|
0,
|
|
[false, false, false, false, false],
|
|
"T5",
|
|
"Hoi nam",
|
|
"D5",
|
|
),
|
|
];
|
|
for (script, flags, [inv, con, ver, pre, lock], tag, prompt, value) in cases {
|
|
let s = run(script);
|
|
assert_eq!(aflags(&s), flags, "{script}");
|
|
let a = attdefs(&s).remove(0);
|
|
assert_eq!((a.invisible, a.constant, a.modes.verify, a.modes.preset, a.modes.lock_position), (inv, con, ver, pre, lock), "{script}");
|
|
assert_eq!((a.tag.as_str(), a.prompt.as_str(), a.text.value.as_str()), (tag, prompt, value), "{script}");
|
|
}
|
|
// ATTDEF-001's height 3 and 30 degrees.
|
|
let a = attdefs(&run(cases[1].0)).remove(0);
|
|
assert!((a.text.height - 3.0).abs() < 1e-12 && (a.text.rotation - 30f64.to_radians()).abs() < 1e-12, "{a:?}");
|
|
}
|
|
|
|
#[test]
|
|
fn tags_justification_and_style_as_autocad_takes_them() {
|
|
// ATTDEF-021, ATTDEF-022: a tag is upper-cased, spaces and all.
|
|
for (typed, tag) in [("maso", "MASO"), ("ten ban ve", "TEN BAN VE")] {
|
|
let s = run(&format!("(command \"_.-ATTDEF\" \"\" \"{typed}\" \"Hoi\" \"D\" '(5.0 10.0) 2.5 0)"));
|
|
assert_eq!(attdefs(&s)[0].tag, tag);
|
|
}
|
|
// ATTDEF-023: an empty prompt stays empty.
|
|
let s = run("(command \"_.-ATTDEF\" \"\" \"T10\" \"\" \"D10\" '(612350.678 2398775.432) 2.5 0)");
|
|
assert_eq!(attdefs(&s)[0].prompt, "");
|
|
// ATTDEF-025: an empty tag is refused and asked again.
|
|
let s = run("(command \"_.-ATTDEF\" \"\" \"\" \"T11\" \"Hoi muoi mot\" \"D11\" '(5.0 10.0) 2.5 0)");
|
|
assert!(s.log.iter().any(|l| l == "Tag cannot be null"), "{:?}", s.log);
|
|
assert_eq!(attdefs(&s)[0].tag, "T11");
|
|
// ATTDEF-005: Justify MC at the middle point.
|
|
let s = run("(command \"_.-ATTDEF\" \"\" \"T6\" \"Hoi sau\" \"D6\" \"_J\" \"_MC\" '(5.0 10.0) 2.5 0)");
|
|
let a = attdefs(&s).remove(0);
|
|
assert_eq!((a.text.halign, a.text.valign), (HAlign::Center, VAlign::Middle));
|
|
assert!(a.text.align_pt.is_some_and(|p| p.x == 5.0 && p.y == 10.0), "{a:?}");
|
|
// ATTDEF-006: Style, then the point.
|
|
let s = run("(command \"_.-ATTDEF\" \"\" \"T7\" \"Hoi bay\" \"D7\" \"_S\" \"Standard\" '(0.505 0.26) 2.5 0)");
|
|
let a = attdefs(&s).remove(0);
|
|
assert!(a.text.style == "Standard" && a.text.insert.x == 0.505 && a.text.insert.y == 0.26, "{a:?}");
|
|
}
|
|
|
|
#[test]
|
|
fn the_prompts_are_autocads() {
|
|
let mut s = session();
|
|
s.cmdline("-attdef").unwrap();
|
|
assert!(s.log.iter().any(|l| l == "Current attribute modes:"), "{:?}", s.log);
|
|
assert_eq!(s.log.last().map(String::as_str), Some("Invisible=N Constant=N Verify=N Preset=N Lock position=Y Annotative=N Multiple line=N"));
|
|
assert!(s.prompt_text().contains("[Invisible/Constant/Verify/Preset/Lock position/Annotative/Multiple lines] <done>:"), "{}", s.prompt_text());
|
|
s.cmdline("X").unwrap();
|
|
assert_eq!(s.log.last().map(String::as_str), Some("Invalid option keyword."));
|
|
s.cmdline("_C").unwrap();
|
|
assert_eq!(s.log.last().map(String::as_str), Some("Invisible=N Constant=Y Verify=N Preset=N Lock position=Y Annotative=N Multiple line=N"));
|
|
s.cmdline("").unwrap();
|
|
assert!(s.prompt_text().ends_with("Enter attribute tag name:"), "{}", s.prompt_text());
|
|
s.cmdline("T3").unwrap();
|
|
assert!(s.prompt_text().ends_with("Enter attribute value:"), "{}", s.prompt_text());
|
|
s.cmdline("GIATRI").unwrap();
|
|
assert_eq!(s.log.last().map(String::as_str), Some("Current text style: \"Standard\" Text height: 2.5000 Justify: Left"));
|
|
assert!(s.prompt_text().ends_with("Specify start point of text or [Justify/Style]:"), "{}", s.prompt_text());
|
|
s.cmdline("_J").unwrap();
|
|
// A wrong answer, or a justification not available here, is asked again.
|
|
for typed in ["X", "_A"] {
|
|
s.cmdline(typed).unwrap();
|
|
assert!(s.prompt_text().contains("[Left/Center/Right/Align/Middle/Fit/TL/"), "{typed}: {}", s.prompt_text());
|
|
}
|
|
s.cmdline("_MC").unwrap();
|
|
assert!(s.prompt_text().ends_with("Specify middle point of text:"), "{}", s.prompt_text());
|
|
s.cmdline("0,0").unwrap();
|
|
assert!(s.prompt_text().ends_with("Specify height <2.5000>:"), "{}", s.prompt_text());
|
|
s.cmdline("").unwrap();
|
|
assert!(s.prompt_text().ends_with("Specify rotation angle of text <0>:"), "{}", s.prompt_text());
|
|
s.cancel();
|
|
}
|
|
|
|
#[test]
|
|
fn a_constant_attribute_stays_in_the_block() {
|
|
// INSERTATT-011: the constant TC is no attribute of the reference; the other one, TN, is.
|
|
let mut s = session();
|
|
s.execute("line", &json!({ "points": [[612345.678, 2398765.432], [612355.678, 2398765.432]] })).unwrap();
|
|
s.cmdline("(setq hl_A (entlast))").unwrap();
|
|
s.cmdline("(progn (command \"_.-ATTDEF\" \"_C\" \"\" \"TC\" \"HANG\" '(612350.678 2398775.432) 2.5 0) (setq hl_a1 (entlast)))").unwrap();
|
|
s.cmdline("(setvar \"AFLAGS\" 16)").unwrap();
|
|
s.cmdline("(progn (command \"_.-ATTDEF\" \"\" \"TN\" \"Hoi N\" \"MN\" '(612350.678 2398785.432) 2.5 0) (setq hl_a2 (entlast)))").unwrap();
|
|
s.cmdline("(command \"_.-BLOCK\" \"B11\" '(612345.678 2398765.432) hl_a1 hl_a2 hl_A \"\")").unwrap();
|
|
s.cmdline("(command \"_.-INSERT\" \"B11\" '(612445.678 2398815.432) 1 1 0 \"XN\")").unwrap();
|
|
assert!(s.running.is_none(), "{}", s.prompt_text());
|
|
let ins = s.doc().unwrap().model.iter().find_map(|e| if let EntityKind::Insert(i) = &e.kind { Some(i.clone()) } else { None }).unwrap();
|
|
let tags: Vec<(String, String)> = ins.attribs.iter().map(|a| (a.tag.clone(), a.text.value.clone())).collect();
|
|
assert_eq!(tags, [("TN".to_string(), "XN".to_string())]);
|
|
}
|
|
|
|
#[test]
|
|
fn a_typed_height_is_a_distance() {
|
|
let mut s = session();
|
|
for typed in ["-attdef", "", "T1", "P", "V", "5,10", "x"] {
|
|
s.cmdline(typed).unwrap();
|
|
}
|
|
assert_eq!(s.log.last().map(String::as_str), Some("Requires a positive height."));
|
|
// Feet and inches, as TEXT takes them.
|
|
for typed in ["1'6\"", "0"] {
|
|
s.cmdline(typed).unwrap();
|
|
}
|
|
assert!(s.running.is_none(), "{}", s.prompt_text());
|
|
assert_eq!(attdefs(&s)[0].text.height, 18.0);
|
|
}
|
|
|
|
#[test]
|
|
fn the_modes_start_from_aflags() {
|
|
// AFLAGS 0, not the default 16: Invisible toggled on gives 1, and nothing else.
|
|
let mut s = session();
|
|
s.execute("setvar", &json!({ "name": "AFLAGS", "value": 0 })).unwrap();
|
|
s.cmdline("(command \"_.-ATTDEF\" \"_I\" \"\" \"T1\" \"Hoi\" \"D\" '(5.0 10.0) 2.5 0)").unwrap();
|
|
assert!(s.running.is_none(), "{}", s.prompt_text());
|
|
assert_eq!(aflags(&s), 1);
|
|
let a = attdefs(&s).remove(0);
|
|
assert_eq!((a.invisible, a.modes.lock_position), (true, false));
|
|
}
|