# PluginContext API

> 文档：`vjcad/doc/plugin/plugin-context.md`、`vjcad/doc/plugin/plugin-examples.md`
> 示例：`vjcad/example/plugin/04pluginmanager.js`、`vjcad/example/plugin/05architecture.js`

`PluginContext` 是插件钩子的入参，封装了所有"安全注册 / 注销" API；只有它的修改会在停用（`onDeactivate` 之后）时被自动跟踪、清理。**例外**：`once()` 监听不跟踪、`registerIcon` 的图标不移除；`registerSidebarPanel` 的面板只在 `unload` 时移除（停用不移除）。

## 1. 命令

```ts
context.registerCommand(name, description, CommandClass, options?);
context.unregisterCommand(name);
context.executeCommand(name);             // 等价 Engine.editor.executerWithOp(name)
context.getRegisteredCommands();          // 列出本插件已注册命令
```

例子：

```js
class DrawCrossCommand {
    async main() {
        const c = new Point2D(0, 0), s = 50;
        const h = new LineEnt([c.x - s, c.y], [c.x + s, c.y]);
        const v = new LineEnt([c.x, c.y - s], [c.x, c.y + s]);
        h.setDefaults(); v.setDefaults();
        Engine.addEntities([h, v], { recordUndo: true });
        Engine.pcanvas.regen();
    }
}
context.registerCommand('DRAWCROSS', '绘制十字', DrawCrossCommand, new CommandOptions());
```

## 2. 图标

```ts
context.registerIcon(name, svgString);
```

- 注册到 IconRegistry 的 `Commands` 分类，键名**统一转小写**，查找也转小写 → **大小写不敏感，注册一次即可**
- 菜单项默认按 **命令同名图标** 渲染；Ribbon 按钮的 `icon` 字段填图标名（大小写随意，惯例小写命令名）
- `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">` 是推荐模板

```js
const ICON = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
    <line x1="4" y1="12" x2="20" y2="12"/>
    <line x1="12" y1="4" x2="12" y2="20"/>
</svg>`;
context.registerIcon('DRAWCROSS', ICON);   // 菜单与 Ribbon（icon: 'drawcross'）共用
```

## 3. 菜单

```ts
context.addMenu(config);                    // 创建动态主菜单 { id, label, after? }（已有同 ID 动态菜单则跳过，返回 false）
context.removeMenu(menuId);

context.addMenuItem(menuId, { command, shortcut?, label?, icon?, before?, after? });
context.removeMenuItem(menuId, command);
```

- 去重只针对 `addMenu` 建的**动态菜单**；`removeMenu`（含停用时的自动清理）会把该菜单连同**其他插件挂进去的项**一起删掉
- 挂到 `addMenu` 建的动态菜单里的项只认 `command / shortcut`，`before / after / label / icon` 无效（这些字段只对内置菜单 `tool`、`insert` 等生效）

默认主菜单 ID：`file | create | edit | view | insert | dim | property | tool | appearance | help`，详见 [menu-ribbon](menu-ribbon.md)。

```js
context.addMenuItem('tool', {
    command:  'DRAWCROSS',
    shortcut: 'Ctrl+M'
});

context.addMenu({ id: 'my-tools', label: '我的工具', after: 'tool' });
context.addMenuItem('my-tools', { command: 'DRAWCROSS' });
```

## 4. Ribbon

```ts
context.addRibbonTab(tabConfig, afterTabId?);
context.removeRibbonTab(tabId);

context.addRibbonGroup(tabId, groupConfig);
context.removeRibbonGroup(tabId, groupId);

context.addRibbonButton(tabId, groupId, buttonConfig, primaryOrMore);  // 'primary' | 'more'
context.removeRibbonButton(tabId, groupId, cmd);
```

```js
// tabId 必须是已存在的标签（默认 default / tools / insert / plugins / recent，没有 'home'）；
// 找不到 tab（或 addRibbonButton 找不到 group）时静默 return，不报错
context.addRibbonGroup('default', {
    id: 'drawing-tools',
    label: '绘图工具',
    pinnable: true,
    displayMode: 'large',
    primaryButtons: [
        { icon: 'drawcross', cmd: 'DRAWCROSS', prompt: '绘制十字', type: 'large' }
    ],
    moreButtons: [
        { icon: 'mycmd2', cmd: 'MYCMD2', prompt: '命令2' }
    ]
});

