refactor(plugin): extract ocs_plugin_api crate for the stable add-on contract
Move the dependency-free, semver-versioned half of the plugin contract into a standalone workspace crate `crates/ocs_plugin_api`: - manifest types: PluginManifest, ApiVersion, API_VERSION - ribbon vocabulary: CadModule trait + ToolDef/RibbonGroup/RibbonItem/IconKind/ ModuleEvent/StyleKey The host re-exports them from `crate::plugin::manifest` and `crate::modules`, so every existing call site is unchanged. The acadrust-typed runtime surface (HostSession) stays in the host binary; lifting it behind a HostApi trait in the same crate is the remaining phase-1b step. Part of #100. Docs updated in docs/plugin-architecture.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
7a4ddbc62e
commit
72ec1146df
9 changed files with 280 additions and 154 deletions
5
Cargo.lock
generated
5
Cargo.lock
generated
|
|
@ -24,6 +24,7 @@ dependencies = [
|
||||||
"image",
|
"image",
|
||||||
"inventory",
|
"inventory",
|
||||||
"lzma-sys",
|
"lzma-sys",
|
||||||
|
"ocs_plugin_api",
|
||||||
"open",
|
"open",
|
||||||
"printpdf",
|
"printpdf",
|
||||||
"rayon",
|
"rayon",
|
||||||
|
|
@ -3646,6 +3647,10 @@ dependencies = [
|
||||||
"objc2-foundation 0.2.2",
|
"objc2-foundation 0.2.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ocs_plugin_api"
|
||||||
|
version = "0.1.0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "once_cell"
|
name = "once_cell"
|
||||||
version = "1.21.4"
|
version = "1.21.4"
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,13 @@ version = "0.5.6"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
build = "build.rs"
|
build = "build.rs"
|
||||||
|
|
||||||
|
[workspace]
|
||||||
|
members = ["crates/ocs_plugin_api"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
# Stable, dependency-free add-on contract (manifest + ribbon/CadModule types).
|
||||||
|
# Plugin authors target this crate's semver, not OpenCADStudio internals.
|
||||||
|
ocs_plugin_api = { path = "crates/ocs_plugin_api" }
|
||||||
# vtkio (pulled in transitively via truck-meshalgo) depends on xz2 → lzma-sys,
|
# vtkio (pulled in transitively via truck-meshalgo) depends on xz2 → lzma-sys,
|
||||||
# which by default links the system liblzma dynamically. On macOS that bakes a
|
# which by default links the system liblzma dynamically. On macOS that bakes a
|
||||||
# Homebrew dylib path (/opt/homebrew/.../liblzma.5.dylib) into the binary, so
|
# Homebrew dylib path (/opt/homebrew/.../liblzma.5.dylib) into the binary, so
|
||||||
|
|
|
||||||
8
crates/ocs_plugin_api/Cargo.toml
Normal file
8
crates/ocs_plugin_api/Cargo.toml
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
[package]
|
||||||
|
name = "ocs_plugin_api"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
description = "Stable, dependency-free contract for Open CAD Studio add-on packages: plugin manifest, version handshake, and ribbon/CadModule types."
|
||||||
|
license = "GPL-3.0-only"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
25
crates/ocs_plugin_api/src/lib.rs
Normal file
25
crates/ocs_plugin_api/src/lib.rs
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
//! # Open CAD Studio plugin API
|
||||||
|
//!
|
||||||
|
//! The stable, semver-versioned contract an add-on package targets instead of
|
||||||
|
//! the `OpenCADStudio` binary internals. It is intentionally **dependency
|
||||||
|
//! free** (no `iced`, no `acadrust`) so engine crates and external tooling can
|
||||||
|
//! depend on it cheaply.
|
||||||
|
//!
|
||||||
|
//! Two pieces live here:
|
||||||
|
//!
|
||||||
|
//! - [`manifest`] — plugin identity ([`PluginManifest`]) and the host ABI
|
||||||
|
//! version handshake ([`ApiVersion`]).
|
||||||
|
//! - [`ribbon`] — the [`CadModule`] trait and the plain-data ribbon types
|
||||||
|
//! ([`RibbonGroup`], [`ToolDef`], …) a plugin uses to describe its tab.
|
||||||
|
//!
|
||||||
|
//! The runtime host surface a plugin uses at *dispatch* time (document access,
|
||||||
|
//! command line, undo) is `acadrust`-typed and therefore lives in the host
|
||||||
|
//! binary for now; see `docs/plugin-architecture.md` (phase 1b).
|
||||||
|
|
||||||
|
pub mod manifest;
|
||||||
|
pub mod ribbon;
|
||||||
|
|
||||||
|
pub use manifest::{ApiVersion, PluginManifest, API_VERSION};
|
||||||
|
pub use ribbon::{
|
||||||
|
CadModule, IconKind, ModuleEvent, RibbonGroup, RibbonItem, StyleKey, ToolDef,
|
||||||
|
};
|
||||||
51
crates/ocs_plugin_api/src/manifest.rs
Normal file
51
crates/ocs_plugin_api/src/manifest.rs
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
//! Plugin identity and capability declaration.
|
||||||
|
|
||||||
|
/// Host plugin API version. Bump when the host runtime surface breaks
|
||||||
|
/// compatibility.
|
||||||
|
pub const API_VERSION: u32 = 1;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub struct ApiVersion {
|
||||||
|
pub major: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ApiVersion {
|
||||||
|
pub const CURRENT: Self = Self { major: API_VERSION };
|
||||||
|
|
||||||
|
pub fn is_compatible_with(host: ApiVersion) -> bool {
|
||||||
|
Self::CURRENT.major == host.major
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn current_matches_const() {
|
||||||
|
assert_eq!(ApiVersion::CURRENT.major, API_VERSION);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn same_major_is_compatible() {
|
||||||
|
assert!(ApiVersion::is_compatible_with(ApiVersion::CURRENT));
|
||||||
|
assert!(!ApiVersion::is_compatible_with(ApiVersion {
|
||||||
|
major: API_VERSION + 1,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Static metadata every plugin supplies at registration time.
|
||||||
|
/// Keep fields in sync with `plugin.toml` beside the package.
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub struct PluginManifest {
|
||||||
|
pub id: &'static str,
|
||||||
|
pub name: &'static str,
|
||||||
|
pub version: &'static str,
|
||||||
|
pub description: &'static str,
|
||||||
|
pub api_version: ApiVersion,
|
||||||
|
/// Sort key for add-on ribbon tabs (lower = further left among plugins).
|
||||||
|
pub ribbon_order: i32,
|
||||||
|
pub xdata_apps: &'static [&'static str],
|
||||||
|
pub command_prefixes: &'static [&'static str],
|
||||||
|
}
|
||||||
159
crates/ocs_plugin_api/src/ribbon.rs
Normal file
159
crates/ocs_plugin_api/src/ribbon.rs
Normal file
|
|
@ -0,0 +1,159 @@
|
||||||
|
//! Ribbon description types — the plain-data vocabulary a [`CadModule`] uses to
|
||||||
|
//! declare its tab. No UI-framework dependency: the host renders these.
|
||||||
|
|
||||||
|
// ── Events ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Events a module tool can emit to the host application.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum ModuleEvent {
|
||||||
|
/// Fire a named CAD command (e.g. "LINE", "CIRCLE").
|
||||||
|
Command(String),
|
||||||
|
/// Open the OS file dialog.
|
||||||
|
OpenFileDialog,
|
||||||
|
/// Remove all loaded models from the scene.
|
||||||
|
#[allow(dead_code)]
|
||||||
|
ClearModels,
|
||||||
|
/// Toggle wireframe rendering.
|
||||||
|
SetWireframe(bool),
|
||||||
|
/// Toggle the layer manager panel.
|
||||||
|
ToggleLayers,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Data types ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Icon source for a ribbon tool button.
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub enum IconKind {
|
||||||
|
/// Unicode glyph rendered as text (fast, no file needed).
|
||||||
|
Glyph(&'static str),
|
||||||
|
/// Raw SVG bytes embedded at compile time via `include_bytes!`.
|
||||||
|
Svg(&'static [u8]),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A single tool button shown in the ribbon.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct ToolDef {
|
||||||
|
/// Unique command id, e.g. "LINE".
|
||||||
|
pub id: &'static str,
|
||||||
|
/// Short label shown under the icon.
|
||||||
|
pub label: &'static str,
|
||||||
|
/// Icon — either a unicode glyph or embedded SVG bytes.
|
||||||
|
pub icon: IconKind,
|
||||||
|
/// Event emitted when the tool is clicked.
|
||||||
|
pub event: ModuleEvent,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One item in a ribbon group — plain button or dropdown, in small (1-row) or large (3-row) size.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub enum RibbonItem {
|
||||||
|
/// 1-row button — icon only, no label.
|
||||||
|
Tool(ToolDef),
|
||||||
|
/// 3-row button — icon + label below; full ribbon height.
|
||||||
|
LargeTool(ToolDef),
|
||||||
|
/// 1-row dropdown — icon + ▾ on right, no label.
|
||||||
|
Dropdown {
|
||||||
|
id: &'static str,
|
||||||
|
icon: IconKind,
|
||||||
|
items: Vec<(&'static str, &'static str, IconKind)>,
|
||||||
|
default: &'static str,
|
||||||
|
},
|
||||||
|
/// 3-row dropdown — icon + label + ▾ below label; full ribbon height.
|
||||||
|
LargeDropdown {
|
||||||
|
id: &'static str,
|
||||||
|
label: &'static str,
|
||||||
|
icon: IconKind,
|
||||||
|
items: Vec<(&'static str, &'static str, IconKind)>,
|
||||||
|
default: &'static str,
|
||||||
|
},
|
||||||
|
/// Layer combo + two rows of small tools below.
|
||||||
|
/// row2: operates on the layer of a selected object (off/freeze/lock/make-current)
|
||||||
|
/// row3: all-layers operations + match (on/thaw/unlock/match)
|
||||||
|
LayerComboGroup {
|
||||||
|
row2: Vec<ToolDef>,
|
||||||
|
row3: Vec<ToolDef>,
|
||||||
|
},
|
||||||
|
/// Match Properties (large button) + Color / Linetype / Lineweight combos on the right.
|
||||||
|
PropertiesGroup { match_prop: ToolDef },
|
||||||
|
/// A style selector combobox (text / dim / mleader / table style) with
|
||||||
|
/// optional small tool rows below it.
|
||||||
|
StyleComboGroup {
|
||||||
|
/// Which style domain this combo controls.
|
||||||
|
style_key: StyleKey,
|
||||||
|
/// Unique dropdown id (must be unique across the ribbon).
|
||||||
|
combo_id: &'static str,
|
||||||
|
/// Optional command to run when the user opens the style manager.
|
||||||
|
manager_cmd: Option<&'static str>,
|
||||||
|
/// Small tool rows rendered below the combo (0–2 rows).
|
||||||
|
rows: Vec<Vec<ToolDef>>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Identifies which style list a `StyleComboGroup` refers to.
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||||
|
pub enum StyleKey {
|
||||||
|
TextStyle,
|
||||||
|
DimStyle,
|
||||||
|
MLeaderStyle,
|
||||||
|
TableStyle,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ToolDef> for RibbonItem {
|
||||||
|
fn from(t: ToolDef) -> Self {
|
||||||
|
RibbonItem::Tool(t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A named group of tool buttons shown together in the ribbon.
|
||||||
|
pub struct RibbonGroup {
|
||||||
|
pub title: &'static str,
|
||||||
|
pub tools: Vec<RibbonItem>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Trait ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// A CAD module owns a set of ribbon groups shown when its tab is active.
|
||||||
|
/// Each module is a stateless unit struct — all UI state lives in Ribbon.
|
||||||
|
pub trait CadModule: Send + Sync {
|
||||||
|
#[allow(dead_code)]
|
||||||
|
fn id(&self) -> &'static str;
|
||||||
|
fn title(&self) -> &'static str;
|
||||||
|
fn ribbon_groups(&self) -> Vec<RibbonGroup>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tool_def_converts_into_small_tool() {
|
||||||
|
let tool = ToolDef {
|
||||||
|
id: "LINE",
|
||||||
|
label: "Line",
|
||||||
|
icon: IconKind::Glyph("/"),
|
||||||
|
event: ModuleEvent::Command("LINE".to_string()),
|
||||||
|
};
|
||||||
|
assert!(matches!(RibbonItem::from(tool), RibbonItem::Tool(_)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cad_module_is_object_safe() {
|
||||||
|
struct Demo;
|
||||||
|
impl CadModule for Demo {
|
||||||
|
fn id(&self) -> &'static str {
|
||||||
|
"demo"
|
||||||
|
}
|
||||||
|
fn title(&self) -> &'static str {
|
||||||
|
"Demo"
|
||||||
|
}
|
||||||
|
fn ribbon_groups(&self) -> Vec<RibbonGroup> {
|
||||||
|
vec![RibbonGroup {
|
||||||
|
title: "Group",
|
||||||
|
tools: vec![],
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let m: Box<dyn CadModule> = Box::new(Demo);
|
||||||
|
assert_eq!(m.title(), "Demo");
|
||||||
|
assert_eq!(m.ribbon_groups().len(), 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -15,7 +15,7 @@ This document is the **authoritative spec** for how add-on packages integrate wi
|
||||||
| Goal | Rationale |
|
| Goal | Rationale |
|
||||||
|------|-----------|
|
|------|-----------|
|
||||||
| **One package, one registration** | Ribbon tab, commands, and manifest ship together — no duplicate hooks in `build.rs` and `commands.rs`. |
|
| **One package, one registration** | Ribbon tab, commands, and manifest ship together — no duplicate hooks in `build.rs` and `commands.rs`. |
|
||||||
| **Stable host surface** | Plugin authors target `HostSession` / future `ocs_plugin_api` with semver, not `OpenCADStudio` internals. |
|
| **Stable host surface** | Plugin authors target the semver-versioned `ocs_plugin_api` crate (manifest + ribbon today) and `HostSession`, not `OpenCADStudio` internals. |
|
||||||
| **Open-source add-on ergonomics** | Separate git repo + workspace crate is supported; in-tree built-ins use the same layout. |
|
| **Open-source add-on ergonomics** | Separate git repo + workspace crate is supported; in-tree built-ins use the same layout. |
|
||||||
| **DWG round-trip** | Domain data on entities (XDATA), not opaque plugin databases. |
|
| **DWG round-trip** | Domain data on entities (XDATA), not opaque plugin databases. |
|
||||||
| **Engine reuse** | Headless crates (`stormsewer`, …) run in WASM/CLI without the CAD host. |
|
| **Engine reuse** | Headless crates (`stormsewer`, …) run in WASM/CLI without the CAD host. |
|
||||||
|
|
@ -69,7 +69,7 @@ This document is the **authoritative spec** for how add-on packages integrate wi
|
||||||
|------|-----------------|
|
|------|-----------------|
|
||||||
| `metadata.txt` (name, version, author, …) | `plugin.toml` beside the package |
|
| `metadata.txt` (name, version, author, …) | `plugin.toml` beside the package |
|
||||||
| `classFactory(iface)` in `__init__.py` | `inventory::submit!(PluginRegistration { construct })` in `register.rs` |
|
| `classFactory(iface)` in `__init__.py` | `inventory::submit!(PluginRegistration { construct })` in `register.rs` |
|
||||||
| `iface` stable API | `HostSession` → future `ocs_plugin_api` crate |
|
| `iface` stable API | `ocs_plugin_api` crate (manifest + ribbon) + `HostSession` |
|
||||||
| User folder `…/python/plugins/<id>/` | Phase 2: `%APPDATA%/OpenCADStudio/plugins/<id>/` |
|
| User folder `…/python/plugins/<id>/` | Phase 2: `%APPDATA%/OpenCADStudio/plugins/<id>/` |
|
||||||
| Plugin repository (plugins.qgis.org) | Future: curated index; today = git + in-tree |
|
| Plugin repository (plugins.qgis.org) | Future: curated index; today = git + in-tree |
|
||||||
| `qgisMinimumVersion` | `api_version` in manifest (host ABI major) |
|
| `qgisMinimumVersion` | `api_version` in manifest (host ABI major) |
|
||||||
|
|
@ -182,7 +182,14 @@ Plugins use `HostSession`, not `OpenCADStudio`:
|
||||||
| Command line | `push_info`, `push_output`, `push_error`, `set_active_command` |
|
| Command line | `push_info`, `push_output`, `push_error`, `set_active_command` |
|
||||||
| Undo / dirty | `push_undo`, `set_dirty` |
|
| Undo / dirty | `push_undo`, `set_dirty` |
|
||||||
|
|
||||||
Phase 1b: extract to `crates/ocs_plugin_api` with semver when the surface stabilizes.
|
**Status:** The dependency-free half of the contract — `PluginManifest` /
|
||||||
|
`ApiVersion` (manifest) and `CadModule` + the ribbon types (`ToolDef`,
|
||||||
|
`RibbonGroup`, …) — now lives in the standalone, semver-versioned
|
||||||
|
[`crates/ocs_plugin_api`](../crates/ocs_plugin_api) crate. The host re-exports it
|
||||||
|
(`crate::plugin::manifest`, `crate::modules`) so in-tree paths are unchanged.
|
||||||
|
The `acadrust`-typed runtime surface in the table above (`document_mut`,
|
||||||
|
`add_entity`, `set_active_command`, …) stays in the host binary for now; lifting
|
||||||
|
it behind a `HostApi` trait in the same crate is the remaining phase-1b step.
|
||||||
|
|
||||||
### Command routing
|
### Command routing
|
||||||
|
|
||||||
|
|
@ -247,7 +254,7 @@ This mirrors QGIS: the application ships core menus; plugins add tabs/tools with
|
||||||
- [x] Per-tab `plugin_state`
|
- [x] Per-tab `plugin_state`
|
||||||
- [x] Storm Sewer off `commands.rs` monolith
|
- [x] Storm Sewer off `commands.rs` monolith
|
||||||
- [x] Single registration (`plugin.toml` + `BuiltinPlugin::ribbon`)
|
- [x] Single registration (`plugin.toml` + `BuiltinPlugin::ribbon`)
|
||||||
- [ ] Extract `ocs_plugin_api` crate
|
- [~] Extract `ocs_plugin_api` crate — manifest + ribbon/`CadModule` done; `acadrust`-typed host surface pending
|
||||||
- [ ] Plugin manager UI stub (list installed, versions)
|
- [ ] Plugin manager UI stub (list installed, versions)
|
||||||
|
|
||||||
### Phase 2 — Dynamic loading (desktop)
|
### Phase 2 — Dynamic loading (desktop)
|
||||||
|
|
@ -321,7 +328,7 @@ OpenCADStudio/
|
||||||
storm_sewer/ # add-on (has plugin.toml)
|
storm_sewer/ # add-on (has plugin.toml)
|
||||||
crates/
|
crates/
|
||||||
stormsewer/ # Layer C engine
|
stormsewer/ # Layer C engine
|
||||||
ocs_plugin_api/ # (phase 1b) stable host API
|
ocs_plugin_api/ # stable contract: manifest + ribbon (host API: phase 1b)
|
||||||
plugins/ # (phase 2) third-party cdylibs
|
plugins/ # (phase 2) third-party cdylibs
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,124 +13,14 @@
|
||||||
// - mod.rs : module definition (ribbon groups + tool layout)
|
// - mod.rs : module definition (ribbon groups + tool layout)
|
||||||
// - <tool>.rs : one file per tool (ribbon def + future command logic)
|
// - <tool>.rs : one file per tool (ribbon def + future command logic)
|
||||||
|
|
||||||
// ── Events ────────────────────────────────────────────────────────────────
|
// ── Ribbon vocabulary (CadModule, ToolDef, RibbonGroup, …) ─────────────────
|
||||||
|
//
|
||||||
/// Events a module tool can emit to the host application.
|
// These types moved to the dependency-free `ocs_plugin_api` crate so add-ons
|
||||||
#[derive(Debug, Clone)]
|
// can target a semver-stable contract. Re-exported here to keep the long-used
|
||||||
pub enum ModuleEvent {
|
// `crate::modules::{CadModule, ToolDef, …}` paths stable across the codebase.
|
||||||
/// Fire a named CAD command (e.g. "LINE", "CIRCLE").
|
pub use ocs_plugin_api::ribbon::{
|
||||||
Command(String),
|
CadModule, IconKind, ModuleEvent, RibbonGroup, RibbonItem, StyleKey, ToolDef,
|
||||||
/// Open the OS file dialog.
|
};
|
||||||
OpenFileDialog,
|
|
||||||
/// Remove all loaded models from the scene.
|
|
||||||
#[allow(dead_code)]
|
|
||||||
ClearModels,
|
|
||||||
/// Toggle wireframe rendering.
|
|
||||||
SetWireframe(bool),
|
|
||||||
/// Toggle the layer manager panel.
|
|
||||||
ToggleLayers,
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Data types ────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
/// Icon source for a ribbon tool button.
|
|
||||||
#[derive(Clone, Copy)]
|
|
||||||
pub enum IconKind {
|
|
||||||
/// Unicode glyph rendered as text (fast, no file needed).
|
|
||||||
Glyph(&'static str),
|
|
||||||
/// Raw SVG bytes embedded at compile time via `include_bytes!`.
|
|
||||||
Svg(&'static [u8]),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A single tool button shown in the ribbon.
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct ToolDef {
|
|
||||||
/// Unique command id, e.g. "LINE".
|
|
||||||
pub id: &'static str,
|
|
||||||
/// Short label shown under the icon.
|
|
||||||
pub label: &'static str,
|
|
||||||
/// Icon — either a unicode glyph or embedded SVG bytes.
|
|
||||||
pub icon: IconKind,
|
|
||||||
/// Event emitted when the tool is clicked.
|
|
||||||
pub event: ModuleEvent,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// One item in a ribbon group — plain button or dropdown, in small (1-row) or large (3-row) size.
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub enum RibbonItem {
|
|
||||||
/// 1-row button — icon only, no label.
|
|
||||||
Tool(ToolDef),
|
|
||||||
/// 3-row button — icon + label below; full ribbon height.
|
|
||||||
LargeTool(ToolDef),
|
|
||||||
/// 1-row dropdown — icon + ▾ on right, no label.
|
|
||||||
Dropdown {
|
|
||||||
id: &'static str,
|
|
||||||
icon: IconKind,
|
|
||||||
items: Vec<(&'static str, &'static str, IconKind)>,
|
|
||||||
default: &'static str,
|
|
||||||
},
|
|
||||||
/// 3-row dropdown — icon + label + ▾ below label; full ribbon height.
|
|
||||||
LargeDropdown {
|
|
||||||
id: &'static str,
|
|
||||||
label: &'static str,
|
|
||||||
icon: IconKind,
|
|
||||||
items: Vec<(&'static str, &'static str, IconKind)>,
|
|
||||||
default: &'static str,
|
|
||||||
},
|
|
||||||
/// Layer combo + two rows of small tools below.
|
|
||||||
/// row2: operates on the layer of a selected object (off/freeze/lock/make-current)
|
|
||||||
/// row3: all-layers operations + match (on/thaw/unlock/match)
|
|
||||||
LayerComboGroup {
|
|
||||||
row2: Vec<ToolDef>,
|
|
||||||
row3: Vec<ToolDef>,
|
|
||||||
},
|
|
||||||
/// Match Properties (large button) + Color / Linetype / Lineweight combos on the right.
|
|
||||||
PropertiesGroup { match_prop: ToolDef },
|
|
||||||
/// A style selector combobox (text / dim / mleader / table style) with
|
|
||||||
/// optional small tool rows below it.
|
|
||||||
StyleComboGroup {
|
|
||||||
/// Which style domain this combo controls.
|
|
||||||
style_key: StyleKey,
|
|
||||||
/// Unique dropdown id (must be unique across the ribbon).
|
|
||||||
combo_id: &'static str,
|
|
||||||
/// Optional command to run when the user opens the style manager.
|
|
||||||
manager_cmd: Option<&'static str>,
|
|
||||||
/// Small tool rows rendered below the combo (0–2 rows).
|
|
||||||
rows: Vec<Vec<ToolDef>>,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Identifies which style list a `StyleComboGroup` refers to.
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
|
||||||
pub enum StyleKey {
|
|
||||||
TextStyle,
|
|
||||||
DimStyle,
|
|
||||||
MLeaderStyle,
|
|
||||||
TableStyle,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<ToolDef> for RibbonItem {
|
|
||||||
fn from(t: ToolDef) -> Self {
|
|
||||||
RibbonItem::Tool(t)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A named group of tool buttons shown together in the ribbon.
|
|
||||||
pub struct RibbonGroup {
|
|
||||||
pub title: &'static str,
|
|
||||||
pub tools: Vec<RibbonItem>,
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Trait ─────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
/// A CAD module owns a set of ribbon groups shown when its tab is active.
|
|
||||||
/// Each module is a stateless unit struct — all UI state lives in Ribbon.
|
|
||||||
pub trait CadModule: Send + Sync {
|
|
||||||
#[allow(dead_code)]
|
|
||||||
fn id(&self) -> &'static str;
|
|
||||||
fn title(&self) -> &'static str;
|
|
||||||
fn ribbon_groups(&self) -> Vec<RibbonGroup>;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Module declarations ───────────────────────────────────────────────────
|
// ── Module declarations ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,32 +1,7 @@
|
||||||
// Plugin identity and capability declaration.
|
//! Plugin identity and capability declaration.
|
||||||
|
//!
|
||||||
|
//! These types now live in the standalone, dependency-free `ocs_plugin_api`
|
||||||
|
//! crate so external add-ons can target a semver-stable contract. Re-exported
|
||||||
|
//! here to keep the `crate::plugin::manifest::*` path stable for in-tree use.
|
||||||
|
|
||||||
/// Host plugin API version. Bump when HostApi breaks compatibility.
|
pub use ocs_plugin_api::manifest::*;
|
||||||
pub const API_VERSION: u32 = 1;
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
||||||
pub struct ApiVersion {
|
|
||||||
pub major: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ApiVersion {
|
|
||||||
pub const CURRENT: Self = Self { major: API_VERSION };
|
|
||||||
|
|
||||||
pub fn is_compatible_with(host: ApiVersion) -> bool {
|
|
||||||
Self::CURRENT.major == host.major
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Static metadata every plugin supplies at registration time.
|
|
||||||
/// Keep fields in sync with `plugin.toml` beside the package.
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
|
||||||
pub struct PluginManifest {
|
|
||||||
pub id: &'static str,
|
|
||||||
pub name: &'static str,
|
|
||||||
pub version: &'static str,
|
|
||||||
pub description: &'static str,
|
|
||||||
pub api_version: ApiVersion,
|
|
||||||
/// Sort key for add-on ribbon tabs (lower = further left among plugins).
|
|
||||||
pub ribbon_order: i32,
|
|
||||||
pub xdata_apps: &'static [&'static str],
|
|
||||||
pub command_prefixes: &'static [&'static str],
|
|
||||||
}
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue