diff --git a/src/app/automation.rs b/src/app/automation.rs index 8012361d7..9b12dcbb5 100644 --- a/src/app/automation.rs +++ b/src/app/automation.rs @@ -283,6 +283,8 @@ impl OpenCADStudio { // selection check has to run on this path too. #[cfg(not(target_arch = "wasm32"))] self.notify_plugins_selection_changed(); + #[cfg(not(target_arch = "wasm32"))] + self.notify_plugins_document_changed(); res } @@ -737,6 +739,36 @@ impl OpenCADStudio { #[cfg(test)] mod tests { + #[cfg(not(target_arch = "wasm32"))] + #[test] + fn built_in_edits_advance_plugin_document_fingerprint_once() { + let mut app = super::OpenCADStudio::new_for_test(); + app.automation_op(r#"{"op":"new"}"#); + let before = app.last_plugin_document.expect("new drawing published"); + app.automation_op(r#"{"op":"run","cmd":"LINE 0,0 10,0"}"#); + let after = app.last_plugin_document.expect("line edit published"); + assert_eq!(after.0, app.tabs[app.active_tab].id); + assert_ne!(after, before); + assert_eq!(after.1, app.tabs[app.active_tab].scene.geometry_epoch); + app.automation_op(r#"{"op":"query","type":"Line"}"#); + assert_eq!(app.last_plugin_document, Some(after)); + } + + #[cfg(not(target_arch = "wasm32"))] + #[test] + fn plugin_edit_publication_is_not_repeated_at_message_boundary() { + use acadrust::entities::Point; + use acadrust::EntityType; + + let mut app = super::OpenCADStudio::new_for_test(); + app.automation_op(r#"{"op":"new"}"#); + let tab = app.active_tab; + let mut host = super::super::plugin_host::HostSession::new(&mut app, tab); + host.add_entity(EntityType::Point(Point::new())); + let published = app.last_plugin_document.expect("plugin write published"); + app.notify_plugins_document_changed(); + assert_eq!(app.last_plugin_document, Some(published)); + } use crate::app::OpenCADStudio; #[test] diff --git a/src/app/mod.rs b/src/app/mod.rs index 69e09fc03..b3d1aec92 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -802,6 +802,11 @@ pub(super) struct OpenCADStudio { /// `SelectionChangedV4` fires once per real change rather than per message. #[cfg(not(target_arch = "wasm32"))] last_plugin_selection: Option<(u64, u64)>, + /// `(tab id, geometry epoch)` last published to the V4 document view. + /// Built-in edits bypass HostSession, so this is checked at message + /// boundaries as well as after plugin-initiated writes. + #[cfg(not(target_arch = "wasm32"))] + last_plugin_document: Option<(u64, u64)>, /// External add-on packages found in the plugins folder, refreshed when the /// Plugin Manager opens. external_plugins: Vec, @@ -3750,6 +3755,8 @@ impl OpenCADStudio { disabled_plugins: rustc_hash::FxHashSet::default(), #[cfg(not(target_arch = "wasm32"))] last_plugin_selection: None, + #[cfg(not(target_arch = "wasm32"))] + last_plugin_document: None, external_plugins: Vec::new(), loaded_plugin_ids: rustc_hash::FxHashSet::default(), plugin_load_errors: rustc_hash::FxHashMap::default(), diff --git a/src/app/plugin_host.rs b/src/app/plugin_host.rs index d60992822..25033addc 100644 --- a/src/app/plugin_host.rs +++ b/src/app/plugin_host.rs @@ -92,7 +92,11 @@ impl<'a> HostSession<'a> { } } #[cfg(not(target_arch = "wasm32"))] - v4_support::publish_document_view_v4(self.tab_id(), doc); + { + v4_support::publish_document_view_v4(self.tab_id(), doc); + self.app.last_plugin_document = + Some((self.tab_id(), self.app.tabs[self.tab].scene.geometry_epoch)); + } } pub fn add_entity(&mut self, entity: EntityType) -> Handle { diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index 1f999afd0..e0f9a23a4 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -262,6 +262,23 @@ impl OpenCADStudio { crate::plugin::v4_support::publish_selection_changed_v4(tab_id, handles); } + /// Refresh the shared V4 document after built-in geometry edits. Plugin + /// writes publish through HostSession immediately; the fingerprint avoids + /// sending a duplicate notification at this message boundary. + #[cfg(not(target_arch = "wasm32"))] + pub(super) fn notify_plugins_document_changed(&mut self) { + if self.active_tab >= self.tabs.len() { + return; + } + let tab = &self.tabs[self.active_tab]; + let key = (tab.id, tab.scene.geometry_epoch); + if self.last_plugin_document == Some(key) { + return; + } + self.last_plugin_document = Some(key); + crate::plugin::v4_support::publish_document_view_v4(tab.id, &tab.scene.document); + } + pub fn update(&mut self, msg: Message) -> Task { self.control_observe_user_message(&msg); let perf_started = crate::perf::enabled().then(Instant::now); @@ -319,6 +336,8 @@ impl OpenCADStudio { // and plugin request draining). #[cfg(not(target_arch = "wasm32"))] self.notify_plugins_selection_changed(); + #[cfg(not(target_arch = "wasm32"))] + self.notify_plugins_document_changed(); // OTRACK acquires tracking points only while a command or grip drag is // running; drop them once neither is active so the temporary tracking // points / vectors disappear when the command ends (issue #64).