# 插件项目开发

独立插件项目的创建、开发、构建和发布流程。

如需了解插件 API（PluginContext、Ribbon、菜单等），请参阅 [ui-plugin.md](ui-plugin.md)。

---

## 创建插件项目

### 手动创建

vjcad 官方未提供脚手架工具，按下文「项目结构」与「package.json」手动创建即可（命名约定：目录 `<id>-plugin/`、包名 `vcad-plugin-<id>`、命令名大写、全局变量 `vjcadPlugin<PascalCase>`）。

```bash
mkdir my-plugin-plugin && cd my-plugin-plugin
npm init -y
npm install --save-dev typescript vite
npm install --save-peer vjcad
```



---

## 项目结构

```
my-plugin-plugin/
├── package.json          # 包配置，含 vjcadPlugin 元数据
├── tsconfig.json         # TypeScript 配置
├── vite.config.ts        # Vite 构建配置（IIFE 格式）
├── .gitignore
└── src/
    ├── index.ts          # 插件主入口
    ├── icons.ts          # SVG 图标定义
    ├── commands/         # 命令实现（可选）
    ├── services/         # 服务层（可选）
    ├── ui/               # UI 组件（可选）
    └── vite-env.d.ts     # Vite 类型声明
```

### package.json

```json
{
  "name": "vcad-plugin-my-plugin",
  "version": "1.0.0",
  "type": "module",
  "main": "dist/vcad-plugin-my-plugin.js",
  "vjcadPlugin": {
    "id": "my-plugin",
    "displayName": "我的插件",
    "autoLoad": false,
    "keywords": ["my-plugin"]
  },
  "scripts": {
    "dev": "vite build --watch",
    "build": "tsc --noEmit && vite build"
  },
  "peerDependencies": {
    "vjcad": "^1.0.0"
  },
  "devDependencies": {
    "typescript": "^5.3.0",
    "vite": "^5.0.0",
    "vjcad": "^1.0.0"
  }
}
```

`vjcadPlugin` 字段说明：
- `id` - 插件唯一标识
- `displayName` - 界面显示名称
- `autoLoad` - 是否自动加载
- `keywords` - 搜索关键词

### vite.config.ts

```typescript
import { defineConfig } from 'vite';
import { resolve } from 'path';
import { readFileSync } from 'fs';

const pkg = JSON.parse(readFileSync(resolve(__dirname, 'package.json'), 'utf-8'));
const pluginFileName = pkg.name;

export default defineConfig({
    build: {
        lib: {
            entry: resolve(__dirname, 'src/index.ts'),
            formats: ['iife'],  // IIFE 格式，vjcad 作为全局变量
            fileName: () => `${pluginFileName}.js`,
            name: 'vjcadPluginMyPlugin'
        },
        rollupOptions: {
            external: ['vjcad'],
            output: {
                globals: { 'vjcad': 'vjcad' }
            }
        },
        outDir: 'dist',
        sourcemap: true
    }
});
```

---

## 开发指南

### 插件入口 (src/index.ts)

```typescript
import type { Plugin, PluginContext } from 'vjcad';
import { Engine } from 'vjcad';   // 值导入（构建时 external → 全局 vjcad）
import { ICON_MYPLUGIN } from './icons';

// 命令实现：满足 CommandClass 构造器类型（实例需有 main()，cancel 可选）
// ⚠️ 内核以 `new cmdDef.symbol()` 无参实例化命令类（且在 try 之外）——
// 构造函数不能依赖参数，写成 constructor(context) 再读 context.engine 会抛 TypeError，命令卡死在激活态。
// 需要 PluginContext 时在 onActivate 里存到模块级变量；引擎直接用导入的 Engine。
class MyPluginCommand {
    async main(): Promise<void> {
        console.log('Command executed');
        // 实现命令逻辑
    }
    
    cancel(): void {
        // 取消处理
    }
}

// 插件定义
const plugin: Plugin = {
    manifest: {
        id: 'my-plugin',
        name: '我的插件',
        version: '1.0.0',
        author: 'Your Name',
        description: '插件描述',
        keywords: ['my-plugin']
    },
    
    onLoad(context: PluginContext): void {
        console.log(`[${context.manifest.name}] 插件已加载`);
    },
    
    onActivate(context: PluginContext): void {
        // 注册图标
        context.registerIcon('MYPLUGIN', ICON_MYPLUGIN);
        
        // 注册命令
        context.registerCommand('MYPLUGIN', '我的插件', MyPluginCommand);
        
        // 添加菜单项
        context.addMenuItem('tool', { command: 'MYPLUGIN' });
        
        // 添加 Ribbon 组
        context.addRibbonGroup('plugins', {
            id: 'my-plugin',
            label: '我的插件',
            pinnable: true,
            primaryButtons: [
                { icon: 'myplugin', cmd: 'MYPLUGIN', prompt: '我的插件', type: 'large' }
            ]
        });
    },
    
    onDeactivate(context: PluginContext): void {
        // 清理资源（框架自动清理注册的命令、菜单等）
    },
    
    onUnload(context: PluginContext): void {
        console.log(`[${context.manifest.name}] 插件已卸载`);
    }
};

export default plugin;
```

