Files
OneCAD-Draft/crates/ocs_plugin_api/src/ipc/server.rs
T
Sebastian ca2f0c9857 feat(plugin): name-addressed XRecord CRUD on the HostApi (API v8)
Add create/read/update/delete/list for XRecord objects under
named-object-dictionary paths to the plugin HostApi, with five additive
PluginRequest/PluginResponse variants, dispatch through the shared
handler, in-process HostSession methods and both out-of-process proxies.

- dictionary paths walk from the NOD root; create seeds missing
  dictionaries (ensure_image_dictionary bootstrap) and refuses existing
  names; update refuses missing ones; refusals precede the undo step
- entry group codes validated (1..=369, never 5/105); payload content is
  the plugin's own
- each mutating call is one undo step: the pending history snapshot
  restores the objects map byte-exactly, auto-created dictionaries
  included (tested)
- typed entries survive DWG and DXF write/reload round-trips (tested);
  update drops verbatim raw bytes so saves emit the new content
- bump API_VERSION 7 -> 8 with the build.rs version_info in lockstep,
  mirroring the v7 addition; MIN_SUPPORTED stays 3
2026-10-11 19:57:20 +02:00

156 lines
6.2 KiB
Rust

//! Host-side IPC request handler.
use crate::host::HostApi;
use crate::ipc::protocol::{PluginRequest, PluginResponse};
fn addressable_document_snapshot(host: &dyn HostApi) -> codec::CadDocument {
let mut snapshot = host.document().clone();
let attributes = host
.document()
.entities()
.filter_map(|entity| match entity {
codec::EntityType::Insert(insert) => Some(insert.attributes.clone()),
_ => None,
})
.flatten()
.map(codec::EntityType::AttributeEntity)
.collect::<Vec<_>>();
for attribute in attributes {
// Attribute entities remain canonical children of INSERT in the host
// document. The IPC snapshot also indexes them by handle so plugin
// document models can address a child exactly like any other entity.
let _ = snapshot.add_entity(attribute);
}
snapshot
}
/// Apply one plugin request to the host's `HostApi` implementation.
///
/// `on_start_interactive` is called when the plugin starts an interactive
/// command; the host should install an adapter that sends
/// `HostRequest::InteractiveEvent` back to the plugin process.
pub fn handle_plugin_request(
host: &mut dyn HostApi,
req: PluginRequest,
on_start_interactive: &mut dyn FnMut(u64),
) -> PluginResponse {
use PluginRequest::*;
match req {
PushInfo(msg) => {
host.push_info(&msg);
PluginResponse::Ok
}
PushOutput(msg) => {
host.push_output(&msg);
PluginResponse::Ok
}
PushError(msg) => {
host.push_error(&msg);
PluginResponse::Ok
}
AddEntity(entity) => PluginResponse::Handle(host.add_entity(entity)),
AddEntities(entities) => PluginResponse::Handles(host.add_entities(entities)),
UpdateEntity(entity) => PluginResponse::Bool(host.update_entity(entity)),
RemoveEntity { handle } => PluginResponse::Bool(host.remove_entity(handle)),
BumpGeometry => {
host.bump_geometry();
PluginResponse::Ok
}
ReadRecord { handle, app_name } => {
PluginResponse::Record(host.read_record(handle, &app_name).cloned())
}
WriteRecord { handle, record } => PluginResponse::Bool(host.write_record(handle, record)),
RemoveRecord { handle, app_name } => {
PluginResponse::Bool(host.remove_record(handle, &app_name))
}
PushUndo { label } => {
host.push_undo(&label);
PluginResponse::Ok
}
SetDirty => {
host.set_dirty();
PluginResponse::Ok
}
StartInteractive { command_id } => {
on_start_interactive(command_id);
PluginResponse::Ok
}
DocumentSnapshot => PluginResponse::Document(Box::new(addressable_document_snapshot(host))),
OpenDocumentView => match host.document_view() {
Some(info) => PluginResponse::DocumentView {
path: info.path,
version: info.version,
},
None => PluginResponse::Error("shared document view unavailable".to_string()),
},
OpenDocumentViewV4 { tab_id } => match host.document_view_v4(tab_id) {
Some(info) => PluginResponse::DocumentViewV4 {
path: info.path,
version: info.version,
},
None => PluginResponse::Error("V4 shared document view unavailable".to_string()),
},
CloseDocumentViewV4 { tab_id } => {
host.close_document_view_v4(tab_id);
PluginResponse::Ok
}
GetTabId => PluginResponse::TabId(host.tab_id()),
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)),
ExecuteCommand(cmd) => PluginResponse::Bool(host.execute_command(&cmd)),
GetSystemVariable { name } => PluginResponse::SystemVariable(host.system_variable(&name)),
SetSystemVariable { name, value } => {
PluginResponse::SystemVariableResult(host.set_system_variable(&name, value))
}
UpdateEntitiesTransaction { label, entities } => PluginResponse::EntityTransactionResult(
host.update_entities_transaction(&label, entities),
),
GetSelection => PluginResponse::Selection(host.selection()),
SetSelection { handles } => PluginResponse::SelectionResult(host.set_selection(&handles)),
SolidOperation { operation } => {
PluginResponse::SolidResult(host.solid_operation(operation))
}
TableOperation { operation } => {
PluginResponse::TableResult(host.table_operation(operation))
}
RunCommand { request } => PluginResponse::CommandResult(host.run_command(request)),
CreateXRecord {
dictionary_path,
name,
entries,
} => {
let path: Vec<&str> = dictionary_path.iter().map(String::as_str).collect();
PluginResponse::XRecordCreateResult(host.create_xrecord(&path, &name, entries))
}
ReadXRecord {
dictionary_path,
name,
} => {
let path: Vec<&str> = dictionary_path.iter().map(String::as_str).collect();
PluginResponse::XRecordEntries(host.read_xrecord(&path, &name))
}
UpdateXRecord {
dictionary_path,
name,
entries,
} => {
let path: Vec<&str> = dictionary_path.iter().map(String::as_str).collect();
PluginResponse::XRecordUpdateResult(host.update_xrecord(&path, &name, entries))
}
DeleteXRecord {
dictionary_path,
name,
} => {
let path: Vec<&str> = dictionary_path.iter().map(String::as_str).collect();
PluginResponse::XRecordDeleteResult(host.delete_xrecord(&path, &name))
}
ListXRecords { dictionary_path } => {
let path: Vec<&str> = dictionary_path.iter().map(String::as_str).collect();
PluginResponse::XRecordNames(host.list_xrecords(&path))
}
}
}