Files
zhengte.babylonjs-sdk/src/babylonjs/AppDropZone.ts
2026-06-05 20:59:35 +08:00

538 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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();
}
}