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
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);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue