feat(plugin): reference-only entity designation on the HostApi (API v9)

The HostApi gains set_reference_only(handle, reference_only): a plugin can
designate display geometry it owns and refreshes. The entity renders like any
other, but the built-in edit paths treat it like an entity on a locked layer,
while the owning plugin keeps full write access (update, erase, XDATA).

- Appended at the trait tail with a refusing default so plugins keep compiling
  against older hosts; the loader gate range is untouched.
- IPC: SetReferenceOnly / SetReferenceOnlyResult round-trip through the
  shared dispatch, the V3 client and the V4 client, with the shared document
  cache invalidated on success.
- build.rs version_info and manifest API_VERSION bumped to 9 in lockstep.
- Evidence: reference_only_request_and_response_roundtrip (bincode round-trip
  of the request and both response payloads).
This commit is contained in:
Sebastian committed 2026-10-13 12:38:53 +02:00
1 parent 9cc890c911
commit 1f552038a8
8 files changed
+123 -3

No files matched your search

+1 -1
View File
@@ -1263,7 +1263,7 @@ fn generate_version_info(out_dir: &Path) {
"acadrust_version": opencadcodec.version.to_string(),
"acadrust_source": opencadcodec.source.as_ref().map(|s| s.to_string()),
"rustc_version": rustc_version,
"api_version": 8,
"api_version": 9,
"api_version_min_supported": 3,
"build_timestamp": build_timestamp,
});
+29
View File
@@ -1175,6 +1175,35 @@ pub trait HostApi {
fn list_xrecords(&self, _dictionary_path: &[&str]) -> Result<Vec<String>, String> {
Err("XRecord listing is not supported by this host".to_owned())
}
// ── Reference-only designation (added in API v9; appended at the end to
// keep vtable indices stable for older plugins) ──────────────────────────
/// Mark (or unmark) an entity as reference-only (API v9, additive).
///
/// A reference-only entity is plugin-owned display geometry: it renders
/// normally, but the built-in edit paths treat it like an entity on a
/// locked layer — it is neither grip-editable nor click-pickable (a click
/// passes through to the editable entity underneath), window and box
/// selections skip it, and user-driven writes refuse it. The owning
/// plugin keeps full write access through this API: `update_entity`,
/// [`update_entities_transaction`](HostApi::update_entities_transaction),
/// `remove_entity` and the XDATA record methods keep working, so derived
/// display geometry can be refreshed in place. The designation survives
/// DWG/DXF save and reload.
///
/// Fails when the entity does not exist or is on a locked layer (the same
/// policy as the XDATA record methods). A call that would not change the
/// current designation succeeds without recording an undo step.
///
/// Undo composition — the entity-write contract: a change performed
/// inside a plugin-opened [`HostApi::push_undo`] scope joins that scope,
/// so an operation composed of several writes is a single undo step under
/// the plugin's own label. With no scope open the change is one undo step
/// of its own. Refusals record no undo step.
fn set_reference_only(&mut self, _handle: Handle, _reference_only: bool) -> Result<(), String> {
Err("The reference-only designation is not supported by this host".to_owned())
}
}
/// Configuration properties for creating or modifying a layer.
+17
View File
@@ -692,6 +692,23 @@ impl HostApi for PluginHostApi {
Err(error) => Err(error.to_string()),
}
}
fn set_reference_only(&mut self, handle: Handle, reference_only: bool) -> Result<(), String> {
match self.client.request(PluginRequest::SetReferenceOnly {
handle,
reference_only,
}) {
Ok(PluginResponse::SetReferenceOnlyResult(result)) => {
if result.is_ok() {
self.document_cache = OnceCell::new();
}
result
}
Ok(PluginResponse::Error(error)) => Err(error),
Ok(other) => Err(format!("unexpected reference-only response: {other:?}")),
Err(error) => Err(error.to_string()),
}
}
}
/// Sentinel reader used when the shared-memory view could not be initialized.
@@ -226,6 +226,11 @@ pub enum PluginRequest {
ListXRecords {
dictionary_path: Vec<String>,
},
/// V9 (additive): mark an entity reference-only (plugin-owned display data).
SetReferenceOnly {
handle: Handle,
reference_only: bool,
},
}
/// Responses the host sends back for `PluginRequest`.
@@ -272,6 +277,8 @@ pub enum PluginResponse {
XRecordDeleteResult(Result<crate::host::XRecord, String>),
/// V8 (additive): XRecord names under a dictionary path.
XRecordNames(Result<Vec<String>, String>),
/// V9 (additive): outcome of the reference-only designation.
SetReferenceOnlyResult(Result<(), String>),
}
/// Messages sent from the host to the plugin runner.
+6
View File
@@ -151,5 +151,11 @@ pub fn handle_plugin_request(
let path: Vec<&str> = dictionary_path.iter().map(String::as_str).collect();
PluginResponse::XRecordNames(host.list_xrecords(&path))
}
SetReferenceOnly {
handle,
reference_only,
} => {
PluginResponse::SetReferenceOnlyResult(host.set_reference_only(handle, reference_only))
}
}
}
@@ -987,6 +987,23 @@ impl HostApi for V4PluginHostApi {
Err(error) => Err(error.to_string()),
}
}
fn set_reference_only(&mut self, handle: Handle, reference_only: bool) -> Result<(), String> {
match self.request(PluginRequest::SetReferenceOnly {
handle,
reference_only,
}) {
Ok(PluginResponse::SetReferenceOnlyResult(result)) => {
if result.is_ok() {
self.document_cache = OnceCell::new();
}
result
}
Ok(PluginResponse::Error(error)) => Err(error),
Ok(other) => Err(format!("unexpected reference-only response: {other:?}")),
Err(error) => Err(error.to_string()),
}
}
}
#[cfg(test)]
@@ -519,6 +519,47 @@ mod tests {
}
}
#[test]
fn reference_only_request_and_response_roundtrip() {
// The V9 request marks an entity reference-only and the response
// carries the operation's outcome.
let bytes = bincode::serialize(&PluginRequest::SetReferenceOnly {
handle: codec::Handle::new(0x2A),
reference_only: true,
})
.unwrap();
assert!(
matches!(bincode::deserialize::<PluginRequest>(&bytes).unwrap(),
PluginRequest::SetReferenceOnly { handle, reference_only } if
handle == codec::Handle::new(0x2A) && reference_only)
);
let bytes = bincode::serialize(&PluginRequest::SetReferenceOnly {
handle: codec::Handle::new(0x2A),
reference_only: false,
})
.unwrap();
assert!(matches!(
bincode::deserialize::<PluginRequest>(&bytes).unwrap(),
PluginRequest::SetReferenceOnly {
reference_only: false,
..
}
));
let bytes = bincode::serialize(&PluginResponse::SetReferenceOnlyResult(Ok(()))).unwrap();
assert!(matches!(
bincode::deserialize::<PluginResponse>(&bytes).unwrap(),
PluginResponse::SetReferenceOnlyResult(Ok(()))
));
let bytes = bincode::serialize(&PluginResponse::SetReferenceOnlyResult(Err(
"entity is on a locked layer".to_owned(),
)))
.unwrap();
assert!(
matches!(bincode::deserialize::<PluginResponse>(&bytes).unwrap(),
PluginResponse::SetReferenceOnlyResult(Err(error)) if error == "entity is on a locked layer")
);
}
#[test]
fn unknown_notification_deserializes_as_unknown() {
// Build a payload with an unknown discriminant (99) followed by some
+5 -2
View File
@@ -24,8 +24,11 @@
/// undoable replacement of a set of existing entities. v8 appends name-addressed
/// XRecord object CRUD under named-object-dictionary paths
/// (`HostApi::create_xrecord` / `read_xrecord` / `update_xrecord` /
/// `delete_xrecord` / `list_xrecords`).
pub const API_VERSION: u32 = 8;
/// `delete_xrecord` / `list_xrecords`). v9 appends the reference-only entity
/// designation (`HostApi::set_reference_only`): display-only geometry a plugin
/// still owns and refreshes while the built-in edit paths treat it like a
/// locked layer.
pub const API_VERSION: u32 = 9;
/// Oldest plugin API major the current host still loads. This keeps previously
/// compiled cdylibs usable as long as their vtable layout is a prefix of the