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

@@ -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