mirror of
https://github.com/storytold/cadcraft.git
synced 2026-11-10 04:19:59 -06:00
* MINSERT: a block reference repeated in rows and columns
MINSERT asks INSERT's questions, then the grid's as AutoCAD 2024 does
(measured with accoreconsole): the numbers of rows and columns (1 to
32767), the distance between rows or a unit cell when there is more than
one row, the distance between columns when there is more than one column,
then the attribute values once. A distance along an axis with one copy is
0, and one row by one column is a plain reference.
EXPLODE leaves a grid whole ("The object is minserted.") instead of
exploding its first copy and dropping the rest. MIRROR mirrors a block
reference as AutoCAD does, negating the X scale factor and a grid's
column distance, so a mirrored grid's rows stay on the right side. An
attribute with no default is asked without "<>".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* MINSERT JSON: refuse a distance that is not a number
`rowSpacing` and `columnSpacing` went through `f64_or`, which takes any
value that is not a number as the default, so `"rowSpacing": "15"` stored 0
and put every copy on top of the others without an error. They are now
checked as `rows` and `columns` are: absent or null is 0, a finite number is
kept, anything else is refused.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
219 lines
10 KiB
Rust
219 lines
10 KiB
Rust
//! MINSERT, one block reference repeated in rows and columns, as AutoCAD 2024 makes and edits it
|
|
//! (accoreconsole: the prompts, and `entget` of the reference after MINSERT, EXPLODE, MIRROR,
|
|
//! SCALE and ROTATE).
|
|
|
|
use std::f64::consts::PI;
|
|
|
|
use cadcraft_engine::Session;
|
|
use cadcraft_engine::doc::{EntityKind, Insert};
|
|
use serde_json::{Value, json};
|
|
|
|
fn handle(v: &Value) -> String {
|
|
v.get("handle").unwrap_or(&v["handles"][0]).as_str().unwrap().to_owned()
|
|
}
|
|
|
|
/// A session with block B (a line from (0, 0) to (10, 0) and a circle of radius 3 round (5, 5),
|
|
/// base point (0, 0)), after the AutoLISP `code`.
|
|
fn run(code: &str) -> Session {
|
|
let mut s = Session::new();
|
|
let a = handle(&s.execute("line", &json!({"points": [[0, 0], [10, 0]]})).unwrap());
|
|
let c = handle(&s.execute("circle", &json!({"center": [5, 5], "radius": 3})).unwrap());
|
|
s.execute("-block", &json!({"name": "B", "base": [0, 0], "handles": [a, c]})).unwrap();
|
|
s.cmdline(code).unwrap();
|
|
assert!(s.running.is_none(), "{}", s.prompt_text());
|
|
s
|
|
}
|
|
|
|
fn inserts(s: &Session) -> Vec<Insert> {
|
|
s.doc()
|
|
.unwrap()
|
|
.model
|
|
.iter()
|
|
.filter_map(|e| match &e.kind {
|
|
EntityKind::Insert(i) => Some(i.clone()),
|
|
_ => None,
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
/// The grid as `entget` gives it: 70 columns, 71 rows, 44 between columns, 45 between rows (a
|
|
/// plain reference, one row and one column, as 1/1/0/0).
|
|
fn grid(i: &Insert) -> (u32, u32, f64, f64) {
|
|
(i.cols, i.rows, i.col_spacing, i.row_spacing)
|
|
}
|
|
|
|
fn near(a: f64, b: f64) -> bool {
|
|
(a - b).abs() < 1e-9
|
|
}
|
|
|
|
#[test]
|
|
fn the_grid_is_stored_as_autocad_2024_stores_it() {
|
|
// (MINSERT's answers after the block name and insertion point, the grid AutoCAD stored.)
|
|
let cases = [
|
|
// One row: no distance between rows asked, and 0 stored for it.
|
|
("1 \"\" 0 1 3 20.0", (3, 1, 20.0, 0.0)),
|
|
("1 \"\" 0 3 1 15.0", (1, 3, 0.0, 15.0)),
|
|
// One of each: a plain reference.
|
|
("1 \"\" 0 1 1", (1, 1, 0.0, 0.0)),
|
|
// A unit cell: its width between columns, its height between rows, signed.
|
|
("1 \"\" 0 2 3 '(0.0 0.0) '(25.0 12.0)", (3, 2, 25.0, 12.0)),
|
|
("1 \"\" 0 2 3 '(25.0 12.0) '(0.0 0.0)", (3, 2, -25.0, -12.0)),
|
|
// Its sides along the drawing's axes, the reference rotated or not.
|
|
("1 \"\" 30 2 3 '(0.0 0.0) '(25.0 12.0)", (3, 2, 25.0, 12.0)),
|
|
// Typed distances are kept as typed, negative or 0 too.
|
|
("1 \"\" 0 2 2 -15.0 -20.0", (2, 2, -20.0, -15.0)),
|
|
("1 \"\" 0 2 2 0.0 20.0", (2, 2, 20.0, 0.0)),
|
|
// The distances are not scaled with the reference.
|
|
("2 \"\" 30 2 2 15.0 20.0", (2, 2, 20.0, 15.0)),
|
|
];
|
|
for (answers, want) in cases {
|
|
let s = run(&format!("(command \"_.MINSERT\" \"B\" '(20.0 10.0) {answers})"));
|
|
let [i] = inserts(&s).try_into().unwrap();
|
|
let got = grid(&i);
|
|
assert!(got.0 == want.0 && got.1 == want.1 && near(got.2, want.2) && near(got.3, want.3), "{answers}: {got:?}");
|
|
assert!(near(i.insert.x, 20.0) && near(i.insert.y, 10.0));
|
|
}
|
|
let s = run("(command \"_.MINSERT\" \"B\" '(20.0 10.0) 2 \"\" 30 2 2 15.0 20.0)");
|
|
let [i] = inserts(&s).try_into().unwrap();
|
|
assert!(near(i.scale.x, 2.0) && near(i.scale.y, 2.0) && near(i.rotation, PI / 6.0));
|
|
}
|
|
|
|
#[test]
|
|
fn the_prompts_follow_autocad_2024() {
|
|
let mut s = Session::new();
|
|
let l = handle(&s.execute("line", &json!({"points": [[0, 0], [10, 0]]})).unwrap());
|
|
s.execute("-block", &json!({"name": "B", "base": [0, 0], "handles": [l]})).unwrap();
|
|
// Each prompt, and what was written to the command line on the way to it.
|
|
let mut asked = Vec::new();
|
|
for input in ["minsert", "B", "20,10", "1", "", "0", "0", "2", "3", "x", "15", "20"] {
|
|
let logged = s.log.len();
|
|
s.cmdline(input).unwrap();
|
|
asked.extend(s.log.iter().skip(logged).filter(|l| l.starts_with("Requires")).cloned());
|
|
if let Some(p) = s.current_prompt() {
|
|
asked.push(p.display());
|
|
}
|
|
}
|
|
// The grid's prompts and messages are AutoCAD 2024's; the ones before are INSERT's.
|
|
assert_eq!(
|
|
asked,
|
|
[
|
|
"Enter block name or [?]:",
|
|
"Specify insertion point or [Basepoint/Scale/X/Y/Z/Rotate]:",
|
|
"Enter X scale factor, specify opposite corner or [Corner/XYZ] <1>:",
|
|
"Enter Y scale factor <use X scale factor>:",
|
|
"Specify rotation angle <0>:",
|
|
"Enter number of rows (---) <1>:",
|
|
"Requires an integer between 1 and 32767.",
|
|
"Enter number of rows (---) <1>:",
|
|
"Enter number of columns (|||) <1>:",
|
|
"Enter distance between rows or specify unit cell (---):",
|
|
"Requires a distance.",
|
|
"Enter distance between rows or specify unit cell (---):",
|
|
"Specify distance between columns (|||):",
|
|
]
|
|
);
|
|
assert!(s.running.is_none());
|
|
let [i] = inserts(&s).try_into().unwrap();
|
|
assert_eq!(grid(&i), (3, 2, 20.0, 15.0));
|
|
}
|
|
|
|
#[test]
|
|
fn counts_run_from_1_to_32767() {
|
|
// AutoCAD 2024: "Requires an integer between 1 and 32767."
|
|
let mut s = Session::new();
|
|
let l = handle(&s.execute("line", &json!({"points": [[0, 0], [10, 0]]})).unwrap());
|
|
s.execute("-block", &json!({"name": "B", "base": [0, 0], "handles": [l]})).unwrap();
|
|
for input in ["minsert", "B", "0,0", "1", "", "0", "32768", "32767", "1", "2"] {
|
|
s.cmdline(input).unwrap();
|
|
}
|
|
assert!(s.running.is_none(), "{}", s.prompt_text());
|
|
assert_eq!(s.log.iter().filter(|l| l.starts_with("Requires an integer between 1 and 32767.")).count(), 1, "{:?}", s.log);
|
|
let [i] = inserts(&s).try_into().unwrap();
|
|
assert_eq!(grid(&i), (1, 32767, 0.0, 2.0));
|
|
}
|
|
|
|
#[test]
|
|
fn attribute_values_are_asked_once_after_the_grid() {
|
|
let mut s = Session::new();
|
|
let l = handle(&s.execute("line", &json!({"points": [[0, 0], [10, 0]]})).unwrap());
|
|
let a = handle(&s.execute("attdef", &json!({"tag": "TAG", "prompt": "Value", "at": [0, 2], "height": 1})).unwrap());
|
|
s.execute("-block", &json!({"name": "BA", "base": [0, 0], "handles": [l, a]})).unwrap();
|
|
for input in ["minsert", "BA", "0,700", "1", "", "0", "5", "4", "15", "20"] {
|
|
s.cmdline(input).unwrap();
|
|
}
|
|
// After the grid, with no "<>" for an empty default (AutoCAD 2024: "Value: hello").
|
|
assert_eq!(s.current_prompt().unwrap().display(), "Value:");
|
|
s.cmdline("hello").unwrap();
|
|
assert!(s.running.is_none());
|
|
let [i] = inserts(&s).try_into().unwrap();
|
|
assert_eq!(grid(&i), (4, 5, 20.0, 15.0));
|
|
assert_eq!(i.attribs.iter().map(|a| a.text.value.as_str()).collect::<Vec<_>>(), ["hello"]);
|
|
}
|
|
|
|
#[test]
|
|
fn explode_leaves_a_grid_whole() {
|
|
let s = run("(progn (command \"_.MINSERT\" \"B\" '(20.0 10.0) 1 \"\" 0 2 2 15.0 20.0) (command \"_.EXPLODE\" (entlast) \"\"))");
|
|
let [i] = inserts(&s).try_into().unwrap();
|
|
assert_eq!(grid(&i), (2, 2, 20.0, 15.0));
|
|
assert_eq!(s.doc().unwrap().model.iter().count(), 1);
|
|
assert!(s.log.iter().any(|l| l.contains("The object is minserted.")), "{:?}", s.log);
|
|
}
|
|
|
|
#[test]
|
|
fn edits_carry_the_grid_as_in_autocad_2024() {
|
|
let made = "(command \"_.MINSERT\" \"B\" '(20.0 10.0) 1 \"\" 0 2 2 15.0 20.0)";
|
|
// (the edit, the copy it leaves last: insertion point, X and Y scale, rotation, grid)
|
|
let cases = [
|
|
("(command \"_.SCALE\" (entlast) \"\" '(20.0 10.0) 2)", ([20.0, 10.0], [2.0, 2.0], 0.0, (2, 2, 40.0, 30.0))),
|
|
("(command \"_.ROTATE\" (entlast) \"\" '(20.0 10.0) 30)", ([20.0, 10.0], [1.0, 1.0], PI / 6.0, (2, 2, 20.0, 15.0))),
|
|
("(command \"_.MOVE\" (entlast) \"\" '(20.0 10.0) '(25.0 13.0))", ([25.0, 13.0], [1.0, 1.0], 0.0, (2, 2, 20.0, 15.0))),
|
|
// Mirrored about the vertical through the insertion point, and about the horizontal: X
|
|
// scale -1, and the columns run the other way.
|
|
("(command \"_.MIRROR\" (entlast) \"\" '(20.0 10.0) '(20.0 30.0) \"_N\")", ([20.0, 10.0], [-1.0, 1.0], 0.0, (2, 2, -20.0, 15.0))),
|
|
("(command \"_.MIRROR\" (entlast) \"\" '(20.0 10.0) '(40.0 10.0) \"_N\")", ([20.0, 10.0], [-1.0, 1.0], PI, (2, 2, -20.0, 15.0))),
|
|
];
|
|
for (edit, (at, scale, rot, want)) in cases {
|
|
let s = run(&format!("(progn {made} {edit})"));
|
|
let i = inserts(&s).pop().unwrap();
|
|
let got = grid(&i);
|
|
assert!(
|
|
near(i.insert.x, at[0])
|
|
&& near(i.insert.y, at[1])
|
|
&& near(i.scale.x, scale[0])
|
|
&& near(i.scale.y, scale[1])
|
|
&& near(i.rotation, rot)
|
|
&& got.0 == want.0
|
|
&& got.1 == want.1
|
|
&& near(got.2, want.2)
|
|
&& near(got.3, want.3),
|
|
"{edit}: {i:?}"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn the_json_form_makes_the_same_grid() {
|
|
let mut s = Session::new();
|
|
let l = handle(&s.execute("line", &json!({"points": [[0, 0], [10, 0]]})).unwrap());
|
|
s.execute("-block", &json!({"name": "B", "base": [0, 0], "handles": [l]})).unwrap();
|
|
s.execute("minsert", &json!({"name": "B", "at": [20, 10], "rows": 2, "columns": 3, "rowSpacing": 15, "columnSpacing": 20})).unwrap();
|
|
// One row: its distance is not kept.
|
|
s.execute("minsert", &json!({"name": "B", "at": [20, 100], "rows": 1, "columns": 3, "rowSpacing": 15, "columnSpacing": 20})).unwrap();
|
|
let got: Vec<_> = inserts(&s).iter().map(grid).collect();
|
|
assert_eq!(got, [(3, 2, 20.0, 15.0), (3, 1, 20.0, 0.0)]);
|
|
for bad in [
|
|
json!({"name": "B", "at": [0, 0], "rows": 0}),
|
|
json!({"name": "B", "at": [0, 0], "columns": 32768}),
|
|
json!({"name": "B", "at": [0, 0], "rows": 2.5}),
|
|
// A distance that is not a number is refused, not taken as 0.
|
|
json!({"name": "B", "at": [0, 0], "rows": 2, "rowSpacing": "15"}),
|
|
json!({"name": "B", "at": [0, 0], "columns": 2, "columnSpacing": true}),
|
|
] {
|
|
assert!(s.execute("minsert", &bad).is_err(), "{bad}");
|
|
}
|
|
// A whole number written as 2.0 is a count; `explode` is INSERT's and leaves the grid made.
|
|
let r = s.execute("minsert", &json!({"name": "B", "at": [0, 200], "rows": 2.0, "rowSpacing": 15, "explode": true})).unwrap();
|
|
assert!(r["handle"].is_string(), "{r}");
|
|
assert_eq!(inserts(&s).last().map(grid), Some((1, 2, 0.0, 15.0)));
|
|
}
|