增加配件吸附功能
This commit is contained in:
4
index.js
4
index.js
@ -220,6 +220,10 @@ export const executeEvent = async (dropzone_data, result, sku) => {
|
||||
enable: true,
|
||||
axis: rotation.y === 0 || rotation.y === 180 ? 'x' : 'z',
|
||||
step: 0.1,
|
||||
boundaryConstraint: true, // 启用边界限制
|
||||
snapToZone: true, // 启用拖拽吸附到最近的分割区域
|
||||
// 拖拽到已占用区域时的行为:'return' 返回原位置,'replace' 替换目标位置的模型(默认 'return')
|
||||
onOccupiedZone: 'replace'
|
||||
},
|
||||
transform: {
|
||||
position: position,
|
||||
|
||||
@ -123,14 +123,7 @@ export class AppDropZone {
|
||||
divisionsMap[item.name] = item.divisions;
|
||||
});
|
||||
|
||||
// 定义方向关键字映射(支持中英文)
|
||||
const directionKeywords: Record<string, string[]> = {
|
||||
'前': ['前', 'front', 'qian'],
|
||||
'后': ['后', 'back', 'hou'],
|
||||
'左': ['左', 'left', 'zuo'],
|
||||
'右': ['右', 'right', 'you']
|
||||
};
|
||||
|
||||
|
||||
// 匹配墙面名称(精确匹配)
|
||||
const matchWallName = (wallName: string): number | null => {
|
||||
// 提取墙面名称的最后部分(最后一个下划线之后)
|
||||
@ -166,6 +159,11 @@ export class AppDropZone {
|
||||
})
|
||||
.filter(wall => wall !== null) as typeof this.dropZoneConfig.walls; // 过滤掉未配置的墙面
|
||||
|
||||
// 更新 wallDivisionsMap(重要:用于后续的自动排列和拖拽检查)
|
||||
this.dropZoneConfig.walls.forEach(wall => {
|
||||
this.wallDivisionsMap.set(wall.name, wall.divisions);
|
||||
});
|
||||
|
||||
// 清除旧的放置区域网格(不清除模型)
|
||||
this.clearZones();
|
||||
|
||||
@ -419,7 +417,7 @@ export class AppDropZone {
|
||||
|
||||
if (this.appModel) {
|
||||
// 计算新位置(从放置区域的中心点加上法线偏移)
|
||||
const offsetDistance = 0.05;
|
||||
const offsetDistance = 0; // 增加偏移距离,让模型更往外
|
||||
const targetPosition = targetZone.center.add(targetZone.normal.scale(offsetDistance));
|
||||
|
||||
// 计算旋转角度(根据法线方向)
|
||||
@ -510,6 +508,14 @@ export class AppDropZone {
|
||||
this.placementWall.show();
|
||||
}
|
||||
|
||||
/**
|
||||
* 只显示指定墙面的放置区域
|
||||
* @param wallName 墙面名称
|
||||
*/
|
||||
showWall(wallName: string): void {
|
||||
this.placementWall.showWall(wallName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 隐藏所有放置区域
|
||||
*/
|
||||
|
||||
@ -729,9 +729,10 @@ export class AppModel extends Monobehiver {
|
||||
* 将模型放置到指定的放置区域
|
||||
* @param modelId 模型ID
|
||||
* @param zoneInfo 放置区域信息
|
||||
* @param offsetDistance 距离墙面的偏移距离(默认0.1,正数向外)
|
||||
* @param offsetDistance 距离墙面的偏移距离(默认0,正数向外)
|
||||
*/
|
||||
placeToZone(modelId: string, zoneInfo: any, offsetDistance: number = 0): void {
|
||||
console.log(zoneInfo);
|
||||
const meshes = this.modelDic.Get(modelId);
|
||||
if (!meshes?.length) {
|
||||
console.warn(`Model not found: ${modelId}`);
|
||||
|
||||
@ -11,6 +11,9 @@ export interface DragConfig {
|
||||
enable: boolean;
|
||||
axis?: 'x' | 'y' | 'z' | 'xy' | 'xz' | 'yz' | 'xyz';
|
||||
step?: number;
|
||||
boundaryConstraint?: boolean; // 边界限制:拖拽不得超出当前墙面的放置区域
|
||||
snapToZone?: boolean; // 拖拽吸附:松开时自动吸附到最近的分割区域
|
||||
onOccupiedZone?: 'return' | 'replace'; // 拖拽到已占用区域时的行为:'return' 返回原位置,'replace' 替换目标位置的模型(默认 'return')
|
||||
}
|
||||
|
||||
/**
|
||||
@ -115,15 +118,33 @@ export class AppModelDrag extends Monobehiver {
|
||||
pointerDragBehavior.onDragStartObservable.add(() => {
|
||||
// 禁用相机控制
|
||||
this.disableCameraControl();
|
||||
|
||||
// 如果启用了拖拽吸附,显示分割区域
|
||||
if (dragInfo.config.snapToZone) {
|
||||
this.showZonesForModel(modelId);
|
||||
}
|
||||
});
|
||||
|
||||
// 监听拖拽中事件(用于边界限制)
|
||||
if (dragInfo.config.boundaryConstraint) {
|
||||
pointerDragBehavior.onDragObservable.add((event) => {
|
||||
this.applyBoundaryConstraint(modelId, event.dragPlanePoint);
|
||||
});
|
||||
}
|
||||
|
||||
// 监听拖拽结束事件
|
||||
pointerDragBehavior.onDragEndObservable.add(() => {
|
||||
// 恢复相机控制
|
||||
this.enableCameraControl();
|
||||
|
||||
// 拖拽结束后,检测模型所属的分割区域并更新映射
|
||||
this.updateModelZoneMapping(modelId);
|
||||
// 如果启用了拖拽吸附,隐藏分割区域并吸附到最近区域
|
||||
if (dragInfo.config.snapToZone) {
|
||||
this.hideZonesForModel(modelId);
|
||||
this.snapModelToZone(modelId);
|
||||
} else {
|
||||
// 否则只更新映射关系
|
||||
this.updateModelZoneMapping(modelId);
|
||||
}
|
||||
});
|
||||
|
||||
return pointerDragBehavior;
|
||||
@ -242,6 +263,291 @@ export class AppModelDrag extends Monobehiver {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示模型所在墙面的分割区域
|
||||
* @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
|
||||
* @param dragPoint 拖拽点位置
|
||||
*/
|
||||
private applyBoundaryConstraint(modelId: string, dragPoint: Vector3): 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;
|
||||
appDropZone['zoneModelMap']?.forEach((id: string, zoneKey: string) => {
|
||||
if (id === modelId) {
|
||||
const match = zoneKey.match(/^(.+)\[(\d+)\]$/);
|
||||
if (match) {
|
||||
wallName = match[1];
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!wallName) return;
|
||||
|
||||
// 获取该墙面的所有分割区域
|
||||
const wallZones = appDropZone.getZonesByWall(wallName);
|
||||
if (!wallZones.length) return;
|
||||
|
||||
// 计算墙面的边界(所有分割区域的包围盒)
|
||||
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;
|
||||
const halfHeight = zone.height / 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;
|
||||
const constrainedPos = currentPos.clone();
|
||||
|
||||
if (minX !== Number.POSITIVE_INFINITY && maxX !== Number.NEGATIVE_INFINITY) {
|
||||
constrainedPos.x = Math.max(minX, Math.min(maxX, currentPos.x));
|
||||
}
|
||||
if (minZ !== Number.POSITIVE_INFINITY && maxZ !== Number.NEGATIVE_INFINITY) {
|
||||
constrainedPos.z = Math.max(minZ, Math.min(maxZ, currentPos.z));
|
||||
}
|
||||
|
||||
// 如果位置被限制,更新模型位置
|
||||
if (!constrainedPos.equals(currentPos)) {
|
||||
rootMesh.position.copyFrom(constrainedPos);
|
||||
console.log(`[边界限制] 模型 ${modelId} 位置被限制在边界内`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将模型吸附到最近的分割区域
|
||||
* @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);
|
||||
|
||||
// 如果启用了边界约束,检查模型是否在墙面边界内
|
||||
if (dragInfo?.config.boundaryConstraint) {
|
||||
// 计算墙面的边界
|
||||
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;
|
||||
let isOutOfBounds = false;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果超出边界,回到原来的区域
|
||||
if (isOutOfBounds && originalZoneIndex !== -1) {
|
||||
console.log(`[拖拽吸附] 模型 ${modelId} 超出边界,检查原区域 ${originalZoneIndex}`);
|
||||
|
||||
// 检查原区域是否已被其他模型占用
|
||||
const originalZoneKey = `${wallName}[${originalZoneIndex}]`;
|
||||
const occupyingModelId = appDropZone['zoneModelMap']?.get(originalZoneKey);
|
||||
|
||||
if (!occupyingModelId || occupyingModelId === modelId) {
|
||||
// 原区域空闲或者就是当前模型,回到原区域
|
||||
closestZoneIndex = originalZoneIndex;
|
||||
} else {
|
||||
// 原区域已被占用,找最近的空闲区域
|
||||
console.log(`[拖拽吸附] 原区域已被模型 ${occupyingModelId} 占用,寻找空闲区域`);
|
||||
|
||||
let foundFreeZone = false;
|
||||
for (let i = 0; i < wallZones.length; i++) {
|
||||
const zoneKey = `${wallName}[${i}]`;
|
||||
const occupant = appDropZone['zoneModelMap']?.get(zoneKey);
|
||||
|
||||
if (!occupant || occupant === modelId) {
|
||||
// 找到空闲区域
|
||||
closestZoneIndex = i;
|
||||
foundFreeZone = true;
|
||||
console.log(`[拖拽吸附] 找到空闲区域 ${i}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有空闲区域,保持在原区域(会触发交换逻辑)
|
||||
if (!foundFreeZone) {
|
||||
console.log(`[拖拽吸附] 没有空闲区域,回到原区域 ${originalZoneIndex}`);
|
||||
closestZoneIndex = originalZoneIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const targetZone = wallZones[closestZoneIndex];
|
||||
|
||||
// 检查目标区域是否已被其他模型占用
|
||||
const targetZoneKey = `${wallName}[${closestZoneIndex}]`;
|
||||
const occupyingModelId = appDropZone['zoneModelMap']?.get(targetZoneKey);
|
||||
const onOccupiedZone = dragInfo?.config.onOccupiedZone || 'return';
|
||||
|
||||
if (occupyingModelId && occupyingModelId !== modelId) {
|
||||
// 目标区域已被其他模型占用
|
||||
console.log(`[拖拽吸附] 目标区域 ${closestZoneIndex} 已被模型 ${occupyingModelId} 占用`);
|
||||
|
||||
if (onOccupiedZone === 'return') {
|
||||
// 返回原位置
|
||||
console.log(`[拖拽吸附] 配置为返回原位置,回到区域 ${originalZoneIndex}`);
|
||||
if (originalZoneIndex !== -1) {
|
||||
const originalZone = wallZones[originalZoneIndex];
|
||||
if (originalZone) {
|
||||
const offsetDistance = -0.05;
|
||||
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 (onOccupiedZone === 'replace') {
|
||||
// 替换目标位置的模型(继续执行后面的逻辑)
|
||||
console.log(`[拖拽吸附] 配置为替换模型,将替换模型 ${occupyingModelId}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 计算吸附位置(区域中心 + 法线偏移)
|
||||
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
|
||||
@ -332,11 +638,31 @@ export class AppModelDrag extends Monobehiver {
|
||||
const existingModelId = appDropZone['zoneModelMap']?.get(newKey);
|
||||
if (existingModelId && existingModelId !== modelId) {
|
||||
console.log(`[边界检测] 目标区域 ${closestZoneIndex} 已有模型 ${existingModelId},交换位置`);
|
||||
|
||||
// 将原有模型移动到旧位置
|
||||
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.05;
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -340,6 +340,30 @@ export class AppPlacementWall {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 只显示指定墙面的放置区域
|
||||
* @param wallName 墙面名称
|
||||
*/
|
||||
showWall(wallName: string): void {
|
||||
// 先隐藏所有
|
||||
this.hide();
|
||||
|
||||
// 只显示指定墙面的区域
|
||||
this.placementZones.forEach(zone => {
|
||||
if (zone.wallName === wallName) {
|
||||
zone.mesh.isVisible = true;
|
||||
}
|
||||
});
|
||||
|
||||
// 显示该墙面的边框(根据名称过滤)
|
||||
this.borderLines.forEach(line => {
|
||||
// 边框线名称格式:block_border_${wallName}_${index}_${edgeIndex} 或 border_${wallName}_${index}
|
||||
if (line.name.includes(`_${wallName}_`)) {
|
||||
line.isVisible = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 隐藏所有放置区域
|
||||
*/
|
||||
|
||||
@ -93,7 +93,7 @@ class AppRay extends Monobehiver {
|
||||
const clickedZone = zones.find(zone => zone.mesh === pickInfo.pickedMesh);
|
||||
if (clickedZone) {
|
||||
// 计算该放置区域的目标位置和旋转
|
||||
const offsetDistance = 0.05; // 增加偏移距离,让模型更往外
|
||||
const offsetDistance = 0; // 增加偏移距离,让模型更往外
|
||||
const targetPosition = clickedZone.center.add(clickedZone.normal.scale(offsetDistance));
|
||||
const targetDirection = clickedZone.normal.scale(-1);
|
||||
const angle = Math.atan2(targetDirection.x, targetDirection.z);
|
||||
|
||||
Reference in New Issue
Block a user