第08章-插件系统开发

第八章:插件系统开发

8.1 插件系统概述

Astral3D采用模块化的插件架构,允许开发者通过插件扩展编辑器的功能。插件系统支持热插拔、沙箱隔离、依赖管理等特性,是Astral3D可扩展性的核心。

8.1.1 插件系统架构

┌─────────────────────────────────────────────────────────────────────┐
│                         插件系统架构                                 │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │                    Plugin Manager                            │   │
│  │                   (插件管理器)                                │   │
│  │  ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐           │   │
│  │  │ 注册    │ │ 加载    │ │ 卸载    │ │ 更新    │           │   │
│  │  │Register │ │ Load    │ │ Unload  │ │ Update  │           │   │
│  │  └─────────┘ └─────────┘ └─────────┘ └─────────┘           │   │
│  └─────────────────────────────────────────────────────────────┘   │
│                              │                                      │
│              ┌───────────────┼───────────────┐                     │
│              ▼               ▼               ▼                     │
│  ┌───────────────┐ ┌───────────────┐ ┌───────────────┐            │
│  │   Plugin A    │ │   Plugin B    │ │   Plugin C    │            │
│  │   内置插件    │ │   第三方插件   │ │   自定义插件   │            │
│  └───────────────┘ └───────────────┘ └───────────────┘            │
│          │                 │                 │                     │
│          └─────────────────┼─────────────────┘                     │
│                            ▼                                       │
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │                    Plugin API                                │   │
│  │  ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌───────┐ │   │
│  │  │ Viewer  │ │ Scene   │ │ UI      │ │ Events  │ │Storage│ │   │
│  │  │ API     │ │ API     │ │ API     │ │ API     │ │ API   │ │   │
│  │  └─────────┘ └─────────┘ └─────────┘ └─────────┘ └───────┘ │   │
│  └─────────────────────────────────────────────────────────────┘   │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

8.1.2 插件类型

类型 说明 示例
功能插件 扩展编辑器功能 测量工具、标注工具
格式插件 支持新的文件格式 自定义模型格式加载器
UI插件 添加界面组件 自定义面板、工具栏
渲染插件 扩展渲染能力 自定义着色器、后处理
集成插件 第三方服务集成 云存储、数据接口

8.1.3 插件生命周期

// 插件生命周期接口
interface PluginLifecycle {
  // 插件安装时调用
  install(app: Application): void | Promise<void>;
  
  // 插件激活时调用
  activate(): void | Promise<void>;
  
  // 插件停用时调用
  deactivate(): void | Promise<void>;
  
  // 插件卸载时调用
  uninstall(): void | Promise<void>;
  
  // 编辑器就绪时调用
  onReady(): void;
  
  // 场景加载时调用
  onSceneLoad(scene: THREE.Scene): void;
  
  // 对象选择变化时调用
  onSelectionChange(objects: THREE.Object3D[]): void;
  
  // 渲染帧更新时调用
  onUpdate(delta: number): void;
}

8.2 创建插件项目

8.2.1 插件项目结构

my-plugin/
├── src/
│   ├── index.ts           # 插件入口
│   ├── plugin.ts          # 插件主类
│   ├── components/        # Vue组件
│   │   └── MyPanel.vue
│   ├── tools/             # 工具类
│   │   └── MyTool.ts
│   └── styles/            # 样式文件
│       └── index.css
├── package.json           # 项目配置
├── tsconfig.json          # TypeScript配置
├── vite.config.ts         # 构建配置
└── README.md              # 文档

8.2.2 插件清单(package.json)

{
  "name": "@astral3d/plugin-my-feature",
  "version": "1.0.0",
  "description": "我的自定义插件",
  "main": "dist/index.js",
  "module": "dist/index.esm.js",
  "types": "dist/index.d.ts",
  "keywords": ["astral3d", "plugin"],
  "author": "Your Name",
  "license": "MIT",
  
  "astral3d": {
    "name": "我的功能插件",
    "description": "这是一个示例插件",
    "version": "1.0.0",
    "icon": "icon.png",
    "category": "tools",
    "minVersion": "1.0.0",
    "maxVersion": "2.0.0",
    "permissions": [
      "scene:read",
      "scene:write",
      "ui:panel",
      "storage:local"
    ],
    "dependencies": [
      "@astral3d/plugin-base"
    ]
  },
  
  "peerDependencies": {
    "@astral3d/engine": "^1.0.0",
    "three": "^0.170.0",
    "vue": "^3.5.0"
  },
  
  "devDependencies": {
    "typescript": "^5.0.0",
    "vite": "^5.0.0",
    "@astral3d/plugin-dev-kit": "^1.0.0"
  }
}

