添加测试信息

This commit is contained in:
yuding
2025-12-08 10:02:24 +08:00
parent 244891ceec
commit c112c87dad
21 changed files with 3355 additions and 2192 deletions

1344
AI_COLLABORATION.md Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,159 +0,0 @@
# Engine3DManager 架构设计说明
## 🎯 设计目标
将第三方 3D 引擎封装为管理器组件,并通过**依赖注入**实现解耦。
## 🏗️ 架构模式
### 依赖注入 (Dependency Injection)
`Engine3DManager` 不直接依赖具体的 `createEngine` 实现,而是通过参数接收引擎创建函数。
```typescript
// Engine3DManager 定义工厂函数类型
export type Engine3DFactory = (config: {
containerId: string;
backgroundColor?: number;
version?: 'v1' | 'v2';
showStats?: boolean;
showViewCube?: boolean;
}) => any;
// initialize 方法接受外部传入的创建函数
public initialize(createEngine: Engine3DFactory, options?: Engine3DOptions): boolean {
this.engine = createEngine({ /* config */ });
}
```
### 职责分离
```
┌─────────────────────────────────────────────┐
│ BimEngine (总控制器) │
│ - 导入 createEngine │
│ - 组合各个 Manager │
│ - 提供简洁的 API │
└──────────────┬──────────────────────────────┘
│ 传入 createEngine
┌─────────────────────────────────────────────┐
│ Engine3DManager (3D引擎管理器) │
│ - ❌ 不导入 createEngine │
│ - ✅ 接受外部传入的创建函数 │
│ - 管理引擎生命周期 │
│ - 提供统一接口 │
└──────────────┬──────────────────────────────┘
│ 调用
┌─────────────────────────────────────────────┐
│ createEngine (第三方3D引擎) │
│ - bim-engine-sdk.es.js │
│ - 被完全封装 │
└─────────────────────────────────────────────┘
```
## 💡 优势
### 1. **解耦 (Decoupling)**
- `Engine3DManager` 不依赖具体的 3D 引擎实现
- 可以轻松切换不同的 3D 引擎,只需更改 `BimEngine` 中的导入
### 2. **可测试性 (Testability)**
```typescript
// 测试时可以注入 mock 引擎
const mockCreateEngine = (config) => ({
loader: { loadModel: jest.fn() }
});
manager.initialize(mockCreateEngine, options);
```
### 3. **灵活性 (Flexibility)**
```typescript
// 可以在运行时决定使用哪个引擎
import { createEngineV1 } from './engine-v1';
import { createEngineV2 } from './engine-v2';
const engineFactory = useV2 ? createEngineV2 : createEngineV1;
manager.initialize(engineFactory, options);
```
### 4. **单一职责 (Single Responsibility)**
- **BimEngine**: 负责集成和协调
- **Engine3DManager**: 负责管理 3D 引擎的生命周期
- **createEngine**: 负责创建引擎实例
## 📊 调用流程
```typescript
// 1. 用户创建 BimEngine
const engine = new BimEngine('container');
// 2. 用户调用 initEngine3D
engine.initEngine3D({ backgroundColor: 0x333333 });
// 3. BimEngine 内部执行
// ↓
// 导入 createEngine
// ↓
// 调用 engine3D.initialize(createEngine, options)
// ↓
// Engine3DManager 使用传入的 createEngine 创建引擎实例
```
## 🔄 与其他 Manager 的对比
### DialogManager / ToolbarManager
```typescript
// 自包含,不依赖外部函数
constructor(container: HTMLElement) {
this.init(); // 直接初始化
}
```
### Engine3DManager
```typescript
// 依赖注入,接受外部函数
constructor(container: HTMLElement) {
this.createContainer(); // 只创建容器
}
initialize(createEngine: Engine3DFactory, options) {
this.engine = createEngine(config); // 使用注入的函数
}
```
## 🎓 设计模式
采用的设计模式:
1. **工厂模式 (Factory Pattern)** - `Engine3DFactory` 类型
2. **依赖注入 (Dependency Injection)** - `createEngine` 通过参数传入
3. **延迟初始化 (Lazy Initialization)** - 构造时不创建引擎
## 🚀 未来扩展
### 支持多引擎
```typescript
engine.initEngine3D(createThreeJSEngine, options);
// 或
engine.initEngine3D(createBabylonEngine, options);
```
### 条件加载
```typescript
// 根据设备性能选择引擎
const factory = isHighPerformance
? createAdvancedEngine
: createLightweightEngine;
engine.initEngine3D(factory, options);
```
## 📝 总结
通过依赖注入模式,`Engine3DManager` 实现了:
- ✅ 与具体 3D 引擎解耦
- ✅ 易于测试和维护
- ✅ 支持灵活切换引擎
- ✅ 保持职责单一清晰

View File

