forked from 1b-agent/opencadstudio-upstream
Merge pull request #1393 from yztink/feat/plugin-layer-properties
feat(plugin): add layer creation and modification host API with GUI refresh
This commit is contained in:
commit
8075caa744
6 files changed
+298
No files matched your search
@@ -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<Handle> {
|
||||
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<acadrust::types::Color>,
|
||||
pub linetype: Option<String>,
|
||||
pub lineweight: Option<acadrust::types::LineWeight>,
|
||||
pub off: Option<bool>,
|
||||
pub frozen: Option<bool>,
|
||||
pub locked: Option<bool>,
|
||||
pub plottable: Option<bool>,
|
||||
pub transparency: Option<acadrust::types::Transparency>,
|
||||
pub description: Option<String>,
|
||||
}
|
||||
|
||||
/// Simplified, read-only entity kind exposed by [`DocumentReader`].
|
||||
|
||||
@@ -416,6 +416,34 @@ impl HostApi for PluginHostApi {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn add_layer(&mut self, config: crate::host::LayerConfig) -> Option<Handle> {
|
||||
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.
|
||||
|
||||
@@ -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<std::ffi::OsString>),
|
||||
/// Optional entity handle (e.g. from AddLayer).
|
||||
OptHandle(Option<Handle>),
|
||||
}
|
||||
|
||||
/// Messages sent from the host to the plugin runner.
|
||||
|
||||
@@ -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)),
|
||||
}
|
||||
}
|
||||
@@ -730,6 +730,34 @@ impl HostApi for V4PluginHostApi {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn add_layer(&mut self, config: crate::host::LayerConfig) -> Option<Handle> {
|
||||
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)]
|
||||
|
||||
@@ -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<Handle> {
|
||||
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<Handle> {
|
||||
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));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user