// 添加按钮到现有组
context.addRibbonButton('default', 'draw', {
    icon: 'drawcross', cmd: 'DRAWCROSS', prompt: '绘制十字', type: 'small'
}, 'primary');
```

> 详细字段在 [menu-ribbon](menu-ribbon.md)。

## 5. 事件

```ts
context.on(event, handler);
context.off(event, handler);
context.once(event, handler);
```

`event` 取自 `CadEvents` 枚举（参见 `event/types.md`）。

```js
context.on(CadEvents.EntityAdded, args => {
    console.log('实体添加', args.entity.type);
});
context.on(CadEvents.CommandStarted, args => {
    console.log('命令开始', args.commandName);
});
// ⚠️ DocumentOpened 在 vjcad@1.0.16 从不派发；打开/切换文档用 DocumentSwitched（args.previousDocument / args.currentDocument）
context.on(CadEvents.DocumentSwitched, args => {
    console.log('当前文档', args.currentDocument?.name);
});
```

停用时 `on` 注册的事件会被自动解绑，无需手动 `off`；但 `once` **不被跟踪**，触发前停用仍会留着，需要时自己 `Engine.eventManager.off`。

## 6. 数据存储

```ts
context.getStorageItem<T>(key): T | null;
context.setStorageItem<T>(key, value): void;
context.removeStorageItem(key): void;
```

存储以插件 `id` 隔离，跨插件不会冲突。

```ts
interface PluginSettings { defaultSize: number; autoSave: boolean; }
const cur = context.getStorageItem<PluginSettings>('settings');
if (cur) console.log('default size', cur.defaultSize);
context.setStorageItem<PluginSettings>('settings', { defaultSize: 100, autoSave: true });
```

## 7. API 速查表

### 命令
| 方法 | 说明 |
|---|---|
| `registerCommand(name, desc, cls, opt?)` | 注册命令 |
| `unregisterCommand(name)` | 注销 |
| `executeCommand(name)` | 立即执行 |
| `getRegisteredCommands()` | 列出本插件命令 |

### 图标 / 菜单 / Ribbon
| 方法 | 说明 |
|---|---|
| `registerIcon(name, svg)` | 注册 SVG |
| `addMenu / removeMenu` | 主菜单 |
| `addMenuItem / removeMenuItem` | 菜单项 |
| `addRibbonTab / removeRibbonTab` | Ribbon 标签 |
| `addRibbonGroup / removeRibbonGroup` | Ribbon 组 |
| `addRibbonButton / removeRibbonButton` | Ribbon 按钮 |

### 事件 / 存储
| 方法 | 说明 |
|---|---|
| `on / off / once` | 事件 |
| `getStorageItem / setStorageItem / removeStorageItem` | 持久化设置 |

## 红旗

- `onDeactivate` 里未手动移除的注册项会由 `context._cleanup()` 自动清理（命令 / `on` 事件 / 菜单 / Ribbon；不含 `once`、图标、侧栏面板），所以手动注销**可选**；若手动做，按 button → group → tab、item → menu 的逆序移除可避免中间态悬挂引用
- `addMenu`、`addRibbonTab` 已存在同 ID 时是 **no-op**，不会报错；多个插件可以共用 `'plugins'` 标签。但 `addMenu` 只对动态菜单去重，且创建者停用时会把别的插件挂在该菜单里的项一起删掉——共享菜单宁可挂到内置菜单（如 `tool`）
- 存储是同步 API，但不要塞 MB 级 JSON，会拖慢初始化
- `registerIcon` 同名会覆盖前者，且名字大小写不敏感（`'DRAWCROSS'` 与 `'drawcross'` 是同一个键）；想"主图标 + 备份"请使用不同 name
- 事件 handler 内 throw 不会自动卸载插件，会在控制台 warn；自己 try/catch

## 参考（官方文档 MCP）

- 文档：`vjcad/doc/plugin/plugin-context.md`、`vjcad/doc/plugin/plugin-examples.md`
- 示例：`vjcad/example/plugin/04pluginmanager.js`、`vjcad/example/plugin/05architecture.js`