8.2.3 插件入口文件

// src/index.ts
import { Plugin, PluginContext } from '@astral3d/engine';
import MyPlugin from './plugin';

// 导出插件类
export default MyPlugin;

// 导出插件信息
export const pluginInfo = {
  name: 'my-plugin',
  displayName: '我的插件',
  version: '1.0.0',
  author: 'Your Name',
  description: '插件描述'
};

// 自动注册(可选)
export function register(context: PluginContext) {
  context.registerPlugin(new MyPlugin());
}

8.2.4 插件主类

// src/plugin.ts
import { 
  Plugin, 
  PluginContext, 
  Application,
  useAddSignal,
  useDispatchSignal 
} from '@astral3d/engine';
import MyPanel from './components/MyPanel.vue';
import { MyTool } from './tools/MyTool';

export default class MyPlugin extends Plugin {
  // 插件标识
  static id = 'my-plugin';
  
  // 插件上下文
  private context: PluginContext;
  private tool: MyTool;
  
  // 安装插件
  async install(app: Application) {
    console.log('插件安装中...');
    
    // 保存应用引用
    this.app = app;
    
    // 初始化工具
    this.tool = new MyTool(app.viewer);
  }
  
  // 激活插件
  async activate() {
    console.log('插件激活');
    
    // 注册UI面板
    this.registerPanel();
    
    // 注册工具栏按钮
    this.registerToolbar();
    
    // 注册事件监听
    this.registerEvents();
    
    // 注册命令
    this.registerCommands();
  }
  
  // 停用插件
  async deactivate() {
    console.log('插件停用');
    
    // 清理资源
    this.unregisterPanel();
    this.unregisterToolbar();
    this.unregisterEvents();
  }
  
  // 卸载插件
  async uninstall() {
    console.log('插件卸载');
    this.tool?.dispose();
  }
  
  // 注册面板
  private registerPanel() {
    this.app.ui.registerPanel({
      id: 'my-panel',
      title: '我的面板',
      component: MyPanel,
      position: 'right',
      width: 300,
      icon: 'tool-icon'
    });
  }
  
  // 注册工具栏
  private registerToolbar() {
    this.app.ui.registerToolbarButton({
      id: 'my-tool-btn',
      title: '我的工具',
      icon: 'my-icon',
      group: 'tools',
      onClick: () => this.activateTool()
    });
  }
  
  // 注册事件
  private registerEvents() {
    useAddSignal('objectSelected', this.onObjectSelected.bind(this));
    useAddSignal('sceneChanged', this.onSceneChanged.bind(this));
  }
  
  // 注册命令
  private registerCommands() {
    this.app.commands.register({
      id: 'my-plugin:do-something',
      name: '执行操作',
      shortcut: 'Ctrl+Shift+M',
      execute: () => this.doSomething()
    });
  }
  
  // 事件处理
  private onObjectSelected(object: THREE.Object3D) {
    console.log('对象被选中:', object?.name);
  }
  
  private onSceneChanged() {
    console.log('场景发生变化');
  }
  
  // 功能方法
  private activateTool() {
    this.tool.activate();
  }
  
  private doSomething() {
    console.log('执行自定义操作');
  }
}

8.3 插件API详解

8.3.1 Viewer API

// 获取Viewer实例
const viewer = this.app.viewer;

// 场景操作
viewer.scene.add(object);
viewer.scene.remove(object);
viewer.scene.traverse(callback);

// 相机操作
viewer.camera.position.set(x, y, z);
viewer.camera.lookAt(target);
viewer.cameraController.flyTo(position, target, duration);
viewer.cameraController.focusOn(object);

// 渲染操作
viewer.render();
viewer.setSize(width, height);
viewer.getScreenshot();

// 选择操作
viewer.selection.select(object);
viewer.selection.deselect();
viewer.selection.getSelected();

// 射线检测
viewer.raycast(x, y);
viewer.raycastAll(x, y);

// 坐标转换
viewer.worldToScreen(worldPos);
viewer.screenToWorld(screenPos, depth);

8.3.2 Scene API

// 获取Scene实例
const scene = this.app.scene;

// 对象查询
scene.getObjectByName(name);
scene.getObjectByUUID(uuid);
scene.getObjectsByType(type);
scene.getObjectsByProperty(key, value);

