From 8164e14a191c6725bec39d3f466ac6397f5d0d7b Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Thu, 2 Jul 2026 20:36:16 +0300 Subject: [PATCH] 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 --- src/app/plugin_host.rs | 41 +++++++++++++++++++++++++++++++++++++++++ src/io/mod.rs | 37 +++++++++++++++++++++++++++++++++++++ src/scene/entity.rs | 30 ++++++++++++++++++++++++++++++ 3 files changed, 108 insertions(+) diff --git a/src/app/plugin_host.rs b/src/app/plugin_host.rs index 5205dad94..d6d82f2fb 100644 --- a/src/app/plugin_host.rs +++ b/src/app/plugin_host.rs @@ -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(); diff --git a/src/io/mod.rs b/src/io/mod.rs index 703ca6743..bba939ae8 100644 --- a/src/io/mod.rs +++ b/src/io/mod.rs @@ -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)] diff --git a/src/scene/entity.rs b/src/scene/entity.rs index 94456d353..599f56682 100644 --- a/src/scene/entity.rs +++ b/src/scene/entity.rs @@ -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;