# Engine API 接口对接指南 本文档详细说明 iflow-engine SDK 中 Toolbar 按钮如何调用底层 3D 引擎 API 的完整调用链。 --- ## 目录 1. [架构概览](#架构概览) 2. [调用链层级](#调用链层级) 3. [完整调用链示例:剖切盒](#完整调用链示例剖切盒) 4. [各层职责说明](#各层职责说明) 5. [新增功能对接步骤](#新增功能对接步骤) 6. [数据流向](#数据流向) --- ## 架构概览 ``` ┌─────────────────────────────────────────────────────────────────────┐ │ 用户交互层 │ │ Toolbar Button (src/components/button-group/toolbar/buttons/*) │ └────────────────────────────────┬────────────────────────────────────┘ │ onClick → registry.xxxManager.show() ▼ ┌─────────────────────────────────────────────────────────────────────┐ │ 对话框管理层 (DialogManager) │ │ src/managers/*-dialog-manager.ts │ │ 继承自 BaseDialogManager,管理 UI 面板生命周期 │ └────────────────────────────────┬────────────────────────────────────┘ │ Panel 回调 → registry.engine3d.xxx() ▼ ┌─────────────────────────────────────────────────────────────────────┐ │ 引擎管理层 (EngineManager) │ │ src/managers/engine-manager.ts │ │ 封装 Engine 组件,提供统一 API │ └────────────────────────────────┬────────────────────────────────────┘ │ 调用 this.engineInstance.xxx() ▼ ┌─────────────────────────────────────────────────────────────────────┐ │ 引擎组件层 (Engine Component) │ │ src/components/engine/index.ts │ │ 封装底层 3D 引擎,处理状态管理和数据转换 │ └────────────────────────────────┬────────────────────────────────────┘ │ 调用 this.engine.xxx.xxx() ▼ ┌─────────────────────────────────────────────────────────────────────┐ │ 底层 3D 引擎 (iflow-engine-base) │ │ 第三方 SDK,通过 createEngine() 创建实例 │ └─────────────────────────────────────────────────────────────────────┘ ``` --- ## 调用链层级 | 层级 | 文件位置 | 职责 | |------|----------|------| | **L1: Toolbar Button** | `src/components/button-group/toolbar/buttons/` | 定义按钮配置,处理点击事件 | | **L2: DialogManager** | `src/managers/*-dialog-manager.ts` | 管理对话框/面板 UI,绑定回调 | | **L3: EngineManager** | `src/managers/engine-manager.ts` | 封装引擎组件,提供公共 API | | **L4: Engine Component** | `src/components/engine/index.ts` | 封装底层引擎,处理状态和转换 | | **L5: 底层引擎** | `iflow-engine-base` (npm 包) | 实际 3D 渲染和功能实现 | --- ## 完整调用链示例:剖切盒 以剖切盒(Section Box)功能为例,展示完整的调用链: ### 1. Toolbar Button 定义 **文件**: `src/components/button-group/toolbar/buttons/section/section-box/index.ts` ```typescript export const createSectionBoxButton = (): ButtonConfig => { return { id: 'section-box', groupId: 'group-1', parentId: 'section', // 父菜单 ID type: 'button', keepActive: true, // 保持激活状态 exclusive: true, // 互斥(同组只能激活一个) label: 'toolbar.sectionBox', // 国际化 key icon: getIcon('剖切盒'), onClick: (button) => { const registry = ManagerRegistry.getInstance(); if (button.isActive) { registry.sectionBox?.show(); // ← 调用 DialogManager } else { registry.sectionBox?.hide(); } } }; }; ``` **要点**: - 通过 `ManagerRegistry.getInstance()` 获取全局管理器实例 - `button.isActive` 表示当前激活状态 - 调用 `registry.sectionBox.show()` 打开对话框 ### 2. DialogManager 处理 **文件**: `src/managers/section-box-dialog-manager.ts` ```typescript export class SectionBoxDialogManager extends BaseDialogManager { private panel: SectionBoxPanel | null = null; // 创建对话框内容,绑定 Panel 回调 protected createContent(): HTMLElement { this.panel = new SectionBoxPanel({ onFitToModel: () => { this.registry.engine3d?.fitSectionBoxToModel(); // ← 调用 EngineManager }, onReset: () => { this.registry.engine3d?.resetSectionBox(); }, onRangeChange: (range) => { this.registry.engine3d?.setSectionBoxRange(range); } }); this.panel.init(); return this.panel.element; } // 对话框创建后,激活剖切盒 protected onDialogCreated(): void { this.registry.engine3d?.activateSectionBox(); // ← 调用 EngineManager this.dialog?.fitHeight(false); } // 对话框销毁前,停用剖切盒 protected onBeforeDestroy(): void { this.registry.engine3d?.deactivateSectionBox(); // ... 清理 } // 对话框关闭时,取消工具栏按钮激活 protected onDialogClose(): void { this.registry.toolbar?.setBtnActive('section-box', false); } } ``` **要点**: - 继承 `BaseDialogManager`,自动获得 `show()/hide()/toggle()` 方法 - 通过 `this.registry.engine3d` 访问 EngineManager - Panel 的回调函数中调用 EngineManager 方法 - 生命周期钩子:`onDialogCreated` / `onBeforeDestroy` / `onDialogClose` ### 3. EngineManager 封装 **文件**: `src/managers/engine-manager.ts` ```typescript export class EngineManager extends BaseManager { private engineInstance: Engine | null = null; // 激活剖切盒 public activateSectionBox(): void { if (!this.engineInstance) { console.warn('[EngineManager] 3D Engine not initialized.'); return; } this.engineInstance.activateSectionBox(); // ← 调用 Engine 组件 } // 设置剖切盒范围 public setSectionBoxRange(range: SectionBoxRange): void { if (!this.engineInstance) { console.warn('[EngineManager] 3D Engine not initialized.'); return; } this.engineInstance.setSectionBoxRange(range); } // ... 其他方法 } ``` **要点**: - 每个方法都要检查 `engineInstance` 是否存在 - 方法签名与 Engine 组件一致,起到代理作用 - 负责错误处理和日志输出 ### 4. Engine 组件实现 **文件**: `src/components/engine/index.ts` ```typescript export class Engine implements IBimComponent { private engine: any = null; // 底层引擎实例 private isSectionBoxActive: boolean = false; // 状态标记 private sectionBoxFullBounds: any = null; // 缓存的包围盒 // 激活剖切盒 public activateSectionBox(): void { if (!this._isInitialized || !this.engine) { console.error('[Engine] Cannot activate section box: engine not initialized.'); return; } if (!this.engine.clipping?.sectionBox) { console.error('[Engine] Section box module not available.'); return; } if (this.isSectionBoxActive) { return; // 幂等操作 } // 保存模型包围盒(用于百分比计算) this.sectionBoxFullBounds = this.engine.octreeBox?.getBoundingBox()?.clone(); // 调用底层 API this.engine.clipping.sectionBox.active(); // ← 底层引擎 API this.isSectionBoxActive = true; } // 设置剖切盒范围(百分比 → 坐标转换) public setSectionBoxRange(range: SectionBoxRange): void { if (!this.sectionBoxFullBounds) { console.error('[Engine] Cannot set section box range: full bounds not available.'); return; } const full = this.sectionBoxFullBounds; // 百分比转实际坐标 const toCoord = (percent: number, min: number, max: number): number => { return min + (max - min) * (percent / 100); }; const xyz = { minX: toCoord(range.x.min, full.min.x, full.max.x), maxX: toCoord(range.x.max, full.min.x, full.max.x), minY: toCoord(range.y.min, full.min.y, full.max.y), maxY: toCoord(range.y.max, full.min.y, full.max.y), minZ: toCoord(range.z.min, full.min.z, full.max.z), maxZ: toCoord(range.z.max, full.min.z, full.max.z), }; // 调用底层 API this.engine.clipping.sectionBox.setboxXyz(xyz); // ← 底层引擎 API } } ``` **要点**: - 维护功能状态(`isSectionBoxActive`) - 缓存必要数据(`sectionBoxFullBounds`) - 进行数据转换(百分比 → 坐标) - 调用底层引擎的实际 API ### 5. 底层引擎 API **来源**: `iflow-engine-base` (第三方 SDK) ```typescript // 剖切盒相关 API engine.clipping.sectionBox.active() // 激活剖切盒 engine.clipping.sectionBox.disActive() // 停用剖切盒 engine.clipping.sectionBox.getboxXyz() // 获取范围 { minX, maxX, minY, maxY, minZ, maxZ } engine.clipping.sectionBox.setboxXyz(xyz) // 设置范围 engine.clipping.sectionBox.setBox(box) // 设置 THREE.Box3 格式范围 engine.clipping.sectionBox.reverseBox() // 反向剖切(当前为空实现) // 获取模型包围盒 engine.octreeBox.getBoundingBox() // 返回 THREE.Box3 ``` --- ## 各层职责说明 ### L1: Toolbar Button | 职责 | 说明 | |------|------| | 定义按钮配置 | `id`, `label`, `icon`, `groupId`, `parentId` 等 | | 处理点击事件 | 在 `onClick` 中调用 DialogManager 的 `show()`/`hide()` | | 控制激活状态 | `keepActive`, `exclusive` 等属性控制按钮行为 | ### L2: DialogManager | 职责 | 说明 | |------|------| | 管理对话框生命周期 | `show()` / `hide()` / `toggle()` | | 创建 UI 面板 | 在 `createContent()` 中实例化 Panel 组件 | | 绑定回调函数 | 将 Panel 的事件回调连接到 EngineManager | | 处理生命周期钩子 | `onDialogCreated` / `onBeforeDestroy` / `onDialogClose` | | 同步工具栏状态 | 关闭时调用 `toolbar.setBtnActive(id, false)` | ### L3: EngineManager | 职责 | 说明 | |------|------| | 代理 Engine 组件 | 提供统一的公共 API | | 检查初始化状态 | 每个方法都检查 `engineInstance` 是否存在 | | 错误处理 | 输出警告日志 | | 暴露给 Registry | 通过 `registry.engine3d` 访问 | ### L4: Engine Component | 职责 | 说明 | |------|------| | 封装底层引擎 | 隔离第三方 SDK 的具体实现 | | 状态管理 | 维护 `isSectionBoxActive` 等状态标记 | | 数据转换 | UI 层数据格式 ↔ 底层引擎数据格式 | | 缓存数据 | 保存 `sectionBoxFullBounds` 等中间数据 | | 幂等操作 | 防止重复激活/停用 | ### L5: 底层引擎 | 职责 | 说明 | |------|------| | 3D 渲染 | WebGL/Three.js 渲染 | | 功能实现 | 测量、剖切、漫游等实际功能 | | 场景管理 | 模型加载、相机控制等 | --- ## 新增功能对接步骤 以新增一个 "XX 功能" 为例: ### Step 1: 创建 Toolbar Button **文件**: `src/components/button-group/toolbar/buttons/xx/index.ts` ```typescript import type { ButtonConfig } from '../../../index.type'; import { getIcon } from '../../../../../utils/icon-manager'; import { ManagerRegistry } from '../../../../../core/manager-registry'; export const createXxButton = (): ButtonConfig => { return { id: 'xx-feature', groupId: 'group-1', type: 'button', keepActive: true, label: 'toolbar.xxFeature', icon: getIcon('XX图标'), onClick: (button) => { const registry = ManagerRegistry.getInstance(); if (button.isActive) { registry.xxFeature?.show(); } else { registry.xxFeature?.hide(); } } }; }; ``` ### Step 2: 注册按钮到 Toolbar **文件**: `src/components/button-group/toolbar/index.ts` ```typescript // 添加导入 const { createXxButton } = await import('./buttons/xx'); // 添加按钮 this.addButton(createXxButton()); ``` ### Step 3: 创建 DialogManager **文件**: `src/managers/xx-dialog-manager.ts` ```typescript import { BaseDialogManager } from '../core/base-dialog-manager'; import { XxPanel } from '../components/xx-panel'; export class XxDialogManager extends BaseDialogManager { private panel: XxPanel | null = null; protected get dialogId(): string { return 'xx-dialog'; } protected get dialogTitle(): string { return 'xxFeature.dialogTitle'; } protected createContent(): HTMLElement { this.panel = new XxPanel({ onSomeAction: () => { this.registry.engine3d?.doSomething(); } }); this.panel.init(); return this.panel.element; } protected onDialogCreated(): void { this.registry.engine3d?.activateXx(); } protected onBeforeDestroy(): void { this.registry.engine3d?.deactivateXx(); this.panel?.destroy(); this.panel = null; } protected onDialogClose(): void { this.registry.toolbar?.setBtnActive('xx-feature', false); } } ``` ### Step 4: 注册到 ManagerRegistry **文件**: `src/core/manager-registry.ts` ```typescript // 添加类型导入 import type { XxDialogManager } from '../managers/xx-dialog-manager'; // 添加属性 public xxFeature: XxDialogManager | null = null; ``` ### Step 5: 在 BimEngine 中初始化 **文件**: `src/bim-engine.ts` ```typescript // 添加导入 import { XxDialogManager } from './managers/xx-dialog-manager'; // 添加属性 public xxFeature: XxDialogManager | null = null; // 在 init() 中初始化 this.xxFeature = new XxDialogManager(); this.registry.xxFeature = this.xxFeature; // 在 destroy() 中销毁 this.xxFeature?.destroy(); ``` ### Step 6: 在 EngineManager 中添加方法 **文件**: `src/managers/engine-manager.ts` ```typescript public activateXx(): void { if (!this.engineInstance) { console.warn('[EngineManager] 3D Engine not initialized.'); return; } this.engineInstance.activateXx(); } public deactivateXx(): void { if (!this.engineInstance) { return; } this.engineInstance.deactivateXx(); } public doSomething(): void { if (!this.engineInstance) { console.warn('[EngineManager] 3D Engine not initialized.'); return; } this.engineInstance.doSomething(); } ``` ### Step 7: 在 Engine 组件中实现 **文件**: `src/components/engine/index.ts` ```typescript private isXxActive: boolean = false; public activateXx(): void { if (!this._isInitialized || !this.engine) { console.error('[Engine] Cannot activate XX: engine not initialized.'); return; } if (this.isXxActive) { return; } // 调用底层 API this.engine.xxModule.active(); this.isXxActive = true; } public deactivateXx(): void { if (!this._isInitialized || !this.engine?.xxModule) { return; } if (!this.isXxActive) { return; } this.engine.xxModule.disActive(); this.isXxActive = false; } public doSomething(): void { if (!this._isInitialized || !this.engine?.xxModule) { return; } this.engine.xxModule.doSomething(); } ``` --- ## 数据流向 ### 用户操作 → 底层引擎 ``` 用户点击按钮 ↓ Button.onClick(button) ↓ registry.xxxManager.show() ↓ BaseDialogManager.show() ↓ createContent() → 创建 Panel ↓ onDialogCreated() → registry.engine3d.activateXxx() ↓ EngineManager.activateXxx() ↓ Engine.activateXxx() ↓ this.engine.xxxModule.active() ← 底层引擎 API ``` ### UI 面板操作 → 底层引擎 ``` 用户操作 Panel(拖动滑块、点击按钮等) ↓ Panel 回调触发 onXxxChange(data) ↓ DialogManager 中的回调:registry.engine3d.setXxx(data) ↓ EngineManager.setXxx(data) ↓ Engine.setXxx(data) ↓ 数据转换(如百分比 → 坐标) ↓ this.engine.xxxModule.setXxx(convertedData) ← 底层引擎 API ``` ### 关闭对话框 ``` 用户点击关闭按钮 / 再次点击工具栏按钮 ↓ BimDialog.onClose() / Button.onClick() ↓ BaseDialogManager.hide() / registry.xxxManager.hide() ↓ destroyDialog() ↓ onBeforeDestroy() → registry.engine3d.deactivateXxx() ↓ EngineManager.deactivateXxx() ↓ Engine.deactivateXxx() ↓ this.engine.xxxModule.disActive() ← 底层引擎 API ↓ onDialogClose() → registry.toolbar.setBtnActive('xxx', false) ``` --- ## 常见问题 ### Q1: 如何确保激活/停用的幂等性? 在 Engine 组件中维护状态标记,在方法开头检查: ```typescript public activateSectionBox(): void { // ... if (this.isSectionBoxActive) { console.log('[Engine] Section box already active, skipping.'); return; // 幂等:已激活则跳过 } // ... } ``` ### Q2: 如何处理底层 API 不支持的功能? 在 DialogManager 的回调中输出日志说明: ```typescript onReverseToggle: (isReversed) => { // 底层暂不支持反向功能 console.log('[SectionBoxDialogManager] 反向切换(底层暂不支持):', isReversed); } ``` ### Q3: 如何进行数据格式转换? 在 Engine 组件中进行,不要在 DialogManager 或 EngineManager 中: ```typescript // Engine 组件中 public setSectionBoxRange(range: SectionBoxRange): void { // UI 层:百分比 (0-100) // 底层:实际坐标 const toCoord = (percent: number, min: number, max: number): number => { return min + (max - min) * (percent / 100); }; const xyz = { minX: toCoord(range.x.min, full.min.x, full.max.x), // ... }; this.engine.clipping.sectionBox.setboxXyz(xyz); } ``` ### Q4: 如何访问其他 Manager? 通过 `ManagerRegistry` 单例访问: ```typescript // 在 Button 中 const registry = ManagerRegistry.getInstance(); registry.sectionBox?.show(); // 在 DialogManager 中(继承自 BaseManager,自动获得 this.registry) this.registry.engine3d?.activateSectionBox(); this.registry.toolbar?.setBtnActive('section-box', false); ``` --- ## 总结 | 层级 | 关注点 | |------|--------| | **Button** | 按钮配置、点击事件、调用 DialogManager | | **DialogManager** | UI 生命周期、Panel 回调绑定、调用 EngineManager | | **EngineManager** | API 代理、初始化检查、错误处理 | | **Engine** | 状态管理、数据转换、调用底层 API | | **底层引擎** | 实际功能实现 | 遵循这个分层架构,可以保持代码的清晰性和可维护性。