forked from 1b-agent/opencadstudio-upstream
fix(plugin): auto-register unknown layers so added entities survive DWG save (#252)
An entity added via a plugin's AddEntity with a common.layer name that no LAYER command ever created kept its layer in-session, but the DWG writer resolves an unknown layer name to a NULL handle, so on reopen the entity collapsed to layer 0. Add a shared Scene::ensure_layer(name) that registers a missing layer with a real handle (allocate_handle, per #67) and call it from add_entity and update_entity. This covers the plugin AddEntity/UpdateEntity IPC and every internal add path without a new IPC request. Existing/empty/"0" layers are no-ops, so normal draw commands are unaffected. Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
1 parent
f9f7138370
commit
8164e14a19
3 files changed
+108
No files matched your search
@@ -529,6 +529,47 @@ mod tests {
|
||||
assert!(!host.update_entity(EntityType::Point(ghost)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_and_update_auto_register_novel_layers_with_real_handles() {
|
||||
// A plugin adds/edits an entity naming a layer no LAYER command ever
|
||||
// created. The layer must gain a real table entry (non-null handle) so
|
||||
// it survives a DWG save instead of collapsing to layer 0 (#252, #67).
|
||||
let mut app = OpenCADStudio::new_for_test();
|
||||
app.tabs[0].is_start = false;
|
||||
let mut host = HostSession::new(&mut app, 0);
|
||||
|
||||
assert!(!host.document().layers.contains("PLUGIN-LAYER"));
|
||||
let mut pt = Point::at(acadrust::types::Vector3::new(3.0, 3.0, 0.0));
|
||||
pt.common.layer = "PLUGIN-LAYER".to_string();
|
||||
host.add_entity(EntityType::Point(pt));
|
||||
|
||||
let layer = host
|
||||
.document()
|
||||
.layers
|
||||
.get("PLUGIN-LAYER")
|
||||
.expect("novel layer auto-registered on add_entity");
|
||||
assert!(
|
||||
!layer.handle.is_null(),
|
||||
"auto-registered layer must carry a real handle (#67)"
|
||||
);
|
||||
|
||||
// Adding again on the same (now-existing) layer must not duplicate or
|
||||
// error — the always-present default "0" is likewise never re-created.
|
||||
let count_before = host.document().layers.len();
|
||||
host.add_entity(EntityType::Point(Point::new())); // default layer "0"
|
||||
let mut pt2 = Point::new();
|
||||
pt2.common.layer = "PLUGIN-LAYER".to_string();
|
||||
host.add_entity(EntityType::Point(pt2));
|
||||
assert_eq!(host.document().layers.len(), count_before);
|
||||
|
||||
// Retargeting an entity to a novel layer via update_entity registers it.
|
||||
let h = host.add_entity(EntityType::Point(Point::new()));
|
||||
let mut edited = host.document().get_entity(h).unwrap().clone();
|
||||
edited.common_mut().layer = "EDIT-LAYER".to_string();
|
||||
assert!(host.update_entity(edited));
|
||||
assert!(host.document().layers.contains("EDIT-LAYER"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remove_entity_deletes_and_clears_caches() {
|
||||
let mut app = OpenCADStudio::new_for_test();
|
||||
|
||||
@@ -995,6 +995,43 @@ mod layer_roundtrip_tests {
|
||||
fn dwg_preserves_multiple_new_layers() {
|
||||
assert!(roundtrip_layers("dwg", 3), "DWG dropped colliding new layers (issue #67)");
|
||||
}
|
||||
|
||||
// #252: an entity added (as a plugin does) on a layer that no LAYER command
|
||||
// ever created must keep that layer across a DWG save — `Scene::add_entity`
|
||||
// auto-registers it so the writer resolves a real handle instead of NULL
|
||||
// (which reopens as layer "0").
|
||||
#[test]
|
||||
fn dwg_preserves_entity_layer_auto_registered_on_add() {
|
||||
use acadrust::entities::Point;
|
||||
use acadrust::EntityType;
|
||||
|
||||
let mut scene = crate::scene::Scene::new();
|
||||
crate::io::linetypes::populate_document(&mut scene.document);
|
||||
|
||||
let mut pt = Point::new();
|
||||
pt.common.layer = "PLUGIN-LAYER".to_string();
|
||||
let h = scene.add_entity(EntityType::Point(pt));
|
||||
assert!(!h.is_null(), "entity was not added");
|
||||
|
||||
let path = std::env::temp_dir().join("ocs_entity_layer_rt.dwg");
|
||||
save_as_version(&scene.document, &path, acadrust::DxfVersion::AC1032).expect("save");
|
||||
let loaded = load_file(&path).expect("load");
|
||||
let _ = std::fs::remove_file(&path);
|
||||
|
||||
assert!(
|
||||
loaded.layers.contains("PLUGIN-LAYER"),
|
||||
"layer table dropped the auto-registered layer (#252)"
|
||||
);
|
||||
let ent = loaded
|
||||
.get_entity(h)
|
||||
.or_else(|| loaded.entities().find(|e| matches!(e, EntityType::Point(_))))
|
||||
.expect("point entity missing after round-trip");
|
||||
assert_eq!(
|
||||
ent.common().layer,
|
||||
"PLUGIN-LAYER",
|
||||
"entity collapsed to layer 0 on DWG save (#252)"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -4,6 +4,24 @@ use super::*;
|
||||
impl Scene {
|
||||
// ── Entity management ─────────────────────────────────────────────────
|
||||
|
||||
/// Register `name` in the layer table if it isn't already there, giving the
|
||||
/// new layer a real handle so it survives a DWG save (handle-based format;
|
||||
/// issue #67). Called whenever an entity is added or edited: an entity that
|
||||
/// names a layer no explicit LAYER command ever created — e.g. one supplied
|
||||
/// by a plugin through `add_entity` — otherwise has no table entry, so the
|
||||
/// DWG writer resolves its layer name to a NULL handle and it reopens on
|
||||
/// layer 0. Auto-registering keeps it on its own layer (#252). Names are
|
||||
/// registered verbatim so the writer's (case-insensitive) lookup matches;
|
||||
/// the always-present default layer "0" and empty names are no-ops.
|
||||
pub fn ensure_layer(&mut self, name: &str) {
|
||||
if name.trim().is_empty() || self.document.layers.contains(name) {
|
||||
return;
|
||||
}
|
||||
let mut layer = acadrust::tables::Layer::new(name);
|
||||
layer.handle = self.document.allocate_handle();
|
||||
let _ = self.document.layers.add(layer);
|
||||
}
|
||||
|
||||
pub fn add_entity(&mut self, mut entity: EntityType) -> Handle {
|
||||
// Only Insert / Block entities can introduce or reference a block
|
||||
// definition that the block cache must (re)build. Adding a plain
|
||||
@@ -59,6 +77,12 @@ impl Scene {
|
||||
}
|
||||
}
|
||||
|
||||
// Register the entity's layer if it names one no LAYER command created
|
||||
// (e.g. a plugin-supplied layer) so it survives a DWG save instead of
|
||||
// collapsing to layer 0 in the reopened file (#252).
|
||||
let layer = entity.common().layer.clone();
|
||||
self.ensure_layer(&layer);
|
||||
|
||||
// Route to the correct block based on current editing mode:
|
||||
// - PSPACE (paper layout, no active viewport): paper-space layout block.
|
||||
// - MSPACE or model layout: model space (document default).
|
||||
@@ -114,6 +138,12 @@ impl Scene {
|
||||
EntityType::Insert(_) | EntityType::Block(_) | EntityType::BlockEnd(_)
|
||||
);
|
||||
|
||||
// A plugin edit may retarget the entity to a novel layer; register it
|
||||
// so the edited entity keeps that layer on save instead of collapsing
|
||||
// to layer 0 in the reopened file (#252).
|
||||
let new_layer = entity.common().layer.clone();
|
||||
self.ensure_layer(&new_layer);
|
||||
|
||||
// Rebuild the derived-model seeds from the new entity (as add_entity).
|
||||
let hatch_seed = if let EntityType::Hatch(dxf) = &entity {
|
||||
let color = self.render_style(&entity).0;
|
||||
|
||||
Reference in new issue
Block a user