@@ -1,207 +0,0 @@
# BimEngine 使用示例
## 基础使用
### 1. 创建 BimEngine 实例
```typescript
import { BimEngine } from './src/bim-engine';
// 方式 1: 使用 DOM 元素 ID
const engine = new BimEngine('my-container', {
locale: 'zh-CN',
theme: 'dark'
});
// 方式 2: 使用 DOM 元素引用
const containerEl = document.getElementById('my-container');
const engine2 = new BimEngine(containerEl, {
locale: 'en-US',
theme: 'light'
});
```
### 2. 初始化 3D 引擎(延迟初始化)
**重要**: 3D 引擎采用延迟初始化模式,需要用户主动调用 `initEngine3D()` 方法。
```typescript
// 先创建 BimEngine 实例
const engine = new BimEngine('my-container');
// 稍后在需要时初始化 3D 引擎
const success = engine.initEngine3D({
backgroundColor: 0x333333,
version: 'v1',
showStats: true,
showViewCube: true
});
if (success) {
console.log('3D 引擎初始化成功!');
}
// 检查引擎是否已初始化
if (engine.isEngine3DInitialized()) {
// 可以加载模型了
}
```
### 3. 加载 3D 模型
```typescript
// 确保先初始化 3D 引擎
engine.initEngine3D({
backgroundColor: 0x1a1a1a,
showStats: true
});
// 加载模型 - 使用默认参数
engine.loadModel('/model/building.glb');
// 加载模型 - 自定义位置、旋转、缩放
engine.loadModel('/model/gujianzhu.glb', {
position: [10, -5, 0],
rotation: [0, Math.PI / 4, 0],
scale: [2, 2, 2]
});
```
### 3. 动态切换主题和语言
```typescript
// 切换主题
engine.setTheme('dark');
engine.setTheme('light');
// 切换语言
engine.setLocale('zh-CN');
engine.setLocale('en-US');
// 设置自定义主题
engine.setCustomTheme({
background: '#1a1a1a',
textPrimary: '#ffffff',
textSecondary: '#999999',
// ... 其他主题配置
});
```
### 4. 访问 3D 引擎实例
```typescript
// 直接访问底层 3D 引擎
if (engine.engine3D) {
// 调用第三方 3D 引擎的其他方法
engine.engine3D.someOtherMethod();
}
```
## 在 HTML 中使用
```html
<!DOCTYPE html>
<html>
<head>
<style>
#bim-container {
width: 100vw;
height: 100vh;
margin: 0;
padding: 0;
}
</style>
</head>
<body>
<div id="bim-container"></div>
<script type="module">
import { BimEngine } from './dist/bim-engine-sdk.es.js';
const engine = new BimEngine('bim-container', {
engine3D: {
backgroundColor: 0x1a1a1a,
showStats: true,
showViewCube: true
}
});
// 加载模型
engine.loadModel('/models/building.glb', {
position: [0, 0, 0],
scale: [1.5, 1.5, 1.5]
});
</script>
</body>
</html>
```
## 在 Vue 3 中使用
```vue
<template>
<div ref="containerRef" class="bim-container"></div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue';
import { BimEngine } from 'bim-engine-sdk';
const containerRef = ref<HTMLElement>();
let engine: BimEngine | null = null;
onMounted(() => {
if (containerRef.value) {
engine = new BimEngine(containerRef.value, {
locale: 'zh-CN',
theme: 'dark',
engine3D: {
backgroundColor: 0x202020,
showStats: true,
showViewCube: true
}
});
// 加载模型
engine.loadModel('/models/building.glb');
}
});
onUnmounted(() => {
engine?.destroy();
});
</script>
<style scoped>
.bim-container {
width: 100%;
height: 100vh;
}
</style>
```
## 配置选项说明
### Engine3DOptions
| 参数 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `backgroundColor` | `number` | `0x333333` | 3D 场景背景色(十六进制颜色值) |
| `version` | `'v1' \| 'v2'` | `'v1'` | WebGL 版本 |
| `showStats` | `boolean` | `false` | 是否显示性能统计 |
| `showViewCube` | `boolean` | `true` | 是否显示视图立方体 |
### LoadModel Options
| 参数 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `position` | `[number, number, number]` | `[0, 0, 0]` | 模型位置 [x, y, z] |
| `rotation` | `[number, number, number]` | `[0, 0, 0]` | 模型旋转(弧度)[x, y, z] |
| `scale` | `[number, number, number]` | `[1, 1, 1]` | 模型缩放 [x, y, z] |
## 注意事项
1. **容器尺寸**: 确保容器元素有明确的宽高,否则 3D 引擎无法正常渲染
2. **模型路径**: 模型文件路径需要是可访问的 URL 或相对路径
3. **销毁实例**: 组件卸载时记得调用 `engine.destroy()` 释放资源
4. **z-index 层级**: UI 组件会自动叠加在 3D 场景之上z-index: 100+

89
dev.sh
View File

