mirror of
https://github.com/storytold/cadcraft.git
synced 2026-11-02 16:31:48 -06:00
LEADER had only its JSON form, so a script's LEADER drew nothing. This adds the command-line command with AutoCAD 2024's prompts: the points, Format (Spline/STraight/Arrow/None) and Undo, then annotation lines, or the options None, Block, Copy, and Mtext and Tolerance (which come back to the first line at the command line, as in accoreconsole). Measured, and followed: - A hook line, a horizontal last segment DIMASZ long, is added when the last segment is straight and more than 15 degrees off horizontal and text follows: 14.9 and 15 degrees keep the end, 15.1 hooks. A spline, no annotation and a block take none. - The text sits DIMGAP past the end on the side the leader runs to: above it, bottom-attached, with DIMTAD on; centred on it with DIMTAD 0. Sizes are DIMSCALE times the style's, with SETVAR overrides. - A copied text keeps its contents, height and width, is centred on its line, and with DIMTAD on the leader ends half the text height lower. The JSON form now draws through the same code. The drawing model has no link from a leader to its annotation, so the two are separate objects. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
341 lines
14 KiB
Rust
341 lines
14 KiB
Rust
//! LEADER at the command line, as AutoCAD 2024 runs it (accoreconsole: these scripts, with
|
|
//! DIMTAD 1, DIMGAP 0.625, DIMASZ 2.5, DIMSCALE 1 and DIMTXT 2.5 unless a case sets another, and
|
|
//! the drawing read back afterwards).
|
|
|
|
use cadcraft_engine::Session;
|
|
use cadcraft_engine::doc::{EntityKind, Insert, Leader, MText};
|
|
use serde_json::json;
|
|
|
|
fn near(a: f64, b: f64) -> bool {
|
|
(a - b).abs() <= 1e-9 * a.abs().max(b.abs()).max(1.0)
|
|
}
|
|
|
|
/// A drawing as the measured scripts start from: the dimension settings, and a text "XYZ" 2.5
|
|
/// high at (100,40) that the Copy cases copy (`(entlast)` after it).
|
|
fn session(setup: &str) -> Session {
|
|
let mut s = Session::new();
|
|
for (name, v) in [("DIMTAD", 1.0), ("DIMGAP", 0.625), ("DIMASZ", 2.5), ("DIMSCALE", 1.0), ("DIMTXT", 2.5)] {
|
|
s.execute("setvar", &json!({ "name": name, "value": v })).unwrap();
|
|
}
|
|
s.execute("text", &json!({ "at": [100, 40], "text": "XYZ", "height": 2.5 })).unwrap();
|
|
s.cmdline("(setq hl_T (entlast))").unwrap();
|
|
if !setup.is_empty() {
|
|
s.cmdline(setup).unwrap();
|
|
}
|
|
s
|
|
}
|
|
|
|
fn run(setup: &str, script: &str) -> Session {
|
|
let mut s = session(setup);
|
|
s.cmdline(script).unwrap();
|
|
assert!(s.running.is_none(), "{script}: {}", s.prompt_text());
|
|
s
|
|
}
|
|
|
|
fn leaders(s: &Session) -> Vec<Leader> {
|
|
s.doc().unwrap().model.iter().filter_map(|e| if let EntityKind::Leader(l) = &e.kind { Some(l.clone()) } else { None }).collect()
|
|
}
|
|
|
|
fn mtexts(s: &Session) -> Vec<MText> {
|
|
s.doc().unwrap().model.iter().filter_map(|e| if let EntityKind::MText(m) = &e.kind { Some(m.clone()) } else { None }).collect()
|
|
}
|
|
|
|
fn inserts(s: &Session) -> Vec<Insert> {
|
|
s.doc().unwrap().model.iter().filter_map(|e| if let EntityKind::Insert(i) = &e.kind { Some(i.clone()) } else { None }).collect()
|
|
}
|
|
|
|
fn vertices(l: &Leader) -> Vec<[f64; 2]> {
|
|
l.vertices.iter().map(|v| [v.x, v.y]).collect()
|
|
}
|
|
|
|
fn same_points(got: &[[f64; 2]], want: &[[f64; 2]]) -> bool {
|
|
got.len() == want.len() && got.iter().zip(want).all(|(g, w)| near(g[0], w[0]) && near(g[1], w[1]))
|
|
}
|
|
|
|
/// (case, setup, script, leader vertices, text insertion point, attachment, height, contents)
|
|
type Case = (&'static str, &'static str, &'static str, &'static [[f64; 2]], [f64; 2], u8, f64, &'static str);
|
|
|
|
#[test]
|
|
fn leaders_and_their_text_are_where_autocad_puts_them() {
|
|
let cases: [Case; 17] = [
|
|
// A horizontal last segment: no hook line; the text DIMGAP right of and above the end.
|
|
(
|
|
"000",
|
|
"",
|
|
"(command \"_.LEADER\" '(0.0 0.0) '(20.0 15.0) '(40.0 15.0) \"\" \"ABC\" \"\")",
|
|
&[[0.0, 0.0], [20.0, 15.0], [40.0, 15.0]],
|
|
[40.625, 15.625],
|
|
7,
|
|
2.5,
|
|
"ABC",
|
|
),
|
|
// More than 15° off horizontal: a hook line DIMASZ long into the end.
|
|
(
|
|
"001",
|
|
"",
|
|
"(command \"_.LEADER\" '(0.0 0.0) '(20.0 15.0) '(35.0 30.0) \"\" \"ABC\" \"\")",
|
|
&[[0.0, 0.0], [20.0, 15.0], [32.5, 30.0], [35.0, 30.0]],
|
|
[35.625, 30.625],
|
|
7,
|
|
2.5,
|
|
"ABC",
|
|
),
|
|
(
|
|
"002",
|
|
"",
|
|
"(command \"_.LEADER\" '(0.0 0.0) '(20.0 15.0) \"\" \"AB\" \"CD\" \"\")",
|
|
&[[0.0, 0.0], [17.5, 15.0], [20.0, 15.0]],
|
|
[20.625, 15.625],
|
|
7,
|
|
2.5,
|
|
"AB\\PCD",
|
|
),
|
|
// To the left: the text right-attached, DIMGAP left of the end.
|
|
(
|
|
"004",
|
|
"",
|
|
"(command \"_.LEADER\" '(0.0 0.0) '(-20.0 15.0) \"\" \"ABC\" \"\")",
|
|
&[[0.0, 0.0], [-17.5, 15.0], [-20.0, 15.0]],
|
|
[-20.625, 15.625],
|
|
9,
|
|
2.5,
|
|
"ABC",
|
|
),
|
|
// A vertical last segment hooks to the right.
|
|
(
|
|
"007-VN",
|
|
"",
|
|
"(command \"_.LEADER\" '(612345.678 2398765.432) '(612345.678 2398795.432) \"\" \"ABC\" \"\")",
|
|
&[[612345.678, 2398765.432], [612343.178, 2398795.432], [612345.678, 2398795.432]],
|
|
[612346.303, 2398796.057],
|
|
7,
|
|
2.5,
|
|
"ABC",
|
|
),
|
|
// 14.9° and 15° keep the end; 15.1° hooks.
|
|
(
|
|
"100",
|
|
"",
|
|
"(command \"_.LEADER\" '(0.0 0.0) '(20.0 15.0) '(40.0 20.32158853384029) \"\" \"ABC\" \"\")",
|
|
&[[0.0, 0.0], [20.0, 15.0], [40.0, 20.32158853384029]],
|
|
[40.625, 20.94658853384029],
|
|
7,
|
|
2.5,
|
|
"ABC",
|
|
),
|
|
(
|
|
"101",
|
|
"",
|
|
"(command \"_.LEADER\" '(0.0 0.0) '(20.0 15.0) '(40.0 20.358983848622454) \"\" \"ABC\" \"\")",
|
|
&[[0.0, 0.0], [20.0, 15.0], [40.0, 20.358983848622454]],
|
|
[40.625, 20.983983848622454],
|
|
7,
|
|
2.5,
|
|
"ABC",
|
|
),
|
|
(
|
|
"102",
|
|
"",
|
|
"(command \"_.LEADER\" '(0.0 0.0) '(20.0 15.0) '(40.0 20.39641415635812) \"\" \"ABC\" \"\")",
|
|
&[[0.0, 0.0], [20.0, 15.0], [37.5, 20.39641415635812], [40.0, 20.39641415635812]],
|
|
[40.625, 21.02141415635812],
|
|
7,
|
|
2.5,
|
|
"ABC",
|
|
),
|
|
// DIMTAD 0: centred on the end, left- or right-attached.
|
|
(
|
|
"020",
|
|
"(setvar \"DIMTAD\" 0)",
|
|
"(command \"_.LEADER\" '(0.5 0.25) '(0.52 0.265) '(0.54 0.265) \"\" \"ABC\" \"\")",
|
|
&[[0.5, 0.25], [0.52, 0.265], [0.54, 0.265]],
|
|
[1.165, 0.265],
|
|
4,
|
|
2.5,
|
|
"ABC",
|
|
),
|
|
(
|
|
"103-VN",
|
|
"(setvar \"DIMTAD\" 0)",
|
|
"(command \"_.LEADER\" '(612345.678 2398765.432) '(612325.678 2398780.432) '(612305.678 2398780.432) \"\" \"ABC\" \"\")",
|
|
&[[612345.678, 2398765.432], [612325.678, 2398780.432], [612305.678, 2398780.432]],
|
|
[612305.053, 2398780.432],
|
|
6,
|
|
2.5,
|
|
"ABC",
|
|
),
|
|
// DIMGAP, DIMSCALE (hook, gap and height) and DIMTXT.
|
|
(
|
|
"021",
|
|
"(setvar \"DIMGAP\" 2.0)",
|
|
"(command \"_.LEADER\" '(0.0 0.0) '(20.0 15.0) '(40.0 15.0) \"\" \"ABC\" \"\")",
|
|
&[[0.0, 0.0], [20.0, 15.0], [40.0, 15.0]],
|
|
[42.0, 17.0],
|
|
7,
|
|
2.5,
|
|
"ABC",
|
|
),
|
|
(
|
|
"022",
|
|
"(setvar \"DIMSCALE\" 2.0)",
|
|
"(command \"_.LEADER\" '(0.0 0.0) '(20.0 15.0) '(35.0 30.0) \"\" \"ABC\" \"\")",
|
|
&[[0.0, 0.0], [20.0, 15.0], [30.0, 30.0], [35.0, 30.0]],
|
|
[36.25, 31.25],
|
|
7,
|
|
5.0,
|
|
"ABC",
|
|
),
|
|
(
|
|
"024",
|
|
"(setvar \"DIMTXT\" 5.0)",
|
|
"(command \"_.LEADER\" '(0.0 0.0) '(20.0 15.0) '(40.0 15.0) \"\" \"AB\" \"CD\" \"\")",
|
|
&[[0.0, 0.0], [20.0, 15.0], [40.0, 15.0]],
|
|
[40.625, 15.625],
|
|
7,
|
|
5.0,
|
|
"AB\\PCD",
|
|
),
|
|
// A spline takes no hook line.
|
|
(
|
|
"026",
|
|
"",
|
|
"(command \"_.LEADER\" '(0.0 0.0) '(20.0 15.0) \"_F\" \"_S\" '(35.0 30.0) \"\" \"ABC\" \"\")",
|
|
&[[0.0, 0.0], [20.0, 15.0], [35.0, 30.0]],
|
|
[35.625, 30.625],
|
|
7,
|
|
2.5,
|
|
"ABC",
|
|
),
|
|
// Undo takes the last point back.
|
|
(
|
|
"014",
|
|
"",
|
|
"(command \"_.LEADER\" '(0.0 0.0) '(20.0 15.0) '(35.0 30.0) \"_U\" '(40.0 15.0) \"\" \"ABC\" \"\")",
|
|
&[[0.0, 0.0], [20.0, 15.0], [40.0, 15.0]],
|
|
[40.625, 15.625],
|
|
7,
|
|
2.5,
|
|
"ABC",
|
|
),
|
|
// Annotation options: Mtext, at the command line, comes back to the first line.
|
|
(
|
|
"106",
|
|
"",
|
|
"(command \"_.LEADER\" '(0.0 0.0) '(20.0 15.0) \"\" \"\" \"_M\" \"ABC\" \"\")",
|
|
&[[0.0, 0.0], [17.5, 15.0], [20.0, 15.0]],
|
|
[20.625, 15.625],
|
|
7,
|
|
2.5,
|
|
"ABC",
|
|
),
|
|
// Copy: centred on its line; with DIMTAD on the leader ends half the text height lower.
|
|
(
|
|
"016",
|
|
"",
|
|
"(command \"_.LEADER\" '(0.0 0.0) '(20.0 15.0) \"\" \"\" \"_C\" hl_T)",
|
|
&[[0.0, 0.0], [17.5, 13.75], [20.0, 13.75]],
|
|
[20.625, 15.625],
|
|
4,
|
|
2.5,
|
|
"XYZ",
|
|
),
|
|
];
|
|
for (case, setup, script, want_vertices, at, attach, height, contents) in cases {
|
|
let s = run(setup, script);
|
|
let ls = leaders(&s);
|
|
assert_eq!(ls.len(), 1, "{case}");
|
|
assert!(same_points(&vertices(&ls[0]), want_vertices), "{case}: {:?}", vertices(&ls[0]));
|
|
let ms = mtexts(&s);
|
|
assert_eq!(ms.len(), 1, "{case}");
|
|
let m = &ms[0];
|
|
assert!(near(m.insert.x, at[0]) && near(m.insert.y, at[1]), "{case}: {:?}", m.insert);
|
|
assert_eq!((m.attach, m.contents.as_str()), (attach, contents), "{case}");
|
|
assert!(near(m.height, height) && m.width == 0.0, "{case}: {} {}", m.height, m.width);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn format_sets_the_path_and_the_arrow() {
|
|
// LEADER-011 Format None; LEADER-012 None then Arrow; LEADER-013 Spline then STraight.
|
|
for (script, spline, arrow) in [
|
|
("(command \"_.LEADER\" '(0.0 0.0) '(20.0 15.0) \"_F\" \"_N\" '(40.0 15.0) \"\" \"ABC\" \"\")", false, false),
|
|
("(command \"_.LEADER\" '(0.0 0.0) '(20.0 15.0) \"_F\" \"_N\" \"_F\" \"_A\" '(40.0 15.0) \"\" \"ABC\" \"\")", false, true),
|
|
("(command \"_.LEADER\" '(0.0 0.0) '(20.0 15.0) \"_F\" \"_S\" \"_F\" \"_ST\" '(40.0 15.0) \"\" \"ABC\" \"\")", false, true),
|
|
("(command \"_.LEADER\" '(0.0 0.0) '(20.0 15.0) \"_F\" \"\" '(40.0 15.0) \"\" \"ABC\" \"\")", false, true),
|
|
] {
|
|
let l = leaders(&run("", script)).remove(0);
|
|
assert_eq!((l.spline, l.arrow), (spline, arrow), "{script}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn no_annotation_a_block_or_a_copied_multiline_text() {
|
|
// LEADER-015: None leaves the leader alone, without a hook line.
|
|
let s = run("", "(command \"_.LEADER\" '(0.0 0.0) '(20.0 15.0) \"\" \"\" \"_N\")");
|
|
assert!(same_points(&vertices(&leaders(&s)[0]), &[[0.0, 0.0], [20.0, 15.0]]));
|
|
assert!(mtexts(&s).is_empty());
|
|
// LEADER-107: a block, scale 2 (Y as X), 30°, where it is placed; no hook line.
|
|
let mut s = session("");
|
|
s.execute("line", &json!({ "points": [[100, 0], [110, 0]] })).unwrap();
|
|
s.cmdline("(command \"_.-BLOCK\" \"LB107\" '(100.0 0.0) (entlast) \"\")").unwrap();
|
|
s.cmdline("(command \"_.LEADER\" '(0.0 0.0) '(20.0 15.0) \"\" \"\" \"_B\" \"LB107\" '(50.0 20.0) 2 \"\" 30)").unwrap();
|
|
assert!(s.running.is_none(), "{}", s.prompt_text());
|
|
assert!(same_points(&vertices(&leaders(&s)[0]), &[[0.0, 0.0], [20.0, 15.0]]));
|
|
let i = inserts(&s).remove(0);
|
|
assert!(i.block == "LB107" && near(i.insert.x, 50.0) && near(i.insert.y, 20.0), "{i:?}");
|
|
assert!(near(i.scale.x, 2.0) && near(i.scale.y, 2.0) && near(i.rotation, 30f64.to_radians()), "{i:?}");
|
|
// LEADER-105: a copied multiline text keeps its width; LEADER-137 to the left.
|
|
let mut s = session("");
|
|
s.cmdline("(command \"_.MTEXT\" '(60.0 40.0) '(90.0 30.0) \"MT\" \"\")").unwrap();
|
|
s.cmdline("(command \"_.LEADER\" '(0.0 0.0) '(20.0 15.0) \"\" \"\" \"_C\" (entlast))").unwrap();
|
|
let copy = mtexts(&s).into_iter().find(|m| near(m.insert.x, 20.625)).unwrap();
|
|
assert!(near(copy.width, 30.0) && copy.attach == 4 && copy.contents == "MT", "{copy:?}");
|
|
let s = run("", "(command \"_.LEADER\" '(0.0 0.0) '(-20.0 15.0) \"\" \"\" \"_C\" hl_T)");
|
|
assert!(same_points(&vertices(&leaders(&s)[0]), &[[0.0, 0.0], [-17.5, 13.75], [-20.0, 13.75]]));
|
|
let m = mtexts(&s).into_iter().next().unwrap();
|
|
assert!(near(m.insert.x, -20.625) && near(m.insert.y, 15.625) && m.attach == 6, "{m:?}");
|
|
}
|
|
|
|
#[test]
|
|
fn the_json_form_places_its_text_as_the_command_line_does() {
|
|
let mut s = session("");
|
|
let v = s.execute("leader", &json!({ "points": [[0, 0], [20, 15], [35, 30]], "text": "ABC" })).unwrap();
|
|
assert_eq!(v["handles"].as_array().map(Vec::len), Some(2));
|
|
assert!(same_points(&vertices(&leaders(&s)[0]), &[[0.0, 0.0], [20.0, 15.0], [32.5, 30.0], [35.0, 30.0]]));
|
|
let m = mtexts(&s).remove(0);
|
|
assert!(near(m.insert.x, 35.625) && near(m.insert.y, 30.625) && m.attach == 7 && near(m.height, 2.5), "{m:?}");
|
|
}
|
|
|
|
#[test]
|
|
fn the_prompts_are_autocads_and_one_point_draws_nothing() {
|
|
let mut s = session("");
|
|
s.cmdline("leader").unwrap();
|
|
assert!(s.prompt_text().ends_with("Specify leader start point:"), "{}", s.prompt_text());
|
|
s.cmdline("0,0").unwrap();
|
|
assert!(s.prompt_text().ends_with("Specify next point:"), "{}", s.prompt_text());
|
|
s.cmdline("20,15").unwrap();
|
|
assert!(s.prompt_text().ends_with("Specify next point or [Annotation/Format/Undo] <Annotation>:"), "{}", s.prompt_text());
|
|
s.cmdline("_F").unwrap();
|
|
assert!(s.prompt_text().ends_with("[Spline/STraight/Arrow/None] <Exit>:"), "{}", s.prompt_text());
|
|
s.cmdline("").unwrap();
|
|
s.cmdline("").unwrap();
|
|
assert!(s.prompt_text().ends_with("Enter first line of annotation text or <options>:"), "{}", s.prompt_text());
|
|
s.cmdline("").unwrap();
|
|
assert!(s.prompt_text().ends_with("[Tolerance/Copy/Block/None/Mtext] <Mtext>:"), "{}", s.prompt_text());
|
|
// LEADER-019: Tolerance, at the command line, comes back to the first line.
|
|
s.cmdline("_T").unwrap();
|
|
assert!(s.prompt_text().ends_with("Enter first line of annotation text or <options>:"), "{}", s.prompt_text());
|
|
// An answer that isn't an option is refused; `?` at the block name lists the blocks.
|
|
s.cmdline("").unwrap();
|
|
s.cmdline("X").unwrap();
|
|
assert_eq!(s.log.last().map(String::as_str), Some("Invalid option keyword."));
|
|
s.cmdline("_B").unwrap();
|
|
assert!(s.prompt_text().ends_with("Enter block name or [?]:"), "{}", s.prompt_text());
|
|
s.cmdline("?").unwrap();
|
|
assert_eq!(s.log.last().map(String::as_str), Some("Defined blocks: (none)"));
|
|
s.cancel();
|
|
// LEADER-139: one point and Enter.
|
|
let s = run("", "(command \"_.LEADER\" '(0.0 0.0) \"\")");
|
|
assert!(leaders(&s).is_empty() && mtexts(&s).is_empty());
|
|
}
|