### 图标定义 (src/icons.ts)

```typescript
export const ICON_MYPLUGIN = `
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
  <rect x="3" y="3" width="18" height="18" rx="2" stroke="#73C5FF" stroke-width="2"/>
  <path d="M7 12h10M12 7v10" stroke="#73C5FF" stroke-width="2" stroke-linecap="round"/>
</svg>
`;
```

图标规范：
- 使用 24x24 viewBox
- 主色调 `#73C5FF`（蓝色）
- 线条宽度 2px

### 命令类实现

命令类需满足 `CommandClass` 构造器类型（无 `BaseCommand` 接口可 implements）：

```typescript
// vjcad 导出类型（types.d.ts）
type CommandClass = new (...args: unknown[]) => {
    main(): Promise<void> | void;   // 主执行方法（必须）
};
// 约定俗成再加可选 cancel(): void
```

类型虽写 `...args`，但内核执行命令时是 `new cmdDef.symbol()` **不传任何参数**——构造函数必须能无参运行（官方 annotation/insert 等插件的命令类都没有构造参数）。

完整命令示例：

```typescript
// 无参构造（内核 new cmdDef.symbol() 不传参）；引擎用 import { Engine } from 'vjcad'
class DrawCircleCommand {
    async main(): Promise<void> {
        const { getPoint, getReal, PointInputOptions, RealInputOptions, 
                InputStatusEnum, CircleEnt, writeMessage } = vjcad;
        
        // 获取圆心
        const opt1 = new PointInputOptions("指定圆心:");
        const result1 = await getPoint(opt1);
        if (result1.status !== InputStatusEnum.OK) return;
        
        // 获取半径
        const opt2 = new RealInputOptions("指定半径:");
        opt2.defaultValue = 100;
        const result2 = await getReal(opt2);
        if (result2.status !== InputStatusEnum.OK) return;
        
        // 创建圆
        const circle = new CircleEnt(result1.value, result2.value);
        circle.setDefaults();
        Engine.addEntities(circle);
        
        writeMessage("<br/>圆已创建");
    }
    
    cancel(): void {
        Engine.clearPreview();
    }
}
```

### 生命周期钩子

| 钩子 | 时机 | 用途 |
|------|------|------|
| `onLoad` | 插件模块加载后 | 初始化、日志 |
| `onActivate` | 插件激活时 | 注册命令、图标、菜单、Ribbon |
| `onDeactivate` | 插件停用时 | 清理资源 |
| `onUnload` | 插件卸载前 | 最终清理 |

### PluginContext API

```typescript
// 命令注册
context.registerCommand(name: string, description: string, commandClass: CommandClass, options?: CommandOptions);
context.unregisterCommand(name: string);

// 图标注册
context.registerIcon(commandName: string, svgContent: string);

// 菜单
context.addMenuItem(menuId: string, item: { command: string, after?: string });

// Ribbon
context.addRibbonButton(tabId: string, groupId: string, button: RibbonButtonConfig);
context.addRibbonGroup(tabId: string, group: RibbonGroupConfig);
context.addRibbonTab(tab: RibbonTabConfig);

// 引擎访问
context.engine;           // typeof Engine（静态引擎类，不是实例）
context.manifest;         // 插件清单信息
```

### RibbonGroupConfig

