diff --git a/ScreenShot_2026-05-18_175704_601.png b/ScreenShot_2026-05-18_175704_601.png new file mode 100644 index 0000000..0ab9646 Binary files /dev/null and b/ScreenShot_2026-05-18_175704_601.png differ diff --git a/examples/app-global.js b/examples/app-global.js index 2a10a1c..cb2c646 100644 --- a/examples/app-global.js +++ b/examples/app-global.js @@ -203,8 +203,8 @@ const isModelExists = (modelId) => { return kernel.model.exists(modelId); } -//换棚子 -const executeEvent2 = async (result) => { +//一般是换棚子/换颜色/显示放置区域 + const executeEvent2 = async (result) => { const kernel = getKernel(); // 检查是否有模型更换事件 @@ -214,10 +214,10 @@ const executeEvent2 = async (result) => { let modelAlreadyExists = false; if (hasModelChange) { const firstModelEvent = result.data.events.find(e => e.event_type === 'change_model'); - if (firstModelEvent) { - const { category } = firstModelEvent.target_data; - modelAlreadyExists = isModelExists(category); - console.log(`检查模型 ${category} 是否存在:`, modelAlreadyExists); + if (firstModelEvent && firstModelEvent.target_data) { + const {name, category } = firstModelEvent.target_data; + modelAlreadyExists = kernel.model.exists(name+'_'+category); + console.log(`检查模型 ${name+'_'+category} 是否存在:`, modelAlreadyExists); } } @@ -241,12 +241,10 @@ const executeEvent2 = async (result) => { }; const { id, name, file_url, model_control_type, category, placement_zone } = target_data; - console.log('替换百叶模型:', event); - console.log('替换百叶模型类型:', category); // 如果模型已存在,跳过加载 if (modelAlreadyExists) { - console.log(`模型 ${category} 已存在,跳过加载`); + console.log(`模型 ${name+'_'+category} 已存在,跳过加载`); continue; } @@ -269,7 +267,7 @@ const executeEvent2 = async (result) => { modelControlType: model_control_type, }) - console.log(`百叶模型已放置为 ${name}`); + console.log(`模型已放置为 ${name}`); } } diff --git a/examples/index.global.js b/examples/index.global.js index 9710cc6..20800a2 100644 --- a/examples/index.global.js +++ b/examples/index.global.js @@ -355762,14 +355762,26 @@ clipPos=viewProjection*worldPos;previousClipPos=previousViewProjection*previousW const color = Color3.FromHexString(options.albedoColor); material.albedoColor.copyFrom(color); } - if (options.albedoTexture) { - material.albedoTexture = new Texture(options.albedoTexture); + if (options.albedoTexture !== void 0) { + if (options.albedoTexture) { + material.albedoTexture = new Texture(options.albedoTexture); + } else { + material.albedoTexture = null; + } } - if (options.normalMap) { - material.bumpTexture = new Texture(options.normalMap); + if (options.normalMap !== void 0) { + if (options.normalMap) { + material.bumpTexture = new Texture(options.normalMap); + } else { + material.bumpTexture = null; + } } - if (options.metallicTexture) { - material.metallicTexture = new Texture(options.metallicTexture); + if (options.metallicTexture !== void 0) { + if (options.metallicTexture) { + material.metallicTexture = new Texture(options.metallicTexture); + } else { + material.metallicTexture = null; + } } if (options.roughness !== void 0) { material.roughness = options.roughness; @@ -357185,6 +357197,19 @@ clipPos=viewProjection*worldPos;previousClipPos=previousViewProjection*previousW */ removeAll: () => { this.mainApp.appModel.removeAll(); + }, + /** + * 检查模型是否已加载 + * @param modelId 模型ID + * @returns 模型是否存在 + * @example + * // 检查模型是否已加载,避免重复加载 + * if (!kernel.model.exists('shed_001')) { + * await kernel.model.add({ modelId: 'shed_001', modelUrl: '...' }); + * } + */ + exists: (modelId) => { + return this.mainApp.appModel.exists(modelId); } }; /** 材质管理 */ diff --git a/index.html b/index.html index 6a3a60f..9c27841 100644 --- a/index.html +++ b/index.html @@ -302,8 +302,8 @@ - - + + @@ -333,11 +333,11 @@
- - + + - - + +
@@ -586,7 +586,7 @@ // 监听模型点击事件 window.addEventListener('model:click', (event) => { console.log('模型被点击:', event.detail); - const { meshName, materialName, modelControlType } = event.detail; + const { meshName, modelName, materialName, modelControlType } = event.detail; const clickInfoDiv = document.getElementById('click-info'); const clickInfoContent = document.getElementById('click-info-content'); @@ -596,8 +596,8 @@ 模型
- 网格名称: - ${meshName} + 模型名称: + ${modelName || meshName}
`; if (materialName) { @@ -866,6 +866,8 @@ // DOM 2D转3D 示例:点击模型时显示信息框 if (data.pickedMesh && data.pickedPoint) { const meshName = data.pickedMesh.name; + // 获取模型根节点名称(modelId) + const modelName = kernel.model.findModelNameByMesh(data.pickedMesh) || meshName; const position = data.pickedPoint; // 使用点击位置的坐标 currentMaterialName = data.materialName || ''; // 保存材质名 currentPickedMesh = data.pickedMesh; // 保存网格对象 @@ -873,7 +875,7 @@ // 获取已创建的DOM元素 const infoDiv = document.getElementById('model-info-box'); // 更新信息内容 - document.getElementById('info-name').textContent = `名称: ${meshName}`; + document.getElementById('info-name').textContent = `模型: ${modelName}`; document.getElementById('info-position').textContent = `坐标: [${position.x.toFixed(2)}, ${position.y.toFixed(2)}, ${position.z.toFixed(2)}]`; // 显示颜色按钮,隐藏旋转按钮 @@ -888,6 +890,8 @@ // 显示旋转控制UI if (data.pickedMesh && data.pickedPoint) { const meshName = data.pickedMesh.name; + // 获取模型根节点名称(modelId) + const modelName = kernel.model.findModelNameByMesh(data.pickedMesh) || meshName; const position = data.pickedPoint; currentPickedMesh = data.pickedMesh; // 保存网格对象 diff --git a/index.js b/index.js index 4340d96..fd8d3c8 100644 --- a/index.js +++ b/index.js @@ -37,9 +37,9 @@ export const init = async (customConfig = {}) => { modelUrlList: [], env: { envPath: 'https://sdk.zguiy.com/resurces/hdr/hdr.env', intensity: 1.2, rotationY: 0.3, background: true }, gizmo: { - position: true, - rotation: true, - scale: true + position: false, + rotation: false, + scale: false }, outline: { enable: true, @@ -67,7 +67,7 @@ export const getAutoLoadModelList = async () => { const models = data.data // 这就是模型列表 models.forEach(model => { - console.log(model.placement_zone); + if (model.placement_zone) { const { alpha, border_color, color, show_border, thickness, walls } = model.placement_zone kernel.dropZone.setData({ @@ -80,8 +80,9 @@ export const getAutoLoadModelList = async () => { walls: walls }); } - + kernel.model.add({ + modelName: model.name+'_'+model.category, modelId: model.category, modelUrl: model.file_url, modelControlType: model.model_control_type, @@ -148,18 +149,18 @@ export const executeEvent = async (dropzone_data, result) => { if (event.event_type === 'change_model') { console.log(event.target_data); - const { id, name, file_url, model_control_type, category } = event.target_data; - console.log('替换百叶模型:', event); - console.log('替换百叶模型类型:', category); + const { name, file_url, model_control_type, category } = event.target_data; + // 生成唯一的模型ID - const modelId = id + '_' + Date.now(); + const modelId = Date.now(); // 先记录模型放置(会自动处理替换逻辑) kernel.dropZone.recordModelPlacement(wallName, index, modelId); console.log(Math.abs(rotation.y - 90), Math.abs(rotation.y - 90) > 5 ? 'x' : 'z'); // 加载并放置模型 await kernel.model.add({ + modelName: name , modelId: modelId, modelUrl: file_url, modelControlType: model_control_type, @@ -174,7 +175,7 @@ export const executeEvent = async (dropzone_data, result) => { } }); - console.log(`百叶模型已放置为 ${name}`); + console.log(`百叶模型已放置为 ${name+'_'+category}`); } if (event.event_type === 'change_color') { @@ -202,12 +203,25 @@ export const executeEvent2 = async (result) => { // 检查是否有模型更换事件 const hasModelChange = result.data.events.some(e => e.event_type === 'change_model'); - const modelExists = await kernel.model.exists(modelId); - console.log(modelExists); - // 只有在需要更换模型时才清除 + + // 检查新模型是否已经存在 + let modelAlreadyExists = false; if (hasModelChange) { + const firstModelEvent = result.data.events.find(e => e.event_type === 'change_model'); + if (firstModelEvent && firstModelEvent.target_data) { + const {name, category } = firstModelEvent.target_data; + modelAlreadyExists = kernel.model.exists(category); + console.log(`检查模型 ${name+'_'+category} 是否存在:`, modelAlreadyExists); + } + } + + // 只有在需要更换模型且模型不存在时才清除 + if (hasModelChange && !modelAlreadyExists) { + console.log('模型不存在,执行清除操作'); kernel.dropZone.clearZones(); kernel.model.removeAll(); + } else if (modelAlreadyExists) { + console.log('模型已存在,跳过清除操作,仅更新材质'); } // 先处理所有 change_model 事件 @@ -221,7 +235,11 @@ export const executeEvent2 = async (result) => { }; const { id, name, file_url, model_control_type, category, placement_zone } = target_data; - + // 如果模型已存在,跳过加载 + if (modelAlreadyExists) { + console.log(`模型 ${name+'_'+category} 已存在,跳过加载`); + continue; + } if (placement_zone) { const { alpha, border_color, color, show_border, thickness, walls } = placement_zone @@ -237,12 +255,13 @@ export const executeEvent2 = async (result) => { // 加载并放置模型(使用 category 作为 modelId) await kernel.model.add({ - modelId: category, + modelName: name, + modelId: category, modelUrl: file_url, modelControlType: model_control_type, }) - console.log(`模型已放置为 ${name}`); + console.log(`模型已放置为 ${name+'_'+category}`); } } diff --git a/src/babylonjs/AppDropZone.ts b/src/babylonjs/AppDropZone.ts index 94b9d4b..52f0e47 100644 --- a/src/babylonjs/AppDropZone.ts +++ b/src/babylonjs/AppDropZone.ts @@ -145,18 +145,24 @@ export class AppDropZone { return null; }; - // 更新配置中的墙面分割数 - this.dropZoneConfig.walls = this.dropZoneConfig.walls.map(wall => { - const newDivisions = matchWallName(wall.name); - const finalDivisions = newDivisions !== null ? newDivisions : (wall.divisions || 1); + // 更新配置中的墙面分割数,只保留后端配置的墙面 + this.dropZoneConfig.walls = this.dropZoneConfig.walls + .map(wall => { + const newDivisions = matchWallName(wall.name); - console.log(`墙面 "${wall.name}" 匹配到分割数: ${finalDivisions}`); + // 如果后端没有配置这个墙面,返回 null 标记 + if (newDivisions === null) { + return null; + } - return { - ...wall, - divisions: finalDivisions - }; - }); + console.log(`墙面 "${wall.name}" 匹配到分割数: ${newDivisions}`); + + return { + ...wall, + divisions: newDivisions + }; + }) + .filter(wall => wall !== null) as typeof this.dropZoneConfig.walls; // 过滤掉未配置的墙面 // 清除旧的放置区域网格(不清除模型) this.clearZones(); diff --git a/src/babylonjs/AppModel copy.ts b/src/babylonjs/AppModel copy.ts new file mode 100644 index 0000000..a3ac236 --- /dev/null +++ b/src/babylonjs/AppModel copy.ts @@ -0,0 +1,748 @@ +import { ImportMeshAsync, ISceneLoaderProgressEvent } from '@babylonjs/core/Loading/sceneLoader'; +import '@babylonjs/loaders/glTF'; +import { AbstractMesh } from '@babylonjs/core/Meshes/abstractMesh'; +import { Mesh } from '@babylonjs/core/Meshes/mesh'; +import { Quaternion, Vector3 } from '@babylonjs/core/Maths/math.vector'; +import { Scene } from '@babylonjs/core/scene'; +import { Monobehiver } from '../base/Monobehiver'; +import { Dictionary } from '../utils/Dictionary'; +import { AppConfig } from './AppConfig'; +import { EventBridge } from '../event/bridge'; +import { DragConfig } from './AppModelDrag'; + +type LoadResult = { + success: boolean; + meshes?: AbstractMesh[]; + skeletons?: unknown[]; + error?: string; +}; + +type ModelConfig = { + name: string; + url: string; +}; + +type ModelControlType = 'rotation' | 'color'; + +type ModelTransform = { + position?: { x: number; y: number; z: number }; + rotation?: { x: number; y: number; z: number }; + scale?: { x: number; y: number; z: number }; +}; + +type ModelMetadata = { + modelName: string; + modelId: string; + modelUrl: string; + modelControlType?: ModelControlType; + drag?: DragConfig; + transform?: ModelTransform; +}; + +/** + * 模型管理类 - 负责加载、缓存和管理3D模型 + */ +export class AppModel extends Monobehiver { + private modelDic: Dictionary; + private modelMetadataDic: Dictionary; + private loadedMeshes: AbstractMesh[]; + private isLoading: boolean; + + constructor(mainApp: any) { + super(mainApp); + this.modelDic = new Dictionary(); + this.modelMetadataDic = new Dictionary(); + this.loadedMeshes = []; + this.isLoading = false; + } + + initManagers(): void { + // 预留接口 + } + + /** 加载配置中的所有模型 */ + async loadModel(): Promise { + if (!AppConfig.modelUrlList?.length || this.isLoading) return; + + this.isLoading = true; + try { + await this.loadMultipleModels(AppConfig.modelUrlList); + EventBridge.modelLoaded({ urls: AppConfig.modelUrlList }); + } finally { + this.isLoading = false; + } + } + + /** + * 批量加载模型(内部方法) + * @param urls 模型URL数组 + */ + 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 = 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: '未找到网格' }; + + this.loadedMeshes.push(...result.meshes); + return { success: true, meshes: result.meshes, skeletons: result.skeletons }; + } catch (e: any) { + console.error(`模型加载失败: ${modelUrl}`, e); + return { success: false, error: e?.message }; + } + } + + /** + * 克隆模型材质,避免多个模型共享同名材质 + * @param meshes 网格数组 + * @param modelId 模型ID + */ + private cloneMaterials(meshes: AbstractMesh[], modelId: string): void { + const scene = this.mainApp.appScene.object; + const clonedMaterials = new Map(); + + meshes.forEach(mesh => { + if (mesh.material) { + const originalMaterial = mesh.material; + const originalName = originalMaterial.name; + + // 如果该材质还没有被克隆过,则克隆它 + if (!clonedMaterials.has(originalName)) { + const newName = `${originalName}_${modelId}`; + const clonedMaterial = originalMaterial.clone(newName); + clonedMaterials.set(originalName, clonedMaterial); + + } + + // 应用克隆的材质 + mesh.material = clonedMaterials.get(originalName); + } + }); + + + } + + /** 为网格设置阴影(投射和接收) */ + private createModelRoot(modelId: string, meshes: AbstractMesh[]): AbstractMesh[] { + const scene = this.mainApp.appScene.object; + const root = new Mesh(`${modelId}__root`, scene); + const meshSet = new Set(meshes); + root.position.copyFrom(this.getMeshesBoundingCenter(meshes)); + + meshes.forEach(mesh => { + if (!mesh.parent || !meshSet.has(mesh.parent as AbstractMesh)) { + mesh.setParent(root, true, true); + } + }); + + this.loadedMeshes.push(root); + return [root, ...meshes]; + } + + private getMeshesBoundingCenter(meshes: AbstractMesh[]): Vector3 { + const renderableMeshes = meshes.filter(mesh => !mesh.isDisposed() && mesh.getTotalVertices() > 0); + if (!renderableMeshes.length) return Vector3.Zero(); + + const min = new Vector3(Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY); + const max = new Vector3(Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY); + + renderableMeshes.forEach(mesh => { + mesh.computeWorldMatrix(true); + const boundingBox = mesh.getBoundingInfo().boundingBox; + min.minimizeInPlace(boundingBox.minimumWorld); + max.maximizeInPlace(boundingBox.maximumWorld); + }); + + return min.add(max).scaleInPlace(0.5); + } + + + setupShadows(meshes: AbstractMesh[]): void { + const appLight = this.mainApp.appLight; + if (!appLight) return; + + meshes.forEach(mesh => { + if (mesh.getTotalVertices() > 0) { + appLight.addShadowCaster(mesh); + mesh.receiveShadows = true; + } + }); + } + + /** 获取缓存的网格 */ + getCachedMeshes(name: string): AbstractMesh[] | undefined { + return this.modelDic.Get(name); + } + + /** 清理所有资源 */ + clean(): void { + this.modelDic.Clear(); + this.loadedMeshes.forEach(m => m?.dispose()); + this.loadedMeshes = []; + this.isLoading = false; + } + + /** + * 添加模型到场景(支持单个或批量) + * @param modelConfig 模型配置对象 或 模型配置数组 + */ + async add( + modelConfig: ModelMetadata | ModelMetadata[] + ): Promise { + // 批量加载 + if (Array.isArray(modelConfig)) { + return await this.addMultiple(modelConfig); + } + + // 单个加载 + return await this.addSingle( + modelConfig.modelName, + modelConfig.modelId, + modelConfig.modelUrl, + modelConfig.modelControlType, + modelConfig.drag, + modelConfig.transform + ); + } + + /** + * 添加单个模型 + */ + private async addSingle(modelName: string, modelId: string, modelUrl: string, modelControlType?: ModelControlType, drag?: DragConfig, transform?: ModelTransform): Promise { + // 检查是否已存在 + const existingMeshes = this.modelDic.Get(modelName+'_'+modelId); + 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) { + // 克隆材质,确保每个模型有独立的材质 + this.cloneMaterials(result.meshes, modelId); + + result.meshes = this.createModelRoot(modelName+'_'+modelId, result.meshes); + this.modelDic.Set(modelName+'_'+modelId, result.meshes); + + // 存储元数据 + this.modelMetadataDic.Set(modelName+'_'+modelId, { + modelName: modelName, + modelId: modelId, + modelUrl: modelUrl, + modelControlType: modelControlType, + drag: drag, + transform: transform + }); + + // 应用 transform + if (transform) { + this.applyTransform(modelName+'_'+modelId, transform); + } + + // 配置拖拽功能 + if (drag) { + this.mainApp.appModelDrag?.configureDrag(modelName+'_'+modelId, drag); + } + + // 更新 GameManager 的字典 + this.mainApp.gameManager?.updateDictionaries(); + + EventBridge.modelLoaded({ urls: [modelUrl] }); + } else { + EventBridge.modelLoadError({ url: modelUrl, error: result.error }); + } + + return result; + } + + /** + * 批量添加模型 + */ + private async addMultiple(models: ModelMetadata[]): 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 { modelName, modelId, modelUrl, modelControlType, drag, transform } = models[i]; + + const result = await this.loadSingleModel(modelUrl, (event) => { + this.emitProgress(i, total, modelUrl, event); + }); + + if (result.success && result.meshes) { + // 克隆材质,确保每个模型有独立的材质 + this.cloneMaterials(result.meshes, modelId); + + result.meshes = this.createModelRoot(modelId, result.meshes); + this.modelDic.Set(modelId, result.meshes); + + // 存储元数据 + this.modelMetadataDic.Set(modelId, { + modelName: modelName, + modelId: modelId, + modelUrl: modelUrl, + modelControlType: modelControlType, + drag: drag, + transform: transform + }); + + // 应用 transform + if (transform) { + this.applyTransform(modelId, transform); + } + + // 配置拖拽功能 + if (drag) { + this.mainApp.appModelDrag?.configureDrag(modelId, drag); + } + } + + results.push(result); + this.emitProgress(i + 1, total, modelUrl, null, result.success); + } + + // 批量加载完成后统一更新字典 + this.mainApp.gameManager?.updateDictionaries(); + + EventBridge.modelLoaded({ urls: models.map(m => m.modelUrl) }); + + 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 名称查找 mesh 对象 + * @param meshName mesh 名称 + * @returns mesh 对象,未找到返回 undefined + */ + private findMeshByName(meshName: string): AbstractMesh | undefined { + const keys = this.modelDic.Keys(); + for (const key of keys) { + const meshes = this.modelDic.Get(key); + const found = meshes?.find(m => m.name === meshName); + if (found) return found; + } + return undefined; + } + + /** + * 根据 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); + meshes.forEach(mesh => { + console.log(mesh.uniqueId); + console.log(mesh.name); + + }); + if (meshes?.some(m => m === mesh || m.uniqueId === mesh.uniqueId)) { + + return key; + } + } + return undefined; + } + + /** + * 根据 mesh 或 mesh 名称移除所属的整个模型 + * @param meshOrName 网格对象或网格名称 + * @returns 是否成功移除 + */ + remove(meshOrName: AbstractMesh | string): boolean { + let mesh: AbstractMesh | undefined; + + + // 判断传入的是对象还是字符串 + if (typeof meshOrName === 'string') { + mesh = this.findMeshByName(meshOrName); + if (!mesh) { + console.warn(`未找到名为 ${meshOrName} 的网格`); + return false; + } + } else { + mesh = meshOrName; + } + + const modelName = this.findModelNameByMesh(mesh); + if (modelName) { + this.removeByName(modelName); + return true; + } + console.warn('未找到该 mesh 所属的模型'); + return false; + } + + /** + * 替换模型 + * @param modelConfig 模型配置对象 + */ + async replaceModel(modelConfig: ModelMetadata): Promise { + + + + this.removeByName(modelConfig.modelId); + return await this.addSingle( + modelConfig.modelName, + modelConfig.modelId, + modelConfig.modelUrl, + modelConfig.modelControlType, + modelConfig.drag, + modelConfig.transform + ); + } + + /** + * 销毁指定模型 + * @param modelName 模型名称 + */ + removeByName(modelName: string): void { + const meshes = this.modelDic.Get(modelName); + if (!meshes?.length) { + console.warn(`Model not found: ${modelName}`); + return; + } + + this.getModelTransformTargets(meshes).forEach(mesh => mesh.dispose(false, true)); + this.modelDic.Remove(modelName); + this.modelMetadataDic.Remove(modelName); + this.mainApp.gameManager?.updateDictionaries(); + + + } + + /** + * 清除所有已添加的模型并释放内存 + * 主要用于切换尺寸后清除不适用的配件 + */ + removeAll(): void { + const modelNames = this.modelDic.Keys(); + + + + modelNames.forEach(modelName => { + const meshes = this.modelDic.Get(modelName); + if (meshes?.length) { + this.getModelTransformTargets(meshes).forEach(mesh => mesh.dispose(false, true)); + } + }); + + this.modelDic.Clear(); + this.modelMetadataDic.Clear(); + this.mainApp.gameManager?.updateDictionaries(); + + console.log('所有模型已清除,内存已释放'); + } + + /** + * 获取模型元数据 + * @param modelName 模型名称 + */ + getModelMetadata(modelName: string): ModelMetadata | undefined { + return this.modelMetadataDic.Get(modelName); + } + + /** + * 根据网格查找模型元数据 + * @param mesh 网格对象 + */ + getMetadataByMesh(mesh: AbstractMesh): ModelMetadata | undefined { + const modelName = this.findModelNameByMesh(mesh); + if (modelName) { + return this.modelMetadataDic.Get(modelName); + } + return undefined; + } + + private getModelTransformTargets(meshes: AbstractMesh[]): AbstractMesh[] { + const meshSet = new Set(meshes); + const rootMeshes = meshes.filter(mesh => !mesh.parent || !meshSet.has(mesh.parent as AbstractMesh)); + + return rootMeshes.length ? rootMeshes : meshes.slice(0, 1); + } + + getModelTransformTargetByMesh(mesh: AbstractMesh): AbstractMesh | undefined { + const modelName = this.findModelNameByMesh(mesh); + if (!modelName) return mesh; + + const meshes = this.modelDic.Get(modelName); + if (!meshes?.length) return mesh; + + return this.getModelTransformTargets(meshes)[0] ?? mesh; + } + + getModelMeshesByMesh(mesh: AbstractMesh): AbstractMesh[] { + const modelName = this.findModelNameByMesh(mesh); + if (!modelName) return [mesh]; + + const meshes = this.modelDic.Get(modelName); + return meshes?.length ? meshes : [mesh]; + } + + /** + * 设置模型旋转 + * @param modelId 模型ID + * @param rotation 旋转向量 {x, y, z}(默认使用角度) + * @param useDegrees 是否使用角度(默认true) + */ + setRotation(modelId: string, rotation: { x: number; y: number; z: number }, useDegrees: boolean = true): void { + const meshes = this.modelDic.Get(modelId); + if (!meshes?.length) { + console.warn(`Model not found: ${modelId}`); + return; + } + + // 如果使用角度,转换为弧度 + const toRadians = (degrees: number) => degrees * Math.PI / 180; + const rotationValues = useDegrees ? { + x: toRadians(rotation.x), + y: toRadians(rotation.y), + z: toRadians(rotation.z) + } : rotation; + + this.getModelTransformTargets(meshes).forEach(mesh => { + if (mesh.rotationQuaternion) { + mesh.rotationQuaternion = Quaternion.FromEulerAngles( + rotationValues.x, + rotationValues.y, + rotationValues.z + ); + return; + } + + mesh.rotation.set(rotationValues.x, rotationValues.y, rotationValues.z); + }); + } + + /** + * 累加模型旋转 + * @param modelId 模型ID + * @param rotation 旋转向量 {x, y, z}(默认使用角度) + * @param useDegrees 是否使用角度(默认true) + */ + addRotation(modelId: string, rotation: { x: number; y: number; z: number }, useDegrees: boolean = true): void { + const meshes = this.modelDic.Get(modelId); + if (!meshes?.length) { + console.warn(`Model not found: ${modelId}`); + return; + } + + // 如果使用角度,转换为弧度 + const toRadians = (degrees: number) => degrees * Math.PI / 180; + const rotationValues = useDegrees ? { + x: toRadians(rotation.x), + y: toRadians(rotation.y), + z: toRadians(rotation.z) + } : rotation; + + this.getModelTransformTargets(meshes).forEach(mesh => { + mesh.addRotation(rotationValues.x, rotationValues.y, rotationValues.z); + }); + } + + /** + * 设置模型位置 + * @param modelId 模型ID + * @param position 位置向量 {x, y, z} + */ + setPosition(modelId: string, position: { x: number; y: number; z: number }): void { + const meshes = this.modelDic.Get(modelId); + if (!meshes?.length) { + console.warn(`Model not found: ${modelId}`); + return; + } + + this.getModelTransformTargets(meshes).forEach(mesh => { + mesh.position.x = position.x; + mesh.position.y = position.y; + mesh.position.z = position.z; + }); + } + + /** + * 设置模型缩放 + * @param modelId 模型ID + * @param scale 缩放向量 {x, y, z} + */ + setScale(modelId: string, scale: { x: number; y: number; z: number }): void { + const meshes = this.modelDic.Get(modelId); + if (!meshes?.length) { + console.warn(`Model not found: ${modelId}`); + return; + } + + this.getModelTransformTargets(meshes).forEach(mesh => { + mesh.scaling.x = scale.x; + mesh.scaling.y = scale.y; + mesh.scaling.z = scale.z; + }); + } + + /** + * 将模型放置到指定的放置区域 + * @param modelId 模型ID + * @param zoneInfo 放置区域信息 + * @param offsetDistance 距离墙面的偏移距离(默认0.1,正数向外) + */ + placeToZone(modelId: string, zoneInfo: any, offsetDistance: number = 0): void { + const meshes = this.modelDic.Get(modelId); + if (!meshes?.length) { + console.warn(`Model not found: ${modelId}`); + return; + } + + // 计算放置位置:中心点 + 法线方向的偏移 + const targetPosition = zoneInfo.center.add(zoneInfo.normal.scale(offsetDistance)); + + // 计算旋转角度:让模型面向墙面(法线的反方向) + const targetDirection = zoneInfo.normal.scale(-1); + const angle = Math.atan2(targetDirection.x, targetDirection.z); + + this.getModelTransformTargets(meshes).forEach(mesh => { + // 设置位置 + mesh.position.copyFrom(targetPosition); + + // 设置旋转(只旋转Y轴,让模型面向墙面) + if (mesh.rotationQuaternion) { + mesh.rotationQuaternion = Quaternion.FromEulerAngles(0, angle, 0); + } else { + mesh.rotation.set(0, angle, 0); + } + }); + } + + /** + * 检查模型是否存在 + * @param modelId 模型ID + * @returns 模型是否存在 + */ + exists(modelId: string): boolean { + return this.modelDic.Has(modelId); + } + + /** + * 应用 transform 到模型 + * @param modelId 模型ID + * @param transform 变换信息 + */ + private applyTransform(modelId: string, transform: ModelTransform): void { + // 应用位置 + if (transform.position) { + this.setPosition(modelId, transform.position); + } + + // 应用旋转(角度制) + if (transform.rotation) { + this.setRotation(modelId, transform.rotation, true); + } + + // 应用缩放 + if (transform.scale) { + this.setScale(modelId, transform.scale); + } + } +} diff --git a/src/babylonjs/AppModel.ts b/src/babylonjs/AppModel.ts index 66774c7..92c073f 100644 --- a/src/babylonjs/AppModel.ts +++ b/src/babylonjs/AppModel.ts @@ -31,6 +31,7 @@ type ModelTransform = { }; type ModelMetadata = { + modelName: string; modelId: string; modelUrl: string; modelControlType?: ModelControlType; @@ -253,6 +254,7 @@ export class AppModel extends Monobehiver { // 单个加载 return await this.addSingle( + modelConfig.modelName, modelConfig.modelId, modelConfig.modelUrl, modelConfig.modelControlType, @@ -264,9 +266,9 @@ export class AppModel extends Monobehiver { /** * 添加单个模型 */ - private async addSingle(modelName: string, modelUrl: string, modelControlType?: ModelControlType, drag?: DragConfig, transform?: ModelTransform): Promise { + private async addSingle(modelName: string, modelId: string, modelUrl: string, modelControlType?: ModelControlType, drag?: DragConfig, transform?: ModelTransform): Promise { // 检查是否已存在 - const existingMeshes = this.modelDic.Get(modelName); + const existingMeshes = this.modelDic.Get(modelId); if (existingMeshes?.length && !existingMeshes[0].isDisposed()) { console.log(`模型 ${modelName} 已存在,直接显示`); this.showMeshes(existingMeshes); @@ -280,14 +282,15 @@ export class AppModel extends Monobehiver { if (result.success && result.meshes) { // 克隆材质,确保每个模型有独立的材质 - this.cloneMaterials(result.meshes, modelName); + this.cloneMaterials(result.meshes, modelId); - result.meshes = this.createModelRoot(modelName, result.meshes); - this.modelDic.Set(modelName, result.meshes); + result.meshes = this.createModelRoot(modelId, result.meshes); + this.modelDic.Set(modelId, result.meshes); // 存储元数据 - this.modelMetadataDic.Set(modelName, { - modelId: modelName, + this.modelMetadataDic.Set(modelId, { + modelName: modelName, + modelId: modelId, modelUrl: modelUrl, modelControlType: modelControlType, drag: drag, @@ -296,12 +299,12 @@ export class AppModel extends Monobehiver { // 应用 transform if (transform) { - this.applyTransform(modelName, transform); + this.applyTransform(modelId, transform); } // 配置拖拽功能 if (drag) { - this.mainApp.appModelDrag?.configureDrag(modelName, drag); + this.mainApp.appModelDrag?.configureDrag(modelId, drag); } // 更新 GameManager 的字典 @@ -325,7 +328,7 @@ export class AppModel extends Monobehiver { EventBridge.modelLoadProgress({ loaded: 0, total, progress: 0, percentage: 0 }); for (let i = 0; i < models.length; i++) { - const { modelId, modelUrl, modelControlType, drag, transform } = models[i]; + const { modelName, modelId, modelUrl, modelControlType, drag, transform } = models[i]; const result = await this.loadSingleModel(modelUrl, (event) => { this.emitProgress(i, total, modelUrl, event); @@ -340,6 +343,7 @@ export class AppModel extends Monobehiver { // 存储元数据 this.modelMetadataDic.Set(modelId, { + modelName: modelName, modelId: modelId, modelUrl: modelUrl, modelControlType: modelControlType, @@ -483,6 +487,7 @@ export class AppModel extends Monobehiver { this.removeByName(modelConfig.modelId); return await this.addSingle( + modelConfig.modelName, modelConfig.modelId, modelConfig.modelUrl, modelConfig.modelControlType, diff --git a/src/babylonjs/AppRay.ts b/src/babylonjs/AppRay.ts index 31b22b7..ed9deb8 100644 --- a/src/babylonjs/AppRay.ts +++ b/src/babylonjs/AppRay.ts @@ -143,9 +143,13 @@ class AppRay extends Monobehiver { // 获取模型元数据 const modelMetadata = this.mainApp.appModel.getMetadataByMesh(pickInfo.pickedMesh); + // 获取模型名称(优先使用 modelName,如果没有则使用 modelId) + const modelName = this.mainApp.appModel.findModelNameByMesh(pickInfo.pickedMesh); +console.log(modelName); EventBridge.modelClick({ meshName: pickInfo.pickedMesh.name, + modelName: modelName, pickedMesh: pickInfo.pickedMesh, pickedPoint: pickInfo.pickedPoint, materialName: materialName, diff --git a/src/babylonjs/GameManager.ts b/src/babylonjs/GameManager.ts index e9fc898..6187538 100644 --- a/src/babylonjs/GameManager.ts +++ b/src/babylonjs/GameManager.ts @@ -786,18 +786,33 @@ export class GameManager extends Monobehiver { } // 应用反照率纹理(颜色贴图) - if (options.albedoTexture) { - material.albedoTexture = new Texture(options.albedoTexture); + if (options.albedoTexture !== undefined) { + if (options.albedoTexture) { + material.albedoTexture = new Texture(options.albedoTexture); + } else { + // 传入空字符串或 null 时清空贴图 + material.albedoTexture = null; + } } // 应用法线贴图 - if (options.normalMap) { - material.bumpTexture = new Texture(options.normalMap); + if (options.normalMap !== undefined) { + if (options.normalMap) { + material.bumpTexture = new Texture(options.normalMap); + } else { + // 传入空字符串或 null 时清空贴图 + material.bumpTexture = null; + } } // 应用金属度贴图 - if (options.metallicTexture) { - material.metallicTexture = new Texture(options.metallicTexture); + if (options.metallicTexture !== undefined) { + if (options.metallicTexture) { + material.metallicTexture = new Texture(options.metallicTexture); + } else { + // 传入空字符串或 null 时清空贴图 + material.metallicTexture = null; + } } // 应用粗糙度值 diff --git a/src/event/types.ts b/src/event/types.ts index fc9ff1b..194b203 100644 --- a/src/event/types.ts +++ b/src/event/types.ts @@ -30,6 +30,7 @@ export type ModelLoadedPayload = { export type ModelClickPayload = { meshName?: string; + modelName?: string; // 模型根节点名称(modelId) pickedMesh?: any; pickedPoint?: any; materialName?: string; diff --git a/src/kernel/Adapter.ts b/src/kernel/Adapter.ts index ad6ecf1..a84af8b 100644 --- a/src/kernel/Adapter.ts +++ b/src/kernel/Adapter.ts @@ -5,6 +5,7 @@ import type { HotspotInput } from '../types/hotspot'; type ModelControlType = 'rotation' | 'color'; type ModelInput = { + modelName: string; modelId: string; modelUrl: string; modelControlType?: ModelControlType; @@ -68,6 +69,19 @@ export class KernelAdapter { */ removeAll: (): void => { this.mainApp.appModel.removeAll(); + }, + /** + * 检查模型是否已加载 + * @param modelId 模型ID + * @returns 模型是否存在 + * @example + * // 检查模型是否已加载,避免重复加载 + * if (!kernel.model.exists('shed_001')) { + * await kernel.model.add({ modelId: 'shed_001', modelUrl: '...' }); + * } + */ + exists: (modelId: string): boolean => { + return this.mainApp.appModel.exists(modelId); } };