Files
cadcraft/crates/engine/src/lisp/block_tests.rs
T
Books dadd1f227f AutoLISP: entmake defines blocks with BLOCK … ENDBLK (fixes #1662) (#1765)
* AutoLISP: entmake defines blocks with BLOCK ... ENDBLK (fixes #1662)

A BLOCK list wasn't an entity, so entmake returned nil and the entities
after it went into model space. Now a BLOCK list begins a definition
(name, flags, base point, description), the entities made meanwhile go
into it, and ENDBLK adds the block (redefining one of the same name, or
naming an anonymous *U block) and returns its name. An open definition,
an xref, an invalid name or a block that inserts itself is refused with
nil, using BLOCK's own name and self-reference checks.

* AutoLISP entmake blocks: review fixes (anonymous names keep a star, no redefinition during REFEDIT, a failed routine drops its open definition)

An anonymous block (flag 1) named without a * got a name like FO1; it now
gets *U. ENDBLK refuses to redefine a block REFEDIT is editing, as BLOCK
does. A routine that fails drops the definition it began, so the next
routine's entities go into the drawing; one that ends normally still
leaves it open.

* AutoLISP entmake blocks: refuse an anonymous block that inserts itself

The self-reference check ran only for named blocks, so an anonymous *U
block holding an INSERT of the name it was about to get (*U1 in an empty
drawing) was stored. The check now runs after the anonymous name is
given, for both kinds.

* AutoLISP entmake blocks: create a definition's layers only when the block is added

Each pending entity created its layer at once, so a definition ENDBLK
refused (a self-reference, REFEDIT) or a failed routine dropped left an
orphan layer. end_block now creates the layers after its checks pass,
just before it adds the block.
2026-10-24 01:49:54 -04:00

237 lines
11 KiB
Rust

//! `entmake` defines blocks (#1662): a BLOCK list begins a definition, the entities made after it
//! go into the block instead of the drawing, and an ENDBLK list adds the block and returns its name.
#![allow(clippy::unwrap_used)]
use super::tests::session;
use super::value::{Value, to_text};
use crate::Session;
fn eval(s: &mut Session, code: &str) -> Result<String, String> {
match s.lisp_eval(code, false, true, Vec::<Value>::new()) {
Ok(Some(Ok(v))) => Ok(to_text(&v, true)),
Ok(Some(Err(e))) => Err(e),
Ok(None) => Err("did not finish".into()),
Err(e) => Err(e.to_string()),
}
}
fn ok(s: &mut Session, code: &str) -> String {
match eval(s, code) {
Ok(v) => v,
Err(e) => panic!("{code}: {e}"),
}
}
fn model_len(s: &Session) -> usize {
s.doc().unwrap().model.len()
}
const BLOCK_EB: &str = r#"(entmake '((0 . "BLOCK") (2 . "EB") (70 . 0) (10 5.0 5.0 0.0) (4 . "made by entmake")))"#;
const LINE: &str = r#"(entmake '((0 . "LINE") (8 . "0") (10 0.0 0.0 0.0) (11 10.0 0.0 0.0)))"#;
const CIRCLE: &str = r#"(entmake '((0 . "CIRCLE") (8 . "PARTS") (10 5.0 5.0 0.0) (40 . 2.5)))"#;
const ENDBLK: &str = r#"(entmake '((0 . "ENDBLK")))"#;
/// The issue's routine: BLOCK, a LINE, ENDBLK, then tblsearch.
#[test]
fn the_issues_routine_defines_the_block() {
let mut s = session();
let code = r#"(list (entmake (quote ((0 . "BLOCK") (2 . "EB") (70 . 0) (10 0.0 0.0 0.0)))) (entmake (quote ((0 . "LINE") (8 . "0") (10 0.0 0.0 0.0) (11 10.0 0.0 0.0)))) (entmake (quote ((0 . "ENDBLK")))) (cdr (assoc 2 (tblsearch "BLOCK" "EB"))))"#;
let v = ok(&mut s, code);
assert!(v.starts_with("(((0 . \"BLOCK\")"), "{v}");
assert!(v.ends_with(" \"EB\" \"EB\")"), "ENDBLK returns the name and tblsearch finds it: {v}");
assert_eq!(model_len(&s), 0, "the LINE went into the block, not model space");
let d = s.doc().unwrap();
assert_eq!(d.block("EB").unwrap().entities.len(), 1);
}
#[test]
fn the_entities_base_point_and_description_are_kept() {
let mut s = session();
assert!(ok(&mut s, BLOCK_EB).starts_with("((0 . \"BLOCK\")"));
assert!(ok(&mut s, LINE).starts_with("((0 . \"LINE\")"), "an entity in a definition returns its list");
ok(&mut s, CIRCLE);
assert_eq!(model_len(&s), 0);
assert!(s.doc().unwrap().block("EB").is_none(), "nothing is added before ENDBLK");
assert_eq!(ok(&mut s, ENDBLK), "\"EB\"");
let d = s.doc().unwrap();
let b = d.block("EB").unwrap();
assert_eq!((b.base.x, b.base.y, b.description.as_str()), (5.0, 5.0, "made by entmake"));
let hs: Vec<_> = b.entities.iter().map(|e| e.handle).collect();
assert_eq!(hs.len(), 2);
assert!(hs[0].0 != 0 && hs[0] != hs[1], "each entity gets its own handle: {hs:?}");
assert!(d.layers.iter().any(|l| l.name == "PARTS"), "a layer the entities name is created");
// It inserts like any block, and entmake makes entities in the drawing again afterwards.
s.execute("insert", &serde_json::json!({"name": "EB", "at": [100, 0]})).unwrap();
ok(&mut s, LINE);
assert_eq!(model_len(&s), 2);
}
#[test]
fn anonymous_blocks_get_a_name() {
let mut s = session();
ok(&mut s, r#"(entmake '((0 . "BLOCK") (2 . "*U") (70 . 1) (10 0.0 0.0 0.0)))"#);
ok(&mut s, LINE);
let name = ok(&mut s, ENDBLK);
assert!(name.starts_with("\"*U") && name.len() > 4, "{name}");
let d = s.doc().unwrap();
let b = d.block(name.trim_matches('"')).unwrap();
assert!(b.anonymous);
assert_eq!(b.entities.len(), 1);
}
#[test]
fn a_block_of_the_same_name_is_redefined() {
let mut s = session();
for (n, code) in [(1, LINE), (2, CIRCLE)] {
ok(&mut s, r#"(entmake '((0 . "BLOCK") (2 . "R") (70 . 0) (10 0.0 0.0 0.0)))"#);
for _ in 0..n {
ok(&mut s, code);
}
assert_eq!(ok(&mut s, ENDBLK), "\"R\"");
}
assert_eq!(s.doc().unwrap().block("R").unwrap().entities.len(), 2, "the second definition");
// Names are case-insensitive: the existing spelling is kept.
ok(&mut s, r#"(entmake '((0 . "BLOCK") (2 . "r") (70 . 0) (10 0.0 0.0 0.0)))"#);
assert_eq!(ok(&mut s, ENDBLK), "\"R\"");
}
#[test]
fn what_cant_be_defined_is_refused() {
let mut s = session();
assert_eq!(ok(&mut s, ENDBLK), "nil", "ENDBLK without a BLOCK");
for bad in [
r#"(entmake '((0 . "BLOCK") (2 . "") (70 . 0) (10 0.0 0.0 0.0)))"#,
r#"(entmake '((0 . "BLOCK") (2 . "A<B") (70 . 0) (10 0.0 0.0 0.0)))"#,
r#"(entmake '((0 . "BLOCK") (2 . "*NOTANON") (70 . 0) (10 0.0 0.0 0.0)))"#,
r#"(entmake '((0 . "BLOCK") (2 . "XR") (70 . 4) (10 0.0 0.0 0.0)))"#,
r#"(entmake '((0 . "BLOCK") (70 . 0) (10 0.0 0.0 0.0)))"#,
] {
assert_eq!(ok(&mut s, bad), "nil", "{bad}");
}
// A BLOCK while one is open is refused; the open one goes on.
ok(&mut s, BLOCK_EB);
assert_eq!(ok(&mut s, r#"(entmake '((0 . "BLOCK") (2 . "OTHER") (70 . 0) (10 0.0 0.0 0.0)))"#), "nil");
ok(&mut s, LINE);
assert_eq!(ok(&mut s, ENDBLK), "\"EB\"");
assert!(s.doc().unwrap().block("OTHER").is_none());
// A block that inserts itself isn't added.
ok(&mut s, r#"(entmake '((0 . "BLOCK") (2 . "SELF") (70 . 0) (10 0.0 0.0 0.0)))"#);
ok(&mut s, r#"(entmake '((0 . "INSERT") (2 . "SELF") (10 0.0 0.0 0.0)))"#);
assert_eq!(ok(&mut s, ENDBLK), "nil");
assert!(s.doc().unwrap().block("SELF").is_none());
assert_eq!(model_len(&s), 0);
}
#[test]
fn a_saved_drawing_keeps_the_block() {
let mut s = session();
for code in [BLOCK_EB, LINE, CIRCLE, ENDBLK] {
ok(&mut s, code);
}
s.execute("insert", &serde_json::json!({"name": "EB", "at": [100, 0]})).unwrap();
let read = cadcraft_io::read_dxf(cadcraft_io::write_dxf(s.doc().unwrap()).as_bytes()).unwrap();
let b = read.block("EB").unwrap();
assert_eq!((b.entities.len(), b.base.x, b.base.y), (2, 5.0, 5.0));
assert_eq!(read.model.len(), 1, "the insert");
}
/// Flag 1 makes an anonymous block whatever the name says: a name that isn't `*X` gets `*U`.
#[test]
fn an_anonymous_block_is_always_named_with_a_star() {
let mut s = session();
for (given, prefix) in [("FOO", "*U"), ("*X", "*X"), ("*", "*U")] {
ok(&mut s, &format!(r#"(entmake '((0 . "BLOCK") (2 . "{given}") (70 . 1) (10 0.0 0.0 0.0)))"#));
ok(&mut s, LINE);
let name = ok(&mut s, ENDBLK);
let name = name.trim_matches('"');
assert!(name.starts_with(prefix) && name.len() > prefix.len(), "{given} -> {name}");
assert!(s.doc().unwrap().block(name).unwrap().anonymous);
}
}
/// As BLOCK: a block being edited in place (REFEDIT) isn't redefined until REFCLOSE.
#[test]
fn a_block_being_edited_in_place_is_not_redefined() {
let mut s = session();
for code in [r#"(entmake '((0 . "BLOCK") (2 . "R") (70 . 0) (10 0.0 0.0 0.0)))"#, LINE, ENDBLK] {
ok(&mut s, code);
}
let ins = s.execute("insert", &serde_json::json!({"name": "R", "at": [0, 0]})).unwrap();
s.execute("refedit", &serde_json::json!({"handle": ins["handle"]})).unwrap();
ok(&mut s, r#"(entmake '((0 . "BLOCK") (2 . "r") (70 . 0) (10 0.0 0.0 0.0)))"#);
ok(&mut s, CIRCLE);
ok(&mut s, CIRCLE);
assert_eq!(ok(&mut s, ENDBLK), "nil");
s.execute("refclose", &serde_json::json!({"option": "discard"})).unwrap();
assert_eq!(s.doc().unwrap().block("R").unwrap().entities.len(), 1, "the definition is unchanged");
// After REFCLOSE it can be redefined.
for code in [r#"(entmake '((0 . "BLOCK") (2 . "R") (70 . 0) (10 0.0 0.0 0.0)))"#, CIRCLE, CIRCLE] {
ok(&mut s, code);
}
assert_eq!(ok(&mut s, ENDBLK), "\"R\"");
assert_eq!(s.doc().unwrap().block("R").unwrap().entities.len(), 2);
}
/// A routine that fails drops the definition it began, with or without an *error* handler: the
/// next routine's entities go into the drawing. One that ends normally keeps it open.
#[test]
fn a_failed_routine_drops_its_open_definition() {
let mut s = session();
for handler in [None, Some("(defun *error* (msg) (princ msg))")] {
if let Some(h) = handler {
ok(&mut s, h);
}
let before = model_len(&s);
assert!(eval(&mut s, r#"(progn (entmake '((0 . "BLOCK") (2 . "FAIL") (70 . 0) (10 0.0 0.0 0.0))) (car 1))"#).is_err());
ok(&mut s, LINE);
assert_eq!(model_len(&s), before + 1, "the line is in the drawing ({handler:?})");
assert_eq!(ok(&mut s, ENDBLK), "nil", "nothing is open");
assert!(s.doc().unwrap().block("FAIL").is_none());
}
// A routine that ends normally leaves its definition open for the next one.
ok(&mut s, BLOCK_EB);
ok(&mut s, LINE);
assert_eq!(ok(&mut s, ENDBLK), "\"EB\"");
}
/// An anonymous block that inserts the name it is about to get (`*U1` in an empty drawing) is a
/// block inserting itself too: refused like a named one.
#[test]
fn an_anonymous_block_that_inserts_itself_is_refused() {
let mut s = session();
assert!(s.doc().unwrap().block("*U1").is_none());
ok(&mut s, r#"(entmake '((0 . "BLOCK") (2 . "*U") (70 . 1) (10 0.0 0.0 0.0)))"#);
ok(&mut s, r#"(entmake '((0 . "INSERT") (2 . "*U1") (10 0.0 0.0 0.0)))"#);
assert_eq!(ok(&mut s, ENDBLK), "nil");
assert!(s.doc().unwrap().block("*U1").is_none());
// The next anonymous block still gets a name and is added.
ok(&mut s, r#"(entmake '((0 . "BLOCK") (2 . "*U") (70 . 1) (10 0.0 0.0 0.0)))"#);
ok(&mut s, LINE);
assert_eq!(ok(&mut s, ENDBLK), "\"*U1\"");
}
/// The layers a definition's entities name are created only when the block is added: a refused
/// definition, or one a failed routine drops, leaves no layer behind.
#[test]
fn a_definition_that_isnt_added_leaves_no_layer() {
let mut s = session();
let has = |s: &Session, n: &str| s.doc().unwrap().layers.iter().any(|l| l.name.eq_ignore_ascii_case(n));
// Refused at ENDBLK: it inserts itself.
ok(&mut s, r#"(entmake '((0 . "BLOCK") (2 . "SELF") (70 . 0) (10 0.0 0.0 0.0)))"#);
ok(&mut s, r#"(entmake '((0 . "LINE") (8 . "ORPHAN1") (10 0.0 0.0 0.0) (11 1.0 0.0 0.0)))"#);
ok(&mut s, r#"(entmake '((0 . "INSERT") (8 . "ORPHAN1") (2 . "SELF") (10 0.0 0.0 0.0)))"#);
assert!(!has(&s, "ORPHAN1"), "nothing before ENDBLK");
assert_eq!(ok(&mut s, ENDBLK), "nil");
assert!(!has(&s, "ORPHAN1"), "a refused block leaves no layer");
// Dropped: the routine fails.
assert!(eval(&mut s, r#"(progn (entmake '((0 . "BLOCK") (2 . "GONE") (70 . 0) (10 0.0 0.0 0.0))) (entmake '((0 . "LINE") (8 . "ORPHAN2") (10 0.0 0.0 0.0) (11 1.0 0.0 0.0))) (car 1))"#).is_err());
assert!(!has(&s, "ORPHAN2"), "a dropped definition leaves no layer");
// Added: its layers are created.
ok(&mut s, BLOCK_EB);
ok(&mut s, CIRCLE);
assert!(!has(&s, "PARTS"));
assert_eq!(ok(&mut s, ENDBLK), "\"EB\"");
assert!(has(&s, "PARTS"), "the block's layer exists once it is added");
}