diff --git a/src/app/command_driver/entity_commit.rs b/src/app/command_driver/entity_commit.rs index 4656fab0c..8caae44ad 100644 --- a/src/app/command_driver/entity_commit.rs +++ b/src/app/command_driver/entity_commit.rs @@ -200,6 +200,7 @@ impl OpenCADStudio { pub(super) fn handle_commit_and_exit( &mut self, entity: codec::EntityType, + preserve_commit_layer: bool, ) -> Option> { let i = self.active_tab; // XATTACH: the definition and its INSERT are created together @@ -255,7 +256,11 @@ impl OpenCADStudio { ); let association_enabled = self.tabs[i].scene.document.header.dimension_associativity == 2; - let committed = self.commit_entity_handle(entity); + let committed = if preserve_commit_layer { + self.commit_entity_handle_preserve_layer(entity) + } else { + self.commit_entity_handle(entity) + }; if is_associative_dimension && association_enabled { if let Some(handle) = committed { let sources = self.infer_dimension_sources_guarded(i, handle); diff --git a/src/app/command_driver/mod.rs b/src/app/command_driver/mod.rs index 76ba3bfb0..21b19199e 100644 --- a/src/app/command_driver/mod.rs +++ b/src/app/command_driver/mod.rs @@ -507,7 +507,7 @@ impl OpenCADStudio { self.handle_copy_to_clipboard(handles, base); } CmdResult::CommitAndExit(entity) => { - if let Some(task) = self.handle_commit_and_exit(entity) { + if let Some(task) = self.handle_commit_and_exit(entity, preserve_commit_layer) { return task; } } diff --git a/src/app/plugin_host.rs b/src/app/plugin_host.rs index edf47fe0c..9f548eb26 100644 --- a/src/app/plugin_host.rs +++ b/src/app/plugin_host.rs @@ -350,6 +350,11 @@ impl<'a> HostSession<'a> { return handle; } let handle = self.app.tabs[self.tab].scene.add_entity(entity); + if self.app.tabs[self.tab].scene.layer_table_dirty { + self.app.tabs[self.tab].scene.layer_table_dirty = false; + self.app.tabs[self.tab].dirty = true; + self.app.refresh_layer_panel(); + } self.publish_document_view(); handle } @@ -362,6 +367,11 @@ impl<'a> HostSession<'a> { let _ = self.normalize_scripted_entity(None, entity); } let handles = self.app.tabs[self.tab].scene.add_entities(entities); + if self.app.tabs[self.tab].scene.layer_table_dirty { + self.app.tabs[self.tab].scene.layer_table_dirty = false; + self.app.tabs[self.tab].dirty = true; + self.app.refresh_layer_panel(); + } self.publish_document_view(); handles } @@ -381,6 +391,11 @@ impl<'a> HostSession<'a> { EntityType::AttributeEntity(attribute) => self.replace_nested_attribute(attribute), entity => self.app.tabs[self.tab].scene.update_entity(entity), }; + if self.app.tabs[self.tab].scene.layer_table_dirty { + self.app.tabs[self.tab].scene.layer_table_dirty = false; + self.app.tabs[self.tab].dirty = true; + self.app.refresh_layer_panel(); + } if ok { self.publish_document_view(); } @@ -439,6 +454,11 @@ impl<'a> HostSession<'a> { }; assert!(updated); } + if self.app.tabs[self.tab].scene.layer_table_dirty { + self.app.tabs[self.tab].scene.layer_table_dirty = false; + self.app.tabs[self.tab].dirty = true; + self.app.refresh_layer_panel(); + } self.set_dirty(); self.publish_document_view(); Ok(()) @@ -9697,6 +9717,44 @@ step('undo_move', lambda: M.move([u], (0, 0, 0), (7, 0, 0))) assert!(!host.modify_layer(non_existent)); } + #[test] + fn test_entity_creation_with_novel_layer_refreshes_gui() { + let mut app = OpenCADStudio::new_for_test(); + app.tabs[0].is_start = false; + + // Verify COGO_LINES does not exist initially + assert!(!app.tabs[0].scene.document.layers.contains("COGO_LINES")); + assert!(!app.ribbon.layer_names.contains(&"COGO_LINES".to_string())); + + // 1. Add entity via HostSession (simulating plugin add_entity / add_line) + { + let mut host = HostSession::new(&mut app, 0); + let mut line = codec::entities::Line::from_coords(0.0, 0.0, 0.0, 10.0, 10.0, 0.0); + line.common.layer = "COGO_LINES".to_string(); + let handle = host.add_entity(codec::EntityType::Line(line)); + assert_ne!(handle, codec::Handle::NULL); + } + + // Verify layer is created in document AND refreshed in ribbon + layer panel + assert!(app.tabs[0].scene.document.layers.contains("COGO_LINES")); + assert!(app.ribbon.layer_names.contains(&"COGO_LINES".to_string()), + "Ribbon layers should contain COGO_LINES after add_entity"); + assert!(app.tabs[0].layers.layers.iter().any(|l| l.name == "COGO_LINES"), + "Layer panel should contain COGO_LINES after add_entity"); + + // 2. Commit entity via command driver (simulating interactive tool / palette commit) + let mut line2 = codec::entities::Line::from_coords(10.0, 10.0, 0.0, 20.0, 20.0, 0.0); + line2.common.layer = "SURVEY_TRAVERSE".to_string(); + let _ = app.commit_entity_handle_preserve_layer(codec::EntityType::Line(line2)); + + // Verify SURVEY_TRAVERSE layer is created and refreshed in ribbon + layer panel + assert!(app.tabs[0].scene.document.layers.contains("SURVEY_TRAVERSE")); + assert!(app.ribbon.layer_names.contains(&"SURVEY_TRAVERSE".to_string()), + "Ribbon layers should contain SURVEY_TRAVERSE after commit_entity"); + assert!(app.tabs[0].layers.layers.iter().any(|l| l.name == "SURVEY_TRAVERSE"), + "Layer panel should contain SURVEY_TRAVERSE after commit_entity"); + } + #[test] fn test_plugin_execute_command() { let mut app = OpenCADStudio::new_for_test(); diff --git a/src/app/properties.rs b/src/app/properties.rs index 6ab2ae1bb..8161a2a1b 100644 --- a/src/app/properties.rs +++ b/src/app/properties.rs @@ -3491,6 +3491,11 @@ handles={handles_ms:.1} panel={:.1} ribbon={ribbon_ms:.1} tail={:.1} selected={} if tracks_dimension_chain { self.tabs[i].scene.last_created_dimension = new_handle; } + if self.tabs[i].scene.layer_table_dirty { + self.tabs[i].scene.layer_table_dirty = false; + self.tabs[i].dirty = true; + self.refresh_layer_panel(); + } new_handle } } diff --git a/src/scene/entity.rs b/src/scene/entity.rs index 6108ac028..de7700ca8 100644 --- a/src/scene/entity.rs +++ b/src/scene/entity.rs @@ -143,13 +143,17 @@ impl Scene { /// 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; + pub fn ensure_layer(&mut self, name: &str) -> bool { + let trimmed = name.trim(); + if trimmed.is_empty() || trimmed == "0" || self.document.layers.contains(trimmed) { + return false; } - let mut layer = codec::tables::Layer::new(name); + let mut layer = codec::tables::Layer::new(trimmed); layer.handle = self.document.allocate_handle(); let _ = self.document.layers.add(layer); + self.invalidate_layer_dependencies(&[trimmed.to_string()]); + self.layer_table_dirty = true; + true } pub(crate) fn ensure_app_id(&mut self, name: &str) { diff --git a/src/scene/mod.rs b/src/scene/mod.rs index a08c183b8..2a6b45ee7 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -2054,6 +2054,9 @@ pub struct Scene { pub preview_text: Vec, /// Committed-segment wire drawn during multi-point commands (normal colour). pub interim_wire: Option, + /// Set when an entity creation or update automatically registers a novel + /// layer into the document layer table, signalling the UI to refresh. + pub layer_table_dirty: bool, pub camera_generation: u64, /// Incremented whenever geometry-affecting state changes (entities, selection, /// preview wires, layer visibility, layout). The GPU pipeline uses this to @@ -2626,6 +2629,7 @@ impl Scene { preview_hatches: Arc::new(Vec::new()), preview_text: vec![], interim_wire: None, + layer_table_dirty: false, camera_generation: 0, geometry_epoch: GEOMETRY_EPOCH.fetch_add(1, Ordering::Relaxed), projection_bounds_epoch: std::cell::Cell::new(0),