docs(plugins): entity-class label convention; demo in template v2

- plugin-architecture.md: new "Entity class display labels (API v10)"
  section documenting the split (stable identity keys in XDATA, display
  labels in the plugin declaration), the live-loaded-plugin gating (missing,
  disabled, cap-excluded, failed, removed, or dead → bare native type), the
  "<Label> (<Native>)" display rule, that raw schema fields (`type`,
  `dxf_name`, `record_type`) and LIST are unchanged, and that the label is
  display-only. `plugin.toml` deliberately does not carry the table: it is
  the pre-load discovery file and labels must activate on live manifests.
- Template v2 demonstrates the whole convention: declares
  `SURVEY-CLASS:POINT -> "Survey Point"` and stamps the identity key in
  MARK_FIRST_SURVEY_POINT, so marked points show as "Survey Point (Point)"
  while the plugin is loaded. plugin.toml's api_version is bumped to 10 (keep
  in sync with ApiVersion::CURRENT) with a NOTE on where the class table
  lives. DESIGN.md documents the flow.
- Template v2's stale `acadrust` pin (8a28c21, predating the host's codec
  bump to 278c49a) is realigned with the host's pin: without it the template
  no longer compiles on main — its `acadrust::EntityType` and the host
  re-exports are distinct types when the revisions disagree (verified the
  same four type errors occur before and after this feature's template edit).
- changelogs/unreleased.md entry (Plugins section).
This commit is contained in:
Sebastian committed 2026-10-15 22:39:09 +02:00
1 parent ab3e0723fd
commit be675f2824
6 files changed
+110 -4

No files matched your search

+3
View File
@@ -52,6 +52,9 @@
* **Selection Event Streaming:** Automation clients now receive real-time selection change notifications. *(Contributed by [@YsDirector](https://github.com/YsDirector))* * **Selection Event Streaming:** Automation clients now receive real-time selection change notifications. *(Contributed by [@YsDirector](https://github.com/YsDirector))*
* **Host API v8:** Added name-addressed `XRecord` CRUD operations on `HostApi`. *(Contributed by [@schoeller](https://github.com/schoeller))* * **Host API v8:** Added name-addressed `XRecord` CRUD operations on `HostApi`. *(Contributed by [@schoeller](https://github.com/schoeller))*
#### Plugins
* **Plugin-Declared Entity Class Labels (Host API v10):** Plugins can now give their XDATA-stamped entities human type names: the plugin stamps a stable class-identity key (e.g. `CVIFC-CLASS:LINE`) into entity XDATA and declares key → label mappings via the new `BuiltinPlugin::entity_classes()` manifest accessor; while the plugin is loaded, the Properties panel title and the records/automation `display_type` field show `<Label> (<Native>)` (e.g. "Linear Element (Line)"). Labels are display-only, resolved live from the loaded plugin set (not installed / disabled / dead process → bare native type), and never round-trip the drawing — identity keys do, so labels stay plugin-localizable without rewriting files. Raw schema fields (`type`, `dxf_name`, `record_type`) and the `LIST` command output are unchanged. *(Contributed by [@schoeller](https://github.com/schoeller))*
--- ---
### 🐛 Bug Fixes ### 🐛 Bug Fixes
+52
View File
@@ -134,6 +134,7 @@ pub trait BuiltinPlugin: Send + Sync {
fn ribbon(&self) -> Box<dyn CadModule>; // the ribbon tab fn ribbon(&self) -> Box<dyn CadModule>; // the ribbon tab
fn dispatch(&self, host: &mut dyn HostApi, cmd: &str) -> bool; fn dispatch(&self, host: &mut dyn HostApi, cmd: &str) -> bool;
fn on_load(&mut self, host: &mut dyn HostApi) {} fn on_load(&mut self, host: &mut dyn HostApi) {}
fn entity_classes(&self) -> Option<&'static EntityClasses> {} // API v10
} }
``` ```
@@ -296,6 +297,57 @@ Store domain data on entities as XDATA (under your `xdata_apps` ids), not in a
side database, so it round-trips through DWG/DXF. `write_record` also registers side database, so it round-trips through DWG/DXF. `write_record` also registers
the APPID. Document your schemas in the plugin's own `PLUGIN.md`. the APPID. Document your schemas in the plugin's own `PLUGIN.md`.
### Entity class display labels (API v10)
A plugin can give its XDATA-stamped entities human type names in the host UI.
The split between *identity* (in the drawing) and *labels* (in the plugin) is
the point:
- **Identity** — the plugin stamps one string value into an entity's XDATA
record: `"<class_prefix><KEY>"`, e.g. `"CVIFC-CLASS:LINE"`. The key is
stable, meaningless to the host, and round-trips the DWG/DXF verbatim.
- **Labels** — the plugin declares a table and exposes it through
`BuiltinPlugin::entity_classes()`:
```rust
use ocs_plugin_api::manifest::EntityClasses;
static ENTITY_CLASSES: EntityClasses = EntityClasses {
app_id: "CVIFC", // one of the manifest's xdata_apps
class_prefix: "CVIFC-CLASS:",
labels: &[
("LINE", "Linear Element"),
("CIRCULARARC", "Circular Arc Element"),
],
};
impl BuiltinPlugin for MyPlugin {
fn entity_classes(&self) -> Option<&'static EntityClasses> {
Some(&ENTITY_CLASSES)
}
// ...
}
```
The host resolves them at display time only: the single-entity Properties
panel title becomes `"Linear Element (Line)"`, and the records/automation
`display_type` field takes the same form. The rule is display-only — a label
never governs behavior, and the raw schema fields (`type`, `dxf_name`,
`record_type`, the command-line `LIST` output) stay native so existing scripts
and filters keep working.
**Gating.** Declarations are consulted through the live plugin manager, never
cached: a plugin that is not installed, is disabled, is excluded by
`OCS_PLUGIN_MAX_API_VERSION`, failed at load, has been removed, or whose
process has died contributes nothing — its entities display as their bare
native type ("Line"), immediately, with no re-open needed. An `app_id` claimed
by more than one loaded plugin resolves through the first-loaded declaration;
later duplicates are ignored (debug-logged). A class key that is empty,
missing from the table, or mapped to an empty label falls back to the native
type. Because labels are display text resolved from the loaded plugin, they
are plugin-localizable and fixable without rewriting drawings — and
`plugin.toml` (the pre-load discovery file) deliberately does not carry them.
--- ---
## Building & distribution ## Building & distribution
+5 -1
View File
@@ -12,4 +12,8 @@ crate-type = ["cdylib"]
[dependencies] [dependencies]
ocs_plugin_api = { path = "../../crates/ocs_plugin_api", features = ["host"] } ocs_plugin_api = { path = "../../crates/ocs_plugin_api", features = ["host"] }
acadrust = { git = "https://github.com/HakanSeven12/opencadcodec.git", rev = "8a28c21" } # Must match the host's opencadcodec pin exactly (its Cargo.toml's `codec`
# entry): the plugin's entities and the host's re-exports are the same types
# only when both resolve the same revision. The registration `rustc` /
# `acadrust_source` gates enforce this at load time too.
acadrust = { package = "opencadcodec", git = "https://github.com/HakanSeven12/opencadcodec.git", rev = "278c49a" }
+19 -1
View File
@@ -20,7 +20,7 @@ rather than a runtime dependency of the host.
|---|---| |---|---|
| `COUNT_SURVEY_POINTS` | Count point entities on the `SURVEY` layer via `document_reader()`. | | `COUNT_SURVEY_POINTS` | Count point entities on the `SURVEY` layer via `document_reader()`. |
| `ADD_SURVEY_POINT` | Add a new point entity on the `SURVEY` layer via `host.add_entity()`. | | `ADD_SURVEY_POINT` | Add a new point entity on the `SURVEY` layer via `host.add_entity()`. |
| `MARK_FIRST_SURVEY_POINT` | Read the first `SURVEY` point and mark it with an XDATA record. | | `MARK_FIRST_SURVEY_POINT` | Read the first `SURVEY` point and mark it with an XDATA record (including the `SURVEY-CLASS:POINT` identity key). |
| `COUNT_MARKED_SURVEY_POINTS` | Count `SURVEY` points that carry the `SURVEYMARK` XDATA record. | | `COUNT_MARKED_SURVEY_POINTS` | Count `SURVEY` points that carry the `SURVEYMARK` XDATA record. |
--- ---
@@ -48,6 +48,24 @@ Each [`ReaderEntity`](ocs_plugin_api::host::ReaderEntity) exposes the entity
This is the primary read→write round-trip pattern the template demonstrates. This is the primary read→write round-trip pattern the template demonstrates.
### Entity class display labels
The template also demonstrates the entity-class label convention (API v10):
- the plugin declares `ENTITY_CLASSES` and exposes it through
`BuiltinPlugin::entity_classes()`: app `SURVEYMARK`, prefix
`SURVEY-CLASS:`, and key → label mappings;
- `MARK_FIRST_SURVEY_POINT` stamps a `SURVEY-CLASS:POINT` string value into
the point's XDATA alongside the existing marker value.
While the plugin is loaded, the host shows marked points as
"Survey Point (Point)" in the Properties panel title and in the
`display_type` field of the records/automation APIs; without the plugin they
are plain "Point" again. Only the stable key round-trips the drawing — the
label text lives in the plugin and can be reworded or localized without
touching saved files. The label is display-only: it never governs snapping,
selection, or any other behavior.
### Ribbon integration ### Ribbon integration
The plugin registers one ribbon module (`SurveyTools`) with one group The plugin registers one ribbon module (`SurveyTools`) with one group
+7 -1
View File
@@ -8,7 +8,9 @@ license = "GPL-3.0-only"
repository = "your-account/your-plugin-repo" repository = "your-account/your-plugin-repo"
[opencad] [opencad]
api_version = 5 # Keep in sync with the Rust manifest's `ApiVersion::CURRENT` — this is the
# pre-load gate; the runner also checks the compiled-in symbol.
api_version = 10
# Replace both placeholders in the packaging step. The v1 template's # Replace both placeholders in the packaging step. The v1 template's
# release workflow contains a working example. # release workflow contains a working example.
rustc_version = "__RUSTC_VERSION__" rustc_version = "__RUSTC_VERSION__"
@@ -21,3 +23,7 @@ command_prefixes = [
"COUNT_MARKED_SURVEY_POINTS", "COUNT_MARKED_SURVEY_POINTS",
] ]
xdata_apps = ["SURVEYMARK"] xdata_apps = ["SURVEYMARK"]
# NOTE: the plugin's entity-class display labels (see `ENTITY_CLASSES` in
# src/lib.rs) travel on the registered manifest over IPC, not in this file —
# they only apply while the plugin is loaded, so plugin.toml (the pre-load
# discovery file) deliberately does not carry them.
+24 -1
View File
@@ -8,7 +8,7 @@ use acadrust::xdata::{ExtendedDataRecord, XDataValue};
use acadrust::{EntityType, Handle}; use acadrust::{EntityType, Handle};
use ocs_plugin_api::export_plugin; use ocs_plugin_api::export_plugin;
use ocs_plugin_api::host::{BuiltinPlugin, HostApi, ReaderEntityKind}; use ocs_plugin_api::host::{BuiltinPlugin, HostApi, ReaderEntityKind};
use ocs_plugin_api::manifest::{ApiVersion, PluginManifest}; use ocs_plugin_api::manifest::{ApiVersion, EntityClasses, PluginManifest};
use ocs_plugin_api::ribbon::{CadModule, IconKind, ModuleEvent, RibbonGroup, RibbonItem, ToolDef}; use ocs_plugin_api::ribbon::{CadModule, IconKind, ModuleEvent, RibbonGroup, RibbonItem, ToolDef};
static MANIFEST: PluginManifest = PluginManifest { static MANIFEST: PluginManifest = PluginManifest {
@@ -27,6 +27,18 @@ static MANIFEST: PluginManifest = PluginManifest {
], ],
}; };
/// Entity-class display labels (API v10): entities carrying a
/// `SURVEY-CLASS:<key>` string in their SURVEYMARK XDATA display the mapped
/// label (e.g. "Survey Point (Point)") in the host's Properties panel and
/// `display_type` — *while this plugin is loaded*. The key is the only thing
/// stored in the drawing; the label text lives here, so it can be reworded or
/// localized without rewriting drawings.
static ENTITY_CLASSES: EntityClasses = EntityClasses {
app_id: "SURVEYMARK",
class_prefix: "SURVEY-CLASS:",
labels: &[("POINT", "Survey Point")],
};
struct PluginTemplateV2; struct PluginTemplateV2;
impl BuiltinPlugin for PluginTemplateV2 { impl BuiltinPlugin for PluginTemplateV2 {
@@ -34,6 +46,12 @@ impl BuiltinPlugin for PluginTemplateV2 {
&MANIFEST &MANIFEST
} }
// API v10: expose the entity-class labels declared above. The default
// (not overriding this) declares none.
fn entity_classes(&self) -> Option<&'static EntityClasses> {
Some(&ENTITY_CLASSES)
}
fn ribbon(&self) -> Box<dyn CadModule> { fn ribbon(&self) -> Box<dyn CadModule> {
Box::new(TemplateModule) Box::new(TemplateModule)
} }
@@ -106,6 +124,11 @@ fn first_survey_point_handle(host: &mut dyn HostApi) -> Option<Handle> {
fn mark_point(host: &mut dyn HostApi, handle: Handle) { fn mark_point(host: &mut dyn HostApi, handle: Handle) {
let mut record = ExtendedDataRecord::new("SURVEYMARK"); let mut record = ExtendedDataRecord::new("SURVEYMARK");
record.add_value(XDataValue::Integer32(1)); record.add_value(XDataValue::Integer32(1));
// Class-identity key for the entity-class labels declared above: the
// host shows marked points as "Survey Point (Point)" while this plugin is
// loaded, and as plain "Point" when it is not. Keys are stable identity —
// display text never goes into the drawing.
record.add_value(XDataValue::String("SURVEY-CLASS:POINT".to_string()));
if host.write_record(handle, record) { if host.write_record(handle, record) {
host.push_info(&format!("Marked SURVEY point {handle}")); host.push_info(&format!("Marked SURVEY point {handle}"));
} else { } else {