forked from 1b-agent/opencadstudio-upstream
The codec and kernel repositories and crates are renamed opencadcodec and opencadkernel. Inside OCS they are depended on under short keys, so code reads `codec::`, `kernel::` and the new `graph::` (opencadgraph, the node graph engine). File-format and protocol spellings stay as they were: the ACIS product id `acadrust`, the ACADRUST_* switches and the plugin version keys `acadrust_version` / `acadrust_source`. A button left of MCP on the command line opens a translucent node graph over the viewport, with a palette of object, input, math, logic, list, text and geometry nodes added by click or drag. An object node owns drawing entities and its rows are the Properties panel's own rows for them: the single-object page is extracted from refresh_properties as entity_property_sections, values use the control protocol's property_json, and writes go through set_property_value with a property target override, so the panel, MCP and the graph share one read/write path. A list reaching an object row makes one entity per element. Each graph action is one undo step; the canvas pans and zooms. Co-Authored-By: Claude Opus 5.5 <[email protected]>
140 lines
5.4 KiB
Rust
140 lines
5.4 KiB
Rust
// Some DWGs written by other programs leave the header's root named-objects
|
|
// dictionary pointer unresolvable — it names a handle that never loaded (or a
|
|
// non-dictionary), while the real named-object sub-dictionaries are owned by an
|
|
// unrelated handle. Navigating that root then silently no-ops, so registering a
|
|
// new named-object entry (a page setup, the variable dictionary, an
|
|
// annotation scale) would vanish instead of persisting.
|
|
//
|
|
// `annotative::root_named_dict_handle` resolves the root robustly and, when it
|
|
// truly can't be found, synthesises a fresh one so registration sticks. These
|
|
// guards exercise that repair through the page-setup and CTAB write paths.
|
|
|
|
use codec::objects::{ObjectType, PlotSettings};
|
|
use codec::Handle;
|
|
use OpenCADStudio::scene::Scene;
|
|
|
|
/// Break a document the way a foreign DWG does: dangle the header root pointer
|
|
/// and re-home the real root under a non-dictionary owner, so neither the
|
|
/// pointer nor the `owner == NULL` heuristic can rediscover it.
|
|
fn break_root_pointer(scene: &mut Scene) {
|
|
let doc = &mut scene.document;
|
|
let real_root = doc.header.named_objects_dict_handle;
|
|
if let Some(ObjectType::Dictionary(d)) = doc.objects.get_mut(&real_root) {
|
|
// Owner points at the dimstyle-control handle (0x0A) — present in the
|
|
// file but not a dictionary, exactly like the repro drawing.
|
|
d.owner = Handle::new(0x0A);
|
|
}
|
|
doc.header.named_objects_dict_handle = Handle::new(0x00DE_AD00);
|
|
doc.header.acad_plotsettings_dict_handle = Handle::new(0x00BA_D000);
|
|
}
|
|
|
|
#[test]
|
|
fn page_setup_persists_when_root_pointer_is_unresolvable() {
|
|
let mut scene = Scene::new();
|
|
break_root_pointer(&mut scene);
|
|
|
|
scene.page_setup_save("Metric-A4", PlotSettings::new("Metric-A4"));
|
|
|
|
// The setup is retrievable again…
|
|
assert!(
|
|
scene.page_setup_names().contains(&"Metric-A4".to_string()),
|
|
"saved page setup must be listed back"
|
|
);
|
|
assert!(
|
|
scene.page_setup_get("Metric-A4").is_some(),
|
|
"saved page setup must be resolvable"
|
|
);
|
|
|
|
// …and it is linked into a *resolvable* root named-objects dictionary, so it
|
|
// will round-trip through a save rather than dangling.
|
|
let root_h = scene.document.header.named_objects_dict_handle;
|
|
let ObjectType::Dictionary(root) = scene
|
|
.document
|
|
.objects
|
|
.get(&root_h)
|
|
.expect("root handle must resolve to a loaded object")
|
|
else {
|
|
panic!("root handle must resolve to a dictionary");
|
|
};
|
|
assert!(
|
|
root.entries.iter().any(|(k, _)| k == "ACAD_PLOTSETTINGS"),
|
|
"ACAD_PLOTSETTINGS must be registered in the (repaired) root NOD"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn page_setup_survives_a_dwg_roundtrip_after_root_repair() {
|
|
let mut scene = Scene::new();
|
|
break_root_pointer(&mut scene);
|
|
scene.page_setup_save("Wide", PlotSettings::new("Wide"));
|
|
|
|
// Full DWG write/read cycle: the repaired root must carry the setup across.
|
|
let bytes = OpenCADStudio::io::save_to_bytes(&scene.document, "dwg", scene.document.version)
|
|
.expect("save to DWG bytes");
|
|
let doc = OpenCADStudio::io::load_bytes("roundtrip.dwg", bytes).expect("reload DWG bytes");
|
|
|
|
let mut reloaded = Scene::new();
|
|
reloaded.document = doc;
|
|
assert!(
|
|
reloaded.page_setup_names().contains(&"Wide".to_string()),
|
|
"page setup must persist across a DWG save/reload once the root is repaired"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn existing_but_unlinked_plot_settings_are_still_listed() {
|
|
// A drawing whose ACAD_PLOTSETTINGS dictionary exists but whose header
|
|
// pointer to it is stale: the setups must still be discoverable via the
|
|
// PlotSettings objects' owner, mirroring the scale-list fallback.
|
|
let mut scene = Scene::new();
|
|
scene.page_setup_save("Draft", PlotSettings::new("Draft"));
|
|
// Now stale the header pointer (leave the dictionary + object intact).
|
|
scene.document.header.acad_plotsettings_dict_handle = Handle::new(0x00BA_D000);
|
|
|
|
assert!(
|
|
scene.page_setup_names().contains(&"Draft".to_string()),
|
|
"page setup must be found via the PlotSettings owner when the header \
|
|
plot-settings pointer is stale"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn ctab_is_created_against_a_repaired_root() {
|
|
let mut scene = Scene::new();
|
|
break_root_pointer(&mut scene);
|
|
|
|
// Switching the active layout records CTAB; with a broken root pointer this
|
|
// used to silently drop the record.
|
|
scene.set_current_layout("Layout1".to_string());
|
|
assert_eq!(
|
|
OpenCADStudio::io::saved_active_layout(&scene.document).as_deref(),
|
|
Some("Layout1"),
|
|
"CTAB must persist even when the root pointer needed repair"
|
|
);
|
|
|
|
let root_h = scene.document.header.named_objects_dict_handle;
|
|
let ObjectType::Dictionary(root) = scene
|
|
.document
|
|
.objects
|
|
.get(&root_h)
|
|
.expect("root must resolve")
|
|
else {
|
|
panic!("root must be a dictionary");
|
|
};
|
|
let variable_dictionary = root
|
|
.entries
|
|
.iter()
|
|
.find(|(key, _)| key == "AcDbVariableDictionary")
|
|
.map(|(_, handle)| *handle)
|
|
.expect("variable dictionary must be registered in the repaired root NOD");
|
|
let Some(ObjectType::Dictionary(variable_dictionary)) =
|
|
scene.document.objects.get(&variable_dictionary)
|
|
else {
|
|
panic!("variable dictionary must resolve");
|
|
};
|
|
assert!(
|
|
variable_dictionary.entries.iter().any(|(key, _)| key == "CTAB"),
|
|
"CTAB must be registered in the variable dictionary"
|
|
);
|
|
}
|