From 856b7bc8a4c598e1661c1692c14d72ad3c2b24d2 Mon Sep 17 00:00:00 2001 From: Sebastian <106036+schoeller@users.noreply.github.com> Date: Tue, 13 Oct 2026 12:38:53 +0200 Subject: [PATCH] docs(plugin): reference-only entities (API v9) Document the designation in the host-model capability table and its own section, and record the increment in the completion-plan ledger with the storage decision, the behaviour split and the test evidence. --- docs/plugin-host-model-completion-plan.md | 38 +++++++++++++++++++++++ docs/plugin-host-model.md | 28 ++++++++++++++++- 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/docs/plugin-host-model-completion-plan.md b/docs/plugin-host-model-completion-plan.md index 00d437ab9..7ded4cf43 100644 --- a/docs/plugin-host-model-completion-plan.md +++ b/docs/plugin-host-model-completion-plan.md @@ -362,3 +362,41 @@ in one scope = one step, undone and redone whole), `xrecords_survive_dxf_and_dwg_round_trips` (typed entries reload from both writers), the IPC round-trips `xrecord_request_and_response_roundtrip` and `xrecord_requests_await_their_typed_responses`. The API is host-level and stands on its own; a Python mapping (`doc.xrecords`-style) is future work. + +**Reference-only entities (2026-10-13, API v9).** The host gained a per-entity "reference-only" +designation for plugin-owned display geometry: `HostApi::set_reference_only` marks (or unmarks) an +entity as display data the person at the screen must not edit, while the owning plugin keeps full +write access. Behaviour split: a reference-only entity renders and snaps normally but is not +grip-editable (`refresh_selected_grips` yields nothing for it and an active grip edit on a newly +designated target is cancelled like a locked-layer one), not click-pickable (the pick-side wire +flag skips it, so a click passes through to the editable entity underneath), and skipped by box, +window, lasso, fence and select-all selection gathers. Built-in user write paths treat it like a +locked-layer entity — `Scene::update_entity` / `update_entities`, `erase_entities`, MCP +`set_properties` and modify machinery all refuse or skip it — while the plugin-owned variants +(`Scene::update_entity_plugin` / `erase_entities_plugin`, mirroring the +`remove_entities(handles, respect_layer_locks)` editor split) and the whole `HostSession` surface +(`add_entity`, `update_entity`, `update_entities_transaction`, `remove_entity`, XDATA records, +layer/block table operations) write through, so a preview refresh never needs unlock/relock cycles. +Locks still apply to the plugin: marking and clearing refuse absent entities and locked layers, +and every refusal is decided before the undo scope is touched. Undo is the entity-write contract: +a change inside a plugin-opened `push_undo` scope joins it (entity writes plus designations are one +step under the plugin's label); a scope-less change is one undo step of its own; an idempotent +re-designation records nothing. Storage: a human-readable marker value in an XDATA record under the +host-owned `OCS` APPID (registered on first use, one record per application per entity, foreign +values preserved verbatim; stale verbatim DWG EED is dropped on rewrite), so the flag round-trips +DWG and DXF save/reload with the entity and cannot collide with plugin APPID namespaces — chosen +over a codec common-data bit because the codec is a pinned external dependency. The MCP `entities` +read exposes the flag as a top-level `reference_only` boolean on every entity record. Known limit: +entities nested inside block instances (the block cache rebuilds instance wires without the flag) — +reference-only targets top-level geometry. Evidence: the storage unit tests +`marking_flags_and_unmarking_clears`, `foreign_values_under_the_host_app_id_survive`, +`other_apps_are_untouched`; the host behaviour tests `reference_only_refusals_record_no_undo_step`, +`reference_only_designation_composes_undo_like_entity_writes` (own step, scope join, whole-scope +undo), `reference_only_survives_dxf_and_dwg_round_trips` (both writers, APPID table included), +`user_paths_refuse_and_plugin_paths_write_reference_only_entities` (grips suppressed, user +update/erase refused, plugin update/erase through, designation survives a refresh-style replace), +`select_all_visible_skips_reference_only_entities`; the pick tests `clicks_pass_through_reference_only_wires` +and `box_selections_skip_reference_only_wires`; the record surface test +`entities_records_expose_the_reference_only_designation`; and the IPC round-trip +`reference_only_request_and_response_roundtrip`. Like the XRecord API, a Python mapping is future +work. diff --git a/docs/plugin-host-model.md b/docs/plugin-host-model.md index e1e42d3f1..8df606e5d 100644 --- a/docs/plugin-host-model.md +++ b/docs/plugin-host-model.md @@ -22,7 +22,7 @@ without it keeps working, and the default `HostApi` methods refuse. | Area | `HostApi` entry | What it does | |---|---|---| | Read | `document()`, `entity_snapshot` | The full `CadDocument` snapshot; a detached JSON view of any typed entity. | -| Entities | `add_entity`, `remove_entity`, `update_entities_transaction` | Create, delete and replace entities. A transaction validates every replacement first and either commits all of them as one undo step or none. | +| Entities | `add_entity`, `remove_entity`, `update_entities_transaction`, `set_reference_only` | Create, delete and replace entities. A transaction validates every replacement first and either commits all of them as one undo step or none. `set_reference_only` designates plugin-owned display geometry (API v9, see below). | | Selection | `selection`, `set_selection` | Ordered, tab-scoped, exact (missing or duplicate handles reject the request). | | Solids | `solid_operation` | Kernel-backed create, transform, booleans, extrude, region and surface from a profile, picture embedding. Every result is verified to lift back from its ACIS payload without loss; a refusal leaves the operands untouched. | | Tables | `table_operation` | Layers, text and dimension styles, blocks and their contents, linetypes, layouts (see below). | @@ -105,6 +105,32 @@ objects map byte-exactly, the auto-created dictionaries included. dictionary's own order, so audits need no prior knowledge. Not yet mapped to Python. +## Reference-only entities (API v9) + +`set_reference_only` designates plugin-owned display geometry: a +reference-only entity renders and snaps normally, but the built-in edit paths +treat it like an entity on a locked layer — it is not grip-editable, not +click-pickable (a click passes through to the editable entity underneath), +and window, box, lasso and fence selections skip it. User-driven writes +(grip drags, modify commands, the properties panel, user erase, MCP +`set_properties`) refuse it; the owning plugin keeps full write access: +`add_entity`, `update_entity`, `update_entities_transaction`, `remove_entity` +and the XDATA record methods are unaffected, so a plugin can refresh derived +display geometry in place. The record-method policy applies first: absent +entities and locked layers refuse, and like them every refusal is decided +before the undo scope is touched, so a failure changes nothing. A +re-designation that would not change the state succeeds without recording an +undo step. Undo composition is the entity-write contract: a change inside a +plugin-opened `push_undo` scope joins it, so an operation composed of several +writes is a single undo step under the plugin's label; with no scope open a +change is one undo step of its own. The designation survives DWG and DXF +save/reload, stored as a marker value in an XDATA record under the host-owned +`OCS` application namespace — plugins register their own APPIDs through their +manifest, so the two cannot collide. Out of scope today: entities nested +inside block instances (the block cache rebuilds instance wires without the +flag) and a Python mapping. The MCP `entities` read exposes the flag as a +`reference_only` boolean on every entity record. + ## Commands `run_command` takes a `CommandRequest` (`Run`, `Start`, `Point`, `Text`, `Token`,