forked from 1b-agent/opencadstudio-upstream
Merge pull request #1747 from schoeller/reference_only_entities
Reference-only entities (plugin API v9): a scene-scoped, in-memory designation for plugin-owned display geometry, plus HostNotification dispatch to native plugins so they can re-mark on DocumentOpened. Resolves the new DeltaSnapshot site in block_editor.rs. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
commit
68330ce39e
40 files changed
+1977
-85
No files matched your search
@@ -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,
|
||||
});
|
||||
|
||||
@@ -396,6 +396,15 @@ pub enum HostNotification {
|
||||
tab_id: u64,
|
||||
epoch: u64,
|
||||
},
|
||||
/// V9 a document became resident in a tab: a whole document replaced the
|
||||
/// tab's previous content — opened from file, created from a template, or
|
||||
/// loaded by an automation/system open. The counterpart of
|
||||
/// [`HostNotification::DocumentTabClosed`]: with both, a plugin can keep
|
||||
/// per-document state (e.g. runtime designations) in sync from load to
|
||||
/// close. Discriminant 10.
|
||||
DocumentOpened {
|
||||
tab_id: u64,
|
||||
},
|
||||
/// Fallback for notification variants added in future minor revisions.
|
||||
/// Carries the raw bincode payload so an older peer can ignore it without
|
||||
/// failing deserialization.
|
||||
@@ -447,6 +456,10 @@ impl Serialize for HostNotification {
|
||||
bincode::serialize_into(&mut bytes, tab_id).map_err(serde::ser::Error::custom)?;
|
||||
bincode::serialize_into(&mut bytes, epoch).map_err(serde::ser::Error::custom)?;
|
||||
}
|
||||
HostNotification::DocumentOpened { tab_id } => {
|
||||
bytes.push(10);
|
||||
bincode::serialize_into(&mut bytes, tab_id).map_err(serde::ser::Error::custom)?;
|
||||
}
|
||||
HostNotification::Unknown(raw) => bytes.extend_from_slice(raw),
|
||||
}
|
||||
bytes.serialize(serializer)
|
||||
@@ -490,6 +503,9 @@ impl<'de> Deserialize<'de> for HostNotification {
|
||||
9 => bincode::deserialize(rest)
|
||||
.map(|(tab_id, epoch)| HostNotification::DrawingChanged { tab_id, epoch })
|
||||
.map_err(serde::de::Error::custom),
|
||||
10 => bincode::deserialize(rest)
|
||||
.map(|tab_id| HostNotification::DocumentOpened { tab_id })
|
||||
.map_err(serde::de::Error::custom),
|
||||
_ => Ok(HostNotification::Unknown(bytes)),
|
||||
}
|
||||
}
|
||||
@@ -1175,6 +1191,45 @@ 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 is a scene-scoped, in-memory set keyed by entity
|
||||
/// handle: it is not written to the saved DWG/DXF (the host stores no
|
||||
/// state of its own in the file), it lives and dies with the drawing tab,
|
||||
/// and a reloaded drawing starts undesignated — the owning plugin
|
||||
/// re-marks its entities when
|
||||
/// [`on_notification`](BuiltinPlugin::on_notification) delivers
|
||||
/// [`HostNotification::DocumentOpened`], making the in-memory designation
|
||||
/// fully equivalent to a persisted one for its use case.
|
||||
///
|
||||
/// 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 — and
|
||||
/// without publishing a document change, so idempotent re-marks cannot
|
||||
/// loop with the notification dispatch.
|
||||
///
|
||||
/// 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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user