538 lines
16 KiB
TypeScript
538 lines
16 KiB
TypeScript
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<string, string> = new Map();
|
||
// 墙面 -> 当前分割数
|
||
private wallDivisionsMap: Map<string, number> = new Map();
|
||
// 墙面 -> 该墙面模型对应的分割数(用于检测分割数变化)
|
||
private wallModelDivisionsMap: Map<string, number> = new Map();
|
||
|
||
// 存储放置区域配置数据
|
||
private dropZoneConfig: DropZoneConfig | null = null;
|
||
// 存储原始墙面配置(用于 updateDivisions 时恢复完整墙面列表)
|
||
private originalWalls: WallConfig[] = [];
|
||
|
||
// 存储所有墙面的区域数据(持久化,不会被覆盖)
|
||
private allWallZonesData: Map<string, PlacementZoneInfo[]> = new Map();
|
||
// 当前激活显示的墙面名称集合(用于控制显示)
|
||
private activeWallNames: Set<string> = 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<string, number> = {};
|
||
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();
|
||
}
|
||
}
|