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 Seven 2026-07-02 15:39:26 +03:00
commit f9f7138370
7 changed files with 295 additions and 4 deletions

View file

@ -90,6 +90,88 @@ impl Scene {
handle
}
/// Replace the entity stored under `entity`'s handle with `entity`, keeping
/// its identity (handle + owning block), and refresh the derived
/// hatch/image/mesh caches so the edit is visible. Returns `false` when no
/// entity has that handle. This is the in-place counterpart to
/// [`add_entity`](Self::add_entity) used to commit a plugin's edit of an
/// existing entity.
pub fn update_entity(&mut self, mut entity: EntityType) -> bool {
let handle = entity.common().handle;
let Some(existing) = self.document.get_entity(handle) else {
return false;
};
// The caller edited a snapshot copy; keep the live entity in its block.
entity.common_mut().owner_handle = existing.common().owner_handle;
// Replacing (or becoming) a block entity forces a full block-cache
// rebuild; a plain entity only needs its own wires re-tessellated.
let affects_blocks = matches!(
existing,
EntityType::Insert(_) | EntityType::Block(_) | EntityType::BlockEnd(_)
) || matches!(
&entity,
EntityType::Insert(_) | EntityType::Block(_) | EntityType::BlockEnd(_)
);
// Rebuild the derived-model seeds from the new entity (as add_entity).
let hatch_seed = if let EntityType::Hatch(dxf) = &entity {
let color = self.render_style(&entity).0;
Self::hatch_model_from_dxf(dxf, color)
} else if let EntityType::Solid(solid) = &entity {
let color = self.render_style(&entity).0;
Some(Self::solid_hatch_model(solid, color))
} else {
None
};
let image_seed = if let EntityType::RasterImage(img) = &entity {
ImageModel::from_raster_image(img)
} else {
None
};
let facet_res = self.document.header.facet_resolution;
let mesh_seed = if matches!(
&entity,
EntityType::Solid3D(_) | EntityType::Region(_) | EntityType::Body(_) | EntityType::Surface(_)
) {
let color = self.render_style(&entity).0;
crate::entities::solid3d::tessellate_volume(&entity, color, facet_res)
.map(|m| offset_mesh_lod_set(m))
} else {
None
};
// Write the new entity into the live slot.
let Some(slot) = self.document.get_entity_mut(handle) else {
return false;
};
*slot = entity;
// Drop stale derived caches for this handle, then reseed for the new
// entity's type (which may differ from the old one).
self.hatches.remove(&handle);
self.images.remove(&handle);
self.meshes.remove(&handle);
self.solid_models.remove(&handle);
if let Some(model) = hatch_seed {
self.hatches.insert(handle, model);
}
if let Some(model) = image_seed {
self.images.insert(handle, model);
}
if let Some(model) = mesh_seed {
self.meshes.insert(handle, model);
}
self.mark_entity_dirty(handle);
if affects_blocks {
self.bump_geometry();
} else {
self.bump_geometry_no_blocks();
}
true
}
/// Returns the RGBA color for the given layer name.
pub fn layer_color(&self, layer: &str) -> [f32; 4] {
let layer_entry = self.document.layers.get(layer);