feat(display): plugin entity-class labels in the panel, records, and automation

Resolve entities' XDATA class-identity keys through the live plugin manager
(new `src/plugin/entity_labels.rs`) and qualify display types as
"<Label> (<Native>)" wherever the bare native type is shown today.

- The single-entity Properties panel title (and the node graph, which shares
  the same computation) becomes e.g. "Linear Element (Line)". Host-owned
  classifications (xref, dynamic constraints, underlays, surfaces) keep the
  native parenthesized part, so nothing they tell the user is lost.
- The records API's `entities` collection: `display_type` carries the
  qualified label; the raw `type` / `record_type` fields stay native so
  existing scripts, type filters, and by-type grouping keep working.
- The automation entity summary gains a `display_type` field with the same
  semantics; `type` stays raw. Exposed by `query`, `user_select` sessions,
  and `get_selection`.

Semantics: the identity (a stable class KEY like "CVIFC-CLASS:LINE") lives
in the entity's XDATA and round-trips the drawing; the display LABELS live
in the declaring plugin's manifest. Keys only — no display text is ever
written to the file, and the label never governs behavior (display-only: it
does not affect snapping, selection, filtering, or grouping). Gating rides
live manager state at query time, so without a loaded declaring plugin every
surface here is byte-identical to today, immediately after any reopen, with
no re-assert pass. The command-line LIST output deliberately stays on the raw
dxf name. An app id claimed by more than one loaded plugin resolves through
the first-loaded declaration and debug-logs the ignored duplicate; a
prefix-only value, an unknown key, or an empty label mapping fall back to
the native type.

Tests: resolver core (known key, foreign app ignored, record without an
identity value ignored, prefix-only / unknown key / empty label fallback,
duplicate app id first-wins), the empty-manager gating end-to-end through
`with_manager`, the "<Label> (<Native>)" form, and the byte-identical
fallbacks asserted at the panel, records, and query layers for entities
stamped with a perfect identity record.
This commit is contained in:
Sebastian committed 2026-10-15 22:39:09 +02:00
1 parent 60eb7b086e
commit ab3e0723fd
6 files changed
+518 -36

No files matched your search

+24 -7
View File
@@ -191,7 +191,16 @@ impl OpenCADStudio {
.scene
.document
.get_entity(*handle)
.map(|entity| entity_json(entity, &session.detail))
.map(|entity| {
entity_json(
entity,
&session.detail,
crate::plugin::entity_labels::custom_entity_class_label(
&self.disabled_plugins,
entity,
),
)
})
})
.collect();
let count = entities.len();
@@ -359,11 +368,11 @@ impl OpenCADStudio {
/// `get_selection` — read-only snapshot of the active tab's selection
/// (the read-back half of the `user_select` sampling flow). `handle`,
/// `type`, `layer` and `bounds` come straight from the `query` output so
/// clients share one parser; `text`/`value` expose the stripped and raw
/// strings for TEXT/MTEXT, and `block`/`position` identify an INSERT's
/// block definition and insertion point (null otherwise). Reads
/// `scene.selection` and never mutates it.
/// `type`, `display_type`, `layer` and `bounds` come straight from the
/// `query` output so clients share one parser; `text`/`value` expose the
/// stripped and raw strings for TEXT/MTEXT, and `block`/`position`
/// identify an INSERT's block definition and insertion point (null
/// otherwise). Reads `scene.selection` and never mutates it.
pub(in crate::app) fn control_get_selection(&self) -> Value {
let scene = &self.tabs[self.active_tab].scene;
let entities: Vec<Value> = scene
@@ -371,10 +380,18 @@ impl OpenCADStudio {
.iter()
.filter_map(|handle| {
scene.document.get_entity(*handle).map(|entity| {
let full = entity_json(entity, "full");
let full = entity_json(
entity,
"full",
crate::plugin::entity_labels::custom_entity_class_label(
&self.disabled_plugins,
entity,
),
);
json!({
"handle": full["handle"],
"type": full["type"],
"display_type": full["display_type"],
"layer": full["layer"],
"bounds": full["bounds"],
"text": full.get("text").cloned().unwrap_or(Value::Null),