diff --git a/assets/textures/Active Oak-扫描.001.png b/assets/textures/Active Oak-扫描.001.png new file mode 100644 index 0000000..27ab6df Binary files /dev/null and b/assets/textures/Active Oak-扫描.001.png differ diff --git a/assets/textures/Active Oak-扫描.jpg b/assets/textures/Active Oak-扫描.jpg new file mode 100644 index 0000000..17a9455 Binary files /dev/null and b/assets/textures/Active Oak-扫描.jpg differ diff --git a/assets/textures/screen-texture-black.jpg b/assets/textures/screen-texture-black.jpg new file mode 100644 index 0000000..d53781c Binary files /dev/null and b/assets/textures/screen-texture-black.jpg differ diff --git a/assets/卷帘大.glb b/assets/卷帘大.glb new file mode 100644 index 0000000..6f71ecb Binary files /dev/null and b/assets/卷帘大.glb differ diff --git a/assets/卷帘小.glb b/assets/卷帘小.glb new file mode 100644 index 0000000..b346f2a Binary files /dev/null and b/assets/卷帘小.glb differ diff --git a/assets/小桌.glb b/assets/小桌.glb new file mode 100644 index 0000000..d92e917 Binary files /dev/null and b/assets/小桌.glb differ diff --git a/assets/拆分.rar b/assets/拆分.rar new file mode 100644 index 0000000..1e752bf Binary files /dev/null and b/assets/拆分.rar differ diff --git a/assets/框架.glb b/assets/框架.glb new file mode 100644 index 0000000..a30815e Binary files /dev/null and b/assets/框架.glb differ diff --git a/assets/百叶窗小.glb b/assets/百叶窗小.glb new file mode 100644 index 0000000..cf43ec3 Binary files /dev/null and b/assets/百叶窗小.glb differ diff --git a/index.html b/index.html index dd29883..c7edc6e 100644 --- a/index.html +++ b/index.html @@ -265,7 +265,7 @@ - +
百叶 @@ -273,22 +273,10 @@
-
- - -
-
- - -
-
- - -
-
- - -
+ + + +
@@ -311,6 +299,69 @@ + + + diff --git a/index.js b/index.js index 0e6ef88..4320302 100644 --- a/index.js +++ b/index.js @@ -10,33 +10,19 @@ import { kernel } from './src/main.ts'; const config = { container: document.querySelector('#renderDom'), - modelUrlList: ['https://sdk.zguiy.com/resurces/model/model.glb'], + modelUrlList: ['https://sdk.zguiy.com/resurces/model/框架.glb'], env: { envPath: 'https://sdk.zguiy.com/resurces/hdr/hdr.env', intensity: 1.2, rotationY: 0.3, background: true }, }; kernel.init(config); - +kernel.model.add("百叶窗小","https://sdk.zguiy.com/resurces/model/百叶窗小.glb") +kernel.model.add("卷帘大","https://sdk.zguiy.com/resurces/model/卷帘大.glb") +kernel.model.add("卷帘小","https://sdk.zguiy.com/resurces/model/卷帘小.glb") +kernel.model.add("小桌","https://sdk.zguiy.com/resurces/model/小桌.glb") kernel.on('model:load:progress', (data) => { console.log('模型加载事件', data); - - - - const progress = data.progress || 0; - const progressBar = document.getElementById('progress-bar'); - const progressText = document.getElementById('progress-text'); - const progressContainer = document.getElementById('progress-container'); - - if (progressContainer) { - progressContainer.style.display = 'block'; - } - if (progressBar) { - progressBar.style.width = `${progress * 100}%`; - } - if (progressText) { - progressText.textContent = `${Math.round(progress * 100)}%`; - } }); @@ -65,6 +51,7 @@ kernel.on('all:ready', (data) => { kernel.hotspot.render([ { id: "h1", + type:'hotspot', name: "卷帘门", meshName: "Valve_01", icon: "https://bpic.588ku.com/element_pic/20/06/30/d1046b01afc0b9586844350d131f4daf.jpg!/fw/253/quality/90/unsharp/true/compress/true", @@ -77,12 +64,43 @@ kernel.on('all:ready', (data) => { }); +// 存储当前选中的材质名和网格 +let currentMaterialName = ''; +let currentPickedMesh = null; + kernel.on('model:click', (data) => { console.log('模型点击事件', data); console.log(data); + // DOM 2D转3D 示例:点击模型时显示信息框 + if (data.pickedMesh && data.pickedPoint) { + const meshName = data.pickedMesh.name; + const position = data.pickedPoint; // 使用点击位置的坐标 + currentMaterialName = data.materialName || ''; // 保存材质名 + currentPickedMesh = data.pickedMesh; // 保存网格对象 + + console.log('点击位置的3D坐标:', position); + console.log('材质名:', currentMaterialName); + + // 获取已创建的DOM元素 + const infoDiv = document.getElementById('model-info-box'); + + // 更新信息内容 + document.getElementById('info-name').textContent = `名称: ${meshName}`; + document.getElementById('info-position').textContent = `坐标: [${position.x.toFixed(2)}, ${position.y.toFixed(2)}, ${position.z.toFixed(2)}]`; + + // 将DOM附加到点击的3D坐标(会自动显示) + kernel.domTo3D.attach('model-info', infoDiv, [position.x, position.y, position.z], { x: -2, y: -2}); + } }); +// 暴露到全局,供 index.html 使用 +window.getCurrentMaterialName = () => currentMaterialName; +window.getCurrentPickedMesh = () => currentPickedMesh; + +// 暴露 kernel 到全局,方便调试 + + kernel.on('hotspot:click', (data) => { console.log('热点被点击:', data); const { id, name } = data @@ -96,7 +114,7 @@ kernel.on('hotspot:click', (data) => { } }); - +window.kernel = kernel; // 添加模型到场景 // await kernel.model.add('https://sdk.zguiy.com/resurces/model/百叶1.glb'); diff --git a/src/babylonjs/AppDomTo3D.ts b/src/babylonjs/AppDomTo3D.ts new file mode 100644 index 0000000..940c131 --- /dev/null +++ b/src/babylonjs/AppDomTo3D.ts @@ -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; + 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'; + }); + } +} diff --git a/src/babylonjs/AppModel.ts b/src/babylonjs/AppModel.ts index 01ccbfc..fe9d6c3 100644 --- a/src/babylonjs/AppModel.ts +++ b/src/babylonjs/AppModel.ts @@ -2,7 +2,6 @@ import { ImportMeshAsync, ISceneLoaderProgressEvent } from '@babylonjs/core/Load import '@babylonjs/loaders/glTF'; import { AbstractMesh } from '@babylonjs/core/Meshes/abstractMesh'; import { Scene } from '@babylonjs/core/scene'; -import { ActionManager, ExecuteCodeAction } from '@babylonjs/core/Actions'; import { Monobehiver } from '../base/Monobehiver'; import { Dictionary } from '../utils/Dictionary'; import { AppConfig } from './AppConfig'; @@ -15,75 +14,37 @@ type LoadResult = { error?: string; }; +type ModelConfig = { + name: string; + url: string; +}; + /** - * 模型管理类- 负责加载、缓存和管理3D模型 + * 模型管理类 - 负责加载、缓存和管理3D模型 */ export class AppModel extends Monobehiver { - private modelDic: Dictionary; + private modelDic: Dictionary; private loadedMeshes: AbstractMesh[]; - private skeletonManager: any; - private outfitManager: any; private isLoading: boolean; - private skeletonMerged: boolean; constructor(mainApp: any) { super(mainApp); - this.modelDic = new Dictionary(); + this.modelDic = new Dictionary(); this.loadedMeshes = []; - this.skeletonManager = null; - this.outfitManager = null; this.isLoading = false; - this.skeletonMerged = false; } - /** 初始化子管理器(占位:实际实现已移除) */ initManagers(): void { - // 这里原本会初始化 SkeletonManager 和 OutfitManager,已留空以避免恢复已删除的实现 + // 预留接口 } /** 加载配置中的所有模型 */ async loadModel(): Promise { if (!AppConfig.modelUrlList?.length || this.isLoading) return; + this.isLoading = true; try { - const total = AppConfig.modelUrlList.length; - EventBridge.modelLoadProgress({ loaded: 0, total, urls: AppConfig.modelUrlList, progress: 0, percentage: 0 }); - for (let i = 0; i < AppConfig.modelUrlList.length; i++) { - const url = AppConfig.modelUrlList[i]; - const handleProgress = (event: ISceneLoaderProgressEvent): void => { - const currentProgress = event.lengthComputable && event.total > 0 - ? Math.min(1, event.loaded / event.total) - : 0; - const overallProgress = Math.min(1, (i + currentProgress) / total); - EventBridge.modelLoadProgress({ - loaded: i + currentProgress, - total, - url, - progress: overallProgress, - percentage: Number((overallProgress * 100).toFixed(2)), - detail: { - url, - lengthComputable: event.lengthComputable, - loadedBytes: event.loaded, - totalBytes: event.total - } - }); - }; - - const result = await this.loadSingleModel(url, handleProgress); - const overallProgress = Math.min(1, (i + 1) / total); - EventBridge.modelLoadProgress({ - loaded: i + 1, - total, - url, - success: result.success, - progress: overallProgress, - percentage: Number((overallProgress * 100).toFixed(2)) - }); - if (!result.success) { - EventBridge.modelLoadError({ url, error: result.error }); - } - } + await this.loadMultipleModels(AppConfig.modelUrlList); EventBridge.modelLoaded({ urls: AppConfig.modelUrlList }); } finally { this.isLoading = false; @@ -91,19 +52,74 @@ export class AppModel extends Monobehiver { } /** - * 加载单个模型 - * @param modelUrl 模型URL + * 批量加载模型(内部方法) + * @param urls 模型URL数组 */ - async loadSingleModel(modelUrl: string, onProgress?: (event: ISceneLoaderProgressEvent) => void): Promise { + private async loadMultipleModels(urls: string[]): Promise { + const total = urls.length; + EventBridge.modelLoadProgress({ loaded: 0, total, urls, progress: 0, percentage: 0 }); + + for (let i = 0; i < urls.length; i++) { + const url = urls[i]; + const result = await this.loadSingleModel(url, (event) => { + this.emitProgress(i, total, url, event); + }); + + this.emitProgress(i + 1, total, url, null, result.success); + + if (!result.success) { + EventBridge.modelLoadError({ url, error: result.error }); + } + } + } + + /** + * 发送加载进度事件 + */ + private emitProgress( + loaded: number, + total: number, + url: string, + event: ISceneLoaderProgressEvent | null, + success?: boolean + ): void { + const currentProgress = event?.lengthComputable && event.total > 0 + ? Math.min(1, event.loaded / event.total) + : 0; + const overallProgress = Math.min(1, (loaded + (event ? currentProgress : 0)) / total); + + EventBridge.modelLoadProgress({ + loaded: loaded + (event ? currentProgress : 0), + total, + url, + success, + progress: overallProgress, + percentage: Number((overallProgress * 100).toFixed(2)), + detail: event ? { + url, + lengthComputable: event.lengthComputable, + loadedBytes: event.loaded, + totalBytes: event.total + } : undefined + }); + } + + /** + * 加载单个模型文件 + * @param modelUrl 模型URL + * @param onProgress 进度回调 + */ + private async loadSingleModel( + modelUrl: string, + onProgress?: (event: ISceneLoaderProgressEvent) => void + ): Promise { try { - const scene: Scene | null = this.mainApp.appScene.object; + const scene = this.mainApp.appScene.object; if (!scene) return { success: false, error: '场景未初始化' }; const result = await ImportMeshAsync(modelUrl, scene, { onProgress }); if (!result?.meshes?.length) return { success: false, error: '未找到网格' }; - // 存储根网格(通常是第一个网格) - const rootMesh = result.meshes[0]; this.loadedMeshes.push(...result.meshes); return { success: true, meshes: result.meshes, skeletons: result.skeletons }; } catch (e: any) { @@ -112,9 +128,20 @@ export class AppModel extends Monobehiver { } } - - - + /** + * 克隆模型材质,避免多个模型共享同名材质 + * @param meshes 网格数组 + * @param modelName 模型名称 + */ + private cloneMaterials(meshes: AbstractMesh[], modelName: string): void { + meshes.forEach(mesh => { + if (mesh.material) { + const originalMaterial = mesh.material; + const clonedMaterial = originalMaterial.clone(`${originalMaterial.name}_${modelName}`); + mesh.material = clonedMaterial; + } + }); + } /** 为网格设置阴影(投射和接收) */ setupShadows(meshes: AbstractMesh[]): void { const appLight = this.mainApp.appLight; @@ -129,64 +156,64 @@ export class AppModel extends Monobehiver { } /** 获取缓存的网格 */ - getCachedMeshes(url: string): AbstractMesh | undefined { - return this.modelDic.Get(url); + getCachedMeshes(name: string): AbstractMesh[] | undefined { + return this.modelDic.Get(name); } - - - /** 清理所有资源 */ clean(): void { this.modelDic.Clear(); this.loadedMeshes.forEach(m => m?.dispose()); this.loadedMeshes = []; - this.skeletonManager?.clean(); - this.outfitManager?.clean(); this.isLoading = false; - this.skeletonMerged = false; } /** - * 添加模型到场景 - * @param modelName 模型名称(用于后续引用) - * @param modelUrl 模型URL路径 + * 添加模型到场景(支持单个或批量) + * @param modelName 模型名称 或 模型配置数组 + * @param modelUrl 模型URL(单个模型时使用) */ - async add(modelName: string, modelUrl: string): Promise { - // 检查是否已经存在该模型 - const existingMesh = this.modelDic.Get(modelName); - if (existingMesh && !existingMesh.isDisposed()) { - console.log(`模型 ${modelName} 已存在,直接显示`); - existingMesh.setEnabled(true); - existingMesh.getChildMeshes().forEach(child => child.setEnabled(true)); - return { success: true, meshes: [existingMesh, ...existingMesh.getChildMeshes()] }; + async add( + modelName: string | ModelConfig[], + modelUrl?: string + ): Promise { + // 批量加载 + if (Array.isArray(modelName)) { + return await this.addMultiple(modelName); } - const handleProgress = (event: ISceneLoaderProgressEvent): void => { - const progress = event.lengthComputable && event.total > 0 - ? Math.min(1, event.loaded / event.total) - : 0; - EventBridge.modelLoadProgress({ - loaded: progress, - total: 1, - url: modelUrl, - progress, - percentage: Number((progress * 100).toFixed(2)), - detail: { - url: modelUrl, - lengthComputable: event.lengthComputable, - loadedBytes: event.loaded, - totalBytes: event.total - } - }); - }; + // 单个加载 + if (!modelUrl) { + return { success: false, error: '缺少模型URL参数' }; + } - const result = await this.loadSingleModel(modelUrl, handleProgress); + return await this.addSingle(modelName, modelUrl); + } + + /** + * 添加单个模型 + */ + private async addSingle(modelName: string, modelUrl: string): Promise { + // 检查是否已存在 + const existingMeshes = this.modelDic.Get(modelName); + if (existingMeshes?.length && !existingMeshes[0].isDisposed()) { + console.log(`模型 ${modelName} 已存在,直接显示`); + this.showMeshes(existingMeshes); + return { success: true, meshes: existingMeshes }; + } + + // 加载模型 + const result = await this.loadSingleModel(modelUrl, (event) => { + this.emitSingleProgress(modelUrl, event); + }); if (result.success && result.meshes) { - // 使用modelName作为key存储根网格 - const rootMesh = result.meshes[0]; - this.modelDic.Set(modelName, rootMesh); + this.cloneMaterials(result.meshes, modelName); + this.modelDic.Set(modelName, result.meshes); + + // 更新 GameManager 的字典 + this.mainApp.gameManager?.updateDictionaries(); + EventBridge.modelLoaded({ urls: [modelUrl] }); } else { EventBridge.modelLoadError({ url: modelUrl, error: result.error }); @@ -196,33 +223,128 @@ export class AppModel extends Monobehiver { } /** - * 替换模型:销毁旧模型并加载新模型 - * @param modelName 要替换的模型名称 - * @param newModelUrl 新模型的URL路径 + * 批量添加模型 + */ + private async addMultiple(models: ModelConfig[]): Promise<{ success: boolean; results: LoadResult[] }> { + const total = models.length; + const results: LoadResult[] = []; + + EventBridge.modelLoadProgress({ loaded: 0, total, progress: 0, percentage: 0 }); + + for (let i = 0; i < models.length; i++) { + const { name, url } = models[i]; + + const result = await this.loadSingleModel(url, (event) => { + this.emitProgress(i, total, url, event); + }); + + if (result.success && result.meshes) { + this.cloneMaterials(result.meshes, name); + this.modelDic.Set(name, result.meshes); + } + + results.push(result); + this.emitProgress(i + 1, total, url, null, result.success); + } + + // 批量加载完成后统一更新字典 + this.mainApp.gameManager?.updateDictionaries(); + + EventBridge.modelLoaded({ urls: models.map(m => m.url) }); + + return { + success: results.every(r => r.success), + results + }; + } + + /** + * 显示网格 + */ + private showMeshes(meshes: AbstractMesh[]): void { + meshes.forEach(mesh => { + mesh.setEnabled(true); + mesh.getChildMeshes().forEach(child => child.setEnabled(true)); + }); + } + + /** + * 发送单个模型加载进度 + */ + private emitSingleProgress(url: string, event: ISceneLoaderProgressEvent): void { + const progress = event.lengthComputable && event.total > 0 + ? Math.min(1, event.loaded / event.total) + : 0; + + EventBridge.modelLoadProgress({ + loaded: progress, + total: 1, + url, + progress, + percentage: Number((progress * 100).toFixed(2)), + detail: { + url, + lengthComputable: event.lengthComputable, + loadedBytes: event.loaded, + totalBytes: event.total + } + }); + } + + /** + * 根据 mesh 查找所属的模型名称 + * @param mesh 网格对象 + * @returns 模型名称,未找到返回 undefined + */ + findModelNameByMesh(mesh: AbstractMesh): string | undefined { + const keys = this.modelDic.Keys(); + for (const key of keys) { + const meshes = this.modelDic.Get(key); + if (meshes?.some(m => m === mesh || m.uniqueId === mesh.uniqueId)) { + return key; + } + } + return undefined; + } + + /** + * 根据 mesh 移除所属的整个模型 + * @param mesh 网格对象 + * @returns 是否成功移除 + */ + remove(mesh: AbstractMesh): boolean { + const modelName = this.findModelNameByMesh(mesh); + if (modelName) { + this.removeByName(modelName); + return true; + } + console.warn('未找到该 mesh 所属的模型'); + return false; + } + + /** + * 替换模型 + * @param modelName 模型名称 + * @param newModelUrl 新模型URL */ async replaceModel(modelName: string, newModelUrl: string): Promise { - // 先销毁旧模型 - this.remove(modelName); - - // 加载新模型 - return await this.add(modelName, newModelUrl); + this.removeByName(modelName); + return await this.addSingle(modelName, newModelUrl); } /** * 销毁指定模型 * @param modelName 模型名称 */ - remove(modelName: string): void { - const mesh = this.modelDic.Get(modelName); - if (mesh) { - // 隐藏网格而不是销毁,以便后续可以重新显示 - mesh.setEnabled(false); - mesh.getChildMeshes().forEach(child => child.setEnabled(false)); - - console.log(`Model hidden: ${modelName}`); - } else { + removeByName(modelName: string): void { + const meshes = this.modelDic.Get(modelName); + if (!meshes?.length) { console.warn(`Model not found: ${modelName}`); + return; } - } + meshes.forEach(mesh => mesh.dispose()); + this.modelDic.Remove(modelName); + console.log(`Model removed: ${modelName}`); + } } diff --git a/src/babylonjs/AppRay.ts b/src/babylonjs/AppRay.ts index 8e45935..90cab04 100644 --- a/src/babylonjs/AppRay.ts +++ b/src/babylonjs/AppRay.ts @@ -9,6 +9,7 @@ import { StandardMaterial, HighlightLayer, PointerInfo, + ElasticEase, } from '@babylonjs/core' import { MainApp } from './MainApp' import { Monobehiver } from '../base/Monobehiver'; @@ -80,11 +81,25 @@ class AppRay extends Monobehiver { // const isSpriteHotspotClick = this.mainApp.appHotspot?.handleSpritePick(); // if (isSpriteHotspotClick) return; - if (pickInfo && pickInfo.pickedMesh) { + if (pickInfo && pickInfo.hit && pickInfo.pickedMesh && pickInfo.pickedPoint) { + // 检查是否点击的是热点 + if (pickInfo.pickedMesh.metadata?.type === 'hotspot') { + return; + } + + const materialName = pickInfo.pickedMesh.material?.name || ''; EventBridge.modelClick({ meshName: pickInfo.pickedMesh.name, + pickedMesh: pickInfo.pickedMesh, + pickedPoint: pickInfo.pickedPoint, + materialName: materialName, }); } + else{ + console.log(1111); + + this.mainApp.appDomTo3D.hideAll() + } } diff --git a/src/babylonjs/GameManager.ts b/src/babylonjs/GameManager.ts index 6d6bb34..a461d72 100644 --- a/src/babylonjs/GameManager.ts +++ b/src/babylonjs/GameManager.ts @@ -72,28 +72,35 @@ export class GameManager extends Monobehiver { return; } - // 初始化材质字典 + // 初始化材质和网格字典 + this.updateDictionaries(); + + this.cacheRollerDoorMeshes(); + console.log('材质字典:', this.materialDic); + this.setRollerDoorScale("Box006.001", new Vector3(0.12, 0.02, 0.118)); + this.setRollerDoorScale("Box005.001", new Vector3(0.13, 0.02, 0.12)); + } + + /** + * 更新材质和网格字典(从场景中同步) + */ + updateDictionaries(): void { + const scene = this.mainApp.appScene?.object; + if (!scene) return; + + // 更新材质字典 for (const mat of scene.materials) { if (!this.materialDic.Has(mat.name)) { - // 初始化材质属性 - // mat.transparencyMode = PBRMaterial.PBRMATERIAL_ALPHABLEND; this.materialDic.Set(mat.name, mat as PBRMaterial); } } - // 初始化网格字典 + // 更新网格字典 for (const mesh of scene.meshes) { - if (mesh instanceof Mesh) { - + if (mesh instanceof Mesh && !this.meshDic.Has(mesh.name)) { this.meshDic.Set(mesh.name, mesh); } } - this.cacheRollerDoorMeshes(); - console.log('材质字典:', this.materialDic); - this.setRollerDoorScale("Box006.001", new Vector3(0.12,0.02,0.118)); - - // 单独设置 Box005.001 的缩放为 (1, 2, 1) - this.setRollerDoorScale("Box005.001", new Vector3(0.13,0.02,0.12)); } /** 初始化设置材质 */ @@ -760,11 +767,14 @@ export class GameManager extends Monobehiver { // 3. 应用材质到目标网格 targetMaterials.forEach(material => { - if (material[attribute]) { + if (attribute === 'baseColor' && typeof value === 'string') { + // 如果是 baseColor 且值是字符串(16进制颜色),转换为 Color3 + material.albedoColor = Color3.FromHexString(value); + console.log(`Applying baseColor ${value} to material: ${material.name}`); + } else if (material[attribute]) { material[attribute] = value; + console.log(`Applying attribute ${attribute} to ${value} to mesh: ${material.name}`); } - console.log(`Applying attribute ${attribute} to ${value} to mesh: ${material.name}`); - // 这里需要根据实际的材质系统实现 }); } } diff --git a/src/babylonjs/MainApp.ts b/src/babylonjs/MainApp.ts index 7493435..24a072d 100644 --- a/src/babylonjs/MainApp.ts +++ b/src/babylonjs/MainApp.ts @@ -14,6 +14,7 @@ import { AppRay } from './AppRay'; import { GameManager } from './GameManager'; import { EventBridge } from '../event/bridge'; import { AppHotspot } from './AppHotspot'; +import { AppDomTo3D } from './AppDomTo3D'; /** * 主应用类 - 3D场景的核心控制器 @@ -28,6 +29,7 @@ export class MainApp { appEnv: AppEnv; appRay: AppRay; appHotspot: AppHotspot; + appDomTo3D: AppDomTo3D; gameManager: GameManager; @@ -40,6 +42,7 @@ export class MainApp { this.appEnv = new AppEnv(this); this.appRay = new AppRay(this); this.appHotspot = new AppHotspot(this); + this.appDomTo3D = new AppDomTo3D(this); this.gameManager = new GameManager(this); window.addEventListener("resize", () => this.appEngin.handleResize()); @@ -58,6 +61,7 @@ export class MainApp { async loadModel(): Promise { await this.appModel.loadModel(); await this.gameManager.Awake(); + console.log(1111111111111111111111); EventBridge.allReady({ scene: this.appScene.object }); } @@ -68,7 +72,8 @@ export class MainApp { this.appCamera.Awake(); this.appLight.Awake(); this.appEnv.Awake(); - this.appRay.Awake() + this.appRay.Awake(); + this.appDomTo3D.init(); this.appModel.initManagers(); this.update(); EventBridge.sceneReady({ scene: this.appScene.object }); @@ -80,6 +85,7 @@ export class MainApp { this.appEngin.object.runRenderLoop(() => { this.appScene.object?.render(); this.appCamera.update(); + this.appDomTo3D.updateDomPositions(); }); } diff --git a/src/hotspot/Point.ts b/src/hotspot/Point.ts index a93d697..9f52cac 100644 --- a/src/hotspot/Point.ts +++ b/src/hotspot/Point.ts @@ -87,7 +87,10 @@ export class Point { this.plane.isVisible = true this.plane.isPickable = true this.plane.renderingGroupId = 1 - + + // 标记为热点类型 + this.plane.metadata = { type: 'hotspot' } + } setupEvents() { diff --git a/src/kernel/Adapter.ts b/src/kernel/Adapter.ts index fd3da62..4932687 100644 --- a/src/kernel/Adapter.ts +++ b/src/kernel/Adapter.ts @@ -24,8 +24,16 @@ export class KernelAdapter { * 销毁指定模型 * @param modelName 模型名称 */ - remove: (modelName: string): void => { - this.mainApp.appModel.remove(modelName); + removeByName: (modelName: string): void => { + this.mainApp.appModel.removeByName(modelName); + }, + /** + * 根据网格移除所属的整个模型 + * @param mesh 网格对象 + * @returns 是否成功移除 + */ + remove: (mesh: any): boolean => { + return this.mainApp.appModel.remove(mesh); }, /** * 替换模型 @@ -94,6 +102,36 @@ export class KernelAdapter { this.mainApp.appHotspot.render(hotspots); } }; + + /** DOM 2D转3D坐标 */ + domTo3D = { + /** + * 将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 => { + this.mainApp.appDomTo3D.attach(id, dom, position, offset); + }, + /** + * 移除DOM元素 + * @param id 唯一标识符 + */ + detach: (id: string): void => { + this.mainApp.appDomTo3D.detach(id); + }, + /** + * 更新DOM元素的3D坐标 + * @param id 唯一标识符 + * @param position 新的3D坐标 [x, y, z] + */ + updatePosition: (id: string, position: [number, number, number]): void => { + this.mainApp.appDomTo3D.updatePosition(id, position); + } + }; + /** 调试工具 */ debug = { /** 列出当前场景网格名称 */