2025-12-04 18:41:11 +08:00
|
|
|
|
import type { ThemeConfig } from '../../themes/types';
|
|
|
|
|
|
import { IBimComponent } from '../../types/component';
|
|
|
|
|
|
import { themeManager } from '../../services/theme';
|
|
|
|
|
|
import type { EngineOptions, ModelLoadOptions } from './types';
|
2026-01-15 14:13:13 +08:00
|
|
|
|
import type { MeasureMode } from '../../types/measure';
|
2026-01-28 11:24:31 +08:00
|
|
|
|
import type { SectionBoxRange } from '../section-box-panel/types';
|
2026-01-22 11:29:51 +08:00
|
|
|
|
// 导入第三方 SDK 的 createEngine 函数(从 npm 包引入)
|
2026-01-28 11:24:31 +08:00
|
|
|
|
// import { createEngine as createEngineSDK } from 'iflow-engine-base';
|
|
|
|
|
|
import { createEngine as createEngineSDK } from '../../../../bim_engine_base/dist/bim-engine-sdk.es';
|
2026-01-27 17:58:56 +08:00
|
|
|
|
|
2025-12-04 18:41:11 +08:00
|
|
|
|
|
|
|
|
|
|
// 重新导出类型,方便外部引用
|
|
|
|
|
|
export type { EngineOptions, ModelLoadOptions };
|
|
|
|
|
|
|
2025-12-22 15:39:58 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 创建 Engine 实例的工厂函数
|
|
|
|
|
|
* 兼容旧代码直接 import { createEngine } 的方式
|
|
|
|
|
|
*/
|
|
|
|
|
|
export const createEngine = (options: EngineOptions) => {
|
|
|
|
|
|
return new Engine(options);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-12-04 18:41:11 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 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;
|
2026-01-15 14:13:13 +08:00
|
|
|
|
/** 当前激活的测量类型 */
|
|
|
|
|
|
private currentMeasureType: MeasureMode | null = null;
|
|
|
|
|
|
/** 主测量功能是否已激活 */
|
|
|
|
|
|
private isMeasureActive: boolean = false;
|
2026-01-27 17:58:56 +08:00
|
|
|
|
/** 当前激活的剖切轴向 */
|
|
|
|
|
|
private currentSectionAxis: 'x' | 'y' | 'z' | null = null;
|
2026-01-28 11:24:31 +08:00
|
|
|
|
/** 剖切盒是否激活 */
|
|
|
|
|
|
private isSectionBoxActive: boolean = false;
|
2026-01-28 11:55:57 +08:00
|
|
|
|
/** 当前选中的构件信息 */
|
|
|
|
|
|
private selectedComponent: { url: string; id: string } | null = null;
|
2025-12-04 18:41:11 +08:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 构造函数
|
|
|
|
|
|
* @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 = {
|
2026-01-21 15:50:07 +08:00
|
|
|
|
backgroundColor: 'linear-gradient(to bottom, rgb(214, 224, 235), rgb(246, 250, 255))', // 固定背景渐变色
|
2026-01-23 16:27:04 +08:00
|
|
|
|
version: options.version ?? 'v2', // 默认使用 v2 版本
|
2025-12-04 18:41:11 +08:00
|
|
|
|
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 {
|
2026-01-15 14:13:13 +08:00
|
|
|
|
// 应用背景色到容器
|
|
|
|
|
|
if (typeof this.options.backgroundColor === 'string') {
|
|
|
|
|
|
this.container.style.background = this.options.backgroundColor;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-04 18:41:11 +08:00
|
|
|
|
// 创建引擎配置对象
|
|
|
|
|
|
const engineConfig = {
|
|
|
|
|
|
containerId: this.containerId,
|
|
|
|
|
|
backgroundColor: this.options.backgroundColor,
|
|
|
|
|
|
version: this.options.version,
|
|
|
|
|
|
showStats: this.options.showStats,
|
|
|
|
|
|
showViewCube: this.options.showViewCube,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-01-15 14:13:13 +08:00
|
|
|
|
// 输出配置信息
|
|
|
|
|
|
console.log('引擎配置信息:', engineConfig);
|
|
|
|
|
|
|
2025-12-04 18:41:11 +08:00
|
|
|
|
// 调用引擎创建函数创建引擎实例
|
|
|
|
|
|
// 将 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());
|
2026-01-28 11:55:57 +08:00
|
|
|
|
|
|
|
|
|
|
// 监听构件点击事件
|
|
|
|
|
|
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] 取消选中');
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
2025-12-04 18:41:11 +08:00
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('[Engine] Failed to initialize engine:', error);
|
|
|
|
|
|
this._isInitialized = false;
|
|
|
|
|
|
throw error;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 设置主题 (接口实现)
|
|
|
|
|
|
* 根据主题调整 3D 引擎的视觉效果(如背景色)
|
|
|
|
|
|
* @param theme 全局主题配置
|
|
|
|
|
|
*/
|
2026-01-15 14:13:13 +08:00
|
|
|
|
public setTheme(_theme: ThemeConfig): void {
|
2025-12-04 18:41:11 +08:00
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 设置语言 (接口实现)
|
|
|
|
|
|
*/
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
2026-01-15 14:13:13 +08:00
|
|
|
|
this.engine.loaderModule.loadModels([url], options);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 回到主视角
|
|
|
|
|
|
* @returns
|
|
|
|
|
|
*/
|
|
|
|
|
|
public CameraGoHome() {
|
|
|
|
|
|
this.engine.viewCube.CameraGoHome();
|
2025-12-04 18:41:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取原始 3D 引擎实例
|
|
|
|
|
|
*/
|
|
|
|
|
|
public getEngine(): any {
|
|
|
|
|
|
return this.engine;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-23 16:27:04 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 暂停渲染
|
|
|
|
|
|
*/
|
|
|
|
|
|
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();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-27 17:58:56 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 销毁 3D 引擎,释放 GPU 资源
|
|
|
|
|
|
*/
|
|
|
|
|
|
public dispose(): void {
|
|
|
|
|
|
if (!this._isInitialized || !this.engine) {
|
|
|
|
|
|
console.warn('[Engine] Engine not initialized.');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
this.engine.dispose();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-15 14:13:13 +08:00
|
|
|
|
// ==================== 测量功能方法 ====================
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 激活具体测量类型的统一入口(私有方法)
|
|
|
|
|
|
* @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) {
|
2026-01-27 17:58:56 +08:00
|
|
|
|
console.log('激活测量功能');
|
2026-01-15 14:13:13 +08:00
|
|
|
|
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(`激活最小距离测量`);
|
2026-01-27 17:58:56 +08:00
|
|
|
|
this.engine.measure.clearDistanceMeasure.active();
|
2026-01-15 14:13:13 +08:00
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 激活角度测量
|
|
|
|
|
|
*/
|
|
|
|
|
|
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(`激活体积测量`);
|
2026-01-27 17:58:56 +08:00
|
|
|
|
this.engine.measure.areaMeasure.active();
|
2026-01-15 14:13:13 +08:00
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 激活激光测距
|
|
|
|
|
|
*/
|
|
|
|
|
|
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) {
|
2026-01-27 17:58:56 +08:00
|
|
|
|
return;
|
2026-01-15 14:13:13 +08:00
|
|
|
|
}
|
2026-01-27 17:58:56 +08:00
|
|
|
|
|
2026-01-15 14:13:13 +08:00
|
|
|
|
console.log('停用测量功能');
|
|
|
|
|
|
this.engine.measure.disActive();
|
|
|
|
|
|
this.isMeasureActive = false;
|
|
|
|
|
|
this.currentMeasureType = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取当前激活的测量类型
|
|
|
|
|
|
* @returns 当前测量类型,如果未激活则返回 null
|
|
|
|
|
|
*/
|
|
|
|
|
|
public getCurrentMeasureType(): MeasureMode | null {
|
|
|
|
|
|
return this.currentMeasureType;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-27 17:58:56 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 清除所有测量标注
|
|
|
|
|
|
*/
|
|
|
|
|
|
public clearAllMeasures(): void {
|
|
|
|
|
|
if (!this._isInitialized || !this.engine?.measure) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
console.log('清除所有测量标注');
|
|
|
|
|
|
this.engine.measure.clearAll();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-15 14:13:13 +08:00
|
|
|
|
// ==================== 结束:测量功能方法 ====================
|
|
|
|
|
|
|
2026-01-27 17:58:56 +08:00
|
|
|
|
// ==================== 轴向剖切功能 ====================
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 激活轴向剖切
|
|
|
|
|
|
* @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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ==================== 结束:轴向剖切功能 ====================
|
|
|
|
|
|
|
2026-01-28 11:24:31 +08:00
|
|
|
|
// ==================== 剖切盒功能 ====================
|
|
|
|
|
|
|
|
|
|
|
|
/** 激活剖切盒 */
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ==================== 结束:漫游功能 ====================
|
|
|
|
|
|
|
2026-01-28 11:55:57 +08:00
|
|
|
|
// ==================== 构件选中 ====================
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取当前选中的构件
|
|
|
|
|
|
* @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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ==================== 结束:构件选中 ====================
|
|
|
|
|
|
|
2026-01-27 17:58:56 +08:00
|
|
|
|
/** 激活框选放大功能 */
|
|
|
|
|
|
public activateZoomBox(): void {
|
|
|
|
|
|
if (!this._isInitialized || !this.engine) {
|
|
|
|
|
|
console.warn('[Engine] Engine not initialized.');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
this.engine.rangeScale?.active();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-04 18:41:11 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 销毁组件 (接口实现)
|
|
|
|
|
|
* 清理资源、取消订阅、销毁引擎实例
|
|
|
|
|
|
*/
|
|
|
|
|
|
public destroy(): void {
|
|
|
|
|
|
if (this._isDestroyed) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-01-15 14:13:13 +08:00
|
|
|
|
|
|
|
|
|
|
// 停用测量功能
|
|
|
|
|
|
this.deactivateMeasure();
|
|
|
|
|
|
|
2025-12-04 18:41:11 +08:00
|
|
|
|
// 取消主题订阅
|
|
|
|
|
|
if (this.unsubscribeTheme) {
|
|
|
|
|
|
this.unsubscribeTheme();
|
|
|
|
|
|
this.unsubscribeTheme = null;
|
|
|
|
|
|
}
|
2026-01-15 14:13:13 +08:00
|
|
|
|
|
2026-01-27 17:58:56 +08:00
|
|
|
|
// 销毁 3D 引擎,释放 GPU 资源
|
|
|
|
|
|
if (this.engine) {
|
|
|
|
|
|
this.engine.dispose();
|
|
|
|
|
|
this.engine = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 清理容器
|
2025-12-04 18:41:11 +08:00
|
|
|
|
this.container.innerHTML = '';
|
2026-01-15 14:13:13 +08:00
|
|
|
|
|
2025-12-04 18:41:11 +08:00
|
|
|
|
// 更新状态
|
2026-01-15 14:13:13 +08:00
|
|
|
|
this.currentMeasureType = null;
|
|
|
|
|
|
this.isMeasureActive = false;
|
2025-12-04 18:41:11 +08:00
|
|
|
|
this._isDestroyed = true;
|
|
|
|
|
|
this._isInitialized = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|