# plugin / 插件系统总览

> 文档：`vjcad/doc/plugin/index.md`、`vjcad/doc/plugin/plugin-basics.md`
> 示例：`vjcad/example/plugin/01basic.js`、`04pluginmanager.js`

## 1. 插件系统能做什么

vjcad 插件系统让你以 **可热插拔** 的方式扩展引擎：

- 新增命令、SVG 图标
- 添加菜单项 / Ribbon 标签 / Ribbon 组 / Ribbon 按钮
- 监听引擎事件（实体增删 / 命令执行 / 文档生命周期）
- 持久化插件设置（localStorage 风格 API）
- 引入新的实体类型（CustomEntityBase 派生）和服务类

官方插件示例：`annotation-plugin`（批注）、`architecture-plugin`（建筑）、`insert-plugin`（图片矢量化）、`view3d-plugin`（三维视图，最小骨架）、`aiassist-plugin`（AI 助手）、`network-graph-plugin`（网络拓扑）。

## 2. 两种写法

| 写法 | 入口 | 适合场景 |
|---|---|---|
| **标准插件**（推荐） | 源码 `export default { manifest, onLoad, onActivate, onDeactivate, onUnload }`，**按 URL 加载须打包成 IIFE**（见 §3 下方说明） | 需要 `PluginManager` 管理 / 跨项目复用 |
| **手写注册类** | 自己 `class MyPlugin { install() / uninstall() }`，调用 `CommandRegistry / IconRegistry / MenuRegistry / Engine.view.ribbonBar` | 快速试验、内嵌业务代码 |

## 3. 标准插件骨架

```js
// my-plugin.js — 标准结构
export default {
    manifest: {
        id:          'my-plugin',
        name:        '我的插件',
        version:     '1.0.0',
        author:      '...',
        description: '...',
        keywords:    ['demo'],
        dependencies: [],
        minEngineVersion: '1.0.0'
    },

    async onLoad(context)   { /* 资源准备，不暴露功能 */ },
    async onActivate(context) {
        context.registerIcon('MYCMD', '<svg>...</svg>');
        context.registerCommand('MYCMD', '我的命令', MyCommandClass);
        context.addMenuItem('tool', { command: 'MYCMD' });
        context.addRibbonGroup('plugins', { id: 'my', label: '我的', primaryButtons: [...] });
    },
    async onDeactivate(context) {
        context.removeRibbonGroup('plugins', 'my');
        context.removeMenuItem('tool', 'MYCMD');
        context.unregisterCommand('MYCMD');
    },
    async onUnload(context) { /* 最终清理 */ }
};
```

> **模块格式**：上面的 `export default` 是**源码**写法。`loadFromUrl / loadFromPath / loadByName` 以及 `new MainView({ plugins })` 用经典 `<script>`（失败再回退 fetch + `<script>` 文本）执行文件，再扫描**新增的全局变量**（带 `manifest` 对象的变量或 `vjcadPlugin*` 变量）找插件——直接给它一个 ES module 文件会因 `export` 语法失败（`Plugin did not export a valid object`）。所以 URL 加载的文件必须是 **IIFE 产物并挂全局**，官方插件统一用 vite：`build.lib: { formats: ['iife'], name: 'vjcadPluginXxx' }`、`rollupOptions: { external: ['vjcad'], output: { globals: { vjcad: 'vjcad' } } }`。裸 `export default` 源码字符串只适用于 `loadFromContent`（检测到 `import/export` 时走 Blob URL + `import()`）。

## 4. 插件管理器（PluginManager）

```js
const { PluginManager } = vjcad;
const pm = PluginManager.getInstance();

// 加载方式（常用 4 种；另有 loadFromCacheEntry(entry) 与通用 load(source) 两个公开入口）
await pm.loadFromUrl('./plugins/vcad-plugin-insert.js', './plugins/vcad-plugin-insert.css');  // (js, css?, options?)
await pm.loadFromPath('/static/plugins/vcad-plugin-architecture.js');
await pm.loadFromContent(pluginCode /* 字符串源码 */);
await pm.loadByName('vcad-plugin-architecture');         // 包名约定路径：默认 baseUrl './plugins/'，只加载 JS

// 状态查询
pm.getLoadedPlugins();            // [{ manifest, state, loadedAt, activatedAt, error, source, origin, registeredCommands }, ...]（无 id 字段，用 info.manifest.id）
pm.isLoaded('my-plugin');
pm.isActive('my-plugin');
pm.getPluginInfo('my-plugin');    // 返回 { ..., state } —— 没有独立 getPluginState；状态经 info.state 取

// 生命周期控制
await pm.activate('my-plugin');
await pm.deactivate('my-plugin');
await pm.unload('my-plugin');
```

