import { Scene, Vector3 } from '@babylonjs/core'; import { AppPlacementWall, WallConfig, PlacementZoneInfo } from './AppPlacementWall'; import { AppModel } from './AppModel'; /** * 放置区域配置 */ export interface DropZoneConfig { walls: WallConfig[]; // 墙面配置数组 color?: string; // 颜色(十六进制) alpha?: number; // 透明度 thickness?: number; // 厚度 showBorder?: boolean; // 是否显示边框 borderColor?: string; // 边框颜色 } /** * 放置区域管理类(使用新的墙面参数化方案) */ export class AppDropZone { private scene: Scene; private placementWall: AppPlacementWall; private appModel: AppModel | null = null; private mainApp: any = null; // 内部映射:放置区域 -> 模型ID private zoneModelMap: Map = new Map(); // 墙面 -> 当前分割数 private wallDivisionsMap: Map = new Map(); // 墙面 -> 该墙面模型对应的分割数(用于检测分割数变化) private wallModelDivisionsMap: Map = new Map(); // 存储放置区域配置数据 private dropZoneConfig: DropZoneConfig | null = null; // 存储原始墙面配置(用于 updateDivisions 时恢复完整墙面列表) private originalWalls: WallConfig[] = []; // 存储所有墙面的区域数据(持久化,不会被覆盖) private allWallZonesData: Map = new Map(); // 当前激活显示的墙面名称集合(用于控制显示) private activeWallNames: Set = new Set(); constructor(scene: Scene) { this.scene = scene; this.placementWall = new AppPlacementWall(scene); } /** * 初始化模型管理器(内部使用) */ setModelManager(appModel: AppModel): void { this.appModel = appModel; } /** * 设置 MainApp 引用(内部使用) */ setMainApp(mainApp: any): void { this.mainApp = mainApp; } /** * 设置放置区域数据 * @param config 配置参数 */ setData(config: DropZoneConfig): void { this.dropZoneConfig = config; // 保存原始墙面配置的深拷贝 this.originalWalls = config.walls.map(wall => ({ ...wall })); } /** * 生成放置区域 * @param config 配置参数(可选,如果不传则使用 setData 设置的数据) */ generateDropZones(config?: DropZoneConfig): PlacementZoneInfo[] { const finalConfig = config || this.dropZoneConfig; if (!finalConfig) { console.error('未设置放置区域配置数据,请先调用 setData 或传入 config 参数'); return []; } // 为墙面设置分割数(使用每个墙面自己的 divisions,默认为 1) const configWithDivisions: DropZoneConfig = { ...finalConfig, // 保留所有配置属性(color, alpha, thickness, showBorder, borderColor) walls: finalConfig.walls.map(wall => ({ ...wall, divisions: wall.divisions || 1 })) }; // 只记录分割数,不自动卸载模型 configWithDivisions.walls.forEach(wall => { this.wallDivisionsMap.set(wall.name, wall.divisions); }); return this.placementWall.generatePlacementAreas(configWithDivisions); } updateDivisions(divisions: Array<{ name: string; divisions: number }>): PlacementZoneInfo[] { if (!this.dropZoneConfig) { console.error('未设置放置区域配置数据,请先调用 setData'); return []; } // 将数组转换为对象映射 const divisionsMap: Record = {}; divisions.forEach(item => { divisionsMap[item.name] = item.divisions; }); // 匹配墙面名称(精确匹配) const matchWallName = (wallName: string): number | null => { // 提取墙面名称的最后部分(最后一个下划线之后) // 例如:"SW10000070_10x20星空篷_前1" → "前1" const wallShortName = wallName.split('_').pop() || wallName; // 精确匹配提取出的简短名称 if (divisionsMap[wallShortName] !== undefined) { return divisionsMap[wallShortName]; } return null; }; // 更新配置中的墙面分割数,从原始配置中恢复墙面列表 this.dropZoneConfig.walls = this.originalWalls .map(wall => { const newDivisions = matchWallName(wall.name); // 如果后端没有配置这个墙面,返回 null 标记 if (newDivisions === null) { return null; } return { ...wall, divisions: newDivisions }; }) .filter(wall => wall !== null) as typeof this.dropZoneConfig.walls; // 过滤掉未配置的墙面 // 更新 wallDivisionsMap(重要:用于后续的自动排列和拖拽检查) this.dropZoneConfig.walls.forEach(wall => { this.wallDivisionsMap.set(wall.name, wall.divisions); }); // 清除旧的放置区域网格(不清除模型) this.clearZones(); // 重新生成放置区域 const zones = this.generateDropZones(); // 显示放置区域 this.show(); return zones; } /** * 卸载指定墙面的所有模型(内部方法) */ private unloadWallModels(wallName: string): void { if (!this.appModel) return; const modelsToUnload: string[] = []; // 找出该墙面的所有模型 this.zoneModelMap.forEach((modelId, zoneKey) => { if (zoneKey.startsWith(`${wallName}[`)) { modelsToUnload.push(modelId); } }); // 卸载模型并清除映射 modelsToUnload.forEach(modelId => { this.appModel!.removeByName(modelId); }); // 清除该墙面的所有映射 const keysToDelete: string[] = []; this.zoneModelMap.forEach((modelId, zoneKey) => { if (zoneKey.startsWith(`${wallName}[`)) { keysToDelete.push(zoneKey); } }); keysToDelete.forEach(key => this.zoneModelMap.delete(key)); } /** * 记录模型放置到区域(内部方法,在点击事件中自动调用) */ recordModelPlacement(wallName: string, index: number, modelId: string): void { const zoneKey = `${wallName}[${index}]`; const currentDivisions = this.wallDivisionsMap.get(wallName); const modelDivisions = this.wallModelDivisionsMap.get(wallName); // 检查分割数是否改变 if (modelDivisions !== undefined && currentDivisions !== undefined && modelDivisions !== currentDivisions) { // 分割数改变了,清空该墙面的所有旧模型 this.unloadWallModels(wallName); // 更新该墙面模型对应的分割数 this.wallModelDivisionsMap.set(wallName, currentDivisions); } else { // 分割数没变,检查该区域是否已有模型(替换逻辑) const existingModelId = this.zoneModelMap.get(zoneKey); if (existingModelId && this.appModel) { this.appModel.removeByName(existingModelId); } } // 如果是该墙面的第一个模型,记录分割数 if (modelDivisions === undefined && currentDivisions !== undefined) { this.wallModelDivisionsMap.set(wallName, currentDivisions); } // 记录新模型 this.zoneModelMap.set(zoneKey, modelId); // 检查该墙面是否已满,如果满了则自动排列 this.checkAndAutoArrange(wallName); } /** * 通知模型被删除(外部调用,用于更新映射和重新启用拖拽) * @param modelId 被删除的模型ID */ notifyModelRemoved(modelId: string): void { // 找到该模型所在的墙面和索引 let removedWallName: string | null = null; let removedZoneKey: string | null = null; this.zoneModelMap.forEach((id, zoneKey) => { if (id === modelId) { removedZoneKey = zoneKey; // 从 zoneKey 中提取墙面名称,格式为 "wallName[index]" const match = zoneKey.match(/^(.+)\[(\d+)\]$/); if (match) { removedWallName = match[1]; } } }); if (removedZoneKey) { // 从映射中删除 this.zoneModelMap.delete(removedZoneKey); } if (removedWallName) { // 检查该墙面是否不满了,如果不满则重新启用拖拽 this.checkAndReenableDrag(removedWallName); } } /** * 检查墙面是否不满,如果不满则重新启用该墙面所有模型的拖拽 * @param wallName 墙面名称 */ private checkAndReenableDrag(wallName: string): void { const currentDivisions = this.wallDivisionsMap.get(wallName); if (!currentDivisions) return; // 统计该墙面已放置的模型数量 let placedCount = 0; const placedModelIds: string[] = []; this.zoneModelMap.forEach((modelId, zoneKey) => { if (zoneKey.startsWith(`${wallName}[`)) { placedCount++; placedModelIds.push(modelId); } }); // 如果墙面不满,重新启用所有模型的拖拽 if (placedCount < currentDivisions) { placedModelIds.forEach(modelId => { if (this.mainApp && this.mainApp.appModelDrag && typeof this.mainApp.appModelDrag.setDragEnabled === 'function') { this.mainApp.appModelDrag.setDragEnabled(modelId, true); } }); } } /** * 检查墙面是否已满 * @param wallName 墙面名称 * @returns 是否已满 */ isWallFull(wallName: string): boolean { const currentDivisions = this.wallDivisionsMap.get(wallName); if (!currentDivisions) return false; // 统计该墙面已放置的模型数量 let placedCount = 0; this.zoneModelMap.forEach((modelId, zoneKey) => { if (zoneKey.startsWith(`${wallName}[`)) { placedCount++; } }); return placedCount >= currentDivisions; } /** * 检查墙面是否已满,如果满了则自动排列模型 * @param wallName 墙面名称 */ private checkAndAutoArrange(wallName: string): void { const currentDivisions = this.wallDivisionsMap.get(wallName); if (!currentDivisions) { return; } // 统计该墙面已放置的模型数量 let placedCount = 0; const placedModels: string[] = []; this.zoneModelMap.forEach((modelId, zoneKey) => { if (zoneKey.startsWith(`${wallName}[`)) { placedCount++; placedModels.push(`${zoneKey} -> ${modelId}`); } }); // 如果该墙面已满(放置数量等于分割数),执行自动排列 if (placedCount === currentDivisions) { this.autoArrangeWall(wallName); } } /** * 自动排列墙面上的所有模型 * @param wallName 墙面名称 */ private autoArrangeWall(wallName: string): void { // 获取该墙面的所有放置区域 const wallZones = this.getZonesByWall(wallName); if (!wallZones.length) { return; } // 收集该墙面已放置的模型信息 const placedModels: Array<{ modelId: string; currentIndex: number }> = []; this.zoneModelMap.forEach((modelId, zoneKey) => { const match = zoneKey.match(new RegExp(`^${wallName}\\[(\\d+)\\]$`)); if (match) { const currentIndex = parseInt(match[1]); placedModels.push({ modelId: modelId, currentIndex: currentIndex }); } }); // 按当前索引排序 placedModels.sort((a, b) => a.currentIndex - b.currentIndex); // 重新排列:将模型按顺序放置到 0, 1, 2... 的位置 placedModels.forEach((model, newIndex) => { // 获取目标放置区域 const targetZone = wallZones[newIndex]; if (!targetZone) { console.warn(`[自动排列] ✗ 找不到索引 ${newIndex} 的放置区域`); return; } if (this.appModel) { // 计算新位置(从放置区域的中心点加上法线偏移) const offsetDistance = 0; // 增加偏移距离,让模型更往外 const targetPosition = targetZone.center.add(targetZone.normal.scale(offsetDistance)); // 计算旋转角度(根据法线方向) const targetDirection = targetZone.normal.scale(-1); const angle = Math.atan2(targetDirection.x, targetDirection.z); // 移动模型到新位置 const meshes = this.appModel.getCachedMeshes(model.modelId); if (meshes && meshes.length > 0) { const rootMesh = meshes[0]; // 更新位置 rootMesh.position.copyFrom(targetPosition); // 更新旋转 rootMesh.rotation.y = angle; } else { console.warn(`[自动排列] ✗ 找不到模型 ${model.modelId} 的网格`); } // 更新映射(无论是否移动,都要确保映射正确) if (model.currentIndex !== newIndex) { const oldKey = `${wallName}[${model.currentIndex}]`; const newKey = `${wallName}[${newIndex}]`; this.zoneModelMap.delete(oldKey); this.zoneModelMap.set(newKey, model.modelId); } } }); // 禁用该墙面所有模型的拖拽功能 placedModels.forEach(model => { // 安全检查:确保 mainApp 和 appModelDrag 都存在 if (this.mainApp && this.mainApp.appModelDrag && typeof this.mainApp.appModelDrag.setDragEnabled === 'function') { this.mainApp.appModelDrag.setDragEnabled(model.modelId, false); } else { console.warn(`[自动排列] ✗ 无法禁用模型 ${model.modelId} 的拖拽功能:appModelDrag 未初始化`); } }); } /** * 获取所有放置区域 */ getPlacementZones(): PlacementZoneInfo[] { return this.placementWall.getPlacementZones(); } /** * 根据墙面名称获取放置区域 */ getZonesByWall(wallName: string): PlacementZoneInfo[] { return this.placementWall.getZonesByWall(wallName); } /** * 根据索引获取特定放置区域 */ getZone(wallName: string, index: number): PlacementZoneInfo | undefined { return this.placementWall.getZone(wallName, index); } /** * 设置点击回调 */ setOnZoneClick(callback: (zoneInfo: PlacementZoneInfo) => void): void { this.placementWall.setOnZoneClick(callback); } /** * 显示所有放置区域 */ show(): void { // this.placementWall.show(); // // 禁用所有已放置模型的拾取 // this.setModelsPickable(false); } /** * 只显示指定墙面的放置区域 * @param wallName 墙面名称 */ showWall(wallName: string): void { this.placementWall.showWall(wallName); // 禁用所有已放置模型的拾取 this.setModelsPickable(false); } /** * 隐藏所有放置区域 */ hide(): void { this.placementWall.hide(); // 恢复所有已放置模型的拾取 this.setModelsPickable(true); } /** * 设置所有已放置模型的可拾取状态 * @param pickable 是否可拾取 */ private setModelsPickable(pickable: boolean): void { if (!this.appModel) return; this.zoneModelMap.forEach((modelId) => { const meshes = this.appModel!.getCachedMeshes(modelId); if (meshes && meshes.length > 0) { meshes.forEach(mesh => { mesh.isPickable = pickable; }); } }); } /** * 清除所有放置区域(只清除网格,不清除模型) */ clearZones(): void { this.placementWall.clearAll(); } /** * 清除所有放置区域 */ clearAll(): void { // 清除所有模型 if (this.appModel) { this.zoneModelMap.forEach(modelId => { this.appModel!.removeByName(modelId); }); } // 清除映射 this.zoneModelMap.clear(); this.wallDivisionsMap.clear(); this.wallModelDivisionsMap.clear(); this.placementWall.clearAll(); } /** * 销毁 */ dispose(): void { this.placementWall.dispose(); } }