feat(registry): 注册 ComponentDetailManager 到全局 Registry 和 BimEngine

This commit is contained in:
yuding
2026-01-28 12:00:55 +08:00
parent 33f1c72791
commit 89789e003b
87 changed files with 37063 additions and 24064 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,664 @@
# 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 |
| **底层引擎** | 实际功能实现 |
遵循这个分层架构,可以保持代码的清晰性和可维护性。

1504
docs/ENGINNE_3D/API文档.md Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,822 +0,0 @@
# iFlow Engine 使用文档
iFlow Engine 是一个用于 3D BIM 模型展示的 JavaScript SDK支持原生 HTML、Vue 2/3 和 React 框架。
## 目录
- [安装](#安装)
- [原生 HTML 使用](#原生-html-使用)
- [Vue 3 使用](#vue-3-使用)
- [Vue 2 使用](#vue-2-使用)
- [React 使用](#react-使用)
- [API 参考](#api-参考)
- [类型定义](#类型定义)
---
## 安装
### NPM 安装(推荐)
```bash
npm install iflow-engine
```
### Yarn 安装
```bash
yarn add iflow-engine
```
### PNPM 安装
```bash
pnpm add iflow-engine
```
### CDN / 本地文件引入
如果不使用包管理器,可以直接下载 JS 文件引入:
```html
<script src="./lib/iflow-engine.umd.js"></script>
```
---
## 原生 HTML 使用
### 方式一:使用 NPM + 构建工具
```html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>BIM Engine Demo</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 100%; height: 100%; overflow: hidden; }
#app { width: 100%; height: 100%; }
</style>
</head>
<body>
<div id="app"></div>
<script type="module">
import { BimEngine } from 'iflow-engine';
// 1. 创建引擎实例
const engine = new BimEngine('app', {
locale: 'zh-CN',
theme: 'light'
});
// 2. 初始化 3D 引擎
const success = engine.engine.initialize({
version: 'v2',
showStats: false,
showViewCube: true
});
if (success) {
// 3. 加载模型
engine.engine.loadModel('./model/your-model', {
position: [0, 0, 0],
rotation: [0, 0, 0],
scale: [1, 1, 1]
});
}
</script>
</body>
</html>
```
### 方式二:使用 UMD无构建工具
```html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>BIM Engine Demo</title>
<!-- 引入 UMD 版本 -->
<script src="https://unpkg.com/iflow-engine/dist/iflow-engine.umd.js"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 100%; height: 100%; overflow: hidden; }
#app { width: 100%; height: 100%; }
</style>
</head>
<body>
<div id="app"></div>
<script>
// 1. 创建引擎实例
const engine = new LyzBimEngineSDK.BimEngine('app', {
locale: 'zh-CN',
theme: 'light'
});
// 2. 初始化 3D 引擎
const success = engine.engine.initialize({
version: 'v2',
showStats: false,
showViewCube: true
});
if (success) {
// 3. 加载模型
engine.engine.loadModel('./model/your-model', {
position: [0, 0, 0],
rotation: [0, 0, 0],
scale: [1, 1, 1]
});
}
</script>
</body>
</html>
```
### 完整功能示例
```javascript
let engine = null;
// 初始化
function init() {
engine = new LyzBimEngineSDK.BimEngine('app', {
locale: 'zh-CN',
theme: 'light'
});
engine.engine.initialize({
version: 'v2',
showStats: false,
showViewCube: true
});
engine.engine.loadModel('./model/building');
}
// 回到主视角
function goHome() {
engine.engine.CameraGoHome();
}
// 激活测量功能
// mode: 'distance' | 'minDistance' | 'angle' | 'elevation' | 'volume' | 'laserDistance' | 'slope' | 'spaceVolume'
function activateMeasure(mode) {
engine.engine.activateMeasure(mode);
}
// 停用测量
function deactivateMeasure() {
engine.engine.deactivateMeasure();
}
// 切换主题
function setTheme(theme) {
engine.setTheme(theme); // 'light' | 'dark'
}
// 切换语言
function setLocale(locale) {
engine.setLocale(locale); // 'zh-CN' | 'en-US'
}
// 销毁
function destroy() {
engine.destroy();
}
window.onload = init;
```
---
## Vue 3 使用
### 1. 安装
```bash
npm install iflow-engine
```
### 2. 组件封装
```vue
<!-- components/BimViewer.vue -->
<template>
<div ref="containerRef" class="bim-viewer"></div>
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';
import { BimEngine } from 'iflow-engine';
// Props
const props = defineProps({
modelUrl: { type: String, default: '' },
modelOptions: { type: Object, default: () => ({}) },
engineOptions: { type: Object, default: () => ({}) },
locale: { type: String, default: 'zh-CN' },
theme: { type: String, default: 'light' }
});
// Emits
const emit = defineEmits(['ready', 'error']);
// Refs
const containerRef = ref(null);
let engine = null;
// 初始化引擎
const initEngine = () => {
if (!containerRef.value) return;
try {
engine = new BimEngine(containerRef.value, {
locale: props.locale,
theme: props.theme
});
const success = engine.engine?.initialize({
version: 'v2',
showStats: false,
showViewCube: true,
...props.engineOptions
});
if (success) {
emit('ready', engine);
if (props.modelUrl) {
engine.engine?.loadModel(props.modelUrl, props.modelOptions);
}
} else {
throw new Error('3D 引擎初始化失败');
}
} catch (error) {
emit('error', error);
}
};
// 暴露方法
defineExpose({
getEngine: () => engine,
loadModel: (url, options) => engine?.engine?.loadModel(url, options),
goHome: () => engine?.engine?.CameraGoHome(),
activateMeasure: (mode) => engine?.engine?.activateMeasure(mode),
deactivateMeasure: () => engine?.engine?.deactivateMeasure(),
setTheme: (theme) => engine?.setTheme(theme),
setLocale: (locale) => engine?.setLocale(locale)
});
onMounted(() => initEngine());
onBeforeUnmount(() => {
engine?.destroy();
engine = null;
});
</script>
<style scoped>
.bim-viewer {
width: 100%;
height: 100%;
}
</style>
```
### 3. 使用组件
```vue
<!-- App.vue -->
<template>
<div class="app">
<BimViewer
ref="viewerRef"
model-url="./model/building"
:model-options="{ position: [0, 0, 0] }"
locale="zh-CN"
theme="light"
@ready="onEngineReady"
@error="onEngineError"
/>
<div class="toolbar">
<button @click="goHome">主视角</button>
<button @click="measure('distance')">距离测量</button>
<button @click="stopMeasure">停止测量</button>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
import BimViewer from './components/BimViewer.vue';
const viewerRef = ref(null);
const onEngineReady = (engine) => {
console.log('引擎已就绪', engine);
};
const onEngineError = (error) => {
console.error('引擎错误', error);
};
const goHome = () => viewerRef.value?.goHome();
const measure = (mode) => viewerRef.value?.activateMeasure(mode);
const stopMeasure = () => viewerRef.value?.deactivateMeasure();
</script>
<style>
.app {
width: 100vw;
height: 100vh;
position: relative;
}
.toolbar {
position: absolute;
top: 20px;
left: 20px;
z-index: 100;
}
</style>
```
---
## Vue 2 使用
### 1. 安装
```bash
npm install iflow-engine
```
### 2. 组件封装
```vue
<!-- components/BimViewer.vue -->
<template>
<div ref="container" class="bim-viewer"></div>
</template>
<script>
import { BimEngine } from 'iflow-engine';
export default {
name: 'BimViewer',
props: {
modelUrl: { type: String, default: '' },
modelOptions: { type: Object, default: () => ({}) },
engineOptions: { type: Object, default: () => ({}) },
locale: { type: String, default: 'zh-CN' },
theme: { type: String, default: 'light' }
},
data() {
return {
engine: null
};
},
mounted() {
this.initEngine();
},
beforeDestroy() {
this.destroyEngine();
},
methods: {
initEngine() {
try {
this.engine = new BimEngine(this.$refs.container, {
locale: this.locale,
theme: this.theme
});
const success = this.engine.engine.initialize({
version: 'v2',
showStats: false,
showViewCube: true,
...this.engineOptions
});
if (success) {
this.$emit('ready', this.engine);
if (this.modelUrl) {
this.engine.engine.loadModel(this.modelUrl, this.modelOptions);
}
} else {
throw new Error('3D 引擎初始化失败');
}
} catch (error) {
this.$emit('error', error);
}
},
destroyEngine() {
if (this.engine) {
this.engine.destroy();
this.engine = null;
}
},
// 公开方法
getEngine() { return this.engine; },
loadModel(url, options) { this.engine?.engine?.loadModel(url, options); },
goHome() { this.engine?.engine?.CameraGoHome(); },
activateMeasure(mode) { this.engine?.engine?.activateMeasure(mode); },
deactivateMeasure() { this.engine?.engine?.deactivateMeasure(); },
setTheme(theme) { this.engine?.setTheme(theme); },
setLocale(locale) { this.engine?.setLocale(locale); }
}
};
</script>
<style scoped>
.bim-viewer {
width: 100%;
height: 100%;
}
</style>
```
### 3. 使用组件
```vue
<!-- App.vue -->
<template>
<div class="app">
<BimViewer
ref="viewer"
model-url="./model/building"
:model-options="{ position: [0, 0, 0] }"
locale="zh-CN"
theme="light"
@ready="onEngineReady"
@error="onEngineError"
/>
<div class="toolbar">
<button @click="goHome">主视角</button>
<button @click="measure('distance')">距离测量</button>
<button @click="stopMeasure">停止测量</button>
</div>
</div>
</template>
<script>
import BimViewer from './components/BimViewer.vue';
export default {
name: 'App',
components: { BimViewer },
methods: {
onEngineReady(engine) { console.log('引擎已就绪', engine); },
onEngineError(error) { console.error('引擎错误', error); },
goHome() { this.$refs.viewer.goHome(); },
measure(mode) { this.$refs.viewer.activateMeasure(mode); },
stopMeasure() { this.$refs.viewer.deactivateMeasure(); }
}
};
</script>
<style>
.app { width: 100vw; height: 100vh; position: relative; }
.toolbar { position: absolute; top: 20px; left: 20px; z-index: 100; }
</style>
```
---
## React 使用
### 1. 安装
```bash
npm install iflow-engine
```
### 2. Hook 封装
```jsx
// hooks/useBimEngine.js
import { useEffect, useRef, useState, useCallback } from 'react';
import { BimEngine } from 'iflow-engine';
export function useBimEngine(options = {}) {
const containerRef = useRef(null);
const engineRef = useRef(null);
const [isReady, setIsReady] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
if (!containerRef.current) return;
try {
const engine = new BimEngine(containerRef.current, {
locale: options.locale || 'zh-CN',
theme: options.theme || 'light'
});
const success = engine.engine?.initialize({
version: 'v2',
showStats: false,
showViewCube: true,
...options.engineOptions
});
if (success) {
engineRef.current = engine;
setIsReady(true);
options.onReady?.(engine);
} else {
throw new Error('3D 引擎初始化失败');
}
} catch (err) {
setError(err);
options.onError?.(err);
}
return () => {
engineRef.current?.destroy();
engineRef.current = null;
setIsReady(false);
};
}, []);
const loadModel = useCallback((url, modelOptions) => {
engineRef.current?.engine?.loadModel(url, modelOptions);
}, []);
const goHome = useCallback(() => {
engineRef.current?.engine?.CameraGoHome();
}, []);
const activateMeasure = useCallback((mode) => {
engineRef.current?.engine?.activateMeasure(mode);
}, []);
const deactivateMeasure = useCallback(() => {
engineRef.current?.engine?.deactivateMeasure();
}, []);
const setTheme = useCallback((theme) => {
engineRef.current?.setTheme(theme);
}, []);
const setLocale = useCallback((locale) => {
engineRef.current?.setLocale(locale);
}, []);
return {
containerRef,
engine: engineRef.current,
isReady,
error,
loadModel,
goHome,
activateMeasure,
deactivateMeasure,
setTheme,
setLocale
};
}
```
### 3. 组件封装
```jsx
// components/BimViewer.jsx
import React, { useEffect, useImperativeHandle, forwardRef } from 'react';
import { useBimEngine } from '../hooks/useBimEngine';
export const BimViewer = forwardRef((props, ref) => {
const {
modelUrl,
modelOptions,
locale = 'zh-CN',
theme = 'light',
className,
style,
onReady,
onError
} = props;
const {
containerRef,
engine,
isReady,
loadModel,
goHome,
activateMeasure,
deactivateMeasure,
setTheme,
setLocale
} = useBimEngine({ locale, theme, onReady, onError });
useImperativeHandle(ref, () => ({
getEngine: () => engine,
loadModel,
goHome,
activateMeasure,
deactivateMeasure,
setTheme,
setLocale
}));
useEffect(() => {
if (isReady && modelUrl) {
loadModel(modelUrl, modelOptions);
}
}, [isReady, modelUrl, modelOptions, loadModel]);
return (
<div
ref={containerRef}
className={className}
style={{ width: '100%', height: '100%', ...style }}
/>
);
});
BimViewer.displayName = 'BimViewer';
```
### 4. 使用组件
```jsx
// App.jsx
import React, { useRef } from 'react';
import { BimViewer } from './components/BimViewer';
function App() {
const viewerRef = useRef(null);
const handleReady = (engine) => console.log('引擎已就绪', engine);
const handleError = (error) => console.error('引擎错误', error);
return (
<div style={{ width: '100vw', height: '100vh', position: 'relative' }}>
<BimViewer
ref={viewerRef}
modelUrl="./model/building"
modelOptions={{ position: [0, 0, 0] }}
locale="zh-CN"
theme="light"
onReady={handleReady}
onError={handleError}
/>
<div style={{ position: 'absolute', top: 20, left: 20, zIndex: 100 }}>
<button onClick={() => viewerRef.current?.goHome()}>主视角</button>
<button onClick={() => viewerRef.current?.activateMeasure('distance')}>距离测量</button>
<button onClick={() => viewerRef.current?.deactivateMeasure()}>停止测量</button>
</div>
</div>
);
}
export default App;
```
---
## API 参考
### BimEngine
主引擎类。
#### 构造函数
```javascript
import { BimEngine } from 'iflow-engine';
new BimEngine(container, options)
```
| 参数 | 类型 | 说明 |
|------|------|------|
| container | HTMLElement \| string | 容器元素或容器 ID |
| options.locale | string | 语言:'zh-CN' \| 'en-US',默认 'zh-CN' |
| options.theme | string | 主题:'light' \| 'dark',默认 'light' |
#### 属性
| 属性 | 类型 | 说明 |
|------|------|------|
| engine | EngineManager | 3D 引擎管理器 |
| toolbar | ToolbarManager | 工具栏管理器 |
| dialog | DialogManager | 弹窗管理器 |
| measure | MeasureDialogManager | 测量面板管理器 |
#### 方法
| 方法 | 参数 | 说明 |
|------|------|------|
| setLocale(locale) | 'zh-CN' \| 'en-US' | 设置语言 |
| getLocale() | - | 获取当前语言 |
| setTheme(theme) | 'light' \| 'dark' | 设置主题 |
| destroy() | - | 销毁引擎 |
---
### EngineManager
3D 引擎管理器,通过 `engine.engine` 访问。
#### 方法
| 方法 | 参数 | 返回值 | 说明 |
|------|------|--------|------|
| initialize(options) | EngineOptions | boolean | 初始化 3D 引擎 |
| isInitialized() | - | boolean | 检查是否已初始化 |
| loadModel(url, options) | string, ModelLoadOptions | void | 加载模型 |
| CameraGoHome() | - | void | 回到主视角 |
| activateMeasure(mode) | MeasureMode | void | 激活测量功能 |
| deactivateMeasure() | - | void | 停用测量功能 |
| getCurrentMeasureType() | - | MeasureMode \| null | 获取当前测量类型 |
| getEngine() | - | any | 获取原始引擎实例 |
| destroy() | - | void | 销毁引擎 |
---
## 类型定义
### EngineOptions
```typescript
interface EngineOptions {
version?: 'v1' | 'v2'; // WebGL 版本,默认 'v2'
showStats?: boolean; // 是否显示性能统计,默认 false
showViewCube?: boolean; // 是否显示视图立方体,默认 true
}
```
### ModelLoadOptions
```typescript
interface ModelLoadOptions {
position?: [number, number, number]; // 位置,默认 [0, 0, 0]
rotation?: [number, number, number]; // 旋转(弧度),默认 [0, 0, 0]
scale?: [number, number, number]; // 缩放,默认 [1, 1, 1]
id?: string; // 模型 ID可选
}
```
### MeasureMode
```typescript
type MeasureMode =
| 'distance' // 距离测量
| 'minDistance' // 最小距离测量
| 'angle' // 角度测量
| 'elevation' // 标高测量
| 'volume' // 体积测量
| 'laserDistance' // 激光测距
| 'slope' // 坡度测量
| 'spaceVolume'; // 空间体积测量
```
---
## 注意事项
1. **容器尺寸**:确保容器元素有明确的宽度和高度
2. **销毁清理**:页面关闭或组件卸载前务必调用 `destroy()`
3. **浏览器兼容**:需要支持 WebGL 的现代浏览器
---
## 常见问题
### Q: 模型加载失败?
检查:
- 模型路径是否正确
- 网络请求是否正常F12 查看 Network
- 模型格式是否支持
### Q: 如何监听引擎事件?
```javascript
engine.on('modelLoaded', (data) => {
console.log('模型已加载', data);
});
```
---
## 技术支持
- NPM 包地址https://www.npmjs.com/package/iflow-engine
- 如有问题,请联系技术支持

View File

@@ -1,616 +0,0 @@
# Engine 组件详细文档
> 本文档详细描述 Engine 组件的实现细节,包括 API、UI 结构、逻辑流程等,供 AI 根据文档重现组件。
---
## 1. 组件概述
### 1.1 基本信息
- **组件名称**: `Engine`
- **文件路径**: `src/components/engine/index.ts`
- **类型定义**: `src/components/engine/types.ts`
- **实现接口**: `IBimComponent`
- **用途**: 封装第三方 3D 引擎 SDK提供统一的 3D 引擎管理接口
### 1.2 在 SDK 中的位置
- Engine 组件是独立的 3D 引擎组件
- 必须通过 `EngineManager` 使用,不允许直接使用
- `EngineManager` 位于 `src/managers/engine-manager.ts`
- 采用延迟初始化模式,需要用户主动调用 `init()` 方法
### 1.3 第三方 SDK 依赖
- 依赖 `iflow-engine-base` npm 包中的 `createEngine` 函数
- 通过依赖注入方式使用,不直接导入
---
## 2. 组件类 API 文档
### 2.1 构造函数
```typescript
constructor(options: EngineOptions)
```
**参数**:
- `options`: `EngineOptions` - 3D 引擎配置选项
**默认配置**:
```typescript
{
backgroundColor: 0x1a1a1a, // 默认深色背景
version: 'v1', // 默认使用 v1 版本
showStats: false, // 默认不显示统计
showViewCube: true // 默认显示视图立方体
}
```
**行为**:
- 解析容器元素
- 如果容器没有 id生成一个唯一的 id
- 保存配置选项(设置默认值)
### 2.2 公共方法
#### `init(): void`
初始化组件(实现 `IBimComponent` 接口)
**功能**:
- 检查是否已初始化或已销毁
- 创建引擎配置对象
- 调用 `createEngineSDK()` 创建引擎实例
- 标记为已初始化
- 订阅主题变化
- 应用当前主题
**错误处理**:
- 如果创建失败,抛出错误并标记为未初始化
#### `setTheme(theme: ThemeConfig): void`
设置主题(实现 `IBimComponent` 接口)
**参数**:
- `theme`: `ThemeConfig` - 全局主题配置
**功能**:
- 根据主题名称选择背景色:
- `dark`: 使用深色背景 `0x1a1a1a`
- `light`: 使用浅色背景 `0xf5f5f5`
- `custom`: 使用配置中的 `backgroundColor` 或默认值
- 尝试更新引擎背景色:
- 如果引擎有 `setBackgroundColor()` 方法,调用它
- 否则,如果引擎有 `scene.background`,设置其颜色
#### `setLocales(): void`
设置语言(实现 `IBimComponent` 接口)
**功能**:
- 3D 引擎组件暂时不需要本地化
- 方法为空实现
#### `isInitialized(): boolean`
检查是否已初始化
**返回**: `boolean` - 是否已初始化
#### `loadModel(url: string, options?: ModelLoadOptions): void`
加载 3D 模型
**参数**:
- `url`: `string` - 模型文件 URL必需
- `options`: `ModelLoadOptions` (可选) - 加载选项
**功能**:
- 检查引擎是否已初始化
- 检查 URL 是否提供
- 调用引擎的 `loader.loadModel()` 方法加载模型
**加载选项**:
- `position`: `[number, number, number]` - 模型位置 [x, y, z]
- `rotation`: `[number, number, number]` - 模型旋转(弧度)[x, y, z]
- `scale`: `[number, number, number]` - 模型缩放 [x, y, z]
- `id`: `string` - 模型 ID可选
#### `getEngine(): any`
获取原始 3D 引擎实例
**返回**: 第三方 3D 引擎实例或 `null`
**功能**:
- 返回底层 3D 引擎实例
- 用于直接调用第三方引擎的其他 API
#### `destroy(): void`
销毁组件(实现 `IBimComponent` 接口)
**功能**:
- 取消主题订阅
- 清空容器内容
- 标记为已销毁和未初始化
**注意**: 不销毁引擎实例本身(由第三方 SDK 管理)
---
## 3. 分化组件说明
**Engine 组件没有分化组件**
---
## 4. Manager API 文档
### 4.1 EngineManager 类
**文件路径**: `src/managers/engine-manager.ts`
**继承关系**: `EngineManager extends BimComponent`
### 4.2 构造函数
```typescript
constructor(engine: BimEngine, container: HTMLElement)
```
**参数**:
- `engine`: `BimEngine` - 引擎实例
- `container`: `HTMLElement` - 3D 引擎挂载的目标容器
**行为**:
- 保存容器引用
- **不自动初始化引擎**(延迟初始化模式)
### 4.3 公共方法
#### `initialize(options?: Omit<EngineOptions, 'container'>): boolean`
初始化 3D 引擎
**参数**:
- `options`: `Omit<EngineOptions, 'container'>` (可选) - 引擎配置选项
**返回**: `boolean` - 是否初始化成功
**功能**:
- 如果已经初始化,先销毁旧的实例
- 创建 `Engine` 组件实例
- 调用组件的 `init()` 方法初始化引擎
- 返回初始化结果
**错误处理**:
- 如果初始化失败,返回 `false` 并输出错误信息
#### `isInitialized(): boolean`
检查 3D 引擎是否已初始化
**返回**: `boolean` - 是否已初始化
#### `loadModel(url: string, options?: ModelLoadOptions): void`
加载 3D 模型
**参数**:
- `url`: `string` - 模型文件 URL
- `options`: `ModelLoadOptions` (可选) - 加载选项
**功能**:
- 检查引擎是否已初始化
- 如果未初始化,输出错误信息
- 调用引擎组件的 `loadModel()` 方法
#### `getEngine(): any`
获取原始 3D 引擎实例
**返回**: 第三方 3D 引擎实例或 `null`
**功能**:
- 用于直接调用第三方引擎的其他 API
- 如果引擎未初始化,返回 `null` 并输出警告
#### `destroy(): void`
销毁 3D 引擎实例
**功能**:
- 调用引擎组件的 `destroy()` 方法
- 清空引擎实例引用
---
## 5. UI 详细描述
### 5.1 DOM 结构
**Engine 组件不直接创建 UI 元素**,而是:
1. **使用容器元素**:
- 容器元素由外部提供(通过 `EngineOptions.container`
- 如果容器没有 id组件会生成一个唯一的 id
2. **第三方 SDK 创建 UI**:
- 第三方 SDK 的 `createEngine()` 函数会在容器内创建 3D 场景
- 可能包括:
- Canvas 元素WebGL 渲染)
- 统计面板(如果 `showStats: true`
- 视图立方体(如果 `showViewCube: true`
### 5.2 容器 ID 生成
**算法**:
```typescript
containerId = `engine-container-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
```
**格式**: `engine-container-[时间戳]-[随机字符串]`
**示例**: `engine-container-1704067200000-k3j9x2m1p`
### 5.3 第三方 SDK 配置
**传递给 `createEngine()` 的配置对象**:
```typescript
{
containerId: string, // 容器 ID
backgroundColor: number, // 背景色(十六进制数字)
version: 'v1' | 'v2', // WebGL 版本
showStats: boolean, // 是否显示统计
showViewCube: boolean // 是否显示视图立方体
}
```
---
## 6. 逻辑流程详细描述
### 6.1 初始化流程
1. **构造函数调用**:
- 解析容器元素
- 生成或使用容器 ID
- 保存配置选项(设置默认值)
2. **init() 方法执行**(用户主动调用):
- 检查是否已初始化,如果是则返回
- 检查是否已销毁,如果是则报错
- 创建引擎配置对象
- 调用 `createEngineSDK()` 创建引擎实例
- 检查引擎实例是否创建成功
- 标记为已初始化
- 订阅主题变化:`themeManager.subscribe()`
- 应用当前主题
### 6.2 生命周期
#### 创建阶段
- 构造函数 → 保存配置
#### 初始化阶段(延迟)
- 用户调用 `init()` → 创建引擎实例 → 订阅主题
#### 运行阶段
- 响应主题变更:`setTheme()` 被调用
- 加载模型:`loadModel()` 被调用
- 用户交互3D 场景交互(由第三方 SDK 处理)
#### 销毁阶段
- `destroy()` 被调用
- 取消主题订阅
- 清空容器
- 更新状态
### 6.3 主题变更流程
1. 主题变更 → `themeManager` 通知订阅者
2. `setTheme()` 方法被调用
3. 根据主题名称选择背景色:
- `dark``0x1a1a1a`
- `light``0xf5f5f5`
- `custom` → 使用配置值或默认值
4. 尝试更新引擎背景色:
- 方法 1: 调用 `engine.setBackgroundColor(backgroundColor)`
- 方法 2: 设置 `engine.scene.background.setHex(backgroundColor)`
### 6.4 模型加载流程
1. 用户调用 `loadModel(url, options)`
2. 检查引擎是否已初始化
3. 检查 URL 是否提供
4. 调用 `engine.loader.loadModel(url, options)`
5. 第三方 SDK 处理模型加载
### 6.5 状态管理
#### 内部状态
- `_isInitialized`: 是否已初始化
- `_isDestroyed`: 是否已销毁
- `engine`: 第三方 3D 引擎实例
- `containerId`: 容器 ID
- `options`: 引擎配置选项
#### 订阅管理
- `unsubscribeTheme`: 主题订阅取消函数
### 6.6 与其他组件的交互
- **与 EngineManager**: 通过 Manager 创建和管理
- **与 ThemeManager**: 订阅主题变更
- **与第三方 SDK**: 通过 `createEngine()` 函数创建引擎实例
---
## 7. 国际化支持
### 7.1 使用的翻译键
**Engine 组件不使用翻译键**3D 引擎不需要国际化)。
### 7.2 语言变更处理
- `setLocales()` 方法为空实现
- 不订阅语言变更
---
## 8. 主题支持
### 8.1 使用的主题变量
- 根据主题名称选择背景色:
- `dark` 主题 → `0x1a1a1a`(深色背景)
- `light` 主题 → `0xf5f5f5`(浅色背景)
- `custom` 主题 → 使用配置值或默认值
### 8.2 主题变更处理
- 组件订阅 `themeManager.subscribe()`
- 主题变更时,`setTheme()` 方法被调用
- 根据主题名称选择背景色
- 尝试更新引擎背景色(通过引擎 API
### 8.3 实现细节
```typescript
public setTheme(theme: ThemeConfig): void {
let backgroundColor: number;
if (theme.name === 'dark') {
backgroundColor = 0x1a1a1a;
} else if (theme.name === 'light') {
backgroundColor = 0xf5f5f5;
} else {
backgroundColor = this.options.backgroundColor ?? 0x1a1a1a;
}
// 尝试更新引擎背景色
if (this.engine && typeof this.engine.setBackgroundColor === 'function') {
this.engine.setBackgroundColor(backgroundColor);
} else if (this.engine && this.engine.scene) {
if (this.engine.scene.background) {
this.engine.scene.background.setHex(backgroundColor);
}
}
}
```
---
## 9. 使用示例
### 9.1 基本使用(通过 EngineManager
```typescript
import { BimEngine } from 'iflow-engine';
const engine = new BimEngine('container');
// 初始化 3D 引擎(延迟初始化)
const success = engine.initEngine({
backgroundColor: 0x333333,
version: 'v1',
showStats: true,
showViewCube: true
});
if (success) {
console.log('3D 引擎初始化成功');
}
// 加载模型
engine.engine.loadModel('/model/building.glb', {
position: [0, 0, 0],
rotation: [0, 0, 0],
scale: [1, 1, 1]
});
```
### 9.2 高级使用
```typescript
// 检查引擎是否已初始化
if (engine.engine.isInitialized()) {
// 加载模型
engine.engine.loadModel('/model/model.glb');
}
// 获取原始引擎实例,调用第三方 API
const rawEngine = engine.engine.getEngine();
if (rawEngine) {
// 调用第三方 SDK 的其他方法
rawEngine.someOtherMethod();
}
```
### 9.3 主题切换
```typescript
// 切换主题会自动更新 3D 场景背景色
engine.setTheme('dark'); // 深色背景
engine.setTheme('light'); // 浅色背景
```
---
## 10. 实现细节(供 AI 重现)
### 10.1 关键算法
#### 容器 ID 生成算法
```typescript
function generateContainerId(): string {
const timestamp = Date.now();
const random = Math.random().toString(36).substr(2, 9);
return `engine-container-${timestamp}-${random}`;
}
```
#### 主题背景色选择算法
```typescript
function selectBackgroundColor(theme: ThemeConfig, defaultColor: number): number {
if (theme.name === 'dark') {
return 0x1a1a1a; // 深色背景
} else if (theme.name === 'light') {
return 0xf5f5f5; // 浅色背景
} else {
return defaultColor; // 自定义主题,使用配置值
}
}
```
#### 引擎背景色更新算法
```typescript
function updateEngineBackground(engine: any, color: number): void {
// 方法 1: 如果引擎有 setBackgroundColor 方法
if (engine && typeof engine.setBackgroundColor === 'function') {
engine.setBackgroundColor(color);
return;
}
// 方法 2: 如果引擎有 scene.background
if (engine && engine.scene && engine.scene.background) {
engine.scene.background.setHex(color);
return;
}
// 方法 3: 无法更新(引擎可能不支持)
}
```
### 10.2 性能优化点
1. **延迟初始化**:
- 引擎不自动初始化,由用户主动调用
- 减少不必要的资源消耗
2. **状态检查**:
- 初始化前检查状态,避免重复初始化
- 销毁后禁止初始化,避免错误
3. **错误处理**:
- 所有操作都有错误检查
- 提供有意义的错误信息
### 10.3 注意事项和边界情况
1. **容器 ID 唯一性**:
- 容器 ID 必须唯一
- 如果容器已有 id使用现有 id
- 如果容器没有 id生成唯一 id
2. **延迟初始化**:
- 引擎不会自动初始化
- 用户必须主动调用 `init()``initialize()`
- 初始化前调用其他方法会报错
3. **引擎实例管理**:
- 引擎实例由第三方 SDK 创建
- 组件不负责销毁引擎实例本身
- 只负责清理订阅和容器
4. **主题更新兼容性**:
- 不同第三方 SDK 的 API 可能不同
- 需要尝试多种方式更新背景色
- 如果都不支持,静默失败
5. **模型加载**:
- 模型 URL 必须是可访问的
- 模型格式由第三方 SDK 支持
- 加载选项由第三方 SDK 处理
6. **错误处理**:
- 初始化失败时,标记为未初始化
- 提供有意义的错误信息
- 不抛出未捕获的异常
7. **第三方 SDK 依赖**:
- 依赖 `iflow-engine-base` npm 包
- 通过 npm 包导入
- 需要确保依赖已安装
---
## 11. 类型定义
### 11.1 EngineOptions
```typescript
interface EngineOptions {
container: HTMLElement; // 容器元素(必需)
backgroundColor?: number; // 背景颜色(可选,默认 0x1a1a1a
version?: 'v1' | 'v2'; // WebGL 版本(可选,默认 'v1'
showStats?: boolean; // 是否显示性能统计(可选,默认 false
showViewCube?: boolean; // 是否显示视图立方体(可选,默认 true
}
```
### 11.2 ModelLoadOptions
```typescript
interface ModelLoadOptions {
position?: [number, number, number]; // 模型位置 [x, y, z](可选)
rotation?: [number, number, number]; // 模型旋转(弧度)[x, y, z](可选)
scale?: [number, number, number]; // 模型缩放 [x, y, z](可选)
id?: string; // 模型 ID可选
}
```
---
## 12. 文件清单
### 12.1 相关文件
- `src/components/engine/index.ts` - 主组件类
- `src/components/engine/types.ts` - 类型定义
- `src/managers/engine-manager.ts` - 管理器类
- `iflow-engine-base` - 第三方 SDKnpm 依赖)
### 12.2 依赖文件
- `src/types/component.ts` - IBimComponent 接口
- `src/themes/types.ts` - ThemeConfig 类型
- `src/services/theme.ts` - ThemeManager
---
## 13. 更新记录
| 日期 | 修改内容 | 修改人 |
|------|---------|--------|
| 2024-XX-XX | 初始创建 | AI Assistant |
---
**重要提醒**: 本文档必须与组件代码保持同步。任何组件修改都必须更新本文档!

View File

@@ -1,211 +0,0 @@
# RightKey 组件详细文档
> 本文档详细描述 RightKey 组件的实现细节,包括 API、UI 结构、逻辑流程等,供 AI 根据文档重现组件。
---
## 1. 组件概述
### 1.1 基本信息
- **组件名称**: `BimRightKey`
- **文件路径**: `src/components/right-key/index.ts`
- **类型定义**: `src/components/right-key/types.ts`
- **样式文件**: `src/components/right-key/index.css`
- **实现接口**: `IBimComponent`
- **用途**: 提供一个全屏感知的定位容器,用于显示右键菜单等浮层内容。
- **特点**: 自动边界检测(防止溢出屏幕)、点击外部自动关闭、内容无关性(支持挂载任意内容)。
### 1.2 在 SDK 中的位置
- RightKey 是右键菜单系统的容器部分。
- 必须通过 `RightKeyManager` 使用,不允许直接使用。
- `RightKeyManager` 位于 `src/managers/right-key-manager.ts`
---
## 2. 组件类 API 文档
### 2.1 构造函数
```typescript
constructor(options?: RightKeyOptions)
```
**<EFBFBD><EFBFBD>数**:
- `options`:
- `zIndex`: number (默认 10000)
- `className`: string
### 2.2 公共方法
#### `init(): void`
初始化组件(实现 `IBimComponent` 接口)
**功能**:
- 创建 DOM 结构
- 将容器挂载到 `document.body`(或其他指定容器,当前实现是 body
- 绑定全局 `mousedown` 事件用于检测“点击外部”
#### `mount(content: IRightKeyContent): void`
挂载内容组件
**参数**:
- `content`: 实现了 `IRightKeyContent` 接口的对象(如 `BimMenu`
**功能**:
- 清空当前容器内容
- 调用 `content.getElement()` 并将结果 append 到容器中
- 保存 content 引用
#### `unmountContent(): void`
卸载内容
**功能**:
- 调用 `content.destroy()`
- 清空容器 DOM
#### `show(x: number, y: number): void`
显示容器
**参数**:
- `x`, `y`: 屏幕坐标(通常是鼠标事件的 `clientX/Y`
**功能**:
- 计算容器尺寸
- 执行边界检测算法,调整坐标防止溢出
- 设置 `top/left` 样式
- 设置 `display: block`
#### `hide(): void`
隐藏容器
**功能**:
- 设置 `display: none`
- 触发 `onClose` 回调(如果设置了)
#### `setOnClose(callback: () => void): void`
设置关闭回调
**功能**:
- 注册一个回调函数,在容器被隐藏(如点击外部)时触发。通常用于通知 Manager 清理状态。
#### `destroy(): void`
销毁组件
**功能**:
- 解绑全局事件
- 卸载内容
- 移除自身 DOM
---
## 3. Manager API 文档
### 3.1 RightKeyManager 类
**文件路径**: `src/managers/right-key-manager.ts`
#### `registerHandler(handler: (e) => MenuItemConfig[] | null): void`
注册上下文菜单处理器
**参数**:
- `handler`: 函数,接收 MouseEvent返回菜单配置数组或 null。
**功能**:
- 将 handler 加入内部列表。
- 右键点击时,遍历列表。只要有一个 handler 返回了非空数组,就显示这些菜单项。
#### `showMenu(x, y, items, groupOrder?): void`
手动显示菜单
**功能**:
- 创建 `BimMenu` 实例
- 调用 `rightKeyPanel.mount(menu)`
- 调用 `rightKeyPanel.show(x, y)`
---
## 4. UI 详细描述
### 4.1 DOM 结构
```html
<!-- 容器 -->
<div class="bim-right-key" style="z-index: 9000; display: none; left: ...; top: ...">
<!-- 挂载的内容 (例如 BimMenu 的 ul) -->
<ul class="bim-menu">...</ul>
</div>
```
### 4.2 CSS 类名
#### `.bim-right-key`
- `position: fixed`: 固定定位,相对于视口。
- `display: none`: 默认隐藏。
- `box-shadow`: 即使内容组件没有阴影容器也可以提供当前<E5BD93><E5898D>要由内容组件自己提供
---
## 5. 逻辑流程详细描述
### 5.1 全局点击检测 (`handleGlobalClick`)
组件初始化时,会在 `document` 上绑定 `mousedown` (捕获阶段或冒泡阶段,通常冒泡即可)。
1.`mousedown` 发生时,检查 `event.target`
2. 如果 `target` 位于 `.bim-right-key` 容器内部,**不做处理**(认为是有效的内部交互)。
3. 如果 `target` 在容器外部,调用 `hide()` 关闭菜单。
**注意**: 子菜单SubMenu通常挂载在 `body` 上,物理上位于 `.bim-right-key` 外部。为了防止点击子菜单时误关主菜单,子菜单容器必须阻止 `mousedown` 冒泡(见 Menu 组件文档)。
### 5.2 边界检测算法
`show(x, y)` 时执行:
1. 获取容器尺寸: `rect = element.getBoundingClientRect()`
2. 获取视口尺寸: `winW = window.innerWidth`, `winH = window.innerHeight`
3. **水平调整**:
- 如果 `x + rect.width > winW`: `x = x - rect.width`(向左展开)
- 否则: `x` 保持不变(向右展开)
4. **垂直调整**:
- 如果 `y + rect.height > winH`: `y = y - rect.height`(向上展开)
- 否则: `y` 保持不变(向下展开)
5. 应用调整后的 `x, y``style.left``style.top`
---
## 6. 类型定义
### 6.1 IRightKeyContent
```typescript
interface IRightKeyContent {
getElement(): HTMLElement;
destroy(): void;
}
```
### 6.2 RightKeyOptions
```typescript
interface RightKeyOptions {
className?: string;
zIndex?: number;
}
```
---
## 7. 文件清单
- `src/components/right-key/index.ts`: 组件核心逻辑
- `src/components/right-key/index.css`: 样式
- `src/components/right-key/types.ts`: 类型定义
- `src/managers/right-key-manager.ts`: 管理器
---
## 8. 更新记录
| 日期 | 修改内容 | 修改人 |
|------|---------|--------|
| 2024-XX-XX | 更新 Manager API 以支持 MenuItemConfig | AI Assistant |

View File

@@ -1,907 +0,0 @@
# Dialog 组件详细文档
> 本文档详细描述 Dialog 组件的实现细节,包括 API、UI 结构、逻辑流程等,供 AI 根据文档重现组件。
---
## 1. 组件概述
### 1.1 基本信息
- **组件名称**: `BimDialog`
- **文件路径**: `src/components/dialog/index.ts`
- **类型定义**: `src/components/dialog/index.type.ts`
- **样式文件**: `src/components/dialog/index.css`
- **实现接口**: `IBimComponent`
- **用途**: 提供可拖拽、可缩放的通用弹窗组件,支持自定义内容和样式
### 1.2 在 SDK 中的位置
- Dialog 组件是独立的 UI 组件
- 必须通过 `DialogManager` 使用,不允许直接使用
- `DialogManager` 位于 `src/managers/dialog-manager.ts`
---
## 2. 组件类 API 文档
### 2.1 构造函数
```typescript
constructor(options: DialogOptions)
```
**参数**:
- `options`: `DialogOptions` - 弹窗配置选项(详见类型定义)
**默认配置**:
```typescript
{
title: 'Dialog',
width: 300,
height: 'auto',
position: 'center',
draggable: true,
resizable: false,
minWidth: 200,
minHeight: 100
}
```
**行为**:
- 合并用户配置和默认配置
- 创建 DOM 结构
- 自动调用 `init()` 方法
### 2.2 公共方法
#### `init(): void`
初始化组件功能(实现 `IBimComponent` 接口)
**功能**:
- 将弹窗元素添加到容器
- 初始化位置
- 如果 `draggable` 为 true初始化拖拽功能
- 如果 `resizable` 为 true初始化缩放功能
- 订阅主题和语言变更
- 调用 `onOpen` 回调
**调用时机**: 构造函数中自动调用,也可手动调用
#### `setTheme(theme: ThemeConfig): void`
设置主题(实现 `IBimComponent` 接口)
**参数**:
- `theme`: `ThemeConfig` - 全局主题配置
**功能**:
- 将主题颜色映射到 CSS 变量
- 如果用户未自定义颜色,使用主题颜色
- 如果用户已自定义颜色,保持用户自定义
**CSS 变量映射**:
- `--bim-dialog-bg``theme.panelBackground` (如果未自定义 `backgroundColor`)
- `--bim-dialog-header-bg``theme.componentHover` (如果未自定义 `headerBackgroundColor`)
- `--bim-dialog-title-color``theme.textPrimary` (如果未自定义 `titleColor`)
- `--bim-dialog-text-color``theme.textPrimary` (如果未自定义 `textColor`)
- `--bim-dialog-border-color``theme.border` (如果未自定义 `borderColor`)
#### `setLocales(): void`
设置语言(实现 `IBimComponent` 接口)
**功能**:
- 更新标题文本(如果标题是翻译键)
- 使用 `t()` 函数获取翻译后的文本
**行为**:
- 查找 `.bim-dialog-title` 元素
- 如果 `options.title` 存在,使用 `t(options.title)` 更新文本
#### `setContent(content: HTMLElement | string): void`
动态设置弹窗内容
**参数**:
- `content`: `HTMLElement | string` - 内容元素或 HTML 字符串
**功能**:
- 清空当前内容
- 设置新内容(支持 HTML 字符串或 DOM 元素)
#### `fitHeight(recenter: boolean = false): void`
根据内容自动调整弹窗高度
**使用场景**
- 弹窗内容存在“展开/收起”等高度变化的交互(例如测量面板展开后需要增高弹窗,避免遮挡底部操作按钮)
**参数**
- `recenter`: 是否根据 `options.position` 重新计算定位(默认 `false`
**行为**
- 先将高度设置为 `auto`,获取自然高度
- 再将高度夹紧到 `[minHeight, containerHeight]` 范围内
- 默认不重置用户拖拽后的 `left/top`,仅做边界夹紧;若 `recenter=true` 则按 `position` 重新定位
#### `close(): void`
关闭弹窗并销毁
**功能**:
- 清理 `requestAnimationFrame`(如果存在)
- 取消主题和语言订阅
- 从 DOM 中移除元素
- 标记为已销毁
- 调用 `onClose` 回调
#### `destroy(): void`
销毁组件(实现 `IBimComponent` 接口)
**功能**:
- 调用 `close()` 方法
### 2.3 私有方法(供理解实现细节)
#### `createDom(): HTMLElement`
创建弹窗的 DOM 结构
**返回**: 创建的弹窗根元素
**DOM 结构**:
```html
<div class="bim-dialog" [id="..."]>
<div class="bim-dialog-header" [class="draggable"]>
<span class="bim-dialog-title">标题文本</span>
<span class="bim-dialog-close">&times;</span>
</div>
<div class="bim-dialog-content">
<!-- 用户内容 -->
</div>
[<div class="bim-dialog-resize-handle"></div>] <!-- 如果 resizable -->
</div>
```
**关键实现**:
1. 创建根元素,应用 CSS 变量和尺寸
2. 创建标题栏,包含标题和关闭按钮
3. 创建内容区域,支持 HTML 字符串或 DOM 元素
4. 如果 `resizable` 为 true创建缩放手柄
5. **事件拦截**: 绑定所有鼠标/触摸事件,使用 `stopPropagation()` 阻止冒泡,防止传递给 3D 引擎
**事件拦截列表**:
```typescript
['click', 'dblclick', 'contextmenu', 'wheel',
'mousedown', 'mouseup', 'mousemove',
'touchstart', 'touchend', 'touchmove',
'pointerdown', 'pointerup', 'pointermove',
'pointerenter', 'pointerleave', 'pointerover', 'pointerout']
```
#### `initPosition(): void`
初始化弹窗位置
**功能**:
- 根据 `position` 选项计算弹窗位置
- 支持预设位置(如 'center', 'top-left' 等)或坐标对象 `{ x, y }`
- 确保弹窗不超出容器边界
**位置计算逻辑**:
- `center`: `left = (containerWidth - dialogWidth) / 2`, `top = (containerHeight - dialogHeight) / 2`
- `top-left`: `left = 0`, `top = 0`
- `top-center`: `left = (containerWidth - dialogWidth) / 2`, `top = 0`
- `top-right`: `left = containerWidth - dialogWidth`, `top = 0`
- `left-center`: `left = 0`, `top = (containerHeight - dialogHeight) / 2`
- `right-center`: `left = containerWidth - dialogWidth`, `top = (containerHeight - dialogHeight) / 2`
- `bottom-left`: `left = 0`, `top = containerHeight - dialogHeight`
- `bottom-center`: `left = (containerWidth - dialogWidth) / 2`, `top = containerHeight - dialogHeight`
- `bottom-right`: `left = containerWidth - dialogWidth`, `top = containerHeight - dialogHeight`
- `{ x, y }`: 直接使用坐标值
**边界限制**:
```typescript
left = Math.max(0, Math.min(left, containerWidth - dialogWidth));
top = Math.max(0, Math.min(top, containerHeight - dialogHeight));
```
#### `initDrag(): void`
初始化拖拽功能
**功能**:
- 在标题栏上绑定 `mousedown` 事件
- 实现拖拽移动功能
- 使用 `requestAnimationFrame` 优化性能
- 使用 `capture: true` 确保事件在捕获阶段处理
**拖拽流程**:
1. `mousedown`: 记录起始位置和弹窗当前位置,缓存容器和弹窗尺寸
2. `mousemove`: 计算偏移量,更新弹窗位置,限制在容器边界内
3. `mouseup`: 清理事件监听和动画帧
**性能优化**:
- 使用 `requestAnimationFrame` 节流更新
- 缓存容器和弹窗尺寸,减少 reflow
- 使用 `capture: true` 确保事件处理
#### `initResize(): void`
初始化缩放功能
**功能**:
- 在缩放手柄上绑定 `mousedown` 事件
- 实现右下角拖拽缩放功能
- 使用 `requestAnimationFrame` 优化性能
- 限制最小尺寸
**缩放流程**:
1. `mousedown`: 记录起始位置和弹窗当前尺寸
2. `mousemove`: 计算偏移量,更新弹窗尺寸,限制最小尺寸
3. `mouseup`: 清理事件监听和动画帧
**尺寸限制**:
```typescript
newWidth = Math.max(minWidth || 100, startWidth + deltaX);
newHeight = Math.max(minHeight || 50, startHeight + deltaY);
```
---
## 3. 分化组件说明
### 3.1 BimInfoDialog
**文件路径**: `src/components/dialog/bimInfoDialog/index.ts`
**继承关系**: `BimInfoDialog extends BimDialog`
**特殊功能**:
- 预定义了信息展示的内容结构
- 包含标题、信息列表和操作按钮
- 使用固定的配置(宽度 320px可缩放可拖拽
**实现方式**:
- 在构造函数中创建内容 DOM
- 调用父类构造函数,传入预定义的配置
- 不需要重写其他方法,直接继承父类功能
**内容结构**:
```html
<div class="bim-info-dialog-content">
<h3>Model Information</h3>
<ul>
<li><strong>Name:</strong> Sample Project</li>
<li><strong>Version:</strong> 1.0.0</li>
<li><strong>Date:</strong> 当前日期</li>
<li><strong>Status:</strong> <span style="color: green;">Active</span></li>
</ul>
<button>Update Status</button>
</div>
```
**与父类的差异**:
- 固定了内容结构
- 固定了部分配置选项
- 其他功能完全继承自 `BimDialog`
---
## 4. Manager API 文档
### 4.1 DialogManager 类
**文件路径**: `src/managers/dialog-manager.ts`
**继承关系**: `DialogManager extends BimComponent`
### 4.2 构造函数
```typescript
constructor(engine: BimEngine, container: HTMLElement)
```
**参数**:
- `engine`: `BimEngine` - 引擎实例
- `container`: `HTMLElement` - 弹窗挂载的目标容器
**行为**:
- 保存容器引用
- 监听 `ui:open-dialog` 事件
- 如果事件 payload.id 是 'info',自动打开信息弹窗
### 4.3 公共方法
#### `create(options: Omit<DialogOptions, 'container'>): BimDialog`
创建一个通用弹窗
**参数**:
- `options`: `Omit<DialogOptions, 'container'>` - 弹窗配置(不需要传 container
**返回**: `BimDialog` 实例
**功能**:
- 自动使用 Manager 绑定的容器
- 创建 `BimDialog` 实例
- 应用当前主题
- 将弹窗添加到活跃列表
-`onClose` 回调中自动从列表移除
**使用示例**:
```typescript
const dialog = engine.dialog.create({
title: '测试弹窗',
content: '这是内容',
width: 400,
height: 300
});
```
#### `showInfoDialog(): void`
显示二次封装的模型信息弹窗
**功能**:
- 创建 `BimInfoDialog` 实例
- 直接显示信息弹窗
**注意**: 此方法创建的弹窗不会自动加入管理列表(遗留逻辑)
#### `updateTheme(theme: ThemeConfig): void`
响应全局主题变更
**参数**:
- `theme`: `ThemeConfig` - 全局主题配置
**功能**:
- 遍历所有活跃弹窗
- 调用每个弹窗的 `setTheme()` 方法
#### `destroy(): void`
销毁管理器
**功能**:
- 销毁所有活跃弹窗
- 清空活跃列表
---
## 5. UI 详细描述
### 5.1 DOM 结构
**完整 HTML 结构**:
```html
<div class="bim-dialog" id="[可选ID]" style="[CSS变量和尺寸]">
<!-- 标题栏 -->
<div class="bim-dialog-header [draggable]">
<span class="bim-dialog-title">标题文本</span>
<span class="bim-dialog-close">&times;</span>
</div>
<!-- 内容区域 -->
<div class="bim-dialog-content">
<!-- 用户内容HTML字符串或DOM元素 -->
</div>
<!-- 缩放手柄(如果 resizable -->
<div class="bim-dialog-resize-handle"></div>
</div>
```
### 5.2 CSS 类名和样式
#### `.bim-dialog` (根元素)
- `position: absolute` - 绝对定位
- `background-color: var(--bim-dialog-bg)` - 背景色CSS 变量)
- `border: 1px solid var(--bim-dialog-border-color)` - 边框
- `border-radius: 6px` - 圆角
- `box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3)` - 阴影
- `display: flex` + `flex-direction: column` - 垂直布局
- `z-index: 10001` - 层级(确保在 3D 引擎之上)
- `min-width: 200px` - 最小宽度
- `min-height: 100px` - 最小高度
- `pointer-events: auto` - 确保可接收事件
#### `.bim-dialog-header` (标题栏)
- `height: 32px` - 固定高度
- `background-color: var(--bim-dialog-header-bg)` - 背景色
- `display: flex` + `align-items: center` + `justify-content: space-between` - 水平布局
- `padding: 0 10px` - 内边距
- `cursor: default` - 默认光标
- `user-select: none` - 禁止选中
- `border-bottom: 1px solid var(--bim-dialog-border-color)` - 底边框
#### `.bim-dialog-header.draggable` (可拖拽标题栏)
- `cursor: move` - 移动光标
#### `.bim-dialog-title` (标题文本)
- `font-size: 14px` - 字体大小
- `font-weight: 500` - 字体粗细
- `white-space: nowrap` - 不换行
- `overflow: hidden` - 隐藏溢出
- `text-overflow: ellipsis` - 文本省略
- `color: var(--bim-dialog-title-color)` - 文字颜色
#### `.bim-dialog-close` (关闭按钮)
- `cursor: pointer` - 指针光标
- `font-size: 18px` - 字体大小
- `color: #999` - 默认颜色
- `line-height: 1` - 行高
- `margin-left: 8px` - 左边距
#### `.bim-dialog-close:hover` (关闭按钮悬停)
- `color: #fff` - 悬停颜色
#### `.bim-dialog-content` (内容区域)
- `flex: 1` - 占据剩余空间
- `padding: 10px` - 内边距
- `overflow: auto` - 内容溢出时滚动
- `font-size: 14px` - 字体大小
- `color: var(--bim-dialog-text-color)` - 文字颜色
#### `.bim-dialog-resize-handle` (缩放手柄)
- `position: absolute` - 绝对定位
- `width: 10px` + `height: 10px` - 尺寸
- `bottom: 0` + `right: 0` - 右下角位置
- `cursor: se-resize` - 缩放光标
- `z-index: 10` - 层级
#### `.bim-dialog-resize-handle::after` (缩放装饰)
- 使用伪元素创建右下角斜线装饰
- `border-right` + `border-bottom` - 创建斜线效果
### 5.3 CSS 变量
**定义位置**: `:root` 或元素内联样式
**变量列表**:
- `--bim-dialog-bg`: 窗体背景颜色(默认 `rgba(17, 17, 17, 0.95)`
- `--bim-dialog-header-bg`: 标题栏背景颜色(默认 `#2a2a2a`
- `--bim-dialog-title-color`: 标题文字颜色(默认 `#fff`
- `--bim-dialog-text-color`: 内容文字颜色(默认 `#ccc`
- `--bim-dialog-border-color`: 边框颜色(默认 `#444`
### 5.4 交互行为
#### 拖拽
- **触发区域**: 标题栏(`.bim-dialog-header`
- **触发条件**: `draggable: true`
- **行为**:
- 鼠标按下标题栏时开始拖拽
- 鼠标移动时弹窗跟随移动
- 鼠标释放时结束拖拽
- 弹窗位置限制在容器边界内
#### 缩放
- **触发区域**: 缩放手柄(`.bim-dialog-resize-handle`
- **触发条件**: `resizable: true`
- **行为**:
- 鼠标按下缩放手柄时开始缩放
- 鼠标移动时弹窗尺寸跟随变化
- 鼠标释放时结束缩放
- 尺寸限制在最小尺寸以上
#### 关闭
- **触发方式**:
- 点击关闭按钮(`.bim-dialog-close`
- 调用 `close()` 方法
- **行为**: 弹窗从 DOM 移除,调用 `onClose` 回调
### 5.5 响应式行为
- 弹窗位置会根据容器尺寸自动调整,确保不超出边界
- 内容区域支持滚动(`overflow: auto`
- 标题文本过长时显示省略号(`text-overflow: ellipsis`
---
## 6. 逻辑流程详细描述
### 6.1 初始化流程
1. **构造函数调用**:
- 合并配置选项(用户配置 + 默认配置)
- 调用 `createDom()` 创建 DOM 结构
- 保存 header 和 contentArea 引用
- 自动调用 `init()`
2. **init() 方法执行**:
- 检查是否已初始化,如果是则返回
- 将弹窗元素添加到容器
- 调用 `initPosition()` 计算并设置位置
- 如果 `draggable` 为 true调用 `initDrag()`
- 如果 `resizable` 为 true调用 `initResize()`
- 标记为已初始化
- 调用 `onOpen` 回调(如果存在)
- 订阅主题变更:`themeManager.subscribe()`
- 订阅语言变更:`localeManager.subscribe()`
### 6.2 生命周期
#### 创建阶段
- 构造函数 → `createDom()``init()`
#### 运行阶段
- 响应主题变更:`setTheme()` 被调用
- 响应语言变更:`setLocales()` 被调用
- 用户交互:拖拽、缩放、关闭
#### 销毁阶段
- `close()``destroy()` 被调用
- 清理 `requestAnimationFrame`
- 取消主题和语言订阅
- 从 DOM 移除元素
- 调用 `onClose` 回调
### 6.3 事件处理流程
#### 拖拽事件流程
1. 用户在标题栏按下鼠标 → `mousedown` 事件
2. 记录起始位置和弹窗当前位置
3. 缓存容器和弹窗尺寸
4. 在 document 上绑定 `mousemove``mouseup`(捕获阶段)
5. 鼠标移动时 → `mousemove` 事件
- 计算偏移量
- 使用 `requestAnimationFrame` 更新位置
- 限制在容器边界内
6. 鼠标释放时 → `mouseup` 事件
- 清理事件监听
- 清理动画帧
#### 缩放事件流程
1. 用户在缩放手柄按下鼠标 → `mousedown` 事件
2. 记录起始位置和弹窗当前尺寸
3. 在 document 上绑定 `mousemove``mouseup`(捕获阶段)
4. 鼠标移动时 → `mousemove` 事件
- 计算偏移量
- 使用 `requestAnimationFrame` 更新尺寸
- 限制在最小尺寸以上
5. 鼠标释放时 → `mouseup` 事件
- 清理事件监听
- 清理动画帧
#### 事件拦截流程
- 弹窗根元素上绑定所有鼠标/触摸事件
- 使用 `stopPropagation()` 阻止事件冒泡
- 防止事件传递给 3D 引擎
- 使用 `passive: false` 允许阻止默认行为
### 6.4 状态管理
#### 内部状态
- `_isInitialized`: 是否已初始化
- `_isDestroyed`: 是否已销毁
- `rafId`: `requestAnimationFrame` 的 ID用于节流
#### 订阅管理
- `unsubscribeTheme`: 主题订阅取消函数
- `unsubscribeLocale`: 语言订阅取消函数
### 6.5 与其他组件的交互
- **与 DialogManager**: 通过 Manager 创建和管理
- **与 ThemeManager**: 订阅主题变更
- **与 LocaleManager**: 订阅语言变更
- **与 3D 引擎**: 通过事件拦截防止交互冲突
---
## 7. 国际化支持
### 7.1 使用的翻译键
- `options.title`: 弹窗标题(如果提供,会通过 `t()` 函数翻译)
### 7.2 语言变更处理
- 组件订阅 `localeManager.subscribe()`
- 语言变更时,`setLocales()` 方法被调用
- 更新标题文本:`titleEl.textContent = t(this.options.title)`
### 7.3 实现细节
```typescript
public setLocales(): void {
if (this.options.title) {
const titleEl = this.header.querySelector('.bim-dialog-title');
if (titleEl) {
titleEl.textContent = t(this.options.title);
}
}
}
```
---
## 8. 主题支持
### 8.1 使用的主题变量
- `theme.panelBackground``--bim-dialog-bg`
- `theme.componentHover``--bim-dialog-header-bg`
- `theme.textPrimary``--bim-dialog-title-color``--bim-dialog-text-color`
- `theme.border``--bim-dialog-border-color`
### 8.2 主题变更处理
- 组件订阅 `themeManager.subscribe()`
- 主题变更时,`setTheme()` 方法被调用
- 如果用户未自定义颜色,使用主题颜色
- 如果用户已自定义颜色,保持用户自定义
### 8.3 实现细节
```typescript
public setTheme(theme: ThemeConfig) {
const style = this.element.style;
if (!this.options.backgroundColor)
style.setProperty('--bim-dialog-bg', theme.panelBackground);
if (!this.options.headerBackgroundColor)
style.setProperty('--bim-dialog-header-bg', theme.componentHover);
// ... 其他颜色映射
}
```
---
## 9. 使用示例
### 9.1 基本使用(通过 Manager
```typescript
import { BimEngine } from 'iflow-engine';
const engine = new BimEngine('container');
// 创建简单弹窗
const dialog = engine.dialog.create({
title: 'dialog.testTitle', // 使用翻译键
content: '这是内容',
width: 400,
height: 300
});
// 创建可缩放弹窗
const resizableDialog = engine.dialog.create({
title: '可缩放弹窗',
content: '<div>内容</div>',
width: 500,
height: 400,
resizable: true,
draggable: true
});
// 创建自定义位置弹窗
const positionedDialog = engine.dialog.create({
title: '自定义位置',
content: '内容',
position: { x: 100, y: 100 },
width: 300,
height: 200
});
```
### 9.2 高级使用
```typescript
// 创建带自定义样式的弹窗
const styledDialog = engine.dialog.create({
title: '自定义样式',
content: '<div>内容</div>',
backgroundColor: 'rgba(100, 0, 0, 0.95)',
headerBackgroundColor: '#cc0000',
titleColor: '#ffffff',
textColor: '#ffcccc',
borderColor: '#ff6666',
width: 300,
height: 200
});
// 创建带回调的弹窗
const callbackDialog = engine.dialog.create({
title: '带回调',
content: '内容',
onOpen: () => {
console.log('弹窗已打开');
},
onClose: () => {
console.log('弹窗已关闭');
}
});
// 动态更新内容
const dynamicDialog = engine.dialog.create({
title: '动态内容',
content: '初始内容'
});
// 稍后更新内容
dynamicDialog.setContent('<div>新内容</div>');
```
### 9.3 使用信息弹窗
```typescript
// 通过 Manager 显示信息弹窗
engine.dialog.showInfoDialog();
// 通过事件总线打开(如果配置了监听)
engine.emit('ui:open-dialog', { id: 'info' });
```
---
## 10. 实现细节(供 AI 重现)
### 10.1 关键算法
#### 位置计算算法
```typescript
// 计算居中位置
function calculateCenterPosition(containerWidth, containerHeight, dialogWidth, dialogHeight) {
return {
left: (containerWidth - dialogWidth) / 2,
top: (containerHeight - dialogHeight) / 2
};
}
// 边界限制算法
function clampPosition(left, top, containerWidth, containerHeight, dialogWidth, dialogHeight) {
return {
left: Math.max(0, Math.min(left, containerWidth - dialogWidth)),
top: Math.max(0, Math.min(top, containerHeight - dialogHeight))
};
}
```
#### 拖拽算法
```typescript
// 拖拽位置更新
function updateDragPosition(startX, startY, currentX, currentY, startLeft, startTop) {
const deltaX = currentX - startX;
const deltaY = currentY - startY;
return {
left: startLeft + deltaX,
top: startTop + deltaY
};
}
```
#### 缩放算法
```typescript
// 缩放尺寸更新
function updateResizeSize(startX, startY, currentX, currentY, startWidth, startHeight, minWidth, minHeight) {
const deltaX = currentX - startX;
const deltaY = currentY - startY;
return {
width: Math.max(minWidth, startWidth + deltaX),
height: Math.max(minHeight, startHeight + deltaY)
};
}
```
### 10.2 性能优化点
1. **requestAnimationFrame 节流**:
- 拖拽和缩放使用 `requestAnimationFrame` 节流更新
- 避免频繁的 DOM 操作
2. **尺寸缓存**:
- 拖拽开始时缓存容器和弹窗尺寸
- 减少 `getBoundingClientRect()` 调用
3. **事件捕获**:
- 使用 `capture: true` 确保事件在捕获阶段处理
- 即使内部元素阻止冒泡,也能捕获事件
4. **CSS 变量**:
- 使用 CSS 变量应用主题
- 主题变更时无需重新渲染 DOM
### 10.3 注意事项和边界情况
1. **容器尺寸为 0**:
- 位置计算时可能出现除零或负数
- 使用 `Math.max()` 确保位置不为负
2. **弹窗尺寸大于容器**:
- 位置计算时确保弹窗不超出容器
- 使用边界限制算法
3. **快速连续操作**:
- 使用 `rafId` 防止多个动画帧同时执行
- 确保动画帧正确清理
4. **事件清理**:
- 组件销毁时必须清理所有事件监听
- 必须取消主题和语言订阅
5. **事件拦截**:
- 必须拦截所有鼠标/触摸事件
- 防止事件传递给 3D 引擎
6. **翻译键处理**:
- 标题可能是翻译键或普通文本
- 只有翻译键才需要通过 `t()` 函数处理
---
## 11. 类型定义
### 11.1 DialogOptions
```typescript
interface DialogOptions extends DialogColors {
container: HTMLElement; // 弹窗挂载的父容器(必需)
title?: string; // 弹窗标题(可选,支持翻译键)
content?: HTMLElement | string; // 弹窗内容(可选)
width?: number | string; // 宽度(可选,默认 300
height?: number | string; // 高度(可选,默认 'auto'
position?: DialogPosition; // 位置(可选,默认 'center'
draggable?: boolean; // 是否可拖拽(可选,默认 true
resizable?: boolean; // 是否可缩放(可选,默认 false
minWidth?: number; // 最小宽度(可选,默认 200
minHeight?: number; // 最小高度(可选,默认 100
onClose?: () => void; // 关闭回调(可选)
onOpen?: () => void; // 打开回调(可选)
id?: string; // 弹窗 ID可选
}
```
### 11.2 DialogPosition
```typescript
type DialogPosition =
| 'center'
| 'top-left' | 'top-center' | 'top-right'
| 'left-center' | 'right-center'
| 'bottom-left' | 'bottom-center' | 'bottom-right'
| { x: number; y: number };
```
### 11.3 DialogColors
```typescript
interface DialogColors {
backgroundColor?: string; // 窗体背景颜色
headerBackgroundColor?: string; // 标题栏背景颜色
titleColor?: string; // 标题文字颜色
textColor?: string; // 内容文字颜色
borderColor?: string; // 边框颜色
}
```
---
## 12. 文件清单
### 12.1 相关文件
- `src/components/dialog/index.ts` - 主组件类
- `src/components/dialog/index.type.ts` - 类型定义
- `src/components/dialog/index.css` - 样式文件
- `src/components/dialog/bimInfoDialog/index.ts` - 信息弹窗(分化组件)
- `src/components/dialog/bimInfoDialog/index.css` - 信息弹窗样式
- `src/managers/dialog-manager.ts` - 管理器类
### 12.2 依赖文件
- `src/types/component.ts` - IBimComponent 接口
- `src/themes/types.ts` - ThemeConfig 类型
- `src/services/theme.ts` - ThemeManager
- `src/services/locale.ts` - LocaleManager
---
## 13. 更新记录
| 日期 | 修改内容 | 修改人 |
|------|---------|--------|
| 2024-XX-XX | 初始创建 | AI Assistant |
---
**重要提醒**: 本文档必须与组件代码保持同步。任何组件修改都必须更新本文档!

View File

@@ -1,97 +0,0 @@
# Collapse 折叠面板组件
`BimCollapse` 是一个通用的折叠面板组件,支持手风琴模式、自定义内容和标题。常用于属性面板、设置菜单等场景。
## 1. 组件概述
- **类名**: `BimCollapse`
- **文件路径**: `src/components/collapse/index.ts`
- **样式文件**: `src/components/collapse/index.css`
- **类型定义**: `src/components/collapse/types.ts`
该组件实现了 `IBimComponent` 接口,具备完整的生命周期管理、主题响应和国际化支持能力。
## 2. API 文档
### 2.1 构造函数
```typescript
const collapse = new BimCollapse(options: CollapseOptions);
```
### 2.2 CollapseOptions 配置项
| 属性 | 类型 | 默认值 | 说明 |
|------|------|-------|------|
| `container` | `HTMLElement \| string` | - | **(必填)** 挂载容器或其 ID |
| `items` | `CollapseItemConfig[]` | - | **(必填)** 面板项列表 |
| `accordion` | `boolean` | `false` | 是否开启手风琴模式(一次只能展开一项) |
| `activeIds` | `string[]` | `[]` | 初始展开的面板 ID 列表 |
| `bordered` | `boolean` | `true` | 是否显示外边框 |
| `ghost` | `boolean` | `false` | 是否开启幽灵模式(无背景、无边框) |
| `className` | `string` | - | 自定义类名 |
| `onChange` | `(activeIds: string[]) => void` | - | 切换面板时的回调函数 |
### 2.3 CollapseItemConfig 面板项配置
| 属性 | 类型 | 说明 |
|------|------|------|
| `id` | `string` | **(必填)** 唯一标识符 |
| `title` | `string` | **(必填)** 标题文本的**翻译键** (例如 `'panel.property.base'`) |
| `content` | `string \| HTMLElement` | **(必填)** 面板内容 |
| `icon` | `string` | 标题左侧图标 (SVG 字符串) |
| `extra` | `string \| HTMLElement` | 标题栏右侧额外内容 |
| `disabled` | `boolean` | 是否禁用该面板 |
| `className` | `string` | 自定义面板类名 |
### 2.4 实例方法
- **`toggleItem(id: string)`**: 切换指定面板的展开/折叠状态。
- **`setTheme(theme: ThemeConfig)`**: 设置组件主题 (CSS 变量映射)。
- **`setLocales()`**: 更新组件文本 (标题翻译)。
- **`destroy()`**: 销毁组件,清理资源。
## 3. 使用示例
```typescript
import { BimCollapse } from '../components/collapse/index';
const collapse = new BimCollapse({
container: document.getElementById('panel'),
accordion: true,
activeIds: ['base'],
items: [
{
id: 'base',
title: 'panel.property.base', // 必须是翻译键
content: document.createElement('div'), // 或 HTML 字符串
icon: '<svg>...</svg>'
},
{
id: 'advanced',
title: 'panel.property.advanced',
content: 'Advanced Content',
disabled: true
}
],
onChange: (ids) => {
console.log('Current active panels:', ids);
}
});
```
## 4. 主题支持
组件自动订阅主题变更,并通过 CSS 变量控制样式:
- **面板背景**: `theme.panelBackground`
- **边框颜色**: `theme.border`
- **文本颜色**: `theme.textPrimary`
- **标题栏背景**: `theme.componentHover` (默认) / `theme.componentBackground`
- **禁用态颜色**: `theme.textSecondary`
## 5. 国际化支持
组件自动订阅语言变更:
- **标题 (`title`)**: 会自动使用 `t(config.title)` 进行翻译。确保传入的是有效的翻译键。
- **内容 (`content`)**: 如果内容包含文本,请确保内容生成时已翻译,或内容本身具有响应国际化的能力(如使用 `BimDescription`)。

File diff suppressed because it is too large Load Diff

View File

@@ -1,127 +0,0 @@
# Description 描述列表组件
`BimDescription` 组件用于展示一组键值对Key-Value数据常用于详情页、属性面板等场景。
## 1. 组件概述
- **类名**: `BimDescription`
- **文件路径**: `src/components/description/index.ts`
- **样式文件**: `src/components/description/index.css`
- **类型定义**: `src/components/description/types.ts`
该组件是一个**纯展示组件**,其特点是:
- **不内置国际化**: 为了最大灵活性Label 和 Value 均直接显示传入的字符串/元素,不调用翻译函数。调用者应负责传入已翻译的文本。
- **高度定制化**: 支持全局或行级的颜色、Padding、字体大小定制。
- **布局灵活**: 支持普通列表模式和带边框的表格模式Key-Value 间有纵向分割线)。
## 2. API 文档
### 2.1 构造函数
```typescript
const description = new BimDescription(options: DescriptionOptions);
```
### 2.2 DescriptionOptions 配置项
| 属性 | 类型 | 默认值 | 说明 |
|------|------|-------|------|
| `container` | `HTMLElement \| string` | - | **(必填)** 挂载容器或其 ID |
| `items` | `DescriptionItem[]` | - | **(必填)** 数据项列表 |
| `bordered` | `boolean` | `false` | 是否显示边框。开启后会显示行间分割线以及 Key-Value 之间的纵向分割线 |
| `labelWidth` | `string` | - | 标签列的固定宽度 (如 `'80px'`),不设置则自适应 |
| `fontSize` | `string` | `'14px'` | 全局字体大小 (如 `'12px'`) |
| `labelColor` | `string` | `theme.textSecondary` | 全局标签颜色 |
| `valueColor` | `string` | `theme.textPrimary` | 全局内容颜色 |
| `labelPadding` | `string` | `'0 4px'` | 标签单元格内边距 (CSS padding 语法) |
| `valuePadding` | `string` | `'0 4px'` | 内容单元格内边距 (CSS padding 语法) |
| `className` | `string` | - | 自定义类名 |
### 2.3 DescriptionItem 数据项
| 属性 | 类型 | 说明 |
|------|------|------|
| `label` | `string` | 标签文本 (直接显示,在非 bordered 模式下会自动添加冒号) |
| `value` | `string \| HTMLElement` | 内容文本或 DOM 元素 |
| `labelColor` | `string` | 行级自定义标签颜色 (优先级高于全局) |
| `valueColor` | `string` | 行级自定义内容颜色 (优先级高于全局) |
| `className` | `string` | 自定义行类名 |
### 2.4 实例方法
- **`setItems(items: DescriptionItem[])`**: 动态更新数据列表。
- **`setTheme(theme: ThemeConfig)`**: <20><>置组件主题 (通常自动调用)。
- **`destroy()`**: 销毁组件,清理 DOM 和事件订阅。
## 3. 使用示例
### 3.1 基础使用
```typescript
new BimDescription({
container: document.getElementById('container'),
labelWidth: '80px',
items: [
{ label: 'Name', value: 'Wall-01' },
{ label: 'ID', value: 'E-1001' },
{ label: 'Level', value: 'Level 1' }
]
});
```
### 3.2 带边框的表格模式 (Bordered)
```typescript
new BimDescription({
container: document.getElementById('container'),
bordered: true,
fontSize: '12px',
labelPadding: '4px 8px',
valuePadding: '4px 8px',
items: [
{ label: 'Material', value: 'Concrete' },
{ label: 'Density', value: '2400 kg/m³' }
]
});
```
### 3.3 自定义样式
```typescript
new BimDescription({
container: 'container',
labelColor: '#999',
valueColor: '#333',
items: [
{
label: 'Status',
value: '<span style="color: green">Active</span>',
labelColor: 'blue' // 单独覆盖此行的标签颜色
}
]
});
```
## 4. 主题支持
组件会自动响应系统主题变更。默认映射关系如下:
- **文本颜色**: `theme.textPrimary`
- **标签颜色**: `theme.textSecondary`
- **边框颜色**: `theme.border`
- **标签背景 (仅 Bordered 模式)**: `theme.background` (用于轻微区分 Key 和 Value)
## 5. 国际化支持
**组件内部不进行翻译**。调用者应在传入 `label` 之前使用 `t()` 函数进行翻译。
```typescript
import { t } from 'iflow-engine/services/locale';
new BimDescription({
// ...
items: [
{ label: t('panel.property.id'), value: '123' }
]
});
```

View File

@@ -1,92 +0,0 @@
## 1. 组件概述
- **名称**BimTab
- **位置**`src/components/tab/`
- **功能**:渲染固定标签页(不支持运行期增删),支持点击切换、禁用、可选内容托管。主题由全局 `ThemeManager` 提供,文案通过 `t()` 翻译。
- **使用方式**:直接通过构造函数 + `init()` 创建;本项目内目前仅在 `ConstructTreeManagerBtn` 弹窗中使用(无专门 Manager
## 2. 组件类 APIBimTab
- `constructor(options: TabOptions)`:创建实例并挂载到 `options.container`
- `init(): void`:渲染头部与内容,订阅主题/语言。
- `activateTab(tabId: string): void`:切换激活标签,触发 `onChange`
- `setTheme(theme: ThemeConfig): void`:应用主题变量。
- `setLocales(): void`:刷新标题文案。
- `destroy(): void`:解绑事件、取消订阅、移除 DOM。
## 3. 分化组件
- 当前无子类或变体,后续可扩展滚动、溢出折叠等能力。
## 4. 使用场景说明
- **ConstructTreeManagerBtn 弹窗**:顶部三段标签(构件/系统/空间),构件标签承载原有 Tree系统/空间暂为空容器。
- 适用于固定分区切换,内容简单或由外部回调控制。
## 5. UI 结构
```
div.bim-tab
div.bim-tab__nav [role=tablist]
button.bim-tab__item[role=tab][aria-selected][aria-disabled?]
span.bim-tab__icon? (可选)
span.bim-tab__title
div.bim-tab__content
div.bim-tab__panel[role=tabpanel][aria-labelledby=tab-xxx]
...
```
- 状态类:`.is-active``.is-disabled`
- 内容区:仅切换显示,不强制填充(可为空)。
## 6. 逻辑流程
1) `constructor`:创建根节点、头部、内容容器;缓存 tab 数据;挂载到传入容器。
2) `init`:渲染头部按钮与内容面板,设置初始激活;应用当前主题/语言;订阅主题、语言变更。
3) 交互:点击非禁用按钮 → `activateTab` 更新头部/面板状态 → 回调 `onChange`
4) 销毁:解绑点击监听、取消订阅、清空映射并移除 DOM。
## 7. 国际化
- 标题:`t(tab.title)`,若翻译键不存在则回退原文。
- 新增翻译键:`tab.component` / `tab.system` / `tab.space`(已在 `zh-CN.ts``en-US.ts` 注册)。
- `setLocales()`:遍历头部按钮刷新标题文案;订阅 `localeManager` 自动响应语言切换。
## 8. 主题支持
- 来自 `themeManager`,不从配置传入。
- 使用的变量(设置在根节点上):
- `--bim-tab-bg``--bim-tab-nav-bg`
- `--bim-tab-text``--bim-tab-text-secondary``--bim-tab-text-active`
- `--bim-tab-border``--bim-tab-hover-bg``--bim-tab-active-bg`
- `--bim-tab-icon`
- `setTheme(theme)`:根据 `ThemeConfig` 写入上述 CSS 变量。
## 9. 使用示例
```ts
const tabMount = document.createElement('div');
const tab = new BimTab({
container: tabMount,
tabs: [
{ id: 'component', title: 'tab.component', content: componentEl },
{ id: 'system', title: 'tab.system', content: systemEl },
{ id: 'space', title: 'tab.space', content: spaceEl },
],
activeId: 'component',
onChange: (id) => {
// 根据 id 做额外逻辑(如埋点、动态加载)
},
});
tab.init();
```
## 10. 实现细节
- 仅支持固定 tabs`TabOptions.tabs` 为初始化列表,不提供新增/删除接口。
- 内容托管两种方式:`content: HTMLElement`append`content: string`innerHTML。未提供内容时面板为空。
- 事件绑定:头部使用事件委托绑定 click销毁时移除。
- 访问性:头部 `role=tab``aria-selected``aria-disabled`;面板 `role=tabpanel``aria-labelledby` 对应头部 id。
- 主题/语言订阅:`localeManager.subscribe``themeManager.subscribe`,销毁时必须取消。
## 11. 类型定义
- 位置:`src/components/tab/index.type.ts`
- 主要类型:
- `TabItem`: `{ id; title; disabled?; icon?; content?; meta? }`
- `TabOptions`: `{ container; tabs; activeId?; onChange? }`
## 12. 文件清单
- `src/components/tab/index.ts`:组件实现
- `src/components/tab/index.type.ts`:类型定义
- `src/components/tab/index.css`:样式
- (无 Manager当前仅在 `ConstructTreeManagerBtn` 中直接使用

View File

@@ -1,108 +0,0 @@
# Tree 组件文档
## 1. 组件概述
**BimTree** 是一个通用的树形组件,支持多级嵌套、复选框、图标和自定义内容。它通常用于展示模型结构、文件目录或层级列表。
该组件设计为被包裹在容器(如 Dialog并通过 `ModelTreeManager` 进行创建和管理。
## 2. 组件类 API (BimTree)
`src/components/tree/index.ts`
### 2.1 核心方法
* `checkNode(id: string, checked: boolean)`: 勾选/取消勾选指定节点。支持父子联动。
* `checkAll(checked: boolean)`: 全选或全不选。
* `expandNode(id: string, expanded: boolean)`: 展开/折叠指定节点。
* `expandAll(expanded: boolean)`: 展开/折叠所有层级。
* `getCheckedNodes(includeHalfChecked?: boolean)`: 获取当前所有被勾选的节点配置列表。
* `getNode(id: string)`: 获取指定 ID 的节点实例。
## 3. Manager API (ModelTreeManager)
`ModelTreeManager` 负责创建和管理 `BimTree` 实例。
### 方法
#### `createTree(options: TreeOptions): BimTree`
创建一个新的树组件实例。
- **参数**: `options` (TreeOptions) - 树组件配置
- **返回**: `BimTree` - 树组件实例
- **功能**:
- 实例化组件
- 自动绑定组件事件到全局事件总线 (`ui:tree-node-check`, `ui:tree-node-select` 等)
- 初始化组件
#### `showStructTree(data: TreeNodeConfig[], title: string): { tree: BimTree, dialog: BimDialog }`
显示带复选框的模型结构树弹窗。
#### `showSimpleTree(data: TreeNodeConfig[], title: string): { tree: BimTree, dialog: BimDialog }`
显示简单的树形弹窗 (无复选框)。
---
## 4. 类型定义
`src/components/tree/types.ts`
```typescript
interface TreeNodeConfig {
id: string;
label: string; // 翻译键
children?: TreeNodeConfig[];
checked?: boolean;
expanded?: boolean;
disabled?: boolean;
icon?: string;
data?: any; // 业务数据
}
interface TreeOptions {
data: TreeNodeConfig[];
checkable?: boolean; // 是否显示复选框
checkStrictly?: boolean; // 是否父子联动 (默认 true)
defaultExpandAll?: boolean;
indent?: number;
// 事件回调
onNodeCheck?: (node: BimTreeNode) => void;
onNodeSelect?: (node: BimTreeNode) => void;
onNodeExpand?: (node: BimTreeNode) => void;
}
```
## 5. UI 结构与样式
* **容器**: `.bim-tree` (Flex column)
* **节点**: `.bim-tree-node`
* **内容行**: `.bim-tree-node-content` (Flex row, align-center)
* **箭头**: `.bim-tree-switcher` (SVG, rotate transform)
* **复选框**: `.bim-tree-checkbox` (自定义样式, support indeterminate)
* **图标**: `.bim-tree-icon`
* **文本**: `.bim-tree-title`
* **子容器**: `.bim-tree-children` (padding-left 缩进)
## 6. 逻辑流程
1. **初始化**:
* 根据 `data` 递归创建 `BimTreeNode` 实例。
* 建立 `id -> Node` 的 Map 索引。
* 订阅主题和语言变更。
2. **联动逻辑 (Check Cascade)**:
* **向下**: 父节点状态变更 -> 递归强制设置所有子节点。
* **向上**: 子节点状态变更 -> 冒泡检查兄弟节点状态 -> 更新父节点 (Checked/Unchecked/Indeterminate)。
3. **事件**:
* 点击复选框 -> 更新状态 -> 触发联动 -> 发送 `ui:tree-node-check`
* 点击内容 -> 发送 `ui:tree-node-select`
## 7. 国际化支持
* 节点 `label` 必须是翻译键。
* 组件订阅 `localeManager`,语言变更时自动刷新文本。
## 8. 使用示例
```typescript
// 通过 ModelTreeManager 创建
const tree = engine.modelTree.createTree({
data: [
{ id: 'root', label: 'tree.root', children: [...] }
],
checkable: true
});
// 挂载到 DOM
document.body.appendChild(tree.element);
```

View File

@@ -1,305 +0,0 @@
# MeasurePanel 组件详细文档
> 本文档详细描述 `MeasurePanel`(测量面板)组件的实现细节,包括 API、UI 结构、逻辑流程等,供后续维护/AI 重现。
>
> 重要说明:**本组件仅实现 UI不实现真实测量算法**(不做拾取、画线、计算等)。测量结果通过对外方法注入,仅用于展示。
---
## 1. 组件概述
### 1.1 基本信息
- **组件名称**`MeasurePanel`
- **文件路径**`src/components/measure-panel/index.ts`
- **类型定义**`src/components/measure-panel/types.ts`
- **样式文件**`src/components/measure-panel/index.css`
- **实现接口**`IBimComponent`
### 1.2 在 SDK 中的位置
- `MeasurePanel` 是内部 UI 组件
-`MeasureDialogManager` 创建并挂载到 `BimDialog`
- 外部业务SDK 使用者)不直接 import 组件类,统一通过 `engine.measure`Manager调用
### 1.3 配置项(单位/精度)与缓存策略(新增)
- **创建 `MeasurePanel` 不传入单位/精度**
- 默认配置由组件内部维护:
- `unit`: `'mm'`
- `precision`: `2`(即 `0.00`
- 组件初始化时会读取缓存(`localStorage`
- key`bim-engine:measure:config`
- 若缓存存在且合法,则使用缓存值覆盖默认配置
- 若缓存不存在/解析失败,则使用默认配置
- 用户在设置面板点击“保存设置”后,组件会写入缓存
---
## 2. 组件类 API 文档
### 2.1 构造函数
```typescript
constructor(options?: MeasurePanelOptions)
```
**参数**
- `options.defaultMode?`: 默认测量方式(不传默认 `distance`
- `options.defaultExpanded?`: 是否默认展开(不传默认 `false`,即只显示前 4 个)
- `options.onModeChange?`: 用户切换测量方式时回调
- `options.onClearAll?`: 用户点击“删除全部”时回调
- `options.onSettings?`: 用户点击“设置”时回调
### 2.2 公共方法
#### `init(): void`
- 初始化订阅(主题/语言),并刷新 UI 状态(展开/选中态/结果区)。
#### `setTheme(theme: ThemeConfig): void`
- 将主题色映射到 CSS 变量按钮背景、hover、active、文字、分割线等
#### `setLocales(): void`
- 更新所有用户可见文本:
- 8 个按钮的 tooltip图标占位时 tooltip 是主要可读文本)
- 展开/收起按钮 tooltip
- “删除全部”文本
- “设置”tooltip
- “当前测量方式”显示文本
- 主值 label随模式变化
- X/Y/Z 标签
#### `destroy(): void`
- 取消主题/语言订阅并移除 DOM。
#### `getActiveMode(): MeasureMode`
- 获取当前选中的测量方式。
#### `switchMode(mode: MeasureMode): void`
- **切换类型的方法**:切换当前测量方式(等价于 `setActiveMode`)。
#### `setActiveMode(mode: MeasureMode): void`
- 设置当前测量方式,并触发 `onModeChange`(如果提供)。
#### `setResult(result: MeasureResult | null): void`
- 外部注入测量结果:
- `null`:清空显示(主值与 xyz 均显示 `--`
- 非 null根据当前 mode 显示对应字段的值
#### `clearAll(): void`
- 清空结果展示并触发 `onClearAll`(如果提供)。
#### `openSettings(): void`
- 进入组件内部“设置面板”(单位/精度选择)。
- 同时触发 `onSettings`(如果提供,作为外部监听)。
#### `getConfig(): MeasureConfig`
- 获取当前测量配置(单位/精度)。
#### `setConfig(partial: Partial<MeasureConfig>, persist = false): void`
- 更新配置:
- `persist=false`:仅更新内存,不写缓存
- `persist=true`:更新并写入 `localStorage`
#### `setExpanded(expanded: boolean): void`
- 展开/收起按钮区(收起时只显示前 4 个)。
#### `getExpanded(): boolean`
- 获取当前是否展开。
---
## 3. 分化组件说明
-
---
## 4. Manager API 文档(关联)
### 4.1 MeasureDialogManager关联文件
- `src/managers/measure-dialog-manager.ts`
### 4.2 相关对外方法(本次新增/补齐)
- `getActiveMode(): MeasureMode | null`
- `switchMode(mode: MeasureMode): void`
- `setResult(result: MeasureResult | null): void`
- `clearAll(): void`
- `openSettings(): void`
---
## 5. UI 详细描述
### 5.1 DOM 结构(核心)
```html
<div class="bim-measure-panel">
<!-- 主视图 -->
<div class="bim-measure-main">
<div class="bim-measure-tools">
<div class="bim-measure-tool-grid">
<!-- 8 个按钮:收起时隐藏后 4 个 -->
<button class="bim-measure-tool-btn is-active" data-mode="distance">
<span class="bim-measure-tool-icon">(圆形占位 svg</span>
</button>
<!-- ... -->
</div>
<div class="bim-measure-toggle">
<button class="bim-measure-toggle-btn">
<!-- 箭头 svg展开时旋转 180deg -->
</button>
</div>
</div>
<div class="bim-measure-result">
<div class="bim-measure-row">
<span class="label">当前测量方式:</span>
<span class="value">距离</span>
</div>
<div class="bim-measure-row">
<span class="label">距离:</span>
<span class="value">--</span>
</div>
<div class="bim-measure-xyz">
<div class="bim-measure-row"><span class="label">X</span><span class="value">--</span></div>
<div class="bim-measure-row"><span class="label">Y</span><span class="value">--</span></div>
<div class="bim-measure-row"><span class="label">Z</span><span class="value">--</span></div>
</div>
</div>
<div class="bim-measure-footer">
<button class="bim-measure-clear-btn">删除全部</button>
<button class="bim-measure-settings-btn">(齿轮 svg</button>
</div>
</div>
<!-- 设置视图(点击设置按钮进入) -->
<div class="bim-measure-settings">
<div class="bim-measure-settings-title">设置</div>
<div class="bim-measure-settings-row">
<div class="label">单位:</div>
<select class="bim-measure-settings-select">
<option value="m">m</option>
<option value="cm">cm</option>
<option value="mm">mm</option>
<option value="km">km</option>
</select>
</div>
<div class="bim-measure-settings-row">
<div class="label">精度:</div>
<select class="bim-measure-settings-select">
<option value="0">0</option>
<option value="1">0.0</option>
<option value="2">0.00</option>
<option value="3">0.000</option>
</select>
</div>
<div class="bim-measure-settings-actions">
<button class="bim-measure-settings-save">保存设置</button>
<button class="bim-measure-settings-cancel">取消</button>
</div>
</div>
</div>
```
### 5.2 CSS 类名说明
- `.bim-measure-tool-grid`: 4 列网格布局
- `.bim-measure-tool-btn.is-active`: 当前选中态
- `.bim-measure-toggle-btn.is-expanded`: 展开态(箭头旋转)
- `.bim-measure-result`: 结果区,顶部与底部用分割线隔开
---
## 6. 逻辑流程详细描述
### 6.1 初始化
- 构造函数创建 DOM但不订阅
- `init()`
- 订阅 `localeManager``themeManager`
- 调用 `setLocales()``setTheme()`
- 应用展开状态(隐藏/显示后 4 个按钮)
- 应用选中态
- 渲染结果(初始为 `--`
### 6.2 切换测量方式
- 点击按钮或调用 `switchMode/setActiveMode`
- 更新内部 `activeMode`
- 刷新按钮选中态
- 更新“当前测量方式”文本
- 更新主值 label
- 触发 `onModeChange`(若提供)
- 重新渲染结果
### 6.3 注入结果
- 调用 `setResult(result)`
- 保存 result 并刷新结果区:
- 主值:根据 mode 显示对应字段
- xyz无则显示 `--`
---
## 7. 国际化支持
### 7.1 使用的翻译键(核心)
- `measure.modes.*`8 种模式名
- `measure.actions.*`:展开/收起/删除全部/设置
- `measure.labels.*`当前方式、X/Y/Z、主值 label
- `measure.units.*`mm/°/m³/% 等单位
---
## 8. 主题支持
### 8.1 CSS 变量(核心)
- `--bim-measure-border`
- `--bim-measure-divider`
- `--bim-measure-btn-bg` / `--bim-measure-btn-hover-bg` / `--bim-measure-btn-active-bg`
- `--bim-measure-label-color` / `--bim-measure-value-color`
- `--bim-measure-icon-color`
---
## 9. 使用示例(通过 Manager
```typescript
// 通过工具栏点击“测量”按钮打开弹窗后:外部可以这样注入展示数据
engine.measure.switchMode('angle');
engine.measure.setResult({ angleDeg: 12.34, xyz: { x: 1, y: 2, z: 3 } });
// 清空
engine.measure.clearAll();
```
---
## 10. 实现细节(供 AI 重现)
### 10.1 关键点
- 模式列表严格按需求顺序渲染
- 收起时隐藏后 4 个按钮(通过 `style.display = 'none'`
- 图标占位统一用圆形 SVG
- 主值显示使用 `formatWithUnit`(单位走国际化)
---
## 11. 类型定义
`src/components/measure-panel/types.ts`
- `MeasureMode`
- `MeasureResult`
- `MeasurePanelOptions`
---
## 12. 文件清单
- `src/components/measure-panel/index.ts`
- `src/components/measure-panel/index.css`
- `src/components/measure-panel/types.ts`
- 关联:`src/managers/measure-dialog-manager.ts`

View File

@@ -1,594 +0,0 @@
# WalkControlPanel 组件详细文档
> 本文档详细描述 WalkControlPanel 组件的实现细节,包括 API、UI 结构、逻辑流程等,供 AI 根据文档重现组件。
---
## 1. 组件概述
### 1.1 基本信息
- **组件名称**: `WalkControlPanel`
- **文件路径**: `src/components/walk-control-panel/index.ts`
- **类型定义**: `src/components/walk-control-panel/types.ts`
- **样式文件**: `src/components/walk-control-panel/index.css`
- **实现接口**: `IBimComponent`
- **用途**: 渲染漫游控制面板,提供平面图切换、路径漫游、人物漫游三种模式,以及速度、重力、碰撞、角色模型、行走模式等设置。
- **特点**: 纯 UI 组件,通过回调函数通知外部状态变化,支持主题和国际化。
### 1.2 在 SDK 中的位置
- WalkControlPanel 组件通常由 `WalkControlManager` 管理(如果存在)
- 也可以单独实例化并挂载到任何容器中
- 用于 3D 场景的漫游控制界面
---
## 2. 组件类 API 文档
### 2.1 构造函数
```typescript
constructor(options: WalkControlPanelOptions = {})
```
**参数**:
- `options`: `WalkControlPanelOptions` - 漫游控制面板配置选项(可选)
**WalkControlPanelOptions 定义**:
```typescript
interface WalkControlPanelOptions {
/** 平面图切换回调 */
onPlanViewToggle?: (isActive: boolean) => void;
/** 路径漫游模式切换回调 */
onPathModeToggle?: (isActive: boolean) => void;
/** 漫游模式切换回调 */
onWalkModeToggle?: (isActive: boolean) => void;
/** 速度变化回调 */
onSpeedChange?: (speed: number) => void;
/** 重力切换回调 */
onGravityToggle?: (enabled: boolean) => void;
/** 碰撞切换回调 */
onCollisionToggle?: (enabled: boolean) => void;
/** 角色模型变化回调 */
onCharacterModelChange?: (model: CharacterModel) => void;
/** 行走模式变化回调 */
onWalkModeChange?: (mode: WalkMode) => void;
/** 退出回调 */
onExit?: () => void;
/** 默认速度 (1-10) */
defaultSpeed?: number;
/** 默认重力状态 */
defaultGravity?: boolean;
/** 默认碰撞状态 */
defaultCollision?: boolean;
/** 默认角色模型 */
defaultCharacterModel?: CharacterModel;
/** 默认行走模式 */
defaultWalkMode?: WalkMode;
}
```
**行为**:
- 创建面板 DOM 结构
- 保存配置选项
- 初始化状态
### 2.2 公共方法
#### `init(): void`
初始化组件(实现 `IBimComponent` 接口)
**功能**:
- 创建并渲染 DOM 结构
- 订阅语言变更:`localeManager.subscribe()`
- 订阅主题变更:`themeManager.subscribe()`
- 更新初始视图状态
#### `setTheme(theme: ThemeConfig): void`
设置主题(实现 `IBimComponent` 接口)
**参数**:
- `theme`: `ThemeConfig`
**功能**:
- 将主题颜色映射到 CSS 变量:
- `--bim-walk-control-bg``theme.panelBackground`
- `--bim-walk-btn-hover``theme.componentHover`
- `--bim-walk-btn-active``theme.componentActive`
- `--bim-primary-color``theme.primary`
- `--bim-primary-hover``theme.primaryHover`
- `--bim-icon-color``theme.icon`
- `--bim-text-color``theme.textPrimary`
- `--bim-divider-color``theme.border`
- 以及其他速度控制、下拉框相关的主题变量
#### `setLocales(): void`
设置语言(实现 `IBimComponent` 接口)
**功能**:
- 更新速度标签文本:`t('walkControl.speed')`
- 更新复选框标签:`t('walkControl.gravity')`, `t('walkControl.collision')`
- 更新角色模型下拉框选项和标签
- 更新行走模式下拉框选项和标签
- 更新退出按钮文本:`t('walkControl.exit')`
#### `destroy(): void`
销毁组件(实现 `IBimComponent` 接口)
**功能**:
- 取消语言和主题订阅
- 从 DOM 中移除自身元素
#### `setPlanViewActive(active: boolean): void`
设置平面图按钮激活状态
**参数**:
- `active`: boolean - 是否激活
**功能**:
- 更新内部状态
- 更新按钮视觉状态
#### `setPathModeActive(active: boolean): void`
设置路径模式按钮激活状态
**参数**:
- `active`: boolean - 是否激活
**功能**:
- 更新模式状态
- 更新按钮视觉状态
- 路径模式激活时禁用重力和碰撞选项
#### `getState(): WalkControlState`
获取当前状态
**返回**: `WalkControlState` - 当前控制面板状态的副本
---
## 3. 分化组件说明
**WalkControlPanel 没有子类或分化组件**
---
## 4. Manager API 文档
目前 WalkControlPanel 没有专门的 Manager,通常由使用方直接创建和管理。
未来可以创建 `WalkControlManager` 来统一管理该组件的实例。
---
## 5. UI 详细描述
### 5.1 DOM 结构
```html
<div class="walk-control-panel">
<!-- 左侧按钮区 -->
<div class="walk-control-left">
<button class="walk-icon-btn walk-icon-btn-plan-view [active]">
[平面图 SVG图标]
</button>
<button class="walk-icon-btn walk-icon-btn-path [active]">
[路径 SVG图标]
</button>
<button class="walk-icon-btn walk-icon-btn-walk [active]">
[漫游 SVG图标]
</button>
</div>
<!-- 分割线 -->
<div class="walk-divider"></div>
<!-- 中间设置区 -->
<div class="walk-control-settings">
<!-- 速度控件(路径模式显示) -->
<div class="walk-speed-control">
<label class="walk-speed-label">速度</label>
<div class="walk-speed-group">
<button class="walk-speed-btn">-</button>
<div class="walk-speed-display">1X</div>
<button class="walk-speed-btn">+</button>
</div>
</div>
<!-- 角色模型选择(漫游模式显示) -->
<div class="walk-select-wrapper walk-select-wrapper-character-model">
<label class="walk-select-label">角色模型</label>
<select class="walk-select walk-select-character-model">
<option value="construction-worker">建筑工人</option>
<option value="office-male">办公室男性</option>
</select>
</div>
<!-- 行走模式选择(漫游模式显示) -->
<div class="walk-select-wrapper walk-select-wrapper-walk-mode">
<label class="walk-select-label">行走模式</label>
<select class="walk-select walk-select-walk-mode">
<option value="walk">行走</option>
<option value="run">奔跑</option>
</select>
</div>
<!-- 重力复选框 -->
<label class="walk-checkbox-wrapper walk-checkbox-gravity">
<input type="checkbox" class="walk-checkbox" [disabled]>
<span class="walk-checkbox-label">重力</span>
</label>
<!-- 碰撞复选框 -->
<label class="walk-checkbox-wrapper walk-checkbox-collision">
<input type="checkbox" class="walk-checkbox" [disabled]>
<span class="walk-checkbox-label">碰撞</span>
</label>
</div>
<!-- 分割线 -->
<div class="walk-divider"></div>
<!-- 右侧退出按钮 -->
<button class="walk-exit-btn">退出</button>
</div>
```
### 5.2 CSS 类名和样式
#### `.walk-control-panel` (根容器)
- `display: flex`, `align-items: center`
- `gap: 20px`, `padding: 8px 16px`
- `background`: `var(--bim-walk-control-bg)`
- `border-radius: 8px`
- `user-select: none`
#### `.walk-icon-btn` (左侧图标按钮)
- `width: 48px`, `height: 48px`
- `padding: 8px` (实际图标 32x32)
- `display: flex`, `align-items: center`, `justify-content: center`
- `background: transparent`
- `border: 2px solid transparent`, `border-radius: 6px`
- `cursor: pointer`, `transition: all 0.2s`
- `color`: `var(--bim-icon-color)`
#### `.walk-icon-btn:hover`
- `background`: `var(--bim-walk-btn-hover)`
#### `.walk-icon-btn.active`
- `background`: `var(--bim-walk-btn-active)`
#### `.walk-icon-btn svg`
- `width: 32px`, `height: 32px`
#### `.walk-divider` (分割线)
- `width: 1px`, `height: 40px`
- `background`: `var(--bim-divider-color)`
- `flex-shrink: 0`
#### `.walk-speed-control` (速度控件)
- `display: flex`, `align-items: center`, `gap: 12px`
#### `.walk-speed-btn` (速度按钮)
- `width: 32px`, `height: 32px`
- `background`: `var(--bim-speed-btn-bg)`
- `border: none`, `border-radius: 4px`
- `cursor: pointer`
- `:disabled``opacity: 0.5`, `cursor: not-allowed`
#### `.walk-checkbox-wrapper` (复选框包装)
- `display: flex`, `align-items: center`, `gap: 8px`
- `cursor: pointer`
#### `.walk-select-wrapper` (下拉框包装)
- `display: flex`, `align-items: center`, `gap: 8px`
#### `.walk-exit-btn` (退出按钮)
- `padding: 10px 24px`
- `background`: `var(--bim-primary-color)`
- `border: none`, `border-radius: 6px`
- `color: #fff`, `font-size: 14px`, `font-weight: 600`
- `cursor: pointer`, `transition: all 0.2s`
---
## 6. 逻辑流程详细描述
### 6.1 初始化流程 (`init`)
1. **创建面板**(`createPanel`):
- 创建根容器 `.walk-control-panel`
- 创建左侧按钮区(`createLeftButtons`)
- 创建中间设置区(`createSettingsContainer`)
- 创建右侧退出按钮(`createExitButton`)
- 组装各部分并添加分割线
2. **订阅变更**:
- 订阅语言变更:`localeManager.subscribe(() => this.setLocales())`
- 订阅主题变更:`themeManager.subscribe((theme) => this.setTheme(theme))`
3. **初始设置**:
- 调用 `setLocales()` 设置初始文本
- 调用 `setTheme()` 应用当前主题
- 调用 `updateSettingsView()` 根据初始模式显示/隐藏控件
### 6.2 模式切换流程
#### 模式类型
- `'none'`: 无模式
- `'path'`: 路径漫游模式
- `'walk'`: 人物漫游模式
#### 模式切换逻辑 (`setMode`)
1. **保存旧模式**:用于触发关闭事件
2. **触发旧模式关闭事件**:
- 如果从 `'walk'` 切换出去,触发 `onWalkModeToggle(false)`
- 如果从 `'path'` 切换出去,触发 `onPathModeToggle(false)`
3. **更新模式状态**:`this.state.mode = newMode`
4. **特殊处理**:
- 路径模式时:禁用重力和碰撞复选框,并设置为 false
- 其他模式:启用重力和碰撞复选框
5. **更新视图**:
- `updateButtonStates()`: 更新按钮激活状态
- `updateSettingsView()`: 显示/隐藏相应的设置控件
- `updateSpeedButtonStates()`: 更新速度按钮禁用状态
### 6.3 设置视图更新 (`updateSettingsView`)
根据当前模式动态显示/隐藏控件:
**漫游模式 (`'walk'`)**:
- 隐藏速度控件
- 显示角色模型选择
- 显示行走模式选择
- 显示重力复选框
- 显示碰撞复选框
**路径模式 (`'path'`)****无模式 (`'none'`)**:
- 显示速度控件
- 隐藏角色模型选择
- 隐藏行走模式选择
- 显示重力复选框
- 显示碰撞复选框
### 6.4 图标管理
使用统一的图标管理器 (`icon-manager.ts`) 获取 SVG 图标:
```typescript
private getIconSVG(type: string): string {
const icons: Record<string, string> = {
'plan-view': getIcon('地图'), // 平面图/鸟瞰
'path': getIcon('地图'), // 路径漫游
'walk': getIcon('漫游') // 人物漫游
};
return icons[type] || '';
}
```
**重要**: 所有按钮图标尺寸统一为 32x32,与 toolbar 底部工具栏保持一致。
---
## 7. 国际化支持
### 7.1 使用的翻译键
```typescript
// 翻译键路径
'walkControl.speed' // 速度标签
'walkControl.gravity' // 重力标签
'walkControl.collision' // 碰撞标签
'walkControl.characterModel.label' // 角色模型标签
'walkControl.characterModel.constructionWorker' // 建筑工人
'walkControl.characterModel.officeMale' // 办公室男性
'walkControl.walkMode.label' // 行走模式标签
'walkControl.walkMode.walk' // 行走
'walkControl.walkMode.run' // 奔跑
'walkControl.exit' // 退出按钮
```
### 7.2 实现方式
- 使用 `src/services/locale.ts` 中的 `t()` 函数
- 组件初始化时订阅语言变更,变更发生时更新所有文本 (`setLocales`)
- 动态创建的下拉框选项也使用 `t()` 函数获取翻译文本
---
## 8. 主题支持
### 8.1 变量映射
`setTheme` 中设置内联样式变量:
- `--bim-walk-control-bg`: 面板背景色
- `--bim-walk-btn-hover`: 按钮悬停背景
- `--bim-walk-btn-active`: 按钮激活背景
- `--bim-primary-color`: 主色(退出按钮)
- `--bim-primary-hover`: 主色悬停
- `--bim-icon-color`: 图标颜色
- `--bim-text-color`: 文本颜色
- `--bim-divider-color`: 分割线颜色
- `--bim-speed-group-bg`: 速度组背景
- `--bim-speed-btn-bg`: 速度按钮背景
- `--bim-speed-btn-hover`: 速度按钮悬停
- `--bim-select-bg`: 下拉框背景
- `--bim-select-border`: 下拉框边框
- `--bim-select-option-bg`: 下拉框选项背景
---
## 9. 使用示例
### 9.1 基本使用
```typescript
import { WalkControlPanel } from './components/walk-control-panel';
// 创建实例
const walkPanel = new WalkControlPanel({
defaultSpeed: 1,
defaultGravity: false,
defaultCollision: false,
onPlanViewToggle: (isActive) => {
console.log('平面图模式:', isActive);
},
onPathModeToggle: (isActive) => {
console.log('路径模式:', isActive);
// 启用/禁用路径漫游功能
},
onWalkModeToggle: (isActive) => {
console.log('漫游模式:', isActive);
// 启用/禁用人物漫游功能
},
onSpeedChange: (speed) => {
console.log('速度:', speed);
// 更新漫游速度
},
onGravityToggle: (enabled) => {
console.log('重力:', enabled);
},
onCollisionToggle: (enabled) => {
console.log('碰撞:', enabled);
},
onCharacterModelChange: (model) => {
console.log('角色模型:', model);
},
onWalkModeChange: (mode) => {
console.log('行走模式:', mode);
},
onExit: () => {
console.log('退出漫游');
// 销毁面板,退出漫游模式
}
});
// 初始化
walkPanel.init();
// 挂载到 DOM
document.body.appendChild(walkPanel.element);
```
### 9.2 外部控制状态
```typescript
// 外部设置平面图激活状态
walkPanel.setPlanViewActive(true);
// 外部设置路径模式激活状态
walkPanel.setPathModeActive(true);
// 获取当前状态
const state = walkPanel.getState();
console.log('当前模式:', state.mode);
console.log('当前速度:', state.speed);
```
---
## 10. 实现细节(供 AI 重现)
### 10.1 状态管理
组件内部维护一个状态对象 `WalkControlState`:
```typescript
private state: WalkControlState = {
mode: 'none', // 当前模式
isPlanViewActive: false, // 平面图是否激活
speed: 1, // 移动速度 1-10
gravity: false, // 重力是否启用
collision: false, // 碰撞是否启用
characterModel: 'construction-worker', // 角色模型
walkMode: 'walk' // 行走模式
};
```
状态变更时:
1. 更新内部状态
2. 更新 UI 视觉效果
3. 触发对应的回调函数
### 10.2 模式互斥逻辑
三个按钮(平面图、路径、漫游)中:
- **平面图**是独立的开关,不影响模式
- **路径**和**漫游**是互斥的,激活一个会关闭另一个
- 点击已激活的模式按钮会关闭该模式(回到 `'none'`)
### 10.3 路径模式的特殊处理
当进入路径模式时:
1. 自动禁用并取消勾选重力和碰撞
2. 禁用重力和碰撞复选框(设置 `disabled` 属性)
3. 退出路径模式时重新启用这两个复选框
### 10.4 速度控制
- 速度范围: 1-10
- 减速按钮在速度为 1 时禁用
- 加速按钮在速度为 10 时禁用
- 速度显示格式: `${speed}X`
### 10.5 下拉框动态创建
角色模型和行走模式的下拉框选项在 `setLocales()` 中动态创建:
1. 清空现有选项:`select.innerHTML = ''`
2. 创建新选项,使用 `t()` 获取翻译文本
3. 根据当前状态设置选中项
这样可以确保语言切换时下拉框文本也会更新。
---
## 11. 类型定义
### 11.1 WalkControlMode
```typescript
export type WalkControlMode = 'none' | 'path' | 'walk';
```
### 11.2 CharacterModel
```typescript
export type CharacterModel = 'office-male' | 'construction-worker';
```
### 11.3 WalkMode
```typescript
export type WalkMode = 'walk' | 'run';
```
### 11.4 WalkControlState
```typescript
export interface WalkControlState {
mode: WalkControlMode;
isPlanViewActive: boolean;
speed: number;
gravity: boolean;
collision: boolean;
characterModel: CharacterModel;
walkMode: WalkMode;
}
```
### 11.5 WalkControlPanelOptions
见 2.1 节
---
## 12. 文件清单
- `src/components/walk-control-panel/index.ts`: 组件核心逻辑
- `src/components/walk-control-panel/index.css`: 组件样式
- `src/components/walk-control-panel/types.ts`: 类型定义
---
## 13. 更新记录
| 日期 | 修改内容 | 修改人 |
| ---------- | ----------------------------------------- | ------------ |
| 2025-12-25 | 创建漫游控制面板组件文档,集成图标管理器 | AI Assistant |

View File

@@ -1,318 +0,0 @@
# Menu 组件详细文档
> 本文档详细描述 Menu 组件的实现细节,包括 API、UI 结构、逻辑流程等,供 AI 根据文档重现组件。
---
## 1. 组件概述
### 1.1 基本信息
- **组件名称**: `BimMenu`
- **文件路径**: `src/components/menu/index.ts`
- **类型定义**: `src/components/menu/item.ts` (配置), `src/components/menu/types.ts` (选项)
- **样式文件**: `src/components/menu/index.css`
- **实现接口**: `IBimComponent`, `IRightKeyContent`
- **用途**: 渲染一组菜单项,支持分组、排序、图标、快捷键提示和无限级递归子菜单。
- **特点**: 纯数据驱动(基于 `MenuItemConfig`),内置国际化支持。
### 1.2 在 SDK 中的位置
- Menu 组件主要由 `RightKeyManager` 使用,作为右键菜单的内容载体。
- 也可以单独实例化并挂载到任何容器中。
- `RightKeyManager` 位于 `src/managers/right-key-manager.ts`
---
## 2. 组件类 API 文档
### 2.1 构造函数
```typescript
constructor(options: MenuOptions)
```
**参数**:
- `options`: `MenuOptions` - 菜单配置选项
**MenuOptions 定义**:
```typescript
interface MenuOptions {
items: MenuItemConfig[]; // 菜单项配置列表
groupOrder?: string[]; // 分组显示顺序
}
```
**行为**:
- 创建根元素 `<ul>`
- 保存配置选项
### 2.2 公共方法
#### `init(): void`
初始化组件(实现 `IBimComponent` 接口)
**功能**:
- 调用 `render()` 渲染 DOM 结构
- 订阅语言变更:`localeManager.subscribe()`
- 订阅主题变更:`themeManager.subscribe()`(虽然目前主要通过 CSS 变量,但保留订阅以备扩展)
#### `setTheme(theme: ThemeConfig): void`
设置主题(实现 `IBimComponent` 接口)
**参数**:
- `theme`: `ThemeConfig`
**功能**:
- 将主题颜色映射到 CSS 变量
- `--bim-ui_bg_color``theme.panelBackground`
- `--bim-ui_text_primary``theme.textPrimary`
- `--bim-ui_border_color``theme.border`
- `--bim-ui_bg_hover``theme.componentHover`
#### `setLocales(): void`
设置语言(实现 `IBimComponent` 接口)
**功能**:
- 清空当前 DOM
- 重新调用 `render()`,使 `t()` 函数获取最新的翻译文本
#### `destroy(): void`
销毁组件(实现 `IBimComponent`, `IRightKeyContent` 接口)
**功能**:
- 取消语言和主题订阅
- 关闭并销毁所有打开的子菜<E5AD90><E88F9C>
- 从 DOM 中移除自身元素
#### `getElement(): HTMLElement`
获取组件根元素(实现 `IRightKeyContent` 接口)
**返回**: `HTMLElement` (根 `<ul>` 元素)
**功能**:
- 允许父容器(如 `BimRightKey`)获取并挂载此菜单
---
## 3. 分化组件说明
**BimMenu 没有子类或分化组件**。它通过递归自身来实现多级菜单。
---
## 4. Manager API 文档
参见 [RightKey 组件文档](./right-key.md) 中的 `RightKeyManager``BimMenu` 本身是一个纯 UI 组件,通常不直接通过 Manager 管理,而是被 `RightKeyManager` 动态创建和销毁。
---
## 5. UI 详细描述
### 5.1 DOM 结构
```html
<ul class="bim-menu">
<!-- 分组 1 -->
<li class="bim-menu-item [disabled]">
<div class="bim-menu-item-icon">[SVG图标]</div>
<div class="bim-menu-item-label">菜单文本</div>
<!-- 如果有子菜单 -->
<div class="bim-menu-item-arrow">
<svg>...</svg> <!-- 右箭头 -->
</div>
</li>
<!-- 分组分割线 -->
<li class="bim-menu-divider"></li>
<!-- 分组 2 -->
<li class="bim-menu-item">...</li>
</ul>
```
### 5.2 CSS 类名和样式
#### `.bim-menu` (根容器)
- `display: flex`, `flex-direction: column`
- `background`: `var(--bim-ui_bg_color)`
- `border-radius: 4px`
- `padding: 4px 0`, `margin: 0`, `list-style: none` (关键:移除列表默认样式)
- `min-width: 160px`
- `box-shadow`: `0 4px 12px rgba(0,0,0,0.2)`
- `user-select: none`
#### `.bim-menu-item` (菜单项)
- `display: flex`, `align-items: center`
- `padding: 6px 12px`
- `cursor: pointer`
- `position: relative`
- `color`: `var(--bim-ui_text_primary)`
#### `.bim-menu-item:hover`
- `background-color`: `var(--bim-ui_bg_hover)`
#### `.bim-menu-item.disabled`
- `opacity: 0.5`
- `cursor: not-allowed`
- `pointer-events: none`
#### `.bim-menu-divider` (分割线)
- `height: 1px`
- `background-color`: `var(--bim-ui_border_color)`
- `margin: 4px 0`
---
## 6. 逻辑流程详细描述
### 6.1 渲染流程 (`render`)
1. **数据分桶**: 遍历 `items`,根据 `item.group` (默认为 'default') 将菜单项放入不同的数组中。
2. **确定顺序**:
- 如果提供了 `groupOrder`,优先按其顺序排列组。
- 未指定的组按遍历顺序追加。
3. **渲染分组**:
- 遍历排序后的组键。
- 如果不是第一组,先插入一个 `.bim-menu-divider`
- 获取组内项,根据 `item.order` 升序排序。
- 遍历组内项,如果 `item.visible !== false`,调用 `createItemElement` 创建 DOM。
### 6.2 交互流程
#### 点击事件
- **绑定**: 在 `.bim-menu-item` 上绑定 `click`
- **逻辑**:
- `e.stopPropagation()`: 阻止冒泡。
- 检查 `!hasChildren`: 只有叶子节点才触发点击。
- 调用 `item.onClick?.()`
- **注意**: 点击后,通常期望菜单关闭。这依赖于 `RightKeyManager` 或外部逻辑(通过 `onClick` 内部调用关闭,或者点击触发全局关闭)。
#### 子菜单展开 (`mouseenter`)
- **触发**: 鼠标移入带有子项的菜单项。
- **逻辑**:
- 调用 `closeSubMenu()` 关闭当前可能已打开的其他子菜单。
- 计算位置:通常位于父项右侧 (`rect.right`),顶部对齐 (`rect.top`)。
- 创建子菜单容器 `div` (fixed 定位, z-index 10001)。
- **关键修复**: 在容器上绑定 `mousedown``stopPropagation`,防止触发 `BimRightKey` 的全局关闭逻辑。
- 实例化新的 `BimMenu` (递归) 并 `init()`
- 将新菜单挂载到容器,容器挂载到 `document.body`
- 保存引用到 `this.activeSubMenu`
- **边界检测**: 如果右侧超出屏幕,改为向左展开。
#### 子菜单关闭
- **触发**: 鼠标移入**不带**子项的菜单项。
- **逻辑**: 调用 `closeSubMenu()`,销毁实例并移除 DOM。
---
## 7. 国际化支持
### 7.1 实现方式
- 使用 `src/services/locale.ts` 中的 `t()` 函数。
-`createItemElement` 中,直接使用 `t(item.label)` 设置文本。
- 组件初始化时订阅语言变更,变更发生时清空并重绘 (`setLocales` -> `render`)。
---
## 8. 主题支持
### 8.1 变量映射
-`setTheme` 中设置内联样式变量:
- `--bim-ui_bg_color`: 面板背景
- `--bim-ui_text_primary`: 主要文字
- `--bim-ui_border_color`: 边框/分割线
- `--bim-ui_bg_hover`: 悬停背景
---
## 9. 使用示例
### 9.1 数据结构示例
```typescript
const menuItems: MenuItemConfig[] = [
{
id: 'view',
label: 'menu.view', // 翻译键
group: 'view',
children: [
{
id: 'home',
label: 'menu.home',
onClick: () => console.log('Go Home')
}
]
},
{
id: 'delete',
label: 'menu.delete',
group: 'edit',
icon: '<svg>...</svg>',
onClick: () => console.log('Delete')
}
];
```
### 9.2 手动创建(通常不直接使用,而是通过 RightKeyManager
```typescript
const menu = new BimMenu({ items: menuItems });
menu.init();
document.body.appendChild(menu.getElement());
```
---
## 10. 实现细节(供 AI 重现)
### 10.1 递归设计
组件自身不包含递归渲染 DOM 的逻辑,而是通过“交互触发”来递归实例化。即:子菜单**不是**一开始就渲染在 DOM 树中的,而是当用户鼠标悬停时,动态创建一个新的 `BimMenu` 实例并挂载到 `body` 上。这种设计避免了深层嵌套 DOM 带来的样式问题(如 `overflow: hidden` 截断子菜单)。
### 10.2 事件冲突处理
由于子菜单挂载在 `body` 上,点击子菜单在 DOM 树看来是“点击了主菜单外部”。这会触发 `BimRightKey` 的“点击外部关闭”逻辑。
**解决方案**: 子菜单容器监听 `mousedown``stopPropagation()`,欺骗 `BimRightKey` 认为点击没有发生(或者发生在内部)。
---
## 11. 类型定义
### 11.1 MenuItemConfig
```typescript
interface MenuItemConfig {
id: string;
label: string;
onClick?: () => void;
icon?: string; // SVG 字符串
group?: string; // 分组标识
order?: number; // 排序权重
children?: MenuItemConfig[];
disabled?: boolean;
visible?: boolean;
}
```
### 11.2 MenuOptions
```typescript
interface MenuOptions {
items: MenuItemConfig[];
groupOrder?: string[];
}
```
---
## 12. 文件清单
- `src/components/menu/index.ts`: 组件核心逻辑
- `src/components/menu/index.css`: 组件样式
- `src/components/menu/item.ts`: 仅包含类型定义 (`MenuItemConfig`)
- `src/components/menu/types.ts`: 组件选项接口
---
## 13. 更新记录
| 日期 | 修改内容 | 修改人 |
| ---------- | ----------------------------------------- | ------------ |
| 2024-XX-XX | 重构为纯配置对象驱动,移除 BimMenuItem 类 | AI Assistant |

View File

@@ -1,423 +0,0 @@
# Icon Manager 图标管理器文档
> 本文档详细描述 Icon Manager 图标管理工具的实现细节,包括 API、使用方式、图标清单等,供 AI 根据文档重现功能。
---
## 1. 工具概述
### 1.1 基本信息
- **工具名称**: `IconManager`
- **文件路径**: `src/utils/icon-manager.ts`
- **导出函数**: `getIcon(name: string): string`
- **用途**: 统一管理所有 SVG 图标资源,提供通过名称获取图标的接口
- **特点**: 单一职责,简单易用,支持默认图标回退
### 1.2 设计目标
- **统一管理**: 所有图标集中在一个文件中管理,避免重复和不一致
- **简化使用**: 通过简单的函数调用获取图标,无需在各处重复 SVG 代码
- **类型安全**: SVG 作为字符串存储,可直接用于 innerHTML 或图标属性
- **容错机制**: 当请求的图标不存在时,返回默认图标而不是报错
---
## 2. API 文档
### 2.1 getIcon 函数
```typescript
function getIcon(name: string): string
```
**参数**:
- `name`: string - 图标名称(中文)
**返回值**:
- string - SVG 图标的完整字符串
**行为**:
1.`ICONS` 对象中查找对应名称的图标
2. 如果找到,返回该图标的 SVG 字符串
3. 如果未找到,在控制台输出警告并返回默认图标
**示例**:
```typescript
import { getIcon } from '../../utils/icon-manager';
// 获取测量图标
const measureIcon = getIcon('测量');
// 获取不存在的图标(会返回默认图标并警告)
const unknownIcon = getIcon('不存在的图标');
// 控制台输出: [IconManager] Icon "不存在的图标" not found, using default icon
```
---
## 3. 图标清单
### 3.1 来自 assets 的图标 (48x48)
这些图标从 `src/assets/` 目录中的 SVG 文件简化而来,用于 toolbar 和主要功能按钮:
| 图标名称 | 用途 | 原始文件 |
|---------|------|----------|
| 测量 | 测量工具 | 测量.svg |
| 地图 | 地图视图/平面图 | 地图.svg |
| 框选放大 | 框选缩放 | 框选放大.svg |
| 漫游 | 漫游模式 | 漫游.svg |
| 目录树 | 构件树 | 目录树.svg |
| 剖切 | 剖切菜单 | 剖切.svg |
| 剖切盒 | 剖切盒 | 剖切盒.svg |
| 全屏 | 全屏模式 | 全屏.svg |
| 设置 | 设置面板 | 设置.svg |
| 拾曲面剖切 | 拾取曲面剖切 | 拾曲面剖切.svg |
| 轴向剖切 | 轴向剖切 | 轴向剖切.svg |
| 主视角 | 主视角/首页 | 主视角.svg |
| 文档 | 文档/属性 | 地图 1.svg |
### 3.2 测量相关图标 (32x32)
用于测量面板的各种测量方式:
| 图标名称 | 用途 |
|---------|------|
| 标高 | 标高测量 |
| 距离 | 距离测量 |
| 最小距离 | 最小距离测量 |
| 激光边距 | 激光边距测量 |
| 角度 | 角度测量 |
| 坡度 | 坡度测量 |
| 体积 | 体积测量 |
| 空间体积 | 空间体积测量 |
### 3.3 通用图标 (24x24)
常用的 UI 图标:
| 图标名称 | 用途 |
|---------|------|
| close | 关闭 |
| check | 勾选/确认 |
| warning | 警告 |
| error | 错误 |
| success | 成功 |
| plus | 加号/新增 |
| minus | 减号/删除 |
| arrowUp | 向上箭头 |
| arrowDown | 向下箭头 |
| arrowLeft | 向左箭头 |
| arrowRight | 向右箭头 |
| search | 搜索 |
| refresh | 刷新 |
| delete | 删除 |
| edit | 编辑 |
| save | 保存 |
| expand | 展开 |
| collapse | 收起 |
### 3.4 默认图标
| 图标名称 | 用途 |
|---------|------|
| default | 当请求的图标不存在时返回 |
---
## 4. 使用场景
### 4.1 在按钮配置中使用
```typescript
import { getIcon } from '../../../utils/icon-manager';
export const createMeasureButton = (engine: BimEngine): ButtonConfig => {
return {
id: 'measure',
groupId: 'group-1',
type: 'button',
label: 'toolbar.measure',
icon: getIcon('测量'), // 使用图标管理器
onClick: () => {
engine.measure?.show();
}
};
};
```
### 4.2 在组件中使用
```typescript
import { getIcon } from '../../utils/icon-manager';
export class WalkControlPanel implements IBimComponent {
private getIconSVG(type: string): string {
const icons: Record<string, string> = {
'plan-view': getIcon('地图'),
'path': getIcon('地图'),
'walk': getIcon('漫游')
};
return icons[type] || '';
}
private createIconButton(type: string, onClick: () => void): HTMLButtonElement {
const btn = document.createElement('button');
btn.className = `walk-icon-btn walk-icon-btn-${type}`;
btn.innerHTML = this.getIconSVG(type);
btn.addEventListener('click', onClick);
return btn;
}
}
```
### 4.3 在树节点中使用
```typescript
import { getIcon } from '../../utils/icon-manager';
const treeData: TreeNodeConfig[] = [
{
id: 'level-1',
label: '一层',
icon: getIcon('目录树'),
children: [...]
}
];
```
---
## 5. SVG 简化规范
### 5.1 简化原则
从 assets 目录中的 SVG 文件简化时,遵循以下原则:
1. **保留核心路径**:只保留 `<path>` 元素及其 `d` 属性
2. **移除冗余属性**:删除 `<defs>`, `<clipPath>`, `<style>`, `<g>` 等非必要元素
3. **保持颜色属性**:保留 `fill="currentColor"` 以支持主题颜色
4. **统一尺寸**:根据用途设置合适的 `viewBox``width/height`
5. **简化变换**:如果有 `transform`,尽量简化或合并到路径中
### 5.2 简化示例
**原始 SVG (从 assets/测量.svg)**:
```xml
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48" viewBox="0 0 48 48">
<defs>
<clipPath id="clip-path">
<rect width="48" height="48" fill="none"/>
</clipPath>
</defs>
<g id="测量" clip-path="url(#clip-path)">
<path d="M0,28.207H3.429v3.526H44.571V28.207H48v7.052H0ZM0,3.526v14.1a3.478,3.478,0,0,0,3.429,3.526H6.857V17.629h3.429v3.526h3.429V17.629h3.429v3.526h3.429V10.578H24V21.155h3.429V17.629h3.429v3.526h3.429V17.629h3.429v3.526h3.429V10.578h3.429V21.155A3.478,3.478,0,0,0,48,17.629V3.526A3.478,3.478,0,0,0,44.571,0H3.429A3.478,3.478,0,0,0,0,3.526Z" transform="translate(0 5.456)" fill="#000"/>
</g>
</svg>
```
**简化后 (在 icon-manager.ts 中)**:
```typescript
测量: '<svg width="48" height="48" viewBox="0 0 48 48"><path fill="currentColor" d="M0,28.207H3.429v3.526H44.571V28.207H48v7.052H0ZM0,3.526v14.1a3.478,3.478,0,0,0,3.429,3.526H6.857V17.629h3.429v3.526h3.429V17.629h3.429v3.526h3.429V10.578H24V21.155h3.429V17.629h3.429v3.526h3.429V17.629h3.429v3.526h3.429V10.578h3.429V21.155A3.478,3.478,0,0,0,48,17.629V3.526A3.478,3.478,0,0,0,44.571,0H3.429A3.478,3.478,0,0,0,0,3.526Z" transform="translate(0 5.456)"/></svg>',
```
**关键变化**:
- 移除了 `xmlns`, `xmlns:xlink`, `<defs>`, `<clipPath>`, `<g>`
-`fill="#000"` 改为 `fill="currentColor"` 以支持主题
- 保留了核心的 `<path>``viewBox`
---
## 6. 图标尺寸规范
### 6.1 尺寸分类
根据使用场景,图标分为三种尺寸:
1. **48x48**: 主要功能按钮(toolbar、工具栏)
2. **32x32**: 测量工具图标
3. **24x24**: 通用 UI 图标(关闭、展开等)
### 6.2 与 CSS 的配合
图标尺寸在 CSS 中可以被覆盖:
```css
/* Toolbar 按钮图标 */
.opt-btn-icon svg {
width: 100%;
height: 100%;
}
/* 漫游控制面板按钮图标 */
.walk-icon-btn svg {
width: 32px;
height: 32px;
}
```
实际显示尺寸由 CSS 控制,SVG 的 `width``height` 属性主要用于:
1. 定义默认尺寸
2. 计算宽高比
3. 作为 `viewBox` 的参考
---
## 7. 添加新图标
### 7.1 添加流程
1. **准备 SVG 文件**:
- 将新图标文件放到 `src/assets/` 目录
- 或直接获取 SVG 代码
2. **简化 SVG**:
- 按照 5.1 节的简化原则处理 SVG
- 确保 `fill="currentColor"` 以支持主题
- 设置合适的尺寸
3. **添加到 ICONS 对象**:
```typescript
const ICONS: Record<string, string> = {
// ... 现有图标
新图标名称: '<svg width="48" height="48" viewBox="0 0 48 48"><path fill="currentColor" d="..."/></svg>',
};
```
4. **使用新图标**:
```typescript
const icon = getIcon('新图标名称');
```
### 7.2 命名规范
- **使用中文名称**:便于理解和维护
- **描述功能**:名称应清晰描述图标用途
- **避免重复**:检查是否已存在类似图标
**推荐命名**:
- ✅ 好的命名: `测量`, `地图`, `全屏`, `剖切盒`
- ❌ 不好的命名: `icon1`, `svg2`, `btn_icon`
---
## 8. 实现细节(供 AI 重现)
### 8.1 核心数据结构
```typescript
const ICONS: Record<string, string> = {
// key: 图标名称(中文)
// value: SVG 字符串(完整的 <svg>...</svg>)
};
```
### 8.2 获取逻辑
```typescript
export function getIcon(name: string): string {
const icon = ICONS[name];
if (!icon) {
console.warn(`[IconManager] Icon "${name}" not found, using default icon`);
return ICONS.default;
}
return icon;
}
```
**关键点**:
1. 简单的字典查找
2. 未找到时返回默认图标而不是 `undefined`
3. 在控制台输出警告信息,便于调试
### 8.3 为什么不使用枚举?
设计选择:使用字符串而不是 TypeScript 枚举
**原因**:
1. **简化使用**:直接传中文字符串,更直观
2. **灵活性**:可以动态添加图标而不需要修改类型定义
3. **国际化友好**:中文名称与 UI 文案保持一致
4. **降低耦合**:使用方不需要导入枚举类型
---
## 9. 与主题系统的配合
### 9.1 currentColor 属性
所有图标使用 `fill="currentColor"`,这样图标颜色会继承父元素的 `color` 属性:
```typescript
测量: '<svg width="48" height="48" viewBox="0 0 48 48"><path fill="currentColor" d="..."/></svg>',
```
### 9.2 在 CSS 中控制颜色
```css
.opt-btn-icon {
color: var(--bim-icon-color, #ccc);
}
.opt-btn-icon svg {
fill: currentColor; /* 继承父元素的 color */
}
```
### 9.3 主题变更时的自动适配
当主题颜色变更时,只需更新 CSS 变量,所有图标会自动适配新颜色:
```typescript
// 在 setTheme 中
element.style.setProperty('--bim-icon-color', theme.icon ?? '#ccc');
// 所有使用 currentColor 的图标自动更新颜色
```
---
## 10. 最佳实践
### 10.1 使用建议
**✅ 推荐做法**:
```typescript
// 在组件顶部导入
import { getIcon } from '../../utils/icon-manager';
// 在需要时调用
const icon = getIcon('测量');
button.innerHTML = icon;
```
**❌ 避免做法**:
```typescript
// 不要在组件中硬编码 SVG
const icon = '<svg>...</svg>'; // 不推荐
// 不要重复定义相同的图标
const myIcon = '<svg>...</svg>'; // 应该使用 getIcon()
```
### 10.2 性能考虑
- **字符串查找很快**:`ICONS` 对象的查找是 O(1) 操作
- **无需缓存**:图标字符串很小,不需要额外的缓存机制
- **按需使用**:只在需要时调用 `getIcon()`,不需要预加载所有图标
### 10.3 维护建议
1. **定期整理**:删除不再使用的图标
2. **统一风格**:新增图标应与现有图标风格一致
3. **文档同步**:添加新图标时更新本文档的图标清单
4. **命名规范**:使用清晰、一致的中文命名
---
## 11. 更新记录
| 日期 | 修改内容 | 修改人 |
| ---------- | ----------------------------------------- | ------------ |
| 2025-12-25 | 创建图标管理器,整合所有 SVG 图标资源 | AI Assistant |
| 2025-12-25 | 添加"文档"图标,替换所有 toolbar 按钮图标 | AI Assistant |

View File

@@ -1,805 +0,0 @@
# 3D Viewer 工具栏实现文档
> **文档说明**: 本文档为 3D Viewer 工具栏的实现规范,包含 UI 设计规格、交互逻辑和参考代码片段,供开发人员复现和实现。
**版本**: v1.0
**更新日期**: 2026-01-16
**技术栈**: React + TypeScript + Ant Design + Tailwind CSS + Framer Motion
---
## 目录
1. [整体布局概览](#1-整体布局概览)
2. [顶部中间工具栏](#2-顶部中间工具栏)
3. [底部右侧工具栏](#3-底部右侧工具栏)
4. [Hover 二级菜单](#4-hover-二级菜单)
5. [右侧抽屉面板](#5-右侧抽屉面板)
6. [状态管理](#6-状态管理)
7. [视觉规范速查表](#7-视觉规范速查表)
8. [组件清单](#8-组件清单)
---
## 1. 整体布局概览
### 1.1 ASCII 原型图
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ 顶栏:项目名/状态 | 标签栏:工作台 | 建筑主体.rvt | ... | 成员/协同/分享 │
├────┬────────────────────────────────────────────────────────────────────────┤
│导航│ 二级面板(已载明,默认展开,可折叠) │
│ ├────────────────────────────────────────────────────────────────────────┤
│ │ 主工作区Viewer │
│ │ │
│ │ ① 顶部中间工具(面板/列表开关,水平居中浮层) │
│ │ ┌────────────────────────────────────────────────┐ │
│ │ │ 📋 视图列表 📄 属性 🔍 过滤 📌 图钉 ⤢ 全屏 │ │
│ │ └────────────────────────────────────────────────┘ │
│ │ │
│ │ [ 3D Viewer 渲染区域(加载中显示文件卡片占位) ] │
│ │ - 右侧抽屉:视图树/属性/过滤/图钉面板(点击顶部按钮打开/收起) │
│ │ │
│ │ ② 底部右侧工具(模型交互,圆形按钮)│
│ │ ┌──────────────┐ │
│ │ │ 🧊 主视图 │ │
│ │ │ 📏 测量 │ │
│ │ │ 🪟 剖切 │ │
│ │ │ 🚶 漫游 │ │
│ │ │ ⚙️ 环境设置 │ │
│ │ └──────────────┘ │
│ └────────────────────────────────────────────────────────────────────────┘
└────┴────────────────────────────────────────────────────────────────────────┘
```
### 1.2 工具栏分区说明
**① 顶部中间工具栏 (面板/列表开关)**:
- **位置**: 水平居中,顶部浮动
- **功能**: 控制右侧抽屉面板的打开/关闭
- **按钮数量**: 5个视图列表、属性、过滤、图钉、全屏
- **交互特点**: 开关型按钮,同时只允许一个面板高亮
**② 底部右侧工具栏 (模型交互工具)**:
- **位置**: 右下角固定
- **功能**: 模型交互工具集
- **按钮数量**: 5个主视图、测量、剖切、漫游、环境设置
- **交互特点**: 圆形按钮Hover 显示二级菜单,支持模式互斥
---
## 2. 顶部中间工具栏
### 2.1 布局示意
```
┌────────────────────────────────────────────────┐
│ 📋 视图列表 📄 属性 🔍 过滤 📌 图钉 ⤢ 全屏 │
└────────────────────────────────────────────────┘
```
### 2.2 容器规范
| 属性 | 值 | Tailwind CSS |
|------|---|-------------|
| **定位** | 顶部居中浮动 | `absolute top-3 left-1/2 -translate-x-1/2` |
| **层级** | 800 | `z-[800]` |
| **背景** | 白色半透明 | `bg-white/80 dark:bg-slate-800/80` |
| **毛玻璃** | 10px 模糊 | `backdrop-blur-xl` |
| **圆角** | 6px | `rounded-md` |
| **阴影** | 轻微阴影 | `shadow-[0_4px_24px_rgba(0,0,0,0.04)]` |
| **边框** | 半透明边框 | `border border-slate-200/50 dark:border-slate-700/50` |
| **内边距** | 水平 6px, 垂直 6px | `px-1.5 py-1.5` |
| **布局** | 水平弹性布局 | `flex items-center gap-1.5` |
### 2.3 按钮规范
#### 通用样式
| 属性 | 值 | Tailwind CSS |
|------|---|-------------|
| **尺寸** | 不固定(自适应内容) | `px-2 py-2 lg:px-3 lg:py-2` |
| **圆角** | 6px | `rounded-md` |
| **图标大小** | 18px | `size={18}` |
| **字体** | 12px 中等字重 | `text-xs font-medium` |
| **过渡** | 200ms 平滑过渡 | `transition-all duration-200` |
#### 按钮状态
**默认状态**:
```css
text-slate-600 dark:text-slate-400
hover:bg-slate-100/60 dark:hover:bg-slate-700/60
```
**激活态** (对应面板已打开):
```css
bg-blue-500 dark:bg-blue-600
text-white
shadow-md
border border-blue-600 dark:border-blue-500
```
### 2.4 按钮列表
| 序号 | 按钮名称 | 图标 | 功能说明 | 面板类型 |
|------|---------|------|---------|---------|
| 1 | 视图列表 | `List` (lucide-react) | 打开视图树面板,显示 3D/2D 视角列表 | `views` |
| 2 | 属性 | `FileText` | 打开属性面板,显示选中构件属性 | `properties` |
| 3 | 过滤 | `Filter` | 打开过滤面板,按楼层/专业/类别筛选 | `filter` |
| 4 | 图钉 | `Pin` | 打开图钉列表面板,支持状态筛选和搜索 | `pins` |
| 5 | 全屏 | `Maximize` / `Minimize` | 直接切换全屏模式(不走抽屉) | - |
### 2.5 交互规则
1. **点击同一按钮**: toggle 打开/关闭抽屉
2. **点击不同按钮**: 切换到对应面板,原面板自动关闭
3. **抽屉关闭时**: 所有按钮(除全屏外)取消高亮
4. **同时只允许一个面板高亮**
### 2.6 参考代码片段
```tsx
// 文件: TopCenterToolbar.tsx
export function TopCenterToolbar() {
const { state, togglePanel } = useViewerState();
const buttons = [
{
key: 'views' as ViewerPanel,
label: '视图列表',
icon: List,
onClick: () => togglePanel('views'),
},
// ... 其他按钮配置
];
return (
<div className="absolute top-3 left-1/2 -translate-x-1/2 z-[800]">
<div className="flex items-center gap-1.5 bg-white/80 dark:bg-slate-800/80 backdrop-blur-xl rounded-md shadow-[0_4px_24px_rgba(0,0,0,0.04)] border border-slate-200/50 px-1.5 py-1.5">
{buttons.map((button) => {
const Icon = button.icon;
const isActive = state.activePanel === button.key && state.drawerOpen;
return (
<button
key={button.key}
onClick={button.onClick}
className={clsx(
'flex items-center gap-1.5 rounded-md transition-all duration-200',
'text-xs font-medium px-2 py-2 lg:px-3 lg:py-2',
isActive
? 'bg-blue-500 text-white shadow-md border border-blue-600'
: 'text-slate-600 hover:bg-slate-100/60'
)}
>
<Icon size={18} strokeWidth={1.5} />
<span className="hidden lg:inline">{button.label}</span>
</button>
);
})}
</div>
</div>
);
}
```
---
## 3. 底部右侧工具栏
### 3.1 布局示意
```
Bottom Right (横向布局)
┌───────────────────────────────┐
│ 🏠 📏 ✂️ 🚶 ⚙️ │
└───────────────────────────────┘
↑ ↑ ↑ ↑ ↑
主 测 剖 漫 环
视 量 切 游 境
```
### 3.2 容器规范
| 属性 | 值 | Tailwind CSS |
|------|---|-------------|
| **定位** | 右下角固定 | `absolute bottom-4 right-4` |
| **层级** | 800 | `z-[800]` |
| **布局** | 水平弹性布局 | `flex items-center gap-2.5` |
| **背景** | 白色半透明 | `bg-white/80 dark:bg-slate-800/80` |
| **毛玻璃** | 10px 模糊 | `backdrop-blur-xl` |
| **圆角** | 完全圆角 | `rounded-full` |
| **阴影** | 轻微阴影 | `shadow-[0_4px_24px_rgba(0,0,0,0.06)]` |
| **边框** | 半透明边框 | `border border-slate-200/50` |
| **内边距** | 8px | `px-2 py-2` |
### 3.3 按钮规范
#### 通用样式
| 属性 | 值 | Tailwind CSS |
|------|---|-------------|
| **尺寸** | 48px × 48px | `w-12 h-12` |
| **形状** | 圆形 | `rounded-full` |
| **图标大小** | 18px | `size={18}` |
| **布局** | 居中 | `flex items-center justify-center` |
| **过渡** | 200ms 平滑 | `transition-all duration-200 ease-out` |
#### 按钮状态
**默认状态**:
```css
bg-white/90 dark:bg-slate-700/50
border border-slate-300/60 dark:border-slate-600/40
text-slate-700 dark:text-slate-200
shadow-[0_2px_8px_rgba(0,0,0,0.1),0_4px_12px_rgba(0,0,0,0.08)]
hover:bg-white
hover:shadow-[0_4px_12px_rgba(0,0,0,0.12),0_6px_20px_rgba(0,0,0,0.1)]
hover:scale-[1.05]
```
**激活态** (模式已启用):
```css
bg-blue-600
border-blue-700
text-white
shadow-[0_2px_8px_rgba(59,130,246,0.3),0_4px_16px_rgba(59,130,246,0.2)]
```
### 3.4 按钮列表(从左到右)
| 序号 | 按钮名称 | 图标 | 功能说明 | 二级菜单 | 模式类型 |
|------|---------|------|---------|---------|---------|
| 1 | 主视图 | `Home` | 快捷视角切换 | ✅ 充满屏幕/缩放/爆炸图 | - |
| 2 | 测量 | `Ruler` | 测量工具集 | ✅ 线性捕捉/垂直净高/高程测量/清除 | `measure` |
| 3 | 剖切 | `Scissors` | 剖切平面工具 | ✅ 任意剖切/单面/多面 | `section` |
| 4 | 漫游 | `PersonStanding` | 第一人称漫游 | ❌ 直接切换模式 | `walk` |
| 5 | 环境设置 | `Settings` | 渲染和环境配置 | ❌ 弹出 Modal | - |
### 3.5 模式互斥规则
**互斥的模式**:
- 测量模式 (`measure`)
- 剖切模式 (`section`)
- 创建图钉模式 (`pin`)
- 漫游模式 (`walk`)
**互斥策略**:
启用某模式时,自动退出其他模式,并清理对应临时 UI。
**示例**: 启用测量模式时,如果剖切模式正在运行,则自动退出剖切并清除剖切平面。
### 3.6 参考代码片段
```tsx
// 文件: BottomRightToolbar.tsx
export function BottomRightToolbar({ onOpenSettings }: BottomRightToolbarProps) {
const { state, setMode, exitMode } = useViewerState();
const toolButtons = [
{
key: 'mainView',
label: '主视图',
icon: Home,
menuItems: mainViewMenuItems, // Hover 二级菜单
},
{
key: 'measure',
label: '测量',
icon: Ruler,
menuItems: measureMenuItems,
mode: 'measure' as const,
},
// ... 其他按钮
];
return (
<div className="absolute bottom-4 right-4 z-[800]">
<div className="flex items-center gap-2.5 bg-white/80 backdrop-blur-xl rounded-full shadow-[0_4px_24px_rgba(0,0,0,0.06)] border border-slate-200/50 px-2 py-2">
{toolButtons.map((button) => {
const Icon = button.icon;
const isActive = button.mode && state.activeMode === button.mode;
// 带 Hover 菜单的按钮
return (
<HoverMenu
key={button.key}
items={button.menuItems!}
placement="top"
trigger={
<button
className={clsx(
'w-12 h-12 rounded-full flex items-center justify-center',
'border transition-all duration-200',
isActive
? 'bg-blue-600 border-blue-700 text-white'
: 'bg-white/90 border-slate-300/60 text-slate-700 hover:scale-[1.05]'
)}
>
<Icon size={18} strokeWidth={2} />
</button>
}
/>
);
})}
</div>
</div>
);
}
```
---
## 4. Hover 二级菜单
### 4.1 布局示意
```
┌───────────────────┐
│ 📏 线性捕捉 1 │ ← 菜单项 (hover 高亮)
│ 📐 垂直净高 2 │
│ 📈 高程测量 3 │
│ 🗑️ 清除测量 4 │
└───────────────────┘
┌──────────┐
│ 📏 │ ← 触发按钮
└──────────┘
```
### 4.2 容器规范
| 属性 | 值 | Tailwind CSS |
|------|---|-------------|
| **背景** | 白色半透明 | `bg-white/95 dark:bg-[rgba(30,41,59,0.98)]` |
| **毛玻璃** | 10px 模糊 | `backdrop-blur-[10px]` |
| **圆角** | 12px | `rounded-xl` |
| **阴影** | 较强阴影 | `shadow-[0_8px_24px_rgba(0,0,0,0.12)]` |
| **边框** | 半透明边框 | `border border-slate-200/60` |
| **内边距** | 6px | `px-1.5 py-1.5` |
| **布局** | 垂直列表 | `flex flex-col gap-0` |
### 4.3 菜单项规范
| 属性 | 值 | Tailwind CSS |
|------|---|-------------|
| **布局** | 水平弹性布局 | `flex items-center gap-2.5` |
| **内边距** | 水平 12px, 垂直 8px | `px-3 py-2` |
| **圆角** | 8px | `rounded-lg` |
| **字体** | 13px 普通字重 | `text-[13px] font-normal` |
| **文字颜色** | 深灰色 | `text-slate-700 dark:text-[rgba(255,255,255,0.85)]` |
| **Hover 背景** | 浅灰色 | `hover:bg-slate-100 dark:hover:bg-[rgba(51,65,85,0.8)]` |
| **过渡** | 100ms 平滑 | `transition-all duration-100 ease-out` |
### 4.4 触发时机
| 事件 | 延迟时间 | 说明 |
|------|---------|------|
| **Hover 进入** | 100ms | 鼠标进入按钮后 100ms 显示菜单 |
| **Hover 离开** | 250ms | 鼠标离开「按钮+菜单」区域后 250ms 隐藏菜单 |
### 4.5 键盘快捷键
- **1-9 键**: 菜单展开时,按数字键触发对应菜单项
- **Esc 键**: 关闭菜单并退出当前模式
### 4.6 参考代码片段
```tsx
// 文件: HoverMenu.tsx
export function HoverMenu({
trigger,
items,
placement = 'top',
openDelay = 100,
closeDelay = 250,
}: HoverMenuProps) {
const [isOpen, setIsOpen] = useState(false);
const handleMouseEnter = () => {
openTimerRef.current = setTimeout(() => {
setIsOpen(true);
}, openDelay);
};
const handleMouseLeave = () => {
closeTimerRef.current = setTimeout(() => {
setIsOpen(false);
}, closeDelay);
};
return (
<div onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}>
{trigger}
<AnimatePresence>
{isOpen && (
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 10 }}
transition={{ duration: 0.15 }}
className="absolute bottom-full mb-3"
>
<div className="bg-white/95 backdrop-blur-[10px] rounded-xl shadow-[0_8px_24px_rgba(0,0,0,0.12)] border border-slate-200/60 px-1.5 py-1.5">
{items.map((item) => (
<button
key={item.key}
onClick={() => item.onClick()}
className="flex items-center gap-2.5 px-3 py-2 rounded-lg hover:bg-slate-100 transition-all"
>
{item.icon}
<span>{item.label}</span>
</button>
))}
</div>
</motion.div>
)}
</AnimatePresence>
</div>
);
}
```
---
## 5. 右侧抽屉面板
### 5.1 布局示意
```
┌────────────────────────┐
│ 视图列表 [×] │ ← 标题栏 (48px)
├────────────────────────┤
│ │
│ [面板内容区域] │ ← 滚动区域
│ │
│ │
└────────────────────────┘
← 320px 宽度(可拖拽 280-400px
```
### 5.2 抽屉通用规范
| 属性 | 值 | Tailwind CSS |
|------|---|-------------|
| **定位** | 右侧固定 | `fixed top-0 bottom-0 right-0` (或 left-0) |
| **宽度** | 320px可拖拽调整 | `w-[320px]` |
| **背景** | 纯白色 | `bg-white dark:bg-slate-900` |
| **阴影** | 向左投影 | `shadow-2xl` |
| **边框** | 左侧边框 | `border-l border-slate-200` (或右侧) |
| **层级** | 800 | `z-[800]` |
| **动画** | 300ms 弹簧动画 | Framer Motion `spring` |
| **布局** | 垂直弹性布局 | `flex flex-col` |
### 5.3 标题栏规范
| 属性 | 值 | Tailwind CSS |
|------|---|-------------|
| **高度** | 48px | `h-12` |
| **布局** | 水平两端对齐 | `flex items-center justify-between` |
| **内边距** | 水平 16px | `px-4` |
| **背景** | 浅灰色 | `bg-slate-50 dark:bg-slate-800/50` |
| **边框** | 底部边框 | `border-b border-slate-200` |
| **标题字体** | 14px 粗体 | `text-sm font-semibold` |
### 5.4 Tab 管理规则
1. **同时只能打开一个 Tab**
2. **切换 Tab**: 点击顶部工具栏不同按钮,抽屉内容切换,原 Tab 自动替换
3. **关闭抽屉**: 点击标题栏关闭按钮 / 点击已激活的顶部按钮 toggle 关闭
### 5.5 面板列表
| 面板名称 | 激活条件 | 主要功能 |
|---------|---------|---------|
| **视图列表** | `activePanel === 'views'` | 显示 3D/2D 视角列表,支持保存视点、搜索 |
| **属性面板** | `activePanel === 'properties'` | 显示选中构件的属性信息(几何、材质等) |
| **过滤面板** | `activePanel === 'filter'` | 按楼层、专业、类别/系统筛选模型 |
| **图钉列表** | `activePanel === 'pins'` | 显示和管理问题图钉,支持状态筛选和搜索 |
### 5.6 参考代码片段
```tsx
// 文件: ViewerDrawer.tsx
export function ViewerDrawer() {
const { state, closePanel } = useViewerState();
const renderActivePanel = () => {
switch (state.activePanel) {
case 'views':
return <ViewListPanel />;
case 'properties':
return <PropertiesPanel />;
case 'filter':
return <FilterPanel />;
case 'pins':
return <PinsPanel />;
default:
return null;
}
};
const isLeftSide = state.activePanel === 'views';
const isRightSide = state.activePanel === 'pins';
const showDrawer = state.drawerOpen && (isLeftSide || isRightSide);
return (
<AnimatePresence>
{showDrawer && (
<motion.div
initial={{ x: isLeftSide ? '-100%' : '100%' }}
animate={{ x: 0 }}
exit={{ x: isLeftSide ? '-100%' : '100%' }}
transition={{ type: 'spring', damping: 30, stiffness: 300 }}
className={clsx(
'fixed top-0 bottom-0 w-[320px] bg-white shadow-2xl z-[800]',
isLeftSide ? 'left-0 border-r' : 'right-0 border-l'
)}
>
{/* 标题栏 */}
<div className="flex items-center justify-between px-4 py-3 border-b">
<h3 className="text-sm font-semibold">{getPanelTitle()}</h3>
<button onClick={closePanel}>
<X size={18} />
</button>
</div>
{/* 面板内容 */}
<div className="flex-1 overflow-hidden">
{renderActivePanel()}
</div>
</motion.div>
)}
</AnimatePresence>
);
}
```
---
## 6. 状态管理
### 6.1 状态类型定义
```tsx
// 文件: use-viewer-state.tsx
/** Viewer 抽屉面板类型 */
export type ViewerPanel = 'views' | 'properties' | 'filter' | 'pins' | null;
/** Viewer 工具模式类型 */
export type ViewerMode = 'measure' | 'section' | 'pin' | 'walk' | null;
/** Viewer 状态接口 */
export interface ViewerState {
// 抽屉状态
drawerOpen: boolean; // 抽屉是否打开
activePanel: ViewerPanel; // 当前激活的面板
drawerWidth: number; // 抽屉宽度(可拖拽调整)
// 工具模式
activeMode: ViewerMode; // 当前激活的工具模式
// 全屏状态
isFullscreen: boolean; // 是否全屏
// 选中构件
selectedElementId: string | null; // 选中构件的 ID
}
```
### 6.2 核心方法
```tsx
interface ViewerContextValue {
state: ViewerState;
// 面板控制方法
openPanel: (panel: ViewerPanel) => void; // 打开指定面板
closePanel: () => void; // 关闭面板
togglePanel: (panel: ViewerPanel) => void; // 切换面板(同一个则关闭)
// 模式控制方法
setMode: (mode: ViewerMode) => void; // 设置工具模式(互斥)
exitMode: () => void; // 退出当前模式
// 全屏控制
toggleFullscreen: () => void; // 切换全屏
// 构件选择
setSelectedElement: (id: string | null) => void; // 设置选中构件
}
```
### 6.3 使用示例
```tsx
import { useViewerState } from '@/hooks/use-viewer-state';
function MyComponent() {
const { state, togglePanel, setMode } = useViewerState();
// 打开/关闭面板
const handleOpenProperties = () => {
togglePanel('properties');
};
// 启用测量模式
const handleStartMeasure = () => {
setMode('measure');
};
// 检查当前状态
const isPropertiesPanelOpen = state.activePanel === 'properties' && state.drawerOpen;
const isMeasureMode = state.activeMode === 'measure';
return (
<div>
{/* 组件内容 */}
</div>
);
}
```
### 6.4 模式互斥逻辑
```tsx
// 设置工具模式时,自动退出其他模式
const setMode = useCallback((mode: ViewerMode) => {
setState(prev => ({ ...prev, activeMode: mode }));
}, []);
// 按 Esc 键退出当前模式
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape' && state.activeMode) {
exitMode();
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [state.activeMode, exitMode]);
```
---
## 7. 视觉规范速查表
### 7.1 尺寸规范
| 元素 | 尺寸 | Tailwind CSS |
|------|------|-------------|
| 顶部工具栏按钮 | 自适应高度 36px | `py-2` |
| 底部工具栏按钮 | 48px × 48px | `w-12 h-12` |
| 顶部工具栏图标 | 18px | `size={18}` |
| 底部工具栏图标 | 18px | `size={18}` |
| 二级菜单图标 | 14px | `size={14}` |
| 抽屉宽度 | 320px | `w-[320px]` |
| 标题栏高度 | 48px | `h-12` |
### 7.2 圆角规范
| 元素 | 圆角 | Tailwind CSS |
|------|------|-------------|
| 顶部工具栏容器 | 6px | `rounded-md` |
| 顶部工具栏按钮 | 6px | `rounded-md` |
| 底部工具栏容器 | 完全圆角 | `rounded-full` |
| 底部工具栏按钮 | 完全圆角 | `rounded-full` |
| 二级菜单容器 | 12px | `rounded-xl` |
| 二级菜单项 | 8px | `rounded-lg` |
### 7.3 阴影规范
| 元素 | 阴影 | Tailwind CSS |
|------|------|-------------|
| 顶部工具栏 | 轻微阴影 | `shadow-[0_4px_24px_rgba(0,0,0,0.04)]` |
| 底部工具栏 | 轻微阴影 | `shadow-[0_4px_24px_rgba(0,0,0,0.06)]` |
| 二级菜单 | 较强阴影 | `shadow-[0_8px_24px_rgba(0,0,0,0.12)]` |
| 抽屉面板 | 较强阴影 | `shadow-2xl` |
### 7.4 颜色规范
| 元素 | 亮色模式 | 暗色模式 |
|------|---------|---------|
| **工具栏背景** | `rgba(255,255,255,0.8)` | `rgba(30,41,59,0.8)` |
| **按钮默认文字** | `rgb(71,85,105)` | `rgba(148,163,184)` |
| **按钮激活背景** | `rgb(59,130,246)` | `rgb(37,99,235)` |
| **按钮激活文字** | `rgb(255,255,255)` | `rgb(255,255,255)` |
| **Hover 背景** | `rgba(241,245,249,0.6)` | `rgba(51,65,85,0.6)` |
| **边框** | `rgba(226,232,240,0.5)` | `rgba(71,85,105,0.5)` |
### 7.5 间距规范
| 元素 | 间距 | Tailwind CSS |
|------|------|-------------|
| 顶部工具栏按钮间距 | 6px | `gap-1.5` |
| 底部工具栏按钮间距 | 10px | `gap-2.5` |
| 二级菜单项间距 | 0px | `gap-0` |
| 二级菜单与按钮距离 | 12px | `mb-3` |
### 7.6 动画规范
| 动画类型 | 时长 | 缓动函数 |
|---------|------|---------|
| 按钮状态切换 | 200ms | `ease-out` |
| 二级菜单出现 | 150ms | `[0.16, 1, 0.3, 1]` (cubic-bezier) |
| 抽屉滑入 | 300ms | `spring` (damping: 30, stiffness: 300) |
| Hover 缩放 | 200ms | `ease-out` |
---
## 8. 组件清单
### 8.1 核心组件文件
| 组件名称 | 文件路径 | 说明 |
|---------|---------|------|
| **TopCenterToolbar** | `viewer/TopCenterToolbar.tsx` | 顶部中间工具栏组件 |
| **BottomRightToolbar** | `viewer/BottomRightToolbar.tsx` | 底部右侧工具栏组件 |
| **HoverMenu** | `viewer/HoverMenu.tsx` | Hover 二级菜单组件 |
| **ViewerDrawer** | `viewer/ViewerDrawer.tsx` | 右侧抽屉面板容器 |
| **ViewListPanel** | `viewer/ViewListPanel.tsx` | 视图列表面板 |
| **PropertiesPanel** | `viewer/PropertiesPanel.tsx` | 属性面板 |
| **FilterPanel** | `viewer/FilterPanel.tsx` | 过滤面板 |
| **PinsPanel** | `viewer/PinsPanel.tsx` | 图钉列表面板 |
| **ViewerStates** | `viewer/ViewerStates.tsx` | Viewer 各种状态组件 |
| **EnvironmentSettingsModal** | `viewer/EnvironmentSettingsModal.tsx` | 环境设置弹窗 |
| **ModeIndicator** | `viewer/ModeIndicator.tsx` | 模式提示组件 |
### 8.2 状态管理
| Hook/Provider | 文件路径 | 说明 |
|--------------|---------|------|
| **useViewerState** | `hooks/use-viewer-state.tsx` | Viewer 状态管理 Hook |
| **ViewerProvider** | `hooks/use-viewer-state.tsx` | Viewer 状态 Context Provider |
### 8.3 依赖库
| 库名称 | 版本 | 用途 |
|-------|------|------|
| **React** | ^18.x | UI 框架 |
| **TypeScript** | ^5.x | 类型系统 |
| **Ant Design** | ^5.x | UI 组件库 |
| **Tailwind CSS** | ^3.x | 样式框架 |
| **Framer Motion** | ^11.x | 动画库 |
| **lucide-react** | ^0.x | 图标库 |
| **clsx** | ^2.x | 类名组合工具 |
| **next-intl** | ^3.x | 国际化 |
---
## 附录:完整组件层次结构
```
ViewerContainer
├── TopCenterToolbar (顶部中间工具栏)
│ └── Button × 5 (视图/属性/过滤/图钉/全屏)
├── BottomRightToolbar (底部右侧工具栏)
│ ├── HoverMenu × 4 (主视图/测量/剖切/漫游)
│ │ └── MenuItem[] (二级菜单项列表)
│ └── Button (环境设置)
├── ViewerDrawer (右侧抽屉)
│ ├── ViewListPanel (视图列表面板)
│ ├── PropertiesPanel (属性面板)
│ ├── FilterPanel (过滤面板)
│ └── PinsPanel (图钉列表面板)
├── EnvironmentSettingsModal (环境设置弹窗)
├── ModeIndicator (模式提示)
└── ViewerStates (各种状态组件)
├── EmptyState (空状态)
├── LoadingState (加载中)
└── ErrorState (错误状态)
```
---
**文档编写完成**
如有任何疑问或需要补充的内容,请联系开发团队。

View File

@@ -1,548 +0,0 @@
# 国际化实现指南
> 本文档详细描述项目的国际化i18n实现规范包括翻译键定义、组件实现、语言切换等。
---
## 📋 目录
1. [国际化的重要性](#1-国际化的重要性)
2. [项目国际化架构](#2-项目国际化架构)
3. [实现步骤](#3-实现步骤)
4. [组件中的国际化实现](#4-组件中的国际化实现)
5. [注意事项](#5-注意事项)
6. [最佳实践](#6-最佳实践)
7. [添加新语言](#7-添加新语言)
8. [检查清单](#8-检查清单)
---
## 1. 国际化的重要性
**所有用户可见的文本都必须支持国际化,这是强制要求。**
- 项目支持多语言(目前:中文 `zh-CN`、英文 `en-US`
- 所有 UI 文本必须通过翻译函数获取
- **严禁在代码中硬编码任何语言的文本**
---
## 2. 项目国际化架构
### 2.1 相关文件
| 文件路径 | 作用 |
|---------|------|
| `src/locales/types.ts` | 翻译键类型定义 |
| `src/locales/zh-CN.ts` | 中文翻译字典 |
| `src/locales/en-US.ts` | 英文翻译字典 |
| `src/services/locale.ts` | 语言管理服务(单例) |
### 2.2 核心 API
```typescript
// 导入语言服务
import { t, localeManager } from '../../services/locale';
// 获取翻译文本
const text = t('toolbar.home'); // 返回 "首页" 或 "Home"
// 订阅语言变更
const unsubscribe = localeManager.subscribe(() => {
// 语言变更时的回调
this.setLocales();
});
// 取消订阅(在组件销毁时调用)
unsubscribe();
// 切换语言
localeManager.setLocale('en-US');
// 获取当前语言
const currentLocale = localeManager.getLocale();
```
---
## 3. 实现步骤
### 步骤 1在类型文件中定义翻译键
**文件:`src/locales/types.ts`**
```typescript
export interface TranslationDictionary {
// ... 现有键
myComponent: {
title: string;
description: string;
buttons: {
save: string;
cancel: string;
};
};
}
```
### 步骤 2添加中文翻译
**文件:`src/locales/zh-CN.ts`**
```typescript
export const zhCN: TranslationDictionary = {
// ... 现有内容
myComponent: {
title: '我的组件',
description: '这是组件描述',
buttons: {
save: '保存',
cancel: '取消',
},
},
};
```
### 步骤 3添加英文翻译
**文件:`src/locales/en-US.ts`**
```typescript
export const enUS: TranslationDictionary = {
// ... 现有内容
myComponent: {
title: 'My Component',
description: 'This is component description',
buttons: {
save: 'Save',
cancel: 'Cancel',
},
},
};
```
### 步骤 4在代码中使用翻译函数
```typescript
import { t } from '../../services/locale';
// 获取翻译文本
const title = t('myComponent.title');
// 在 DOM 中使用
const titleEl = document.createElement('div');
titleEl.textContent = t('myComponent.title');
// 在 HTML 字符串中使用
const html = `<div>${t('myComponent.description')}</div>`;
```
---
## 4. 组件中的国际化实现
### 4.1 完整实现示例
```typescript
import { t, localeManager } from '../../services/locale';
import { IBimComponent } from '../../types/component';
import { ThemeConfig } from '../../themes/types';
export class MyComponent implements IBimComponent {
private element: HTMLElement;
private unsubscribeLocale: (() => void) | null = null;
private unsubscribeTheme: (() => void) | null = null;
constructor(container: HTMLElement) {
this.element = this.createDOM();
container.appendChild(this.element);
}
/**
* 创建 DOM 结构
*/
private createDOM(): HTMLElement {
const root = document.createElement('div');
root.className = 'my-component';
root.innerHTML = `
<div class="my-component-title"></div>
<div class="my-component-content"></div>
<div class="my-component-actions">
<button class="btn-save"></button>
<button class="btn-cancel"></button>
</div>
`;
return root;
}
/**
* 初始化组件
*/
public init(): void {
// 订阅语言变更
this.unsubscribeLocale = localeManager.subscribe(() => {
this.setLocales();
});
// 初始设置语言
this.setLocales();
}
/**
* 设置/更新所有文本(语言变更时调用)
*/
public setLocales(): void {
// 更新标题
const titleEl = this.element.querySelector('.my-component-title');
if (titleEl) {
titleEl.textContent = t('myComponent.title');
}
// 更新内容
const contentEl = this.element.querySelector('.my-component-content');
if (contentEl) {
contentEl.textContent = t('myComponent.description');
}
// 更新按钮
const saveBtn = this.element.querySelector('.btn-save');
if (saveBtn) {
saveBtn.textContent = t('myComponent.buttons.save');
}
const cancelBtn = this.element.querySelector('.btn-cancel');
if (cancelBtn) {
cancelBtn.textContent = t('myComponent.buttons.cancel');
}
}
/**
* 设置主题
*/
public setTheme(theme: ThemeConfig): void {
// 主题设置逻辑
}
/**
* 销毁组件
*/
public destroy(): void {
// 取消语言订阅
if (this.unsubscribeLocale) {
this.unsubscribeLocale();
this.unsubscribeLocale = null;
}
// 取消主题订阅
if (this.unsubscribeTheme) {
this.unsubscribeTheme();
this.unsubscribeTheme = null;
}
// 移除 DOM
this.element.remove();
}
}
```
### 4.2 关键点说明
1. **在 `init()` 中订阅语言变更**
2. **实现 `setLocales()` 方法更新所有文本**
3. **在 `destroy()` 中取消订阅**
4. **使用 `t('key.path')` 获取翻译文本**
---
## 5. 注意事项
### 5.1 ✅ 必须做的
#### 所有用户可见文本使用 `t(key)`
```typescript
// ✅ 正确
button.textContent = t('toolbar.home');
// ❌ 错误
button.textContent = '首页';
button.textContent = 'Home';
```
#### 翻译键使用有意义的路径
```typescript
// ✅ 正确:按功能模块组织
t('toolbar.home')
t('dialog.title')
t('button.save')
t('message.success')
// ❌ 错误:键名不清晰
t('text1')
t('label')
t('str')
```
#### 在所有语言文件中添加翻译
- 添加新键时,必须同时更新 `zh-CN.ts``en-US.ts`
- 确保所有语言文件的结构一致
#### 实现 `setLocales()` 方法
- 所有组件必须实现 `setLocales()` 方法
- 在方法中更新所有用户可见的文本
#### 订阅语言变更
- 组件初始化时订阅 `localeManager.subscribe()`
- 组件销毁时取消订阅
### 5.2 ❌ 禁止做的
#### 禁止硬编码文本
```typescript
// ❌ 错误:硬编码中文
const title = '首页';
// ❌ 错误:硬编码英文
const title = 'Home';
// ✅ 正确:使用翻译函数
const title = t('toolbar.home');
```
#### 禁止在翻译键中使用变量
```typescript
// ❌ 错误:动态拼接键名(难以追踪和维护)
t(`toolbar.${buttonId}`);
// ✅ 正确:使用完整的键名
t('toolbar.home');
t('toolbar.settings');
```
#### 禁止忽略语言变更
- 组件必须响应语言切换
- 不能只在初始化时设置文本
---
## 6. 最佳实践
### 6.1 翻译键的组织结构
```typescript
// 按功能模块组织
interface TranslationDictionary {
// 工具栏相关
toolbar: {
home: string;
settings: string;
measure: string;
};
// 弹窗相关
dialog: {
title: string;
content: string;
close: string;
};
// 按钮相关
button: {
save: string;
cancel: string;
confirm: string;
delete: string;
};
// 消息相关
message: {
success: string;
error: string;
warning: string;
loading: string;
};
// 表单相关
form: {
required: string;
invalid: string;
placeholder: {
name: string;
email: string;
};
};
}
```
### 6.2 嵌套键的使用
对于复杂组件,可以使用嵌套结构:
```typescript
// 类型定义
measurePanel: {
modes: {
distance: string;
angle: string;
area: string;
};
labels: {
currentMode: string;
result: string;
};
actions: {
clear: string;
settings: string;
};
};
// 使用
t('measurePanel.modes.distance') // "距离"
t('measurePanel.labels.result') // "结果"
```
### 6.3 处理动态内容
如果需要在翻译中插入动态内容,建议:
```typescript
// 方案 1翻译后拼接简单场景
const message = t('file.selected') + `: ${fileName}`;
// 方案 2使用模板需要扩展 LocaleManager
// 翻译字典: "已选择 {count} 个文件"
const message = t('file.selectedCount', { count: 5 });
```
---
## 7. 添加新语言
### 步骤 1扩展语言类型
**文件:`src/locales/types.ts`**
```typescript
export type LocaleType = 'zh-CN' | 'en-US' | 'ja-JP'; // 添加日语
```
### 步骤 2创建翻译文件
**文件:`src/locales/ja-JP.ts`**
```typescript
import { TranslationDictionary } from './types';
export const jaJP: TranslationDictionary = {
toolbar: {
home: 'ホーム',
settings: '設定',
// ... 所有翻译
},
// ... 完整的翻译字典
};
```
### 步骤 3注册新语言
**文件:`src/services/locale.ts`**
```typescript
import { jaJP } from '../locales/ja-JP';
class LocaleManager {
private messages: Record<LocaleType, TranslationDictionary> = {
'zh-CN': zhCN,
'en-US': enUS,
'ja-JP': jaJP, // 添加新语言
};
// ...
}
```
---
## 8. 检查清单
在开发新功能时,确保完成以下检查:
### 8.1 翻译键
- [ ]`types.ts` 中定义了新的翻译键类型
- [ ]`zh-CN.ts` 中添加了中文翻译
- [ ]`en-US.ts` 中添加了英文翻译
- [ ] 翻译键名清晰、有意义
- [ ] 翻译键结构与类型定义一致
### 8.2 组件实现
- [ ] 所有用户可见的文本都使用 `t(key)` 函数
- [ ] 组件实现了 `setLocales()` 方法
- [ ] 组件在 `init()` 中订阅了语言变更事件
- [ ] 组件在 `destroy()` 中取消了语言订阅
### 8.3 代码质量
- [ ] 没有硬编码任何语言的文本
- [ ] 没有使用动态拼接的翻译键
- [ ] 翻译文本语法正确、表达清晰
### 8.4 测试验证
- [ ] 在中文环境下测试文本显示正确
- [ ] 在英文环境下测试文本显示正确
- [ ] 切换语言后所有文本正确更新
- [ ] 没有遗漏的未翻译文本
---
## 附录:现有翻译键参考
### 工具栏 (toolbar)
```typescript
toolbar: {
home: '首页' | 'Home',
settings: '设置' | 'Settings',
info: '信息' | 'Info',
location: '定位' | 'Location',
measure: '测量' | 'Measure',
// ...
}
```
### 弹窗 (dialog)
```typescript
dialog: {
title: '弹窗标题' | 'Dialog Title',
close: '关闭' | 'Close',
// ...
}
```
### 测量面板 (measure)
```typescript
measure: {
modes: {
distance: '距离' | 'Distance',
angle: '角度' | 'Angle',
// ...
},
// ...
}
```
---
**文档版本**v1.0
**最后更新**2026-01-21