// 对象操作
scene.add(object, parent);
scene.remove(object);
scene.clone(object);
scene.group(objects);
scene.ungroup(group);

// 变换操作
scene.transform.move(object, delta);
scene.transform.rotate(object, euler);
scene.transform.scale(object, scale);

// 历史操作
scene.history.undo();
scene.history.redo();
scene.history.push(action);

// 序列化
scene.serialize();
scene.deserialize(data);
scene.export(format, options);
scene.import(file, options);

8.3.3 UI API

// 获取UI实例
const ui = this.app.ui;

// 面板管理
ui.registerPanel(config);
ui.unregisterPanel(id);
ui.showPanel(id);
ui.hidePanel(id);
ui.togglePanel(id);

// 工具栏
ui.registerToolbarButton(config);
ui.unregisterToolbarButton(id);
ui.setToolbarButtonState(id, state);

// 菜单
ui.registerMenuItem(config);
ui.unregisterMenuItem(id);
ui.showContextMenu(items, position);

// 对话框
ui.showDialog(config);
ui.showConfirm(message, options);
ui.showPrompt(message, options);
ui.showNotification(message, type);
ui.showToast(message, duration);

// 状态栏
ui.setStatusText(text);
ui.showProgress(value, max);
ui.hideProgress();

8.3.4 Events API

// 事件订阅
useAddSignal('eventName', callback);
useAddSignal('objectAdded', (obj) => console.log('添加:', obj));
useAddSignal('objectRemoved', (obj) => console.log('移除:', obj));
useAddSignal('objectSelected', (obj) => console.log('选中:', obj));
useAddSignal('sceneChanged', () => console.log('场景变化'));
useAddSignal('cameraChanged', (camera) => console.log('相机变化'));

// 事件派发
useDispatchSignal('eventName', data);
useDispatchSignal('customEvent', { message: 'hello' });

// 事件取消
useRemoveSignal('eventName', callback);

// 内置事件列表
const builtInEvents = [
  // 场景事件
  'objectAdded', 'objectRemoved', 'objectChanged',
  'objectSelected', 'objectDeselected',
  'sceneLoaded', 'sceneSaved', 'sceneCleared',
  
  // 相机事件
  'cameraChanged', 'cameraAnimationStart', 'cameraAnimationEnd',
  
  // 渲染事件
  'beforeRender', 'afterRender', 'resized',
  
  // 加载事件
  'loadStart', 'loadProgress', 'loadComplete', 'loadError',
  
  // 工具事件
  'toolActivated', 'toolDeactivated',
  
  // 编辑事件
  'historyChanged', 'undoPerformed', 'redoPerformed'
];

8.3.5 Storage API

// 获取Storage实例
const storage = this.app.storage;

// 本地存储
storage.local.set(key, value);
storage.local.get(key);
storage.local.remove(key);
storage.local.clear();

// 会话存储
storage.session.set(key, value);
storage.session.get(key);

// 插件存储(隔离的存储空间)
const pluginStorage = storage.plugin('my-plugin');
pluginStorage.set('settings', { theme: 'dark' });
pluginStorage.get('settings');

// 文件存储
await storage.file.save(blob, filename);
await storage.file.load(filename);
await storage.file.list(directory);
await storage.file.delete(filename);

8.4 创建UI组件

8.4.1 Vue组件示例

<!-- src/components/MyPanel.vue -->
<template>
  <div class="my-panel">
    <div class="panel-header">
      <h3>{{ title }}</h3>
    </div>
    
    <div class="panel-content">
      <!-- 选中对象信息 -->
      <div v-if="selectedObject" class="object-info">
        <p>名称: {{ selectedObject.name }}</p>
        <p>类型: {{ selectedObject.type }}</p>
        <p>位置: {{ formatPosition(selectedObject.position) }}</p>
      </div>
      <div v-else class="no-selection">
        请选择一个对象
      </div>
      
      <!-- 操作按钮 -->
      <div class="actions">
        <n-button @click="doAction1">操作1</n-button>
        <n-button @click="doAction2">操作2</n-button>
      </div>
      
      <!-- 设置选项 -->
      <div class="settings">
        <n-form>
          <n-form-item label="选项1">
            <n-switch v-model:value="settings.option1" />
          </n-form-item>
          <n-form-item label="选项2">
            <n-slider v-model:value="settings.option2" :min="0" :max="100" />
          </n-form-item>
          <n-form-item label="选项3">
            <n-color-picker v-model:value="settings.option3" />
          </n-form-item>
        </n-form>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, reactive, onMounted, onUnmounted, inject } from 'vue';