```typescript
interface RibbonGroupConfig {
    id: string;
    label: string;
    pinnable?: boolean;
    displayMode?: 'large' | 'compact' | 'small-icons' | 'layer' | 'recent';
    primaryButtons: RibbonButtonConfig[];
    moreButtons?: RibbonButtonConfig[];   // 收进浮动面板的按钮（无 secondaryButtons 字段）
}

interface RibbonButtonConfig {
    icon: string;      // 图标名（小写）
    cmd: string;       // 命令名（大写）
    prompt?: string;   // 悬停提示（可选）
    label?: string;    // 紧凑模式显示文本（可选）
    type?: 'large' | 'small' | 'icon-only' | 'list';
}
```

---

## 构建与发布

### 构建命令

```bash
cd my-plugin-plugin
npm install    # 安装依赖
npm run dev    # 开发模式（watch）
npm run build  # 生产构建
```

### 构建产物

```
dist/
├── vcad-plugin-my-plugin.js      # IIFE 格式模块
├── vcad-plugin-my-plugin.js.map  # Source Map
└── vcad-plugin-my-plugin.css     # 样式文件（如有）
```

### 加载插件

```typescript
import { PluginManager } from 'vjcad';

const pm = PluginManager.getInstance();

// 从 URL 加载
await pm.loadFromUrl('https://your-server.com/plugins/vcad-plugin-my-plugin.js');

// 从本地路径加载
await pm.loadFromPath('./plugins/vcad-plugin-my-plugin.js');

// 从代码内容加载
await pm.loadFromContent(jsContent, cssContent);
```

### 插件管理

```typescript
const pm = PluginManager.getInstance();

pm.getLoadedPlugins();     // 获取已加载插件列表
pm.getPluginInfo(id);      // 获取指定插件信息
pm.isLoaded(id);           // 检查是否已加载
pm.isActive(id);           // 检查是否已激活
pm.activate(id);           // 激活插件
pm.deactivate(id);         // 停用插件
pm.unload(id);             // 卸载插件
```

---

## 示例插件

### annotation-plugin（批注）

提供云线、箭头、矩形、文字、引线等批注功能（`FINDREPLACE`、`QSELECT` 是内核内置命令，非插件命令——自定义命令名不要与内置命令重名）：

| 命令 | 功能 |
|------|------|
| `ANNOTATION` | 批注工具栏入口 |
| `ANNO_CLOUD` / `ANNO_RECT` / `ANNO_ELLIPSE` / `ANNO_FREEHAND` | 云线/矩形/椭圆/手绘批注 |
| `ANNO_ARROW` / `ANNO_LEADER` / `ANNO_TEXT` / `ANNO_STAMP` | 箭头/引线/文字/图章 |
| `ANNO_HIGHLIGHT` / `ANNO_SHOW` / `ANNO_HIDE` / `ANNO_CLEAR` / `ANNO_LIST` | 高亮/显隐/清除/列表 |

目录结构：

```
annotation-plugin/
├── src/
│   ├── index.ts
│   ├── icons.ts
│   ├── constants.ts
│   ├── i18n/
│   ├── commands/
│   │   ├── AnnotationCommand.ts
│   │   ├── AnnoCloudCommand.ts
│   │   └── ...（每个 ANNO_* 命令一个文件）
│   ├── services/
│   └── ui/
```

### insert-plugin（插入）

提供图片转矢量和表格功能：

| 命令 | 功能 |
|------|------|
| `INSERTIMAGE` | 图片转矢量导入 |
| `INSERTTABLE` | 插入表格 |
| `EXTRACTTABLE` | 提取表格 |

特性：
- 使用 WASM 实现图片矢量化
- 使用 x-spreadsheet 进行表格编辑
- 支持交互式插入（指定点、缩放、旋转）

---

## 最佳实践

1. **模块化设计** - 将命令、服务、UI 分离到不同目录
2. **使用 TypeScript** - 获得更好的类型提示和错误检查
3. **遵循命名规范** - 命令名大写，插件 ID 小写连字符
4. **资源清理** - 在 `onDeactivate` 中清理大型资源（如单例服务）
5. **使用 LitElement** - UI 组件推荐使用 vjcad 提供的 LitElement
6. **撤销支持** - 命令中使用 `undoManager.start_undoMark()` 支持撤销

```typescript
async main(): Promise<void> {
    const undoMgr = Engine.undoManager;   // Engine 来自 import { Engine } from 'vjcad'
    undoMgr.start_undoMark();
    try {
        // 命令逻辑
    } finally {
        undoMgr.end_undoMark();
        Engine.clearPreview();
    }
}
```
