mirror of
https://github.com/storytold/cadcraft.git
synced 2026-10-24 10:56:51 -06:00
* Dynamic blocks: read parameters and actions from AutoCAD files A dynamic block's evaluation graph (kept as foreign data since #528) is read into a typed model, `cadcraft_doc::dynblock`, without changing the kept data: parameters (linear with value sets, point, visibility states, lookup, base point; polar, XY, rotation, alignment and flip by geometry) and actions (stretch with its frame and moved vertices, lookup tables; move, scale, rotate, flip, array, polar stretch by kind), each with its graph id and the parameters it follows. - `blocks.dynamic {name}` returns the model as JSON (handles as hex); `blocks.list` marks dynamic blocks. - Counts and values come from the file: capped by the groups present, non-finite numbers and missing members skipped. - Tests: a synthetic DOOR block (visibility, linear list, stretch, lookup), its survival through a save, damaged data, and the engine commands. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Dynamic block references: their values and custom properties A dynamic block reference's extension dictionary says which dynamic block it shows (AcDbBlockRepresentation ▸ AcDbRepData) and the values it was given (AppDataCache ▸ ACAD_ENHANCEDBLOCKDATA, one XRECORD per parameter graph id). `dynblock::reference` reads them; a reference to the dynamic block itself has the block's values. - `dynblock::custom_properties`: the Properties palette's Custom section, in parameter order, only parameters shown in Properties: visibility state, lookup value worked out from the lookup tables (their "no match" name, e.g. Custom, when the inputs match no row), distances and point positions in drawing units (block units × the reference's scale), with the choices of each (states, lookup values, a distance's list). - Properties: a dynamic block reference shows its dynamic block's name instead of its anonymous block's, and a read-only Custom section. - `blocks.reference {handle}` returns the definition, values and properties. - Tests: synthetic references (own values, an anonymous block scaled 2, a width no lookup row has), survival through a save, the engine command. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Dynamic block references: change the visibility state Choosing a visibility state (or a lookup value whose tables set only visibility states) in Properties ▸ Custom, or `blocks.reference.set {handle, name, value}`, changes a dynamic block reference as AutoCAD does: - The reference gets a new anonymous block: its current one with the state's objects shown and the parameter's other objects hidden (matched to the dynamic block's objects by their AcDbBlockRepETag element tags). The old anonymous block goes when nothing else uses it. - AutoCAD's link data is written: the new block's record carries AcDbBlockRepBTag (the dynamic block's record), copied objects keep their element tags, and the reference's AcDbBlockRepresentation data records the value (an existing value record is updated; a new one copies another reference's record for the same parameter when there is one). - An attribute whose definition the state hides is written hidden (60). - Changes CADCraft can't make yet (distances, positions, lookups that set them) are refused with a message and change nothing; Properties shows only what can change as a dropdown (`CustomProperty::editable`). - Tests: the synthetic DOOR references (own values and an anonymous block), survival through a save, refusals, and the engine command with undo. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * DWG: dynamic block lookup parameters keep their name and lookup action AutoCAD writes a lookup parameter's subclass marker as AcDbBlockLookUpParameter; the DWG bridge reads AcDbBlockLookupParameter and matches it exactly, so the parameter's name (303) and lookup action (94) were lost in DWG files and AutoCAD's lookups stopped working (a visibility change no longer resized the block). The bridge DXF now uses its spelling. Found by driving AutoCAD on CADCraft's output. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Cody Vogt <18035288+codyvogt@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com>
141 lines
6.6 KiB
Rust
141 lines
6.6 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));
|
|
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}");
|
|
}
|
|
}
|