feat(plugin): persist XDATA to DWG and let plugins modify/delete entities (#249, #250)

#249 — plugin XDATA written via write_record survived only in memory: the
acadrust DWG writer dropped ExtendedData::records on save. Bump acadrust to
e88a9a6 (records now encode to EED and decode back on read) and fix the host
side that fed it:
  - ensure_app_id allocates a real APPID handle; a null handle serializes as
    0 and the EED reference can't resolve, so the XDATA vanished on reopen.
  - write_record / remove_record drop stale raw_dwg_eed for the target app so
    an edit made after a save/reopen wins over the pre-edit bytes.

#250 — out-of-process plugins got a throwaway document_mut() snapshot, so
edits to existing entities were silently discarded and deletion wasn't
expressible at all. Add the missing mutation surface:
  - UpdateEntity / RemoveEntity IPC requests + HostApi::update_entity /
    remove_entity (default in-process impls, RPC overrides on the client that
    invalidate the stale document cache).
  - Scene::update_entity replaces the entity in place, preserving its handle
    and owning block, and reseeds only its derived caches; remove reuses the
    cache-coherent erase_entities (which also honours layer locks).
  - document_mut() is documented as a local-only snapshot out-of-process.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hakan SevenandClaude Opus 4.8 committed 2026-07-02 15:39:26 +03:00
1 parent a763144622
commit f9f7138370
7 files changed
+295 -4

No files matched your search

+72
View File
@@ -151,6 +151,46 @@ impl HostApi for PluginHostApi {
}
}
fn update_entity(&mut self, entity: EntityType) -> bool {
match self.client.request(PluginRequest::UpdateEntity(entity)) {
Ok(PluginResponse::Bool(b)) => {
if b {
// The cached snapshot is now stale; drop it so a later
// document() re-fetches the host's post-edit truth.
self.document_cache = OnceCell::new();
}
b
}
Ok(other) => {
eprintln!("[plugin] unexpected UpdateEntity response: {other:?}");
false
}
Err(e) => {
eprintln!("[plugin] UpdateEntity failed: {e}");
false
}
}
}
fn remove_entity(&mut self, handle: Handle) -> bool {
match self.client.request(PluginRequest::RemoveEntity { handle }) {
Ok(PluginResponse::Bool(b)) => {
if b {
self.document_cache = OnceCell::new();
}
b
}
Ok(other) => {
eprintln!("[plugin] unexpected RemoveEntity response: {other:?}");
false
}
Err(e) => {
eprintln!("[plugin] RemoveEntity failed: {e}");
false
}
}
}
fn bump_geometry(&mut self) {
let _ = self.client.request(PluginRequest::BumpGeometry);
}
@@ -438,4 +478,36 @@ mod tests {
peer_handle.join().unwrap();
assert_eq!(handle, Handle::new(42));
}
#[test]
fn update_entity_awaits_bool_response() {
let (mut api, mut peer) = make_client();
let peer_handle = thread::spawn(move || {
let msg = recv::<PluginToHost>(&mut peer).unwrap();
match msg {
PluginToHost::Request(PluginRequest::UpdateEntity(_)) => {}
other => panic!("unexpected: {other:?}"),
}
send(&mut peer, &HostToPlugin::Response(PluginResponse::Bool(true))).unwrap();
});
assert!(api.update_entity(EntityType::Point(Point::new())));
peer_handle.join().unwrap();
}
#[test]
fn remove_entity_awaits_bool_response() {
let (mut api, mut peer) = make_client();
let peer_handle = thread::spawn(move || {
let msg = recv::<PluginToHost>(&mut peer).unwrap();
match msg {
PluginToHost::Request(PluginRequest::RemoveEntity { handle }) => {
assert_eq!(handle, Handle::new(7));
}
other => panic!("unexpected: {other:?}"),
}
send(&mut peer, &HostToPlugin::Response(PluginResponse::Bool(true))).unwrap();
});
assert!(api.remove_entity(Handle::new(7)));
peer_handle.join().unwrap();
}
}
@@ -75,6 +75,12 @@ pub enum PluginRequest {
PushOutput(String),
PushError(String),
AddEntity(EntityType),
/// Replace the existing entity carrying this entity's handle in place.
UpdateEntity(EntityType),
/// Delete the entity with `handle`.
RemoveEntity {
handle: Handle,
},
BumpGeometry,
ReadRecord {
handle: Handle,
+2
View File
@@ -28,6 +28,8 @@ pub fn handle_plugin_request(
PluginResponse::Ok
}
AddEntity(entity) => PluginResponse::Handle(host.add_entity(entity)),
UpdateEntity(entity) => PluginResponse::Bool(host.update_entity(entity)),
RemoveEntity { handle } => PluginResponse::Bool(host.remove_entity(handle)),
BumpGeometry => {
host.bump_geometry();
PluginResponse::Ok