diff --git a/crates/ocs_plugin_api/src/host.rs b/crates/ocs_plugin_api/src/host.rs index 19130c596..c64ff4e91 100644 --- a/crates/ocs_plugin_api/src/host.rs +++ b/crates/ocs_plugin_api/src/host.rs @@ -552,6 +552,37 @@ pub trait HostApi { let _ = tab_id; None } + + /// Add a layer to the active document with full initial properties. + /// If an optional property in `config` is `None`, standard CAD defaults are applied. + /// Returns `None` if the layer already exists or `config.name` is invalid. + fn add_layer(&mut self, config: LayerConfig) -> Option { + let _ = config; + None + } + + /// Modify specified properties of an existing layer in the active document. + /// Properties that are `None` in `config` are left untouched as-is. + /// Returns `false` if the layer does not exist or `config.name` is invalid. + fn modify_layer(&mut self, config: LayerConfig) -> bool { + let _ = config; + false + } +} + +/// Configuration properties for creating or modifying a layer. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] +pub struct LayerConfig { + pub name: String, + pub color: Option, + pub linetype: Option, + pub lineweight: Option, + pub off: Option, + pub frozen: Option, + pub locked: Option, + pub plottable: Option, + pub transparency: Option, + pub description: Option, } /// Simplified, read-only entity kind exposed by [`DocumentReader`]. diff --git a/crates/ocs_plugin_api/src/ipc/client.rs b/crates/ocs_plugin_api/src/ipc/client.rs index 3a617fc02..0a7baa779 100644 --- a/crates/ocs_plugin_api/src/ipc/client.rs +++ b/crates/ocs_plugin_api/src/ipc/client.rs @@ -416,6 +416,34 @@ impl HostApi for PluginHostApi { } } } + + fn add_layer(&mut self, config: crate::host::LayerConfig) -> Option { + match self.client.request(PluginRequest::AddLayer(config)) { + Ok(PluginResponse::OptHandle(h)) => h, + Ok(other) => { + eprintln!("[plugin] unexpected AddLayer response: {other:?}"); + None + } + Err(e) => { + eprintln!("[plugin] AddLayer request failed: {e}"); + None + } + } + } + + fn modify_layer(&mut self, config: crate::host::LayerConfig) -> bool { + match self.client.request(PluginRequest::ModifyLayer(config)) { + Ok(PluginResponse::Bool(b)) => b, + Ok(other) => { + eprintln!("[plugin] unexpected ModifyLayer response: {other:?}"); + false + } + Err(e) => { + eprintln!("[plugin] ModifyLayer request failed: {e}"); + false + } + } + } } /// Sentinel reader used when the shared-memory view could not be initialized. diff --git a/crates/ocs_plugin_api/src/ipc/protocol.rs b/crates/ocs_plugin_api/src/ipc/protocol.rs index c20cad1b2..81a1ea3cf 100644 --- a/crates/ocs_plugin_api/src/ipc/protocol.rs +++ b/crates/ocs_plugin_api/src/ipc/protocol.rs @@ -138,6 +138,10 @@ pub enum PluginRequest { GetTabId, /// V5: ask the host for the filesystem path of the document in `tab_id`. DocumentPath { tab_id: u64 }, + /// Add a layer to the active document with full initial properties. + AddLayer(crate::host::LayerConfig), + /// Modify specified properties of an existing layer in the active document. + ModifyLayer(crate::host::LayerConfig), } /// Responses the host sends back for `PluginRequest`. @@ -164,6 +168,8 @@ pub enum PluginResponse { TabId(u64), /// V5: filesystem path of the document in the requested tab, if any. DocumentPath(Option), + /// Optional entity handle (e.g. from AddLayer). + OptHandle(Option), } /// Messages sent from the host to the plugin runner. diff --git a/crates/ocs_plugin_api/src/ipc/server.rs b/crates/ocs_plugin_api/src/ipc/server.rs index 055880ef0..05d03998f 100644 --- a/crates/ocs_plugin_api/src/ipc/server.rs +++ b/crates/ocs_plugin_api/src/ipc/server.rs @@ -77,5 +77,7 @@ pub fn handle_plugin_request( DocumentPath { tab_id } => PluginResponse::DocumentPath( host.document_path(tab_id).map(|path| path.into_os_string()), ), + AddLayer(config) => PluginResponse::OptHandle(host.add_layer(config)), + ModifyLayer(config) => PluginResponse::Bool(host.modify_layer(config)), } } diff --git a/crates/ocs_plugin_api/src/ipc/v4/client.rs b/crates/ocs_plugin_api/src/ipc/v4/client.rs index 03cbf20ae..1e5149c53 100644 --- a/crates/ocs_plugin_api/src/ipc/v4/client.rs +++ b/crates/ocs_plugin_api/src/ipc/v4/client.rs @@ -730,6 +730,34 @@ impl HostApi for V4PluginHostApi { } } } + + fn add_layer(&mut self, config: crate::host::LayerConfig) -> Option { + match self.request(PluginRequest::AddLayer(config)) { + Ok(PluginResponse::OptHandle(h)) => h, + Ok(other) => { + eprintln!("[plugin] unexpected AddLayer response: {other:?}"); + None + } + Err(e) => { + eprintln!("[plugin] AddLayer request failed: {e}"); + None + } + } + } + + fn modify_layer(&mut self, config: crate::host::LayerConfig) -> bool { + match self.request(PluginRequest::ModifyLayer(config)) { + Ok(PluginResponse::Bool(b)) => b, + Ok(other) => { + eprintln!("[plugin] unexpected ModifyLayer response: {other:?}"); + false + } + Err(e) => { + eprintln!("[plugin] ModifyLayer request failed: {e}"); + false + } + } + } } #[cfg(test)] diff --git a/src/app/plugin_host.rs b/src/app/plugin_host.rs index d60992822..5ac7204c2 100644 --- a/src/app/plugin_host.rs +++ b/src/app/plugin_host.rs @@ -257,6 +257,144 @@ impl<'a> HostSession<'a> { pub fn push_error(&mut self, msg: &str) { self.app.command_line.push_error(msg); } + + pub fn add_layer(&mut self, config: ocs_plugin_api::host::LayerConfig) -> Option { + let trimmed = config.name.trim(); + if trimmed.is_empty() { + return None; + } + + let doc = self.document_mut(); + if doc.layers.contains(trimmed) { + return None; + } + + let resolved_lt = if let Some(ref lt_name) = config.linetype { + let matched = doc + .line_types + .iter() + .find(|lt| lt.name.eq_ignore_ascii_case(lt_name)) + .map(|lt| lt.name.clone()); + if let Some(m) = matched { + m + } else { + crate::io::linetypes::populate_document(doc); + doc.line_types + .iter() + .find(|lt| lt.name.eq_ignore_ascii_case(lt_name)) + .map(|lt| lt.name.clone()) + .unwrap_or_else(|| lt_name.clone()) + } + } else { + "Continuous".to_string() + }; + + let mut layer = acadrust::tables::Layer::new(trimmed); + let handle = doc.allocate_handle(); + layer.handle = handle; + layer.color = config.color.unwrap_or(acadrust::types::Color::Index(7)); + layer.line_type = resolved_lt; + layer.line_weight = config.lineweight.unwrap_or(acadrust::types::LineWeight::ByLayer); + layer.flags.off = config.off.unwrap_or(false); + if let Some(frz) = config.frozen { + if frz { + layer.freeze(); + } else { + layer.thaw(); + } + } + layer.flags.locked = config.locked.unwrap_or(false); + layer.is_plottable = config.plottable.unwrap_or(true); + layer.transparency = config.transparency.unwrap_or(acadrust::types::Transparency::ByLayer); + layer.description = config.description.unwrap_or_default(); + + let _ = doc.layers.add(layer); + + self.app.tabs[self.tab].dirty = true; + self.app.tabs[self.tab] + .scene + .invalidate_layer_dependencies(&[trimmed.to_string()]); + self.app.refresh_layer_panel(); + self.publish_document_view(); + Some(handle) + } + + pub fn modify_layer(&mut self, config: ocs_plugin_api::host::LayerConfig) -> bool { + let trimmed = config.name.trim(); + if trimmed.is_empty() { + return false; + } + + let resolved_lt = if let Some(ref lt_name) = config.linetype { + let doc = self.document_mut(); + let matched = doc + .line_types + .iter() + .find(|lt| lt.name.eq_ignore_ascii_case(lt_name)) + .map(|lt| lt.name.clone()); + if let Some(m) = matched { + Some(m) + } else { + crate::io::linetypes::populate_document(doc); + Some( + doc.line_types + .iter() + .find(|lt| lt.name.eq_ignore_ascii_case(lt_name)) + .map(|lt| lt.name.clone()) + .unwrap_or_else(|| lt_name.clone()), + ) + } + } else { + None + }; + + let doc = self.document_mut(); + let Some(existing) = doc.layers.get_mut(trimmed) else { + return false; + }; + + if let Some(c) = config.color { + existing.color = c; + existing.color_name = None; + existing.book_name = None; + } + if let Some(lt) = resolved_lt { + existing.line_type = lt; + } + if let Some(lw) = config.lineweight { + existing.line_weight = lw; + } + if let Some(off) = config.off { + existing.flags.off = off; + } + if let Some(frz) = config.frozen { + if frz { + existing.freeze(); + } else { + existing.thaw(); + } + } + if let Some(lck) = config.locked { + existing.flags.locked = lck; + } + if let Some(plt) = config.plottable { + existing.is_plottable = plt; + } + if let Some(tr) = config.transparency { + existing.transparency = tr; + } + if let Some(desc) = config.description { + existing.description = desc; + } + + self.app.tabs[self.tab].dirty = true; + self.app.tabs[self.tab] + .scene + .invalidate_layer_dependencies(&[trimmed.to_string()]); + self.app.refresh_layer_panel(); + self.publish_document_view(); + true + } } /// The stable contract a plugin's `dispatch` sees. Each method forwards to the @@ -360,6 +498,12 @@ impl HostApi for HostSession<'_> { fn close_document_view_v4(&mut self, tab_id: u64) { self.close_document_view_v4(tab_id) } + fn add_layer(&mut self, config: ocs_plugin_api::host::LayerConfig) -> Option { + self.add_layer(config) + } + fn modify_layer(&mut self, config: ocs_plugin_api::host::LayerConfig) -> bool { + self.modify_layer(config) + } } /// Bridges a plugin's [`InteractiveCommand`](ocs_plugin_api::host::InteractiveCommand) @@ -860,4 +1004,63 @@ mod tests { assert_eq!(got.values.len(), 1); assert!(matches!(got.values[0], XDataValue::Integer32(123))); } + + #[test] + fn test_plugin_add_layer_with_defaults_and_modify() { + use ocs_plugin_api::host::LayerConfig; + + let mut app = OpenCADStudio::new_for_test(); + app.tabs[0].is_start = false; + let mut host = HostSession::new(&mut app, 0); + + // 1. Add layer with minimal config (only name specified) + let config = LayerConfig { + name: "ELECTRICAL".to_string(), + ..Default::default() + }; + let handle = host.add_layer(config).expect("should create layer"); + assert_ne!(handle, acadrust::Handle::NULL); + + // Verify defaults were applied + let layer = host.document().layers.get("ELECTRICAL").expect("layer should exist"); + assert_eq!(layer.color, acadrust::types::Color::Index(7)); + assert_eq!(layer.line_type, "Continuous"); + assert_eq!(layer.line_weight, acadrust::types::LineWeight::ByLayer); + assert!(!layer.flags.off); + assert!(!layer.flags.frozen); + assert!(!layer.flags.locked); + assert!(layer.is_plottable); + + // 2. Duplicate add_layer should be rejected (return None) + let dup_config = LayerConfig { + name: "ELECTRICAL".to_string(), + color: Some(acadrust::types::Color::Index(1)), + ..Default::default() + }; + assert!(host.add_layer(dup_config).is_none(), "duplicate layer should return None"); + + // 3. Modify only color and locked; other properties should remain untouched + let mod_config = LayerConfig { + name: "ELECTRICAL".to_string(), + color: Some(acadrust::types::Color::Index(1)), + locked: Some(true), + ..Default::default() + }; + assert!(host.modify_layer(mod_config)); + + let updated = host.document().layers.get("ELECTRICAL").expect("layer should exist"); + assert_eq!(updated.color, acadrust::types::Color::Index(1)); // Modified to red + assert!(updated.flags.locked); // Modified to locked + assert_eq!(updated.line_type, "Continuous"); // Kept as-is + assert_eq!(updated.line_weight, acadrust::types::LineWeight::ByLayer); // Kept as-is + assert!(!updated.flags.off); // Kept as-is + + // 4. Modify nonexistent layer returns false + let non_existent = LayerConfig { + name: "DOES_NOT_EXIST".to_string(), + color: Some(acadrust::types::Color::Index(2)), + ..Default::default() + }; + assert!(!host.modify_layer(non_existent)); + } }