import { NButton, NForm, NFormItem, NSwitch, NSlider, NColorPicker } from 'naive-ui';
import { useAddSignal, useRemoveSignal } from '@astral3d/engine';

// 注入插件上下文
const context = inject('pluginContext');

// 响应式数据
const title = ref('我的面板');
const selectedObject = ref(null);
const settings = reactive({
  option1: false,
  option2: 50,
  option3: '#ffffff'
});

// 格式化位置
const formatPosition = (pos) => {
  return `(${pos.x.toFixed(2)}, ${pos.y.toFixed(2)}, ${pos.z.toFixed(2)})`;
};

// 事件处理
const onSelectionChange = (objects) => {
  selectedObject.value = objects[0] || null;
};

// 操作方法
const doAction1 = () => {
  console.log('执行操作1');
  context.emit('action1', { settings: settings });
};

const doAction2 = () => {
  console.log('执行操作2');
  if (selectedObject.value) {
    // 对选中对象进行操作
    selectedObject.value.visible = !selectedObject.value.visible;
  }
};

// 生命周期
onMounted(() => {
  useAddSignal('objectSelected', onSelectionChange);
});

onUnmounted(() => {
  useRemoveSignal('objectSelected', onSelectionChange);
});
</script>

<style scoped>
.my-panel {
  padding: 16px;
}

.panel-header {
  border-bottom: 1px solid #eee;
  margin-bottom: 16px;
}

.object-info {
  background: #f5f5f5;
  padding: 12px;
  border-radius: 4px;
  margin-bottom: 16px;
}

.no-selection {
  color: #999;
  text-align: center;
  padding: 20px;
}

.actions {
  display: flex;
  gap: 8px;
  margin-bottom: 16px;
}

.settings {
  margin-top: 16px;
}
</style>

8.4.2 工具栏组件

<!-- src/components/MyToolbar.vue -->
<template>
  <div class="my-toolbar">
    <n-tooltip v-for="tool in tools" :key="tool.id">
      <template #trigger>
        <n-button 
          :type="activeTool === tool.id ? 'primary' : 'default'"
          @click="selectTool(tool.id)"
        >
          <template #icon>
            <n-icon :component="tool.icon" />
          </template>
        </n-button>
      </template>
      {{ tool.name }}
    </n-tooltip>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue';
import { NButton, NTooltip, NIcon } from 'naive-ui';
import { MeasureIcon, AnnotateIcon, CutIcon } from './icons';

const tools = [
  { id: 'measure', name: '测量', icon: MeasureIcon },
  { id: 'annotate', name: '标注', icon: AnnotateIcon },
  { id: 'cut', name: '剖切', icon: CutIcon }
];

const activeTool = ref('');

const emit = defineEmits(['toolSelected']);

const selectTool = (id: string) => {
  activeTool.value = activeTool.value === id ? '' : id;
  emit('toolSelected', activeTool.value);
};
</script>

8.5 创建自定义工具

8.5.1 工具基类

// src/tools/BaseTool.ts
import { Viewer, useAddSignal, useRemoveSignal } from '@astral3d/engine';

export abstract class BaseTool {
  protected viewer: Viewer;
  protected active: boolean = false;
  
  constructor(viewer: Viewer) {
    this.viewer = viewer;
  }
  
  // 激活工具
  activate() {
    if (this.active) return;
    this.active = true;
    this.onActivate();
    this.bindEvents();
  }
  
  // 停用工具
  deactivate() {
    if (!this.active) return;
    this.active = false;
    this.unbindEvents();
    this.onDeactivate();
  }
  
  // 切换状态
  toggle() {
    if (this.active) {
      this.deactivate();
    } else {
      this.activate();
    }
  }
  
  // 子类实现
  protected abstract onActivate(): void;
  protected abstract onDeactivate(): void;
  protected abstract bindEvents(): void;
  protected abstract unbindEvents(): void;
  
  // 销毁
  dispose() {
    this.deactivate();
  }
}

8.5.2 测量工具示例

// src/tools/MeasureTool.ts
import * as THREE from 'three';
import { BaseTool } from './BaseTool';

export class MeasureTool extends BaseTool {
  private points: THREE.Vector3[] = [];
  private lines: THREE.Line[] = [];
  private labels: HTMLElement[] = [];
  private tempLine: THREE.Line | null = null;
  
