This commit is contained in:
2026-04-24 19:17:31 +08:00
parent 6c94559383
commit 01fdc0ee37
18 changed files with 696 additions and 196 deletions

198
src/babylonjs/AppDomTo3D.ts Normal file
View File

@ -0,0 +1,198 @@
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<string, DomElement>;
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';
});
}
}