Files
bim_engine/src/components/engine/index.ts

783 lines
24 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import type { ThemeConfig } from '../../themes/types';
import { IBimComponent } from '../../types/component';
import { themeManager } from '../../services/theme';
import type { EngineOptions, ModelLoadOptions } from './types';
import type { MeasureMode } from '../../types/measure';
import type { SectionBoxRange } from '../section-box-panel/types';
// 导入第三方 SDK 的 createEngine 函数(从 npm 包引入)
// import { createEngine as createEngineSDK } from 'iflow-engine-base';
import { createEngine as createEngineSDK } from '../../../../bim_engine_base/dist/bim-engine-sdk.es';
// 重新导出类型,方便外部引用
export type { EngineOptions, ModelLoadOptions };
/**
* 创建 Engine 实例的工厂函数
* 兼容旧代码直接 import { createEngine } 的方式
*/
export const createEngine = (options: EngineOptions) => {
return new Engine(options);
};
/**
* 3D 引擎组件
* 负责创建和管理第三方 3D 引擎实例
*/
export class Engine implements IBimComponent {
/** 第三方 3D 引擎实例 */
private engine: any = null;
/** 引擎挂载的容器元素 */
private container: HTMLElement;
/** 引擎容器 ID用于传递给 createEngine */
private containerId: string;
/** 引擎配置选项(不包含 container */
private options: Omit<EngineOptions, 'container'>;
/** 是否已初始化 */
private _isInitialized = false;
/** 是否已销毁 */
private _isDestroyed = false;
/** 主题订阅取消函数 */
private unsubscribeTheme: (() => void) | null = null;
/** 当前激活的测量类型 */
private currentMeasureType: MeasureMode | null = null;
/** 主测量功能是否已激活 */
private isMeasureActive: boolean = false;
/** 当前激活的剖切轴向 */
private currentSectionAxis: 'x' | 'y' | 'z' | null = null;
/** 剖切盒是否激活 */
private isSectionBoxActive: boolean = false;
/** 当前选中的构件信息 */
private selectedComponent: { url: string; id: string } | null = null;
/**
* 构造函数
* @param options 3D 引擎配置选项
*/
constructor(options: EngineOptions) {
// 解析容器元素
this.container = options.container;
// 如果容器没有 id生成一个唯一的 id
if (!this.container.id) {
this.containerId = `engine-container-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
this.container.id = this.containerId;
} else {
this.containerId = this.container.id;
}
// 保存配置选项(设置默认值)
this.options = {
backgroundColor: 'linear-gradient(to bottom, rgb(214, 224, 235), rgb(246, 250, 255))', // 固定背景渐变色
version: options.version ?? 'v2', // 默认使用 v2 版本
showStats: options.showStats ?? false, // 默认不显示统计
showViewCube: options.showViewCube ?? true, // 默认显示视图立方体
};
}
/**
* 初始化组件 (接口实现)
* 创建 div 容器并初始化引擎
*/
public init(): void {
if (this._isInitialized) {
console.warn('[Engine] Engine already initialized.');
return;
}
if (this._isDestroyed) {
console.error('[Engine] Cannot initialize destroyed engine.');
return;
}
try {
// 应用背景色到容器
if (typeof this.options.backgroundColor === 'string') {
this.container.style.background = this.options.backgroundColor;
}
// 创建引擎配置对象
const engineConfig = {
containerId: this.containerId,
backgroundColor: this.options.backgroundColor,
version: this.options.version,
showStats: this.options.showStats,
showViewCube: this.options.showViewCube,
};
// 输出配置信息
console.log('引擎配置信息:', engineConfig);
// 调用引擎创建函数创建引擎实例
// 将 options 中的配置复制给 createEngine
this.engine = createEngineSDK(engineConfig);
if (!this.engine) {
throw new Error('Failed to create engine instance');
}
// 标记为已初始化
this._isInitialized = true;
// 订阅主题变化
this.unsubscribeTheme = themeManager.subscribe((theme) => {
this.setTheme(theme);
});
// 应用当前主题
this.setTheme(themeManager.getTheme());
// 监听构件点击事件
this.engine.events.on('click', (hit: any) => {
if (hit && hit.object) {
this.selectedComponent = {
url: hit.object.url,
id: hit.object.name
};
console.log('[Engine] 构件选中:', this.selectedComponent);
} else {
this.selectedComponent = null;
console.log('[Engine] 取消选中');
}
});
} catch (error) {
console.error('[Engine] Failed to initialize engine:', error);
this._isInitialized = false;
throw error;
}
}
/**
* 设置主题 (接口实现)
* 根据主题调整 3D 引擎的视觉效果(如背景色)
* @param theme 全局主题配置
*/
public setTheme(_theme: ThemeConfig): void {
}
/**
* 设置语言 (接口实现)
*/
public setLocales(): void {
// 3D 引擎组件暂时不需要本地化
}
/**
* 检查是否已初始化
*/
public isInitialized(): boolean {
return this._isInitialized;
}
/**
* 加载 3D 模型
* @param url 模型文件 URL
* @param options 加载选项(位置、旋转、缩放)
*/
public loadModel(url: string, options?: ModelLoadOptions): void {
if (!this._isInitialized || !this.engine) {
console.error('[Engine] Engine not initialized. Please call init() first.');
return;
}
if (!url) {
console.error('[Engine] Model URL is required.');
return;
}
this.engine.loaderModule.loadModels([url], options);
}
/**
* 回到主视角
* @returns
*/
public CameraGoHome() {
this.engine.viewCube.CameraGoHome();
}
/**
* 获取原始 3D 引擎实例
*/
public getEngine(): any {
return this.engine;
}
/**
* 暂停渲染
*/
public pauseRendering(): void {
if (!this._isInitialized || !this.engine) {
console.warn('[Engine] Engine not initialized.');
return;
}
this.engine.pauseRendering();
}
/**
* 恢复渲染
*/
public resumeRendering(): void {
if (!this._isInitialized || !this.engine) {
console.warn('[Engine] Engine not initialized.');
return;
}
this.engine.resumeRendering();
}
/**
* 销毁 3D 引擎,释放 GPU 资源
*/
public dispose(): void {
if (!this._isInitialized || !this.engine) {
console.warn('[Engine] Engine not initialized.');
return;
}
this.engine.dispose();
}
// ==================== 测量功能方法 ====================
/**
* 激活具体测量类型的统一入口(私有方法)
* @param type 测量类型
* @param activateFunc 第三方引擎的激活函数
*/
private activateMeasureType(type: MeasureMode, activateFunc: () => void): void {
// 1. 检查引擎是否初始化
if (!this._isInitialized || !this.engine) {
console.error('Cannot activate measure: engine not initialized.');
return;
}
// 2. 检查 measure 模块是否存在
if (!this.engine.measure) {
console.error('Measure module not available.');
return;
}
if (!this.isMeasureActive) {
console.log('激活测量功能');
this.engine.measure.active();
this.isMeasureActive = true;
}
// 4. 激活具体的测量类型(直接切换,不需要停用前一个)
activateFunc();
this.currentMeasureType = type;
}
/**
* 激活距离测量
*/
public activateDistanceMeasure(): void {
this.activateMeasureType('distance', () => {
console.log(`激活距离测量`);
this.engine.measure.distanceMeasure.active();
});
}
/**
* 激活最小距离测量
*/
public activateMinDistanceMeasure(): void {
this.activateMeasureType('minDistance', () => {
console.log(`激活最小距离测量`);
this.engine.measure.clearDistanceMeasure.active();
});
}
/**
* 激活角度测量
*/
public activateAngleMeasure(): void {
this.activateMeasureType('angle', () => {
console.log(`激活角度测量`);
this.engine.measure.angleMeasure.active();
console.log('[Engine] Angle measure activated (placeholder)');
});
}
/**
* 激活标高测量
*/
public activateElevationMeasure(): void {
this.activateMeasureType('elevation', () => {
console.log(`激活标高测量`);
this.engine.measure.elevationMeasure.active();
});
}
/**
* 激活体积测量
*/
public activateVolumeMeasure(): void {
this.activateMeasureType('volume', () => {
console.log(`激活体积测量`);
this.engine.measure.areaMeasure.active();
});
}
/**
* 激活激光测距
*/
public activateLaserDistanceMeasure(): void {
this.activateMeasureType('laserDistance', () => {
// TODO: 调用第三方引擎方法(当前先空着)
// this.engine.measure.laserDistanceMeasure.active();
console.log('[Engine] Laser distance measure activated (placeholder)');
});
}
/**
* 激活坡度测量
*/
public activateSlopeMeasure(): void {
this.activateMeasureType('slope', () => {
console.log(`激活坡度测量`);
this.engine.measure.slopeMeasure.active();
});
}
/**
* 激活空间体积测量
*/
public activateSpaceVolumeMeasure(): void {
this.activateMeasureType('spaceVolume', () => {
// TODO: 调用第三方引擎方法(当前先空着)
// this.engine.measure.spaceVolumeMeasure.active();
console.log('[Engine] Space volume measure activated (placeholder)');
});
}
/**
* 激活测量功能(根据类型统一入口)
* @param mode 测量类型
*/
public activateMeasure(mode: MeasureMode): void {
switch (mode) {
case 'distance':
this.activateDistanceMeasure();
break;
case 'minDistance':
this.activateMinDistanceMeasure();
break;
case 'angle':
this.activateAngleMeasure();
break;
case 'elevation':
this.activateElevationMeasure();
break;
case 'volume':
this.activateVolumeMeasure();
break;
case 'laserDistance':
this.activateLaserDistanceMeasure();
break;
case 'slope':
this.activateSlopeMeasure();
break;
case 'spaceVolume':
this.activateSpaceVolumeMeasure();
break;
}
}
/**
* 停用测量功能(关闭测量时调用)
*/
public deactivateMeasure(): void {
if (!this._isInitialized || !this.engine?.measure) {
return;
}
if (!this.isMeasureActive) {
return;
}
console.log('停用测量功能');
this.engine.measure.disActive();
this.isMeasureActive = false;
this.currentMeasureType = null;
}
/**
* 获取当前激活的测量类型
* @returns 当前测量类型,如果未激活则返回 null
*/
public getCurrentMeasureType(): MeasureMode | null {
return this.currentMeasureType;
}
/**
* 清除所有测量标注
*/
public clearAllMeasures(): void {
if (!this._isInitialized || !this.engine?.measure) {
return;
}
console.log('清除所有测量标注');
this.engine.measure.clearAll();
}
// ==================== 结束:测量功能方法 ====================
// ==================== 轴向剖切功能 ====================
/**
* 激活轴向剖切
* @param axis 要激活的轴向 ('x' | 'y' | 'z')
* @remarks
* - 如果传入的轴向与当前激活的轴向相同,则静默返回(幂等操作)
* - 如果当前有不同的轴向激活,先调用该轴向的 disActive() 再激活新轴向
* - 使用单个 plane 的 disActive() 而非 clipping.disActive(),避免影响其他剖切功能
* - 如果引擎未初始化或 clipping 模块不可用,方法会静默返回并输出错误日志
*/
public activateSectionAxis(axis: 'x' | 'y' | 'z'): void {
// 1. 检查引擎初始化
if (!this._isInitialized || !this.engine) {
console.error('[Engine] Cannot activate section axis: engine not initialized.');
return;
}
// 2. 检查 clipping 模块
if (!this.engine.clipping) {
console.error('[Engine] Clipping module not available.');
return;
}
// 3. 如果是同一轴向,静默返回(幂等操作)
if (this.currentSectionAxis === axis) {
console.log(`[Engine] Section axis ${axis} already active, skipping.`);
return;
}
// 4. 如果当前有激活的轴向且不同,先停用当前轴向
if (this.currentSectionAxis) {
this.deactivateCurrentSectionAxis();
}
// 5. 激活新轴向
const planeMap: Record<'x' | 'y' | 'z', any> = {
'x': this.engine.clipping.sectionPlaneX,
'y': this.engine.clipping.sectionPlaneY,
'z': this.engine.clipping.sectionPlaneZ
};
const plane = planeMap[axis];
if (plane && typeof plane.active === 'function') {
console.log(`[Engine] Activating section axis: ${axis}`);
plane.active();
this.currentSectionAxis = axis;
} else {
console.error(`[Engine] Section plane ${axis} not available.`);
}
}
/**
* 停用当前轴向剖切(内部方法)
* @remarks 只停用当前激活的单个轴向,不影响其他剖切功能
*/
private deactivateCurrentSectionAxis(): void {
if (!this.currentSectionAxis || !this.engine?.clipping) {
return;
}
const planeMap: Record<'x' | 'y' | 'z', any> = {
'x': this.engine.clipping.sectionPlaneX,
'y': this.engine.clipping.sectionPlaneY,
'z': this.engine.clipping.sectionPlaneZ
};
const plane = planeMap[this.currentSectionAxis];
if (plane && typeof plane.disActive === 'function') {
console.log(`[Engine] Deactivating section axis: ${this.currentSectionAxis}`);
plane.disActive();
}
}
/**
* 停用轴向剖切(公共方法,关闭弹窗时调用)
* @remarks 使用 clipping.disActive() 停用所有剖切,清理状态
*/
public deactivateSectionAxis(): void {
if (!this._isInitialized || !this.engine?.clipping) {
return;
}
if (this.currentSectionAxis) {
console.log('[Engine] Deactivating all section axis');
this.engine.clipping.disActive();
this.currentSectionAxis = null;
}
}
/**
* 获取当前剖切轴向
* @returns 当前激活的轴向,如果未激活则返回 null
*/
public getCurrentSectionAxis(): 'x' | 'y' | 'z' | null {
return this.currentSectionAxis;
}
// ==================== 结束:轴向剖切功能 ====================
// ==================== 剖切盒功能 ====================
/** 激活剖切盒 */
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) {
console.log('[Engine] Section box already active, skipping.');
return;
}
console.log('[Engine] Activating section box');
this.engine.clipping.sectionBox.active();
this.isSectionBoxActive = true;
}
/** 停用剖切盒 */
public deactivateSectionBox(): void {
if (!this._isInitialized || !this.engine?.clipping?.sectionBox) {
return;
}
if (!this.isSectionBoxActive) {
return;
}
console.log('[Engine] Deactivating section box');
this.engine.clipping.sectionBox.disActive();
this.isSectionBoxActive = false;
}
/** 设置剖切盒范围(百分比 0-100 */
public setSectionBoxRange(range: SectionBoxRange): void {
if (!this._isInitialized || !this.engine?.clipping?.sectionBox) {
console.error('[Engine] Cannot set section box range: engine not initialized.');
return;
}
console.log('[Engine] Setting section box range:', range);
this.engine.clipping.sectionBox.setboxPercent(range);
}
/** 适应剖切盒到模型(将范围设置为整个模型包围盒) */
public fitSectionBoxToModel(): void {
if (!this._isInitialized || !this.engine?.clipping?.sectionBox) {
console.error('[Engine] Cannot fit section box: engine not initialized.');
return;
}
const box = this.engine.octreeBox?.getBoundingBox();
if (!box) {
console.error('[Engine] Cannot fit section box: model bounding box not available.');
return;
}
console.log('[Engine] Fitting section box to model');
this.engine.clipping.sectionBox.setBox(box);
}
/**
* 重置剖切盒
* @remarks 将剖切盒范围恢复为整个模型的包围盒
*/
public resetSectionBox(): void {
this.fitSectionBoxToModel();
}
// ==================== 结束:剖切盒功能 ====================
// ==================== 漫游功能 ====================
/** 漫游模式是否激活 */
private isWalkModeActive: boolean = false;
/**
* 激活第一人称漫游模式
* @remarks 切换到第一人称控制器,禁用轨道控制器
*/
public activateFirstPersonMode(): void {
if (!this._isInitialized || !this.engine) {
console.error('[Engine] Cannot activate first person mode: engine not initialized.');
return;
}
if (!this.engine.controlModule) {
console.error('[Engine] Control module not available.');
return;
}
if (this.isWalkModeActive) {
console.log('[Engine] First person mode already active, skipping.');
return;
}
console.log('[Engine] Activating first person mode');
this.engine.controlModule.switchFirstPersonMode();
this.isWalkModeActive = true;
}
/**
* 停用第一人称漫游模式
* @remarks 切换回轨道控制器
*/
public deactivateFirstPersonMode(): void {
if (!this._isInitialized || !this.engine?.controlModule) {
return;
}
if (!this.isWalkModeActive) {
return;
}
console.log('[Engine] Deactivating first person mode');
this.engine.controlModule.switchDefaultMode();
this.isWalkModeActive = false;
}
/**
* 设置漫游移动速度
* @param speed 移动速度(默认 0.02
*/
public setWalkSpeed(speed: number): void {
if (!this._isInitialized || !this.engine?.controlModule?.firstPersonControls) {
console.error('[Engine] Cannot set walk speed: engine not initialized.');
return;
}
console.log('[Engine] Setting walk speed:', speed);
this.engine.controlModule.firstPersonControls.moveSpeed = speed;
}
/**
* 设置漫游重力开关
* @param enabled 是否启用重力
*/
public setWalkGravity(enabled: boolean): void {
if (!this._isInitialized || !this.engine?.controlModule?.firstPersonControls) {
console.error('[Engine] Cannot set walk gravity: engine not initialized.');
return;
}
console.log('[Engine] Setting walk gravity:', enabled);
this.engine.controlModule.firstPersonControls.applyGravity = enabled;
}
/**
* 设置漫游碰撞检测开关
* @param enabled 是否启用碰撞检测
*/
public setWalkCollision(enabled: boolean): void {
if (!this._isInitialized || !this.engine?.controlModule?.firstPersonControls) {
console.error('[Engine] Cannot set walk collision: engine not initialized.');
return;
}
console.log('[Engine] Setting walk collision:', enabled);
this.engine.controlModule.firstPersonControls.applyCollision = enabled;
}
/**
* 获取漫游模式是否激活
* @returns 是否处于漫游模式
*/
public isFirstPersonModeActive(): boolean {
return this.isWalkModeActive;
}
// ==================== 结束:漫游功能 ====================
// ==================== 构件选中 ====================
/**
* 获取当前选中的构件
* @returns 选中构件的 URL 和 ID未选中时返回 null
*/
public getSelectedComponent(): { url: string; id: string } | null {
return this.selectedComponent;
}
/**
* 获取构件属性
* @param url 模型 URL
* @param id 构件 ID
* @param callback 回调函数,接收属性数据
*/
public getComponentProperties(
url: string,
id: string,
callback: (data: any) => void
): void {
if (!this._isInitialized || !this.engine) {
console.error('[Engine] Cannot get component properties: engine not initialized.');
return;
}
if (!this.engine.modelProperties) {
console.error('[Engine] modelProperties not available');
return;
}
console.log('[Engine] Getting component properties:', { url, id });
this.engine.modelProperties.getModelProperties(url, id, callback);
}
// ==================== 结束:构件选中 ====================
/** 激活框选放大功能 */
public activateZoomBox(): void {
if (!this._isInitialized || !this.engine) {
console.warn('[Engine] Engine not initialized.');
return;
}
this.engine.rangeScale?.active();
}
/**
* 销毁组件 (接口实现)
* 清理资源、取消订阅、销毁引擎实例
*/
public destroy(): void {
if (this._isDestroyed) {
return;
}
// 停用测量功能
this.deactivateMeasure();
// 取消主题订阅
if (this.unsubscribeTheme) {
this.unsubscribeTheme();
this.unsubscribeTheme = null;
}
// 销毁 3D 引擎,释放 GPU 资源
if (this.engine) {
this.engine.dispose();
this.engine = null;
}
// 清理容器
this.container.innerHTML = '';
// 更新状态
this.currentMeasureType = null;
this.isMeasureActive = false;
this._isDestroyed = true;
this._isInitialized = false;
}
}