  protected onActivate() {
    this.viewer.setCursor('crosshair');
    this.showHelper();
  }
  
  protected onDeactivate() {
    this.viewer.setCursor('default');
    this.hideHelper();
    this.clear();
  }
  
  protected bindEvents() {
    this.viewer.domElement.addEventListener('click', this.onClick);
    this.viewer.domElement.addEventListener('mousemove', this.onMouseMove);
    document.addEventListener('keydown', this.onKeyDown);
  }
  
  protected unbindEvents() {
    this.viewer.domElement.removeEventListener('click', this.onClick);
    this.viewer.domElement.removeEventListener('mousemove', this.onMouseMove);
    document.removeEventListener('keydown', this.onKeyDown);
  }
  
  private onClick = (event: MouseEvent) => {
    const intersect = this.viewer.raycast(event.clientX, event.clientY);
    
    if (intersect) {
      this.addPoint(intersect.point);
    }
  };
  
  private onMouseMove = (event: MouseEvent) => {
    if (this.points.length === 0) return;
    
    const intersect = this.viewer.raycast(event.clientX, event.clientY);
    if (intersect) {
      this.updateTempLine(intersect.point);
    }
  };
  
  private onKeyDown = (event: KeyboardEvent) => {
    if (event.key === 'Escape') {
      this.finishMeasure();
    }
    if (event.key === 'Delete') {
      this.clear();
    }
  };
  
  private addPoint(point: THREE.Vector3) {
    this.points.push(point.clone());
    
    if (this.points.length > 1) {
      this.createSegment(
        this.points[this.points.length - 2],
        this.points[this.points.length - 1]
      );
    }
    
    this.createPointMarker(point);
  }
  
  private createSegment(start: THREE.Vector3, end: THREE.Vector3) {
    // 创建线段
    const geometry = new THREE.BufferGeometry().setFromPoints([start, end]);
    const material = new THREE.LineBasicMaterial({ color: 0xff0000 });
    const line = new THREE.Line(geometry, material);
    this.viewer.scene.add(line);
    this.lines.push(line);
    
    // 创建标签
    const distance = start.distanceTo(end);
    const midPoint = start.clone().add(end).multiplyScalar(0.5);
    this.createLabel(midPoint, `${distance.toFixed(2)} m`);
  }
  
  private createLabel(position: THREE.Vector3, text: string) {
    const label = document.createElement('div');
    label.className = 'measure-label';
    label.textContent = text;
    
    const screenPos = this.viewer.worldToScreen(position);
    label.style.left = `${screenPos.x}px`;
    label.style.top = `${screenPos.y}px`;
    
    this.viewer.domElement.parentElement?.appendChild(label);
    this.labels.push(label);
  }
  
  private createPointMarker(position: THREE.Vector3) {
    const geometry = new THREE.SphereGeometry(0.1);
    const material = new THREE.MeshBasicMaterial({ color: 0xff0000 });
    const marker = new THREE.Mesh(geometry, material);
    marker.position.copy(position);
    this.viewer.scene.add(marker);
  }
  
  private updateTempLine(endPoint: THREE.Vector3) {
    if (this.tempLine) {
      this.viewer.scene.remove(this.tempLine);
    }
    
    const startPoint = this.points[this.points.length - 1];
    const geometry = new THREE.BufferGeometry().setFromPoints([startPoint, endPoint]);
    const material = new THREE.LineDashedMaterial({
      color: 0xff0000,
      dashSize: 0.1,
      gapSize: 0.1
    });
    this.tempLine = new THREE.Line(geometry, material);
    this.tempLine.computeLineDistances();
    this.viewer.scene.add(this.tempLine);
  }
  
  private finishMeasure() {
    if (this.tempLine) {
      this.viewer.scene.remove(this.tempLine);
      this.tempLine = null;
    }
    this.points = [];
  }
  
  private clear() {
    // 清除线段
    this.lines.forEach(line => this.viewer.scene.remove(line));
    this.lines = [];
    
    // 清除标签
    this.labels.forEach(label => label.remove());
    this.labels = [];
    
    // 清除临时线
    if (this.tempLine) {
      this.viewer.scene.remove(this.tempLine);
      this.tempLine = null;
    }
    
    this.points = [];
  }
  
  private showHelper() {
    // 显示辅助信息
  }
  
  private hideHelper() {
    // 隐藏辅助信息
  }
  
