import { Vector3, Matrix } from '@babylonjs/core/Maths/math.vector'; import { Scene } from '@babylonjs/core/scene'; import { Camera } from '@babylonjs/core/Cameras/camera'; import { Monobehiver } from '../base/Monobehiver'; import { PointerEventTypes, PointerInfo } from '@babylonjs/core'; interface DomElement { dom: HTMLElement; position: Vector3; offset?: { x: number; y: number }; visible: boolean; } /** * DOM 2D转3D坐标管理类 * 将DOM元素固定在3D场景的特定坐标上 */ export class AppDomTo3D extends Monobehiver { private domElements: Map; private scene: Scene | null; private camera: Camera | null; private pointerDownPos: Vector3; private pointerUpPos: Vector3; constructor(mainApp: any) { super(mainApp); this.domElements = new Map(); this.scene = null; this.camera = null; this.pointerDownPos = Vector3.Zero(); this.pointerUpPos = Vector3.Zero(); } /** * 初始化 */ init(): void { this.scene = this.mainApp.appScene.object; this.camera = this.mainApp.appCamera.object; // 监听场景点击事件,点击空白处隐藏DOM if (this.scene) { this.scene.onPointerObservable.add((pointerInfo: PointerInfo) => { const { type, event, pickInfo } = pointerInfo; if (type === PointerEventTypes.POINTERDOWN) { this.pointerDownPos.set(event.clientX, 0, event.clientY); } else if (type === PointerEventTypes.POINTERUP) { this.pointerUpPos.set(event.clientX, 0, event.clientY); const distance = Vector3.Distance(this.pointerDownPos, this.pointerUpPos); // 只有在没有移动的情况下才处理单击(距离小于5像素) if (distance < 5) { // 如果没有点击到任何物体,隐藏所有DOM if (!pickInfo || !pickInfo.hit) { this.hideAll(); } } } }); } } /** * 添加DOM元素到3D坐标 * @param id 唯一标识符 * @param dom DOM元素 * @param position 3D坐标 [x, y, z] * @param offset 2D偏移量 { x, y },可选 */ attach(id: string, dom: HTMLElement, position: [number, number, number], offset?: { x: number; y: number }): void { const vector3 = new Vector3(position[0], position[1], position[2]); // 设置DOM样式 dom.style.position = 'absolute'; dom.style.pointerEvents = 'auto'; dom.style.zIndex = '1000'; dom.style.display = 'block'; // 存储DOM元素信息 this.domElements.set(id, { dom, position: vector3, offset: offset || { x: 0, y: 0 }, visible: true }); // 立即更新一次位置 this.updateSingleDomPosition(id); } /** * 移除DOM元素 * @param id 唯一标识符 */ detach(id: string): void { const element = this.domElements.get(id); if (element) { element.dom.style.display = 'none'; this.domElements.delete(id); } } /** * 更新DOM元素的3D坐标 * @param id 唯一标识符 * @param position 新的3D坐标 [x, y, z] */ updatePosition(id: string, position: [number, number, number]): void { const element = this.domElements.get(id); if (element) { element.position.set(position[0], position[1], position[2]); } } /** * 更新所有DOM元素的位置 */ updateDomPositions(): void { this.domElements.forEach((_, id) => { this.updateSingleDomPosition(id); }); } /** * 更新单个DOM元素的位置 */ private updateSingleDomPosition(id: string): void { const element = this.domElements.get(id); if (!element || !this.scene || !this.camera) return; const { dom, position, offset, visible } = element; // 如果标记为不可见,直接隐藏 if (!visible) { dom.style.display = 'none'; return; } // 将3D坐标转换为2D屏幕坐标 const engine = this.scene.getEngine(); const width = engine.getRenderWidth(); const height = engine.getRenderHeight(); // 使用正确的矩阵:单位矩阵 + 变换矩阵 const worldMatrix = Matrix.Identity(); const transformMatrix = this.scene.getTransformMatrix(); const viewport = this.camera.viewport.toGlobal(width, height); const screenPos = Vector3.Project( position, worldMatrix, transformMatrix, viewport ); // 检查是否在相机视野内 if (screenPos.z < 0 || screenPos.z > 1) { console.log('DOM 不在视野内,隐藏'); dom.style.display = 'none'; return; } // 应用偏移量并更新DOM位置 dom.style.display = 'block'; dom.style.left = `${screenPos.x + (offset?.x || 0)}px`; dom.style.top = `${screenPos.y + (offset?.y || 0)}px`; } /** * 清理所有DOM元素 */ clean(): void { this.domElements.forEach((element) => { element.dom.style.display = 'none'; }); this.domElements.clear(); } /** * 获取所有已附加的DOM元素ID列表 */ getAttachedIds(): string[] { return Array.from(this.domElements.keys()); } /** * 隐藏所有DOM元素 */ hideAll(): void { console.log('hideAll 被调用,当前元素数量:', this.domElements.size); this.domElements.forEach((element, id) => { console.log('隐藏元素:', id, element.dom); element.visible = false; element.dom.style.display = 'none'; }); } }