@@ -1,89 +0,0 @@
#!/bin/bash
# 颜色输出
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
NC='\033[0m' # No Color
echo -e "${GREEN}🚀 开始自动化构建和启动流程...${NC}\n"
# 1. 构建 SDK
echo -e "${YELLOW}📦 步骤 1/3: 构建 SDK...${NC}"
npm run build
if [ $? -ne 0 ]; then
echo -e "${RED}❌ SDK 构建失败!${NC}"
exit 1
fi
echo -e "${GREEN}✅ SDK 构建成功${NC}\n"
# 2. 复制 SDK 文件到 demo 目录
echo -e "${YELLOW}📋 步骤 2/3: 复制 SDK 文件到 demo 目录...${NC}"
# 复制到 demo/lib
echo " 复制到 demo/lib..."
cd demo
npm run copy-sdk
if [ $? -ne 0 ]; then
echo -e "${RED}❌ 复制到 demo/lib 失败!${NC}"
exit 1
fi
cd ..
# 复制到 demo-vue/public/lib
echo " 复制到 demo-vue/public/lib..."
cd demo-vue
npm run copy-sdk
if [ $? -ne 0 ]; then
echo -e "${RED}❌ 复制到 demo-vue/public/lib 失败!${NC}"
exit 1
fi
cd ..
echo -e "${GREEN}✅ SDK 文件复制完成${NC}\n"
# 3. 启动开发服务器
echo -e "${YELLOW}🌐 步骤 3/3: 启动开发服务器...${NC}\n"
# 清理函数:当脚本退出时停止所有后台进程
cleanup() {
echo -e "\n${YELLOW}🛑 正在停止开发服务器...${NC}"
kill $DEMO_PID $DEMO_VUE_PID 2>/dev/null
exit 0
}
# 注册清理函数
trap cleanup SIGINT SIGTERM
# 启动 demo (端口 8080)
echo -e "${GREEN}启动 demo (http://localhost:8080)...${NC}"
cd demo
npm run dev &
DEMO_PID=$!
cd ..
# 等待一下确保服务器启动
sleep 3
# 启动 demo-vue (端口 8081)
echo -e "${GREEN}启动 demo-vue (http://localhost:8081)...${NC}"
cd demo-vue
npm run dev &
DEMO_VUE_PID=$!
cd ..
# 等待一下确保服务器启动
sleep 3
echo -e "\n${GREEN}✅ 所有服务已启动!${NC}"
echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo -e "${GREEN}📱 Demo (HTML): http://localhost:8080${NC}"
echo -e "${GREEN}📱 Demo (Vue3): http://localhost:8081${NC}"
echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo -e "${YELLOW}💡 按 Ctrl+C 停止所有服务器${NC}\n"
# 等待用户中断
wait

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

99
dist/index.d.ts vendored
View File