> 完整代码：`vjcad/example/plugin/04pluginmanager.js`

**加载选项**（`loadFromUrl/loadFromPath/loadFromContent` 的第 3 参、`loadByName`/`load` 的第 2 参）：`{ autoActivate = true, timeout = 30000, forceReload, origin }`；`loadByName` 另收 `baseUrl`。

- `loadByName(pkg)` 只拼 `${baseUrl}${pkg}.js`，**从不加载 CSS**；自带样式的官方插件（`insert` / `view3d` / `aiassist` 的 dist 都有 `.css`）请用 `loadFromUrl(js, css)`
- 同 `manifest.id` 已加载时直接返回已有 id（不重跑 `onLoad`）；要重载传 `{ forceReload: true }`（先 `unload` 再注册）
- 插件激活/停用/卸载时 RibbonBar 会自动刷新；激活之后再改 `RibbonConfigManager.getConfig()`（或用 `RibbonConfigManager.patchConfig({...})` 做增量修改）要调 `RibbonConfigManager.refresh()` 才会重绘

## 5. MainView 中预声明插件

```js
const cadView = new MainView({
    appname: '唯杰CAD',
    serviceUrl: '...',
    accessToken: '...',
    plugins: [
        { url: './plugins/vcad-plugin-annotation.js' },
        { url: './plugins/vcad-plugin-architecture.js' },
        { url: './plugins/vcad-plugin-insert.js', cssUrl: './plugins/vcad-plugin-insert.css', autoActivate: true }
    ]
});
```

`MainView` 会在 `onLoad` 阶段自动按顺序加载（内部即 `pm.loadFromUrl(url, cssUrl, { autoActivate })`，`autoActivate` 默认 true）、激活这些插件；单个失败只 `console.error`，不中断其余。文件同样必须是 IIFE 产物（见 §3 说明）。

## 6. 子文档

- 生命周期 / Plugin 接口 / Manifest / 事件 → [lifecycle](lifecycle.md)
- PluginContext API（命令、图标、菜单、Ribbon、事件、存储） → [context-api](context-api.md)
- 菜单与 Ribbon 详解（默认 ID、增量/全量、MainView 配置） → [menu-ribbon](menu-ribbon.md)
- 官方插件指南（annotation / architecture / view3d / insert；另有 aiassist / network-graph，无 "tools" 插件） → [builtin-plugins](builtin-plugins.md)

## 7. 心智模型

```mermaid
flowchart LR
    subgraph PluginManager
        P[Plugin] --> L[Loaded]
        L --> A[Active]
        A -.-> D[Deactivated]
        D --> A
        D --> U[Unloaded]
    end

    A -->|context.registerXxx| Eng[Engine\nIconRegistry / CommandRegistry / MenuRegistry / RibbonBar / EventManager]
    D -->|removeXxx| Eng
```

激活态才会在 UI 露出；停用即可隐藏，无需重新加载。

## 红旗

- **生命周期与函数对照**：注册放 `onActivate`，注销放 `onDeactivate`，加载/卸载阶段不要碰 UI
- 多实例 / 多次激活：插件 `id` 必须唯一（同 id 再 load 直接复用旧实例）；`addMenu / addRibbonTab` 重复 ID 会被忽略（`addMenu` 只对 `addMenu` 建的动态菜单去重，细节见 [context-api](context-api.md) 红旗）
- 资源清理顺序遵循"添加的逆序"：先移按钮，再移组，再移标签
- 命令名规范全大写、图标名小写、菜单/组/标签 ID 用 kebab-case
- 用 `loadFromContent` 时插件代码内用全局 `vjcad.<导出名>`，**不能** 直接 `import 'vjcad'`（沙箱里没有打包器）
- **顶层没有 `registerPlugin` / `pluginManager` 导出**：唯一入口是 `PluginManager.getInstance()`；`PluginSourceType` 枚举已导出（`Url/Path/Content`），`pm.load({type: PluginSourceType.Content, js})` 可加载 `export default {manifest, ...}` 的 ES module 字符串
- `deactivate` 后 `context._cleanup()` 确实自动注销命令/事件/菜单/Ribbon（`executerWithOp` 不再执行已注销命令；不含 `once`、`registerIcon`，侧栏面板仅 `unload` 时移除）；但注意 `executerWithOp` 对不存在命令是**静默 resolve**，无法用抛错判断注销与否

## 参考（官方文档 MCP）

- 文档：`vjcad/doc/plugin/index.md`、`vjcad/doc/plugin/plugin-basics.md`
- 示例：`vjcad/example/plugin/01basic.js`、`04pluginmanager.js`
