Files
cadcraft/crates/engine/tests/it/dynamic_block_data.rs
T
Dylan M. Taylor 1b7c705fac Dynamic blocks: index kept objects once per listing; copy each value record once (#2678)
* Dynamic blocks: index kept objects once per listing and copy each value record once

Listing blocks rebuilt a handle index of every kept foreign object for each
block with an extension dictionary, multiplying the block count by the object
count. The listing now builds one ObjectIndex and asks is_dynamic_block_in per
block; dynamic_block answers None before indexing when the block has no
extension dictionary.

templates() copied a value record's tags once per dictionary key, so a record
listed under many keys was copied many times, and it looked up each
dictionary entry by scanning every object. It now resolves entries through
its handle index and copies each record at most once (CWE-400, CWE-407).

* Dynamic blocks: list a block as dynamic from its graph object without walking the graph's members
2026-11-02 22:52:35 -05:00

145 lines
7.0 KiB
Rust

//! `blocks.dynamic` and `blocks.list` report a dynamic block's parameters and actions as read
//! from another program's file (kept foreign data, built by hand here).
use std::sync::Arc;
use cadcraft_engine::Session;
use cadcraft_engine::doc::foreign::{Foreign, ForeignExtra, ForeignObject};
use cadcraft_engine::doc::{Handle, RawTag};
use serde_json::json;
fn obj(handle: u64, kind: &str, tags: &[(i32, &str)]) -> ForeignObject {
ForeignObject { handle: Handle(handle), kind: kind.into(), tags: tags.iter().map(|(c, v)| RawTag { code: *c, value: (*v).into() }).collect() }
}
/// A session whose block "Door" has a visibility parameter with two states.
fn session() -> Session {
let mut s = Session::new();
let l = s.execute("line", &json!({ "points": [[0, 0], [1, 0]] })).unwrap()["handles"][0].clone();
s.execute("block", &json!({ "name": "Door", "base": [0, 0], "handles": [l] })).unwrap();
let line = s.doc().unwrap().block("Door").unwrap().entities.handles()[0].hex();
let mut f = Foreign::default();
f.records.insert(("BLOCK_RECORD".into(), "DOOR".into()), ForeignExtra { xdict: Some(Handle(0xF00)), ..Default::default() });
f.objects.push(obj(0xF00, "DICTIONARY", &[(100, "AcDbDictionary"), (3, "ACAD_ENHANCEDBLOCK"), (360, "F01")]));
f.objects.push(obj(0xF01, "ACAD_EVALUATION_GRAPH", &[(100, "AcDbEvalGraph"), (91, "0"), (95, "1"), (360, "F02")]));
f.objects.push(obj(
0xF02,
"BLOCKVISIBILITYPARAMETER",
&[
(100, "AcDbEvalExpr"),
(90, "1"),
(100, "AcDbBlockElement"),
(300, "Visibility State"),
(100, "AcDbBlockParameter"),
(280, "1"),
(100, "AcDbBlock1PtParameter"),
(1010, "2"),
(1020, "3"),
(100, "AcDbBlockVisibilityParameter"),
(301, "Visibility1"),
(302, ""),
(93, "1"),
(331, &line),
(92, "2"),
(303, "Open"),
(94, "1"),
(332, &line),
(95, "0"),
(303, "Closed"),
(94, "0"),
(95, "0"),
],
));
let mut d = s.doc().unwrap().clone();
d.foreign = Arc::new(f);
s.open_drawing(d, "dynamic", None);
s
}
#[test]
fn dynamic_block_data_is_reported() {
let mut s = session();
let list = s.execute("blocks.list", &json!({})).unwrap();
let flags: Vec<_> = list["blocks"].as_array().unwrap().iter().map(|b| (b["name"].as_str().unwrap().to_string(), b["dynamic"].clone())).collect();
assert!(flags.contains(&("Door".into(), json!(true))), "{flags:?}");
let m = s.execute("blocks.dynamic", &json!({ "name": "DOOR" })).unwrap();
let p = &m["parameters"][0];
assert_eq!((p["node"].clone(), p["element"].clone(), p["showProperties"].clone()), (json!(1), json!("Visibility State"), json!(true)));
assert_eq!(p["kind"]["type"], "visibility");
assert_eq!(p["kind"]["label"], "Visibility1");
assert_eq!(p["kind"]["at"], json!({ "x": 2.0, "y": 3.0, "z": 0.0 }));
let states: Vec<_> =
p["kind"]["states"].as_array().unwrap().iter().map(|st| (st["name"].clone(), st["visible"].as_array().unwrap().len())).collect();
assert_eq!(states, [(json!("Open"), 1), (json!("Closed"), 0)]);
assert!(p["kind"]["controlled"][0].is_string(), "handles are hex strings");
assert_eq!(m["actions"], json!([]));
}
#[test]
fn blocks_without_dynamic_data_and_bad_names() {
let mut s = session();
let l = s.execute("line", &json!({ "points": [[0, 0], [1, 1]] })).unwrap()["handles"][0].clone();
s.execute("block", &json!({ "name": "Window", "base": [0, 0], "handles": [l] })).unwrap();
assert_eq!(s.execute("blocks.dynamic", &json!({ "name": "Window" })).unwrap(), json!(null));
assert_eq!(s.execute("blocks.dynamic", &json!({ "name": "*Model_Space" })).unwrap(), json!(null));
let list = s.execute("blocks.list", &json!({})).unwrap();
let flags: Vec<_> = list["blocks"].as_array().unwrap().iter().map(|b| (b["name"].as_str().unwrap().to_string(), b["dynamic"].clone())).collect();
assert!(flags.contains(&("Window".into(), json!(false))), "{flags:?}");
assert!(flags.contains(&("Door".into(), json!(true))), "{flags:?}");
for p in [json!({}), json!({ "name": 3 }), json!({ "name": "Nope" }), json!(null)] {
assert!(s.execute("blocks.dynamic", &p).is_err(), "{p}");
}
}
#[test]
fn a_reference_to_a_dynamic_block_reports_its_custom_properties() {
let mut s = session();
// BLOCK made the line a reference to Door, which has Door's own values.
let insert = s.doc().unwrap().model.handles()[0].hex();
let r = s.execute("blocks.reference", &json!({ "handle": insert })).unwrap();
assert_eq!(r["definition"], "Door");
assert_eq!(r["values"], json!({}));
assert_eq!(r["properties"], json!([{ "name": "Visibility1", "value": "Open", "choices": ["Open", "Closed"], "editable": true }]));
// A plain block's reference, objects that aren't references and bad parameters.
let l = s.execute("line", &json!({ "points": [[0, 0], [1, 1]] })).unwrap()["handles"][0].clone();
let plain = s.execute("block", &json!({ "name": "Window", "base": [0, 0], "handles": [l.clone()] })).unwrap()["insert"].clone();
assert_eq!(s.execute("blocks.reference", &json!({ "handle": plain })).unwrap(), json!(null));
let l = s.execute("line", &json!({ "points": [[0, 0], [2, 2]] })).unwrap()["handles"][0].clone();
assert!(s.execute("blocks.reference", &json!({ "handle": l })).is_err());
for p in [json!({}), json!({ "handle": "zz" }), json!({ "handle": "FFFFFF" }), json!(null)] {
assert!(s.execute("blocks.reference", &p).is_err(), "{p}");
}
}
#[test]
fn a_visibility_state_is_changed_in_one_undo_step() {
let mut s = session();
let insert = s.doc().unwrap().model.handles()[0].hex();
let r = s.execute("blocks.reference.set", &json!({ "handle": insert, "name": "Visibility1", "value": "Closed" })).unwrap();
assert_eq!(r["properties"][0]["value"], "Closed");
let block = |s: &Session| match s.doc().unwrap().model.iter().next().map(|e| e.kind.clone()) {
Some(cadcraft_engine::doc::EntityKind::Insert(i)) => i.block,
other => panic!("{other:?}"),
};
assert!(block(&s).starts_with("*U"));
// Still a reference to Door, in its new state.
let r = s.execute("blocks.reference", &json!({ "handle": insert })).unwrap();
assert_eq!((r["definition"].clone(), r["properties"][0]["value"].clone()), (json!("Door"), json!("Closed")));
s.execute("undo", &json!({})).unwrap();
assert_eq!(block(&s), "Door");
let r = s.execute("blocks.reference", &json!({ "handle": insert })).unwrap();
assert_eq!(r["properties"][0]["value"], "Open");
for p in [
json!({ "handle": insert, "name": "Visibility1", "value": "Ajar" }),
json!({ "handle": insert, "name": "Visibility1" }),
json!({ "name": "Visibility1", "value": "Open" }),
json!({ "handle": insert, "value": "Open" }),
] {
assert!(s.execute("blocks.reference.set", &p).is_err(), "{p}");
}
}