插件项目开发
独立插件项目的创建、开发、构建和发布流程。
如需了解插件 API(PluginContext、Ribbon、菜单等),请参阅 ui-plugin.md。
创建插件项目
手动创建
vjcad 官方未提供脚手架工具,按下文「项目结构」与「package.json」手动创建即可(命名约定:目录 <id>-plugin/、包名 vcad-plugin-<id>、命令名大写、全局变量 vjcadPlugin<PascalCase>)。
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
{
"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
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)
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)
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):
// vjcad 导出类型(types.d.ts)
type CommandClass = new (...args: unknown[]) => {
main(): Promise<void> | void; // 主执行方法(必须)
};
// 约定俗成再加可选 cancel(): void
类型虽写 ...args,但内核执行命令时是 new cmdDef.symbol() 不传任何参数——构造函数必须能无参运行(官方 annotation/insert 等插件的命令类都没有构造参数)。
完整命令示例:
// 无参构造(内核 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
// 命令注册
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
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';
}
构建与发布
构建命令
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 # 样式文件(如有)
加载插件
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);
插件管理
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 进行表格编辑
- 支持交互式插入(指定点、缩放、旋转)
最佳实践
- 模块化设计 - 将命令、服务、UI 分离到不同目录
- 使用 TypeScript - 获得更好的类型提示和错误检查
- 遵循命名规范 - 命令名大写,插件 ID 小写连字符
- 资源清理 - 在
onDeactivate中清理大型资源(如单例服务) - 使用 LitElement - UI 组件推荐使用 vjcad 提供的 LitElement
- 撤销支持 - 命令中使用
undoManager.start_undoMark()支持撤销
async main(): Promise<void> {
const undoMgr = Engine.undoManager; // Engine 来自 import { Engine } from 'vjcad'
undoMgr.start_undoMark();
try {
// 命令逻辑
} finally {
undoMgr.end_undoMark();
Engine.clearPreview();
}
}