唯杰地图 logo唯杰地图
VJMAP
EN

插件项目开发

vjcad/references/quickref/plugin-development.md 11.7 KB422 行
原文

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

如需了解插件 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 进行表格编辑
  • 支持交互式插入(指定点、缩放、旋转)

最佳实践

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