Files
zhengte.babylonjs-sdk/src/babylonjs/AppModelDrag.ts
2026-06-04 19:05:53 +08:00

669 lines
25 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 { AbstractMesh } from '@babylonjs/core/Meshes/abstractMesh';
import { PointerDragBehavior } from '@babylonjs/core/Behaviors/Meshes/pointerDragBehavior';
import { Vector3 } from '@babylonjs/core/Maths/math.vector';
import { Scene } from '@babylonjs/core/scene';
import { Monobehiver } from '../base/Monobehiver';
/**
* 拖拽配置接口
*/
export interface DragConfig {
enable: boolean;
axis?: 'x' | 'y' | 'z' | 'xy' | 'xz' | 'yz' | 'xyz';
step?: number;
snapToZone?: boolean; // 拖拽吸附:松开时自动吸附到最近的分割区域
returnWhenOutOfBounds?: boolean; // 拖拽到区域外时返回原位置
handleOccupiedZone?: boolean; // 拖拽到已占用区域时的处理true=返回原位置或替换false=允许重叠
occupiedZoneAction?: 'return' | 'replace'; // 当 handleOccupiedZone=true 时的具体行为:'return' 返回原位置,'replace' 替换目标位置的模型
}
/**
* 模型拖拽信息
*/
interface ModelDragInfo {
config: DragConfig;
behavior: PointerDragBehavior | null;
currentAxis: 'x' | 'y' | 'z' | null;
}
/**
* 模型拖拽管理器 - 负责处理模型的拖拽交互
*/
export class AppModelDrag extends Monobehiver {
private modelDragMap: Map<string, ModelDragInfo>;
private scene: Scene | null;
constructor(mainApp: any) {
super(mainApp);
this.modelDragMap = new Map();
this.scene = null;
}
/**
* 初始化拖拽管理器
*/
Awake(): void {
this.scene = this.mainApp.appScene.object;
if (!this.scene) {
console.warn('Scene not initialized');
return;
}
}
/**
* 为模型配置拖拽
* @param modelId 模型ID
* @param config 拖拽配置
*/
configureDrag(modelId: string, config: DragConfig): void {
// 获取模型的根网格
const meshes = this.mainApp.appModel?.modelDic?.Get(modelId);
if (!meshes || !meshes.length) {
console.warn(`Model not found: ${modelId}`);
return;
}
const rootMesh = meshes[0]; // 第一个是根节点
// 如果已存在,先移除旧的行为
const existingInfo = this.modelDragMap.get(modelId);
if (existingInfo?.behavior) {
rootMesh.removeBehavior(existingInfo.behavior);
}
// 创建拖拽信息
const dragInfo: ModelDragInfo = {
config: { ...config },
behavior: null,
currentAxis: this.getFirstAvailableAxis(config.axis || 'xyz')
};
if (config.enable) {
// 创建并配置拖拽行为
dragInfo.behavior = this.createDragBehavior(modelId, dragInfo);
rootMesh.addBehavior(dragInfo.behavior);
}
this.modelDragMap.set(modelId, dragInfo);
}
/**
* 创建拖拽行为
*/
private createDragBehavior(modelId: string, dragInfo: ModelDragInfo): PointerDragBehavior {
const axis = dragInfo.currentAxis;
let dragAxis: Vector3;
// 根据当前激活的轴创建拖拽向量
switch (axis) {
case 'x':
dragAxis = new Vector3(1, 0, 0);
break;
case 'y':
dragAxis = new Vector3(0, 1, 0);
break;
case 'z':
dragAxis = new Vector3(0, 0, 1);
break;
default:
dragAxis = new Vector3(1, 0, 0);
}
// 创建拖拽行为
const pointerDragBehavior = new PointerDragBehavior({ dragAxis: dragAxis });
// 使用世界坐标系而不是物体本地坐标系
pointerDragBehavior.useObjectOrientationForDragging = false;
// 记录拖拽起始位置和状态
let dragStartPosition: Vector3 | null = null;
let hasShownZones = false; // 是否已显示分割区域
// 监听拖拽开始事件
pointerDragBehavior.onDragStartObservable.add(() => {
// 记录起始位置
const meshes = this.mainApp.appModel?.modelDic?.Get(modelId);
if (meshes && meshes.length > 0) {
dragStartPosition = meshes[0].position.clone();
}
// 禁用相机控制
this.disableCameraControl();
// 不在这里显示分割区域,等到实际拖动时再显示
hasShownZones = false;
});
// 监听拖拽中事件(用于延迟显示分割区域)
pointerDragBehavior.onDragObservable.add((event) => {
// 检查是否实际移动了
const meshes = this.mainApp.appModel?.modelDic?.Get(modelId);
if (meshes && meshes.length > 0 && dragStartPosition) {
const distance = Vector3.Distance(dragStartPosition, meshes[0].position);
// 如果移动距离超过阈值且还没显示分割区域,则显示
if (distance > 0.01 && !hasShownZones && dragInfo.config.snapToZone) {
this.showZonesForModel(modelId);
hasShownZones = true;
}
}
});
// 监听拖拽结束事件
pointerDragBehavior.onDragEndObservable.add(() => {
// 检查是否实际移动了
const meshes = this.mainApp.appModel?.modelDic?.Get(modelId);
let hasMoved = false;
if (meshes && meshes.length > 0 && dragStartPosition) {
const distance = Vector3.Distance(dragStartPosition, meshes[0].position);
hasMoved = distance > 0.01; // 移动距离大于 0.01 才算拖拽
}
// 恢复相机控制
this.enableCameraControl();
// 只有在实际移动的情况下才执行拖拽逻辑
if (hasMoved) {
// 如果启用了拖拽吸附,隐藏分割区域并吸附到最近区域
if (dragInfo.config.snapToZone && hasShownZones) {
this.hideZonesForModel(modelId);
this.snapModelToZone(modelId);
} else {
// 否则只更新映射关系
this.updateModelZoneMapping(modelId);
}
}
// 清除状态
dragStartPosition = null;
hasShownZones = false;
});
return pointerDragBehavior;
}
/**
* 获取模型的拖拽配置
* @param modelId 模型ID
*/
getDragConfig(modelId: string): DragConfig | undefined {
return this.modelDragMap.get(modelId)?.config;
}
/**
* 启用/禁用模型拖拽
* @param modelId 模型ID
* @param enable 是否启用
*/
setDragEnabled(modelId: string, enable: boolean): void {
const dragInfo = this.modelDragMap.get(modelId);
if (!dragInfo) return;
dragInfo.config.enable = enable;
const meshes = this.mainApp.appModel?.modelDic?.Get(modelId);
if (!meshes || !meshes.length) return;
const rootMesh = meshes[0];
if (enable) {
// 启用:创建并添加行为
if (!dragInfo.behavior) {
dragInfo.behavior = this.createDragBehavior(modelId, dragInfo);
rootMesh.addBehavior(dragInfo.behavior);
}
} else {
// 禁用:移除行为
if (dragInfo.behavior) {
rootMesh.removeBehavior(dragInfo.behavior);
dragInfo.behavior = null;
}
}
}
/**
* 切换激活的轴向
* @param modelId 模型ID
* @param axis 要激活的轴向
*/
switchAxis(modelId: string, axis: 'x' | 'y' | 'z'): void {
const dragInfo = this.modelDragMap.get(modelId);
if (!dragInfo) return;
// 检查该轴是否在允许的轴向中
if (!this.isAxisAllowed(axis, dragInfo.config.axis || 'xyz')) {
console.warn(`Axis ${axis} is not allowed for model ${modelId}`);
return;
}
// 更新当前轴
dragInfo.currentAxis = axis;
// 重新创建拖拽行为
const meshes = this.mainApp.appModel?.modelDic?.Get(modelId);
if (!meshes || !meshes.length) return;
const rootMesh = meshes[0];
// 移除旧行为
if (dragInfo.behavior) {
rootMesh.removeBehavior(dragInfo.behavior);
}
// 创建新行为
if (dragInfo.config.enable) {
dragInfo.behavior = this.createDragBehavior(modelId, dragInfo);
rootMesh.addBehavior(dragInfo.behavior);
}
}
/**
* 获取配置中的第一个可用轴
*/
private getFirstAvailableAxis(axisConfig: string): 'x' | 'y' | 'z' | null {
if (axisConfig.includes('x')) return 'x';
if (axisConfig.includes('y')) return 'y';
if (axisConfig.includes('z')) return 'z';
return null;
}
/**
* 检查轴是否在允许的配置中
*/
private isAxisAllowed(axis: 'x' | 'y' | 'z', axisConfig: string): boolean {
return axisConfig.includes(axis);
}
/**
* 禁用相机控制
*/
private disableCameraControl(): void {
const camera = this.mainApp.appCamera?.object;
if (camera) {
camera.detachControl();
}
}
/**
* 启用相机控制
*/
private enableCameraControl(): void {
const camera = this.mainApp.appCamera?.object;
const canvas = this.mainApp.appEngin?.object?.getRenderingCanvas();
if (camera && canvas) {
camera.attachControl(canvas, true);
}
}
/**
* 显示模型所在墙面的分割区域
* @param modelId 模型ID
*/
private showZonesForModel(modelId: string): void {
const appDropZone = this.mainApp.appDropZone;
if (!appDropZone) return;
// 查找模型所在的墙面
let wallName: string | null = null;
appDropZone['zoneModelMap']?.forEach((id: string, zoneKey: string) => {
if (id === modelId) {
const match = zoneKey.match(/^(.+)\[(\d+)\]$/);
if (match) {
wallName = match[1];
}
}
});
if (wallName) {
console.log(`[拖拽吸附] 显示墙面 ${wallName} 的分割区域`);
// 只显示该墙面的分割区域
appDropZone.showWall(wallName);
}
}
/**
* 隐藏分割区域
* @param modelId 模型ID
*/
private hideZonesForModel(modelId: string): void {
const appDropZone = this.mainApp.appDropZone;
if (!appDropZone) return;
console.log(`[拖拽吸附] 隐藏分割区域`);
appDropZone.hide();
}
/**
* 将模型吸附到最近的分割区域
* @param modelId 模型ID
*/
private snapModelToZone(modelId: string): void {
const meshes = this.mainApp.appModel?.modelDic?.Get(modelId);
if (!meshes || !meshes.length) return;
const rootMesh = meshes[0];
const appDropZone = this.mainApp.appDropZone;
if (!appDropZone) return;
// 查找模型原来所在的墙面和区域索引
let wallName: string | null = null;
let originalZoneIndex: number = -1;
appDropZone['zoneModelMap']?.forEach((id: string, zoneKey: string) => {
if (id === modelId) {
const match = zoneKey.match(/^(.+)\[(\d+)\]$/);
if (match) {
wallName = match[1];
originalZoneIndex = parseInt(match[2]);
}
}
});
if (!wallName) return;
// 获取该墙面的所有分割区域
const wallZones = appDropZone.getZonesByWall(wallName);
if (!wallZones.length) return;
// 找到最近的区域
let closestZoneIndex = -1;
let minDistance = Number.POSITIVE_INFINITY;
wallZones.forEach((zone, index) => {
const distance = rootMesh.position.subtract(zone.center).length();
if (distance < minDistance) {
minDistance = distance;
closestZoneIndex = index;
}
});
if (closestZoneIndex === -1) return;
// 获取拖拽配置
const dragInfo = this.modelDragMap.get(modelId);
const returnWhenOutOfBounds = dragInfo?.config.returnWhenOutOfBounds ?? false;
const handleOccupiedZone = dragInfo?.config.handleOccupiedZone ?? false;
const occupiedZoneAction = dragInfo?.config.occupiedZoneAction ?? 'return';
// 检查是否拖拽到区域外
let isOutOfBounds = false;
// 计算墙面的边界
let minX = Number.POSITIVE_INFINITY;
let maxX = Number.NEGATIVE_INFINITY;
let minZ = Number.POSITIVE_INFINITY;
let maxZ = Number.NEGATIVE_INFINITY;
wallZones.forEach(zone => {
const halfWidth = zone.width / 2;
// 根据法线方向计算边界
if (Math.abs(zone.normal.x) > 0.5) {
// 左右墙面法线沿X轴
minZ = Math.min(minZ, zone.center.z - halfWidth);
maxZ = Math.max(maxZ, zone.center.z + halfWidth);
} else if (Math.abs(zone.normal.z) > 0.5) {
// 前后墙面法线沿Z轴
minX = Math.min(minX, zone.center.x - halfWidth);
maxX = Math.max(maxX, zone.center.x + halfWidth);
}
});
// 检查当前位置是否在边界内
const currentPos = rootMesh.position;
if (minX !== Number.POSITIVE_INFINITY && maxX !== Number.NEGATIVE_INFINITY) {
if (currentPos.x < minX || currentPos.x > maxX) {
isOutOfBounds = true;
}
}
if (minZ !== Number.POSITIVE_INFINITY && maxZ !== Number.NEGATIVE_INFINITY) {
if (currentPos.z < minZ || currentPos.z > maxZ) {
isOutOfBounds = true;
}
}
// 处理超出边界的情况开关2returnWhenOutOfBounds
if (isOutOfBounds) {
console.log(`[拖拽吸附] 模型 ${modelId} 超出边界`);
if (returnWhenOutOfBounds) {
// 启用了边界返回,回到原来的区域
console.log(`[拖拽吸附] 启用边界返回,回到原区域 ${originalZoneIndex}`);
if (originalZoneIndex !== -1) {
const originalZone = wallZones[originalZoneIndex];
if (originalZone) {
const offsetDistance = 0;
const returnPosition = originalZone.center.add(originalZone.normal.scale(offsetDistance));
rootMesh.position.copyFrom(returnPosition);
const targetDirection = originalZone.normal.scale(-1);
const angle = Math.atan2(targetDirection.x, targetDirection.z);
rootMesh.rotation.y = angle;
console.log(`[拖拽吸附] 模型 ${modelId} 已返回原区域 ${originalZoneIndex}`);
return; // 不更新映射,保持原映射
}
}
} else {
// 未启用边界返回,保持当前位置,不做吸附
console.log(`[拖拽吸附] 未启用边界返回,保持当前位置,不做吸附`);
// 更新映射关系(可能移出了原区域)
this.updateModelZoneMapping(modelId);
return;
}
}
const targetZone = wallZones[closestZoneIndex];
// 检查目标区域是否已被其他模型占用开关2handleOccupiedZone
const targetZoneKey = `${wallName}[${closestZoneIndex}]`;
const occupyingModelId = appDropZone['zoneModelMap']?.get(targetZoneKey);
if (occupyingModelId && occupyingModelId !== modelId) {
// 目标区域已被其他模型占用
console.log(`[拖拽吸附] 目标区域 ${closestZoneIndex} 已被模型 ${occupyingModelId} 占用`);
if (handleOccupiedZone) {
// 启用了占用区域处理
if (occupiedZoneAction === 'return') {
// 返回原位置
console.log(`[拖拽吸附] 配置为返回原位置,回到区域 ${originalZoneIndex}`);
if (originalZoneIndex !== -1) {
const originalZone = wallZones[originalZoneIndex];
if (originalZone) {
const offsetDistance = 0;
const returnPosition = originalZone.center.add(originalZone.normal.scale(offsetDistance));
rootMesh.position.copyFrom(returnPosition);
const targetDirection = originalZone.normal.scale(-1);
const angle = Math.atan2(targetDirection.x, targetDirection.z);
rootMesh.rotation.y = angle;
console.log(`[拖拽吸附] 模型 ${modelId} 返回原区域 ${originalZoneIndex}`);
return; // 不更新映射,保持原映射
}
}
} else if (occupiedZoneAction === 'replace') {
// 替换目标位置的模型(继续执行后面的逻辑)
console.log(`[拖拽吸附] 配置为替换模型,将替换模型 ${occupyingModelId}`);
}
} else {
// 未启用占用区域处理,允许重叠(继续执行后面的逻辑)
console.log(`[拖拽吸附] 未启用占用区域处理,允许重叠`);
}
}
// 计算吸附位置(区域中心 + 法线偏移)
const offsetDistance = 0;
const snapPosition = targetZone.center.add(targetZone.normal.scale(offsetDistance));
// 吸附到目标位置
rootMesh.position.copyFrom(snapPosition);
// 更新旋转
const targetDirection = targetZone.normal.scale(-1);
const angle = Math.atan2(targetDirection.x, targetDirection.z);
rootMesh.rotation.y = angle;
console.log(`[拖拽吸附] 模型 ${modelId} 吸附到区域 ${closestZoneIndex}`);
// 更新映射关系
this.updateModelZoneMapping(modelId);
}
/**
* 更新模型所属的分割区域映射
* @param modelId 模型ID
*/
private updateModelZoneMapping(modelId: string): void {
const meshes = this.mainApp.appModel?.modelDic?.Get(modelId);
if (!meshes || !meshes.length) return;
const rootMesh = meshes[0];
const modelPosition = rootMesh.position;
console.log(`[边界检测] 模型 ${modelId} 拖拽结束,当前位置:`, modelPosition);
// 获取 AppDropZone
const appDropZone = this.mainApp.appDropZone;
if (!appDropZone) return;
// 查找该模型原本所在的墙面
let originalWallName: string | null = null;
appDropZone['zoneModelMap']?.forEach((id: string, zoneKey: string) => {
if (id === modelId) {
const match = zoneKey.match(/^(.+)\[(\d+)\]$/);
if (match) {
originalWallName = match[1];
}
}
});
if (!originalWallName) {
console.log(`[边界检测] 模型 ${modelId} 未找到原始墙面,跳过检测`);
return;
}
console.log(`[边界检测] 模型 ${modelId} 原始墙面: ${originalWallName}`);
// 获取该墙面的所有分割区域
const wallZones = appDropZone.getZonesByWall(originalWallName);
if (!wallZones.length) return;
console.log(`[边界检测] 墙面 ${originalWallName}${wallZones.length} 个分割区域`);
// 计算模型与每个分割区域的距离,找到最近的区域
let closestZoneIndex = -1;
let minDistance = Number.POSITIVE_INFINITY;
wallZones.forEach((zone, index) => {
// 计算模型位置到区域中心的距离
const distance = modelPosition.subtract(zone.center).length();
console.log(`[边界检测] 区域 ${index} 中心:`, zone.center, `距离: ${distance.toFixed(3)}`);
if (distance < minDistance) {
minDistance = distance;
closestZoneIndex = index;
}
});
if (closestZoneIndex === -1) {
console.log(`[边界检测] 未找到最近的区域`);
return;
}
console.log(`[边界检测] 模型 ${modelId} 最接近区域 ${closestZoneIndex},距离: ${minDistance.toFixed(3)}`);
// 查找模型当前所在的区域索引
let currentZoneIndex = -1;
appDropZone['zoneModelMap']?.forEach((id: string, zoneKey: string) => {
if (id === modelId) {
const match = zoneKey.match(/^.+\[(\d+)\]$/);
if (match) {
currentZoneIndex = parseInt(match[1]);
}
}
});
// 如果模型移动到了新的区域,更新映射
if (currentZoneIndex !== closestZoneIndex) {
console.log(`[边界检测] 模型 ${modelId} 从区域 ${currentZoneIndex} 移动到区域 ${closestZoneIndex}`);
// 删除旧映射
if (currentZoneIndex !== -1) {
const oldKey = `${originalWallName}[${currentZoneIndex}]`;
appDropZone['zoneModelMap']?.delete(oldKey);
console.log(`[边界检测] 删除旧映射: ${oldKey}`);
}
// 检查目标区域是否已有模型
const newKey = `${originalWallName}[${closestZoneIndex}]`;
const existingModelId = appDropZone['zoneModelMap']?.get(newKey);
// 获取拖拽配置
const dragInfo = this.modelDragMap.get(modelId);
const handleOccupiedZone = dragInfo?.config.handleOccupiedZone ?? false;
const occupiedZoneAction = dragInfo?.config.occupiedZoneAction ?? 'return';
if (existingModelId && existingModelId !== modelId) {
console.log(`[边界检测] 目标区域 ${closestZoneIndex} 已有模型 ${existingModelId}`);
// 只有在启用占用区域处理且为 'replace' 模式下才交换位置
if (handleOccupiedZone && occupiedZoneAction === 'replace') {
console.log(`[边界检测] 配置为替换模式,交换位置`);
// 将原有模型移动到旧位置
if (currentZoneIndex !== -1) {
const swapKey = `${originalWallName}[${currentZoneIndex}]`;
appDropZone['zoneModelMap']?.set(swapKey, existingModelId);
console.log(`[边界检测] 模型 ${existingModelId} 移动到区域 ${currentZoneIndex}`);
// 实际移动被替换模型的物理位置
const existingMeshes = this.mainApp.appModel?.modelDic?.Get(existingModelId);
if (existingMeshes && existingMeshes.length) {
const existingRootMesh = existingMeshes[0];
const swapZone = wallZones[currentZoneIndex];
if (swapZone) {
const offsetDistance = 0;
const swapPosition = swapZone.center.add(swapZone.normal.scale(offsetDistance));
existingRootMesh.position.copyFrom(swapPosition);
// 更新旋转
const targetDirection = swapZone.normal.scale(-1);
const angle = Math.atan2(targetDirection.x, targetDirection.z);
existingRootMesh.rotation.y = angle;
console.log(`[边界检测] 已将模型 ${existingModelId} 物理移动到区域 ${currentZoneIndex}`);
}
}
}
} else {
console.log(`[边界检测] 未启用替换模式或未启用占用区域处理,允许重叠`);
}
}
// 添加新映射
appDropZone['zoneModelMap']?.set(newKey, modelId);
console.log(`[边界检测] 添加新映射: ${newKey} -> ${modelId}`);
} else {
console.log(`[边界检测] 模型 ${modelId} 仍在区域 ${currentZoneIndex},无需更新映射`);
}
}
/**
* 清理资源
*/
dispose(): void {
// 移除所有拖拽行为
this.modelDragMap.forEach((dragInfo, modelId) => {
if (dragInfo.behavior) {
const meshes = this.mainApp.appModel?.modelDic?.Get(modelId);
if (meshes && meshes.length) {
meshes[0].removeBehavior(dragInfo.behavior);
}
}
});
this.modelDragMap.clear();
}
}