@@ -12,8 +12,11 @@ export declare class BimButtonGroup implements IBimComponent {
private customColors; private customColors;
private unsubscribeLocale; private unsubscribeLocale;
private unsubscribeTheme; private unsubscribeTheme;
protected engine: BimEngine | null;
private readonly DEFAULT_ICON; private readonly DEFAULT_ICON;
constructor(options: ButtonGroupOptions); constructor(options: ButtonGroupOptions);
setEngine(engine: BimEngine): void;
protected emit<K extends keyof EngineEvents>(event: K, payload: EngineEvents[K]): void;
private initContainer; private initContainer;
private updatePosition; private updatePosition;
/** /**
@@ -55,6 +58,21 @@ export declare class BimButtonGroup implements IBimComponent {
destroy(): void; destroy(): void;
} }
declare abstract class BimComponent {
protected engine: BimEngine;
constructor(engine: BimEngine);
/**
* Helper to send events easily
*/
protected emit<K extends keyof EngineEvents>(event: K, payload: EngineEvents[K]): void;
/**
* Helper to listen to events easily
* Returns an unsubscribe function
*/
protected on<K extends keyof EngineEvents>(event: K, listener: (payload: EngineEvents[K]) => void): () => void;
abstract destroy(): void;
}
/** /**
* 通用弹窗组件类 * 通用弹窗组件类
* 支持拖拽、缩放、自定义内容和位置。 * 支持拖拽、缩放、自定义内容和位置。
@@ -120,7 +138,7 @@ declare class BimDialog implements IBimComponent {
destroy(): void; destroy(): void;
} }
export declare class BimEngine { export declare class BimEngine extends EventEmitter {
private container; private container;
private wrapper; private wrapper;
private topLeftGroup; private topLeftGroup;
@@ -134,6 +152,8 @@ export declare class BimEngine {
locale?: LocaleType; locale?: LocaleType;
theme?: ThemeType; theme?: ThemeType;
}); });
emit<K extends keyof EngineEvents>(event: K, payload: EngineEvents[K]): void;
on<K extends keyof EngineEvents>(event: K, listener: (payload: EngineEvents[K]) => void): () => void;
setLocale(locale: LocaleType): void; setLocale(locale: LocaleType): void;
getLocale(): LocaleType; getLocale(): LocaleType;
setTheme(theme: 'dark' | 'light'): void; setTheme(theme: 'dark' | 'light'): void;
@@ -189,19 +209,16 @@ declare interface ButtonGroupColors {
} }
/** /**
* 通用按钮组管理器 * 通用按钮组管理器 (ButtonGroupManager)
* 负责创建和管理除底部工具栏以外的其他按钮组。 * 负责创建和管理通用的按钮组实例
*/ */
declare class ButtonGroupManager { declare class ButtonGroupManager extends BimComponent {
private activeGroups; private groups;
private container; private container;
constructor(container: HTMLElement); constructor(engine: BimEngine, container: HTMLElement);
/** create(id: string, options: Omit<ButtonGroupOptions, 'container'>): BimButtonGroup;
* 创建一个新的按钮组 get(id: string): BimButtonGroup | undefined;
*/
create(options: Omit<ButtonGroupOptions, 'container'>): BimButtonGroup;
updateTheme(theme: ThemeConfig): void; updateTheme(theme: ThemeConfig): void;
refresh(): void;
destroy(): void; destroy(): void;
} }
@@ -232,11 +249,11 @@ export declare function createEngine(r) {
const e = r.version || "v1"; const e = r.version || "v1";
switch (e) { switch (e) {
case "v2": case "v2":
return new Uc(r); return new Nc(r);
case "v1": case "v1":
return new L_(r); return new I_(r);
: :
return console.warn(`Version '${e}' not found. Falling back to v2.`), new Uc(r); return console.warn(`Version '${e}' not found. Falling back to v2.`), new Nc(r);
} }
} }
@@ -260,16 +277,17 @@ declare interface DialogColors {
* 弹窗管理器 * 弹窗管理器
* 负责创建和管理应用中的各类弹窗。 * 负责创建和管理应用中的各类弹窗。
*/ */
declare class DialogManager { declare class DialogManager extends BimComponent {
/** 弹窗挂载的父容器 */ /** 弹窗挂载的父容器 */
private container; private container;
/** 活跃的弹窗实例列表 */ /** 活跃的弹窗实例列表 */
private activeDialogs; private activeDialogs;
/** /**
* 构造函数 * 构造函数
* @param engine 引擎实例
* @param container 弹窗挂载的目标容器 * @param container 弹窗挂载的目标容器
*/ */
constructor(container: HTMLElement); constructor(engine: BimEngine, container: HTMLElement);
/** /**
* 创建一个通用弹窗 * 创建一个通用弹窗
* @param options 弹窗配置选项(不需要传 container自动使用管理器绑定的容器 * @param options 弹窗配置选项(不需要传 container自动使用管理器绑定的容器
@@ -286,6 +304,7 @@ declare class DialogManager {
* @param theme 全局主题配置 * @param theme 全局主题配置
*/ */
updateTheme(theme: ThemeConfig): void; updateTheme(theme: ThemeConfig): void;
destroy(): void;
} }
/** /**
@@ -330,21 +349,49 @@ declare type DialogPosition = 'center' | 'top-left' | 'top-center' | 'top-right'
y: number; y: number;
}; };
declare interface EngineEvents {
'ui:open-dialog': {
id: string;
data?: any;
};
'ui:close-dialog': {
id: string;
};
'engine:model-loaded': {
url: string;
};
'engine:object-clicked': {
objectId: string;
position: {
x: number;
y: number;
z: number;
};
};
'sys:theme-changed': {
theme: string;
};
'sys:locale-changed': {
locale: string;
};
}
/** /**
* 3D 引擎管理器 * 3D 引擎管理器
* 负责连接 Engine 组件和 BimEngine向外部暴露简化的 API * 负责连接 Engine 组件和 BimEngine向外部暴露简化的 API
* 采用延迟初始化模式,用户需主动调用 initialize() 方法 * 采用延迟初始化模式,用户需主动调用 initialize() 方法
*/ */
declare class EngineManager { declare class EngineManager extends BimComponent {
/** 3D 引擎挂载的父容器 */ /** 3D 引擎挂载的父容器 */
private container; private container;
/** 3D 引擎组件实例 */ /** 3D 引擎组件实例 */
private engine; private engineInstance;
/** /**
* 构造函数 * 构造函数
* @param engine 引擎实例
* @param container 3D 引擎挂载的目标容器 * @param container 3D 引擎挂载的目标容器
*/ */
constructor(container: HTMLElement); constructor(engine: BimEngine, container: HTMLElement);
/** /**
* 初始化 3D 引擎 * 初始化 3D 引擎
* @param options 引擎配置选项(可选,如果不提供则使用默认配置) * @param options 引擎配置选项(可选,如果不提供则使用默认配置)
@@ -389,6 +436,14 @@ export declare interface EngineOptions {
showViewCube?: boolean; showViewCube?: boolean;
} }
declare class EventEmitter {
private events;
on(event: string, listener: Listener): () => void;
off(event: string, listener: Listener): void;
emit(event: string, payload?: any): void;
clear(): void;
}
/** 二级菜单展开方向 */ /** 二级菜单展开方向 */
declare type ExpandDirection = 'up' | 'down' | 'left' | 'right'; declare type ExpandDirection = 'up' | 'down' | 'left' | 'right';
@@ -428,6 +483,8 @@ declare interface IBimComponent {
destroy(): void; destroy(): void;
} }
declare type Listener<T = any> = (payload: T) => void;
declare type LocaleChangeListener = (locale: LocaleType) => void; declare type LocaleChangeListener = (locale: LocaleType) => void;
/** /**
@@ -568,11 +625,11 @@ export declare class Toolbar extends BimButtonGroup {
* 底部工具栏管理器 (ToolbarManager) * 底部工具栏管理器 (ToolbarManager)
* 仅负责管理底部工具栏实例。 * 仅负责管理底部工具栏实例。
*/ */
declare class ToolbarManager { declare class ToolbarManager extends BimComponent {
private toolbar; private toolbar;
private toolbarContainer; private toolbarContainer;
private container; private container;
constructor(container: HTMLElement); constructor(engine: BimEngine, container: HTMLElement);
private init; private init;
updateTheme(theme: ThemeConfig): void; updateTheme(theme: ThemeConfig): void;
refresh(): void; refresh(): void;

View File

@@ -18,9 +18,9 @@
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "tsc && vite build", "build": "tsc && vite build",
"dev:all": "./dev.sh", "dev:demo": "cd demo && npm run dev",
"dev:demo": "cd demo && npm run copy-sdk && npm run dev", "dev:demo-vue": "cd demo-vue && npm run dev",
"dev:demo-vue": "cd demo-vue && npm run copy-sdk && npm run dev" "dev:all": "npm run dev:demo & npm run dev:demo-vue"
}, },
"keywords": [ "keywords": [
"bim", "bim",

View File

@@ -8,12 +8,12 @@ import { localeManager } from './services/locale';
import { themeManager } from './services/theme'; import { themeManager } from './services/theme';
import type { LocaleType } from './locales/types'; import type { LocaleType } from './locales/types';
import type { ThemeType, ThemeConfig } from './themes/types'; import type { ThemeType, ThemeConfig } from './themes/types';
import { EventEmitter } from './core/event-emitter';
import { EngineEvents } from './types/events';
export type { EngineOptions, ModelLoadOptions }; export type { EngineOptions, ModelLoadOptions };
export class BimEngine extends EventEmitter {
export class BimEngine {
private container: HTMLElement; private container: HTMLElement;
private wrapper: HTMLElement | null = null; private wrapper: HTMLElement | null = null;
private topLeftGroup: any = null; // 保存左上角按钮组的引用 private topLeftGroup: any = null; // 保存左上角按钮组的引用
@@ -33,6 +33,7 @@ export class BimEngine {
theme?: ThemeType; theme?: ThemeType;
} }
) { ) {
super();
const el = typeof container === 'string' ? document.getElementById(container) : container; const el = typeof container === 'string' ? document.getElementById(container) : container;
if (!el) throw new Error('Container not found'); if (!el) throw new Error('Container not found');
this.container = el; this.container = el;
@@ -49,6 +50,15 @@ export class BimEngine {
this.init(); this.init();
} }
// Typed wrappers for events
public emit<K extends keyof EngineEvents>(event: K, payload: EngineEvents[K]) {
super.emit(event, payload);
}
public on<K extends keyof EngineEvents>(event: K, listener: (payload: EngineEvents[K]) => void): () => void {
return super.on(event, listener);
}
public setLocale(locale: LocaleType) { localeManager.setLocale(locale); } public setLocale(locale: LocaleType) { localeManager.setLocale(locale); }
public getLocale(): LocaleType { return localeManager.getLocale(); } public getLocale(): LocaleType { return localeManager.getLocale(); }
public setTheme(theme: 'dark' | 'light') { themeManager.setTheme(theme); } public setTheme(theme: 'dark' | 'light') { themeManager.setTheme(theme); }
@@ -61,12 +71,12 @@ export class BimEngine {
this.container.appendChild(this.wrapper); this.container.appendChild(this.wrapper);
// 创建 3D 引擎管理器 // 创建 3D 引擎管理器
this.engine = new EngineManager(this.wrapper); this.engine = new EngineManager(this, this.wrapper);
// 初始化其他管理器 // 初始化其他管理器
this.dialog = new DialogManager(this.wrapper); this.dialog = new DialogManager(this, this.wrapper);
this.toolbar = new ToolbarManager(this.wrapper); this.toolbar = new ToolbarManager(this, this.wrapper);
this.buttonGroup = new ButtonGroupManager(this.wrapper); this.buttonGroup = new ButtonGroupManager(this, this.wrapper);
// 初始主题 // 初始主题
@@ -112,5 +122,6 @@ export class BimEngine {
this.engine?.destroy(); this.engine?.destroy();
this.dialog = null; this.dialog = null;
this.container.innerHTML = ''; this.container.innerHTML = '';
this.clear(); // Clear all events
} }
} }

View File

@@ -10,6 +10,8 @@ import { t, localeManager } from '../../services/locale';
import { themeManager } from '../../services/theme'; import { themeManager } from '../../services/theme';
import type { ThemeConfig } from '../../themes/types'; import type { ThemeConfig } from '../../themes/types';
import { IBimComponent } from '../../types/component'; import { IBimComponent } from '../../types/component';
import type { BimEngine } from '../../bim-engine';
import { EngineEvents } from '../../types/events';
/** /**
* 通用按钮组组件 (BimButtonGroup) * 通用按钮组组件 (BimButtonGroup)
@@ -26,6 +28,8 @@ export class BimButtonGroup implements IBimComponent {
private unsubscribeLocale: (() => void) | null = null; private unsubscribeLocale: (() => void) | null = null;
private unsubscribeTheme: (() => void) | null = null; private unsubscribeTheme: (() => void) | null = null;
protected engine: BimEngine | null = null;
private readonly DEFAULT_ICON = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect></svg>'; private readonly DEFAULT_ICON = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect></svg>';
constructor(options: ButtonGroupOptions) { constructor(options: ButtonGroupOptions) {
@@ -63,6 +67,18 @@ export class BimButtonGroup implements IBimComponent {
this.applyStyles(); this.applyStyles();
} }
public setEngine(engine: BimEngine) {
this.engine = engine;
}
protected emit<K extends keyof EngineEvents>(event: K, payload: EngineEvents[K]) {
if (this.engine) {
this.engine.emit(event, payload);
} else {
console.warn('[BimButtonGroup] Engine not set, cannot emit event:', event);
}
}
private initContainer(): void { private initContainer(): void {
this.container.innerHTML = ''; this.container.innerHTML = '';
this.container.classList.add('bim-btn-group-root'); this.container.classList.add('bim-btn-group-root');

View File

@@ -1,16 +1,28 @@
import type { ButtonConfig } from '../../../index.type'; import type { ButtonConfig } from '../../../index.type';
import type { BimEngine } from '../../../../../bim-engine';
/** /**
* 首页按钮配置 * 首页按钮配置
* 使用工厂函数模式,注入 engine 实例
*/ */
export const homeButton: ButtonConfig = { export const createHomeButton = (engine: BimEngine): ButtonConfig => {
id: 'home', return {
groupId: 'group-1', id: 'home',
type: 'button', groupId: 'group-1',
label: 'toolbar.home', type: 'button',
icon: '<svg width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M4 21V9l8-6l8 6v12h-6v-7h-4v7z"/></svg>', label: 'toolbar.home',
keepActive: true, icon: '<svg width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M4 21V9l8-6l8 6v12h-6v-7h-4v7z"/></svg>',
onClick: (button) => { keepActive: true,
console.log('首页按钮被点击:', button.id); onClick: (button) => {
} console.log('首页按钮被点击:', button.id);
// 演示:使用 engine 发送事件
// engine.dialog?.showInfoDialog()
engine.emit('ui:open-dialog', { id: 'home-info' });
// 或者直接调用 engine 的方法
// if (engine.engine) {
// engine.engine.loadModel('...');
// }
}
};
}; };

View File

@@ -12,7 +12,7 @@ export class Toolbar extends BimButtonGroup {
await super.init(); await super.init();
// 动态加载默认按钮配置 // 动态加载默认按钮配置
const { homeButton } = await import('./buttons/home'); const { createHomeButton } = await import('./buttons/home');
const { locationButton } = await import('./buttons/location'); const { locationButton } = await import('./buttons/location');
const { walkMenuButton } = await import('./buttons/walk/walk-menu'); const { walkMenuButton } = await import('./buttons/walk/walk-menu');
const { walkPersonButton } = await import('./buttons/walk/walk-person'); const { walkPersonButton } = await import('./buttons/walk/walk-person');
@@ -21,7 +21,14 @@ export class Toolbar extends BimButtonGroup {
const { infoButton } = await import('./buttons/info'); const { infoButton } = await import('./buttons/info');
this.addGroup('group-1'); this.addGroup('group-1');
this.addButton(homeButton);
// 使用工厂函数创建按钮,并注入 engine
if (this.engine) {
this.addButton(createHomeButton(this.engine));
} else {
console.warn('[Toolbar] Engine not available when creating buttons.');
}
this.addButton(walkMenuButton); this.addButton(walkMenuButton);
this.addButton(walkPersonButton); this.addButton(walkPersonButton);
this.addButton(walkBirdButton); this.addButton(walkBirdButton);

27
src/core/component.ts Normal file
View File

@@ -0,0 +1,27 @@
import { BimEngine } from '../bim-engine';
import { EngineEvents } from '../types/events';
export abstract class BimComponent {
protected engine: BimEngine;
constructor(engine: BimEngine) {
this.engine = engine;
}
/**
* Helper to send events easily
*/
protected emit<K extends keyof EngineEvents>(event: K, payload: EngineEvents[K]): void {
this.engine.emit(event, payload);
}
/**
* Helper to listen to events easily
* Returns an unsubscribe function
*/
protected on<K extends keyof EngineEvents>(event: K, listener: (payload: EngineEvents[K]) => void): () => void {
return this.engine.on(event, listener);
}
abstract destroy(): void;
}

42
src/core/event-emitter.ts Normal file
View File

@@ -0,0 +1,42 @@
type Listener<T = any> = (payload: T) => void;
export class EventEmitter {
private events: Map<string, Listener[]> = new Map();
public on(event: string, listener: Listener): () => void {
if (!this.events.has(event)) {
this.events.set(event, []);
}
this.events.get(event)!.push(listener);
// Return unsubscribe function
return () => this.off(event, listener);
}
public off(event: string, listener: Listener): void {
const listeners = this.events.get(event);
if (!listeners) return;
const index = listeners.indexOf(listener);
if (index !== -1) {
listeners.splice(index, 1);
}
}
public emit(event: string, payload?: any): void {
const listeners = this.events.get(event);
if (listeners) {
listeners.forEach(listener => {
try {
listener(payload);
} catch (error) {
console.error(`[EventEmitter] Error in listener for event "${event}":`, error);
}
});
}
}
public clear(): void {
this.events.clear();
}
}

View File

@@ -1,48 +1,46 @@
import { BimButtonGroup } from '../components/button-group'; import { BimButtonGroup } from '../components/button-group';
import type { ButtonGroupOptions } from '../components/button-group/index.type'; import type { ButtonGroupOptions } from '../components/button-group/index.type';
import type { ThemeConfig } from '../themes/types'; import type { ThemeConfig } from '../themes/types';
import { BimComponent } from '../core/component';
import type { BimEngine } from '../bim-engine';
/** /**
* 通用按钮组管理器 * 通用按钮组管理器 (ButtonGroupManager)
* 负责创建和管理除底部工具栏以外的其他按钮组。 * 负责创建和管理通用的按钮组实例
*/ */
export class ButtonGroupManager { export class ButtonGroupManager extends BimComponent {
private activeGroups: BimButtonGroup[] = []; private groups: Map<string, BimButtonGroup> = new Map();
private container: HTMLElement; private container: HTMLElement;
constructor(container: HTMLElement) { constructor(engine: BimEngine, container: HTMLElement) {
super(engine);
this.container = container; this.container = container;
} }
/** public create(id: string, options: Omit<ButtonGroupOptions, 'container'>): BimButtonGroup {
* 创建一个新的按钮组
*/
public create(options: Omit<ButtonGroupOptions, 'container'>): BimButtonGroup {
// 自动创建一个 div 作为容器
const groupContainer = document.createElement('div');
this.container.appendChild(groupContainer);
const group = new BimButtonGroup({ const group = new BimButtonGroup({
container: groupContainer, container: this.container,
...options ...options
}); });
// 立即初始化 // @ts-ignore
group.setEngine(this.engine);
group.init(); group.init();
this.activeGroups.push(group); this.groups.set(id, group);
return group; return group;
} }
public updateTheme(theme: ThemeConfig) { public get(id: string): BimButtonGroup | undefined {
this.activeGroups.forEach(g => g.setTheme(theme)); return this.groups.get(id);
} }
public refresh() { public updateTheme(theme: ThemeConfig) {
this.activeGroups.forEach(g => g.render()); this.groups.forEach(group => group.setTheme(theme));
} }
public destroy() { public destroy() {
this.activeGroups.forEach(g => g.destroy()); this.groups.forEach(group => group.destroy());
this.activeGroups = []; this.groups.clear();
} }
} }

View File

@@ -2,13 +2,15 @@ import { BimDialog } from '../components/dialog';
import { BimInfoDialog } from '../components/dialog/bimInfoDialog'; import { BimInfoDialog } from '../components/dialog/bimInfoDialog';
import type { DialogOptions } from '../components/dialog/index.type'; import type { DialogOptions } from '../components/dialog/index.type';
import type { ThemeConfig } from '../themes/types'; import type { ThemeConfig } from '../themes/types';
import { themeManager } from '../services/theme'; // 修正路径 import { themeManager } from '../services/theme';
import { BimComponent } from '../core/component';
import type { BimEngine } from '../bim-engine';
/** /**
* 弹窗管理器 * 弹窗管理器
* 负责创建和管理应用中的各类弹窗。 * 负责创建和管理应用中的各类弹窗。
*/ */
export class DialogManager { export class DialogManager extends BimComponent {
/** 弹窗挂载的父容器 */ /** 弹窗挂载的父容器 */
private container: HTMLElement; private container: HTMLElement;
/** 活跃的弹窗实例列表 */ /** 活跃的弹窗实例列表 */
@@ -16,10 +18,22 @@ export class DialogManager {
/** /**
* 构造函数 * 构造函数
* @param engine 引擎实例
* @param container 弹窗挂载的目标容器 * @param container 弹窗挂载的目标容器
*/ */
constructor(container: HTMLElement) { constructor(engine: BimEngine, container: HTMLElement) {
super(engine);
this.container = container; this.container = container;
// 监听打开弹窗事件
this.on('ui:open-dialog', (payload) => {
// 这里可以根据 payload.id 做更复杂的逻辑,目前简单演示
console.log('[DialogManager] Received open-dialog event:', payload);
// 示例:如果 payload.id 是 'info',则打开 info dialog
if (payload.id === 'info') {
this.showInfoDialog();
}
});
} }
/** /**
@@ -66,4 +80,9 @@ export class DialogManager {
} }
}); });
} }
public destroy() {
this.activeDialogs.forEach(d => d.destroy());
this.activeDialogs = [];
}
} }

View File

@@ -1,21 +1,25 @@
import { Engine, type EngineOptions, type ModelLoadOptions } from '../components/engine'; import { Engine, type EngineOptions, type ModelLoadOptions } from '../components/engine';
import { BimComponent } from '../core/component';
import type { BimEngine } from '../bim-engine';
/** /**
* 3D 引擎管理器 * 3D 引擎管理器
* 负责连接 Engine 组件和 BimEngine向外部暴露简化的 API * 负责连接 Engine 组件和 BimEngine向外部暴露简化的 API
* 采用延迟初始化模式,用户需主动调用 initialize() 方法 * 采用延迟初始化模式,用户需主动调用 initialize() 方法
*/ */
export class EngineManager { export class EngineManager extends BimComponent {
/** 3D 引擎挂载的父容器 */ /** 3D 引擎挂载的父容器 */
private container: HTMLElement; private container: HTMLElement;
/** 3D 引擎组件实例 */ /** 3D 引擎组件实例 */
private engine: Engine | null = null; private engineInstance: Engine | null = null;
/** /**
* 构造函数 * 构造函数
* @param engine 引擎实例
* @param container 3D 引擎挂载的目标容器 * @param container 3D 引擎挂载的目标容器
*/ */
constructor(container: HTMLElement) { constructor(engine: BimEngine, container: HTMLElement) {
super(engine);
this.container = container; this.container = container;
} }
@@ -26,27 +30,27 @@ export class EngineManager {
*/ */
public initialize(options?: Omit<EngineOptions, 'container'>): boolean { public initialize(options?: Omit<EngineOptions, 'container'>): boolean {
// 如果已经初始化,先销毁旧的实例 // 如果已经初始化,先销毁旧的实例
if (this.engine && this.engine.isInitialized()) { if (this.engineInstance && this.engineInstance.isInitialized()) {
console.warn('[EngineManager] 3D Engine already initialized. Destroying old instance...'); console.warn('[EngineManager] 3D Engine already initialized. Destroying old instance...');
this.engine.destroy(); this.engineInstance.destroy();
this.engine = null; this.engineInstance = null;
} }
try { try {
// 创建 Engine 组件实例 // 创建 Engine 组件实例
// options 中的配置会自动复制给 createEngine 使用 // options 中的配置会自动复制给 createEngine 使用
this.engine = new Engine({ this.engineInstance = new Engine({
container: this.container, container: this.container,
...options, // 合并配置选项 ...options, // 合并配置选项
}); });
// 调用组件的 init 方法初始化引擎 // 调用组件的 init 方法初始化引擎
this.engine.init(); this.engineInstance.init();
return this.engine.isInitialized(); return this.engineInstance.isInitialized();
} catch (error) { } catch (error) {
console.error('[EngineManager] Failed to initialize 3D engine:', error); console.error('[EngineManager] Failed to initialize 3D engine:', error);
this.engine = null; this.engineInstance = null;
return false; return false;
} }
} }
@@ -55,7 +59,7 @@ export class EngineManager {
* 检查 3D 引擎是否已初始化 * 检查 3D 引擎是否已初始化
*/ */
public isInitialized(): boolean { public isInitialized(): boolean {
return this.engine !== null && this.engine.isInitialized(); return this.engineInstance !== null && this.engineInstance.isInitialized();
} }
/** /**
@@ -64,11 +68,11 @@ export class EngineManager {
* @param options 加载选项(位置、旋转、缩放) * @param options 加载选项(位置、旋转、缩放)
*/ */
public loadModel(url: string, options?: ModelLoadOptions): void { public loadModel(url: string, options?: ModelLoadOptions): void {
if (!this.engine || !this.engine.isInitialized()) { if (!this.engineInstance || !this.engineInstance.isInitialized()) {
console.error('[EngineManager] 3D Engine not initialized. Please call initialize() first.'); console.error('[EngineManager] 3D Engine not initialized. Please call initialize() first.');
return; return;
} }
this.engine.loadModel(url, options); this.engineInstance.loadModel(url, options);
} }
/** /**
@@ -76,20 +80,20 @@ export class EngineManager {
* 用于直接调用第三方引擎的其他 API * 用于直接调用第三方引擎的其他 API
*/ */
public getEngine(): any { public getEngine(): any {
if (!this.engine) { if (!this.engineInstance) {
console.warn('[EngineManager] 3D Engine not initialized.'); console.warn('[EngineManager] 3D Engine not initialized.');
return null; return null;
} }
return this.engine.getEngine(); return this.engineInstance.getEngine();
} }
/** /**
* 销毁 3D 引擎实例 * 销毁 3D 引擎实例
*/ */
public destroy(): void { public destroy(): void {
if (this.engine) { if (this.engineInstance) {
this.engine.destroy(); this.engineInstance.destroy();
this.engine = null; this.engineInstance = null;
} }
} }
} }

View File

@@ -1,17 +1,20 @@
import type { ButtonGroupColors, ButtonConfig } from '../components/button-group/index.type'; import type { ButtonGroupColors, ButtonConfig } from '../components/button-group/index.type';
import { Toolbar } from '../components/button-group/toolbar'; import { Toolbar } from '../components/button-group/toolbar';
import type { ThemeConfig } from '../themes/types'; import type { ThemeConfig } from '../themes/types';
import { BimComponent } from '../core/component';
import type { BimEngine } from '../bim-engine';
/** /**
* 底部工具栏管理器 (ToolbarManager) * 底部工具栏管理器 (ToolbarManager)
* 仅负责管理底部工具栏实例。 * 仅负责管理底部工具栏实例。
*/ */
export class ToolbarManager { export class ToolbarManager extends BimComponent {
private toolbar: Toolbar | null = null; private toolbar: Toolbar | null = null;
private toolbarContainer: HTMLElement | null = null; private toolbarContainer: HTMLElement | null = null;
private container: HTMLElement; private container: HTMLElement;
constructor(container: HTMLElement) { constructor(engine: BimEngine, container: HTMLElement) {
super(engine);
this.container = container; this.container = container;
this.init(); this.init();
} }
@@ -32,6 +35,10 @@ export class ToolbarManager {
expand: 'up' // 向上展开 expand: 'up' // 向上展开
}); });
// 注入 engine 到 Toolbar
// @ts-ignore - Toolbar 还没更新类型,暂时忽略
this.toolbar.setEngine(this.engine);
this.toolbar.init(); this.toolbar.init();
} }

13
src/types/events.ts Normal file
View File

@@ -0,0 +1,13 @@
export interface EngineEvents {
// UI Events
'ui:open-dialog': { id: string; data?: any };
'ui:close-dialog': { id: string };
// Engine Events
'engine:model-loaded': { url: string };
'engine:object-clicked': { objectId: string; position: { x: number, y: number, z: number } };
// System Events
'sys:theme-changed': { theme: string };
'sys:locale-changed': { locale: string };
}