  getTotalDistance(): number {
    let total = 0;
    for (let i = 1; i < this.points.length; i++) {
      total += this.points[i - 1].distanceTo(this.points[i]);
    }
    return total;
  }
}

8.6 插件通信

8.6.1 插件间通信

// 通过事件系统通信
// Plugin A
useDispatchSignal('pluginA:dataReady', { data: myData });

// Plugin B
useAddSignal('pluginA:dataReady', (payload) => {
  console.log('收到Plugin A的数据:', payload.data);
});

// 通过插件API通信
// 获取其他插件实例
const otherPlugin = this.app.plugins.get('other-plugin');
if (otherPlugin) {
  otherPlugin.someMethod();
}

// 插件服务注册
// Plugin A 注册服务
this.app.services.register('myService', {
  getData: () => this.data,
  setData: (data) => { this.data = data; }
});

// Plugin B 使用服务
const myService = this.app.services.get('myService');
const data = myService.getData();

8.6.2 与编辑器通信

// 发送消息到编辑器
this.app.postMessage({
  type: 'PLUGIN_EVENT',
  plugin: 'my-plugin',
  action: 'action-name',
  data: { ... }
});

// 接收编辑器消息
this.app.onMessage((message) => {
  if (message.type === 'EDITOR_EVENT') {
    // 处理编辑器消息
  }
});

8.7 插件配置与设置

8.7.1 配置Schema

// 定义配置Schema
const configSchema = {
  title: '插件设置',
  type: 'object',
  properties: {
    enabled: {
      type: 'boolean',
      title: '启用',
      default: true
    },
    color: {
      type: 'string',
      title: '颜色',
      format: 'color',
      default: '#ff0000'
    },
    size: {
      type: 'number',
      title: '大小',
      minimum: 0,
      maximum: 100,
      default: 50
    },
    mode: {
      type: 'string',
      title: '模式',
      enum: ['mode1', 'mode2', 'mode3'],
      enumNames: ['模式1', '模式2', '模式3'],
      default: 'mode1'
    }
  }
};

// 注册配置
this.app.settings.registerSchema('my-plugin', configSchema);

// 获取配置
const config = this.app.settings.get('my-plugin');

// 更新配置
this.app.settings.set('my-plugin', { color: '#00ff00' });

// 监听配置变化
this.app.settings.onChange('my-plugin', (newConfig, oldConfig) => {
  console.log('配置变化:', newConfig);
  this.applyConfig(newConfig);
});

8.8 打包与发布

8.8.1 构建配置

// vite.config.ts
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import dts from 'vite-plugin-dts';

export default defineConfig({
  plugins: [
    vue(),
    dts({
      include: ['src/**/*.ts', 'src/**/*.vue']
    })
  ],
  build: {
    lib: {
      entry: 'src/index.ts',
      name: 'MyPlugin',
      formats: ['es', 'cjs'],
      fileName: (format) => `index.${format === 'es' ? 'esm' : format}.js`
    },
    rollupOptions: {
      external: ['vue', 'three', '@astral3d/engine', 'naive-ui'],
      output: {
        globals: {
          vue: 'Vue',
          three: 'THREE',
          '@astral3d/engine': 'AstralEngine',
          'naive-ui': 'NaiveUI'
        }
      }
    }
  }
});

8.8.2 发布到NPM

# 构建
pnpm build

# 登录NPM
npm login

# 发布
npm publish --access public

8.8.3 本地安装测试

# 在Astral3D项目中安装本地插件
pnpm add ../path/to/my-plugin

# 或使用link
cd my-plugin
pnpm link

cd ../Astral3D
pnpm link @astral3d/plugin-my-feature

8.9 本章小结

本章详细介绍了Astral3D的插件系统开发,主要内容包括:

  1. 插件系统架构:管理器、生命周期、插件类型
  2. 创建插件项目:项目结构、清单配置、入口文件
  3. 插件API:Viewer、Scene、UI、Events、Storage API
  4. UI组件开发:Vue组件、工具栏、面板
  5. 自定义工具:工具基类、测量工具示例
  6. 插件通信:插件间通信、与编辑器通信
  7. 配置管理:配置Schema、设置面板
  8. 打包发布:构建配置、NPM发布

通过本章的学习,读者应该能够独立开发Astral3D插件,扩展编辑器的功能。


下一章预告:第九章将介绍Astral3D的脚本运行时开发,包括脚本API、热更新机制、调试工具等内容。


posted @ 2026-01-10 13:17  我才是银古  阅读(5)  评论(0)    收藏  举报