增加配件吸附功能

This commit is contained in:
2026-05-25 12:35:19 +08:00
parent 62eda81895
commit c6257883e5
6 changed files with 374 additions and 13 deletions

View File

@ -220,6 +220,10 @@ export const executeEvent = async (dropzone_data, result, sku) => {
enable: true, enable: true,
axis: rotation.y === 0 || rotation.y === 180 ? 'x' : 'z', axis: rotation.y === 0 || rotation.y === 180 ? 'x' : 'z',
step: 0.1, step: 0.1,
boundaryConstraint: true, // 启用边界限制
snapToZone: true, // 启用拖拽吸附到最近的分割区域
// 拖拽到已占用区域时的行为:'return' 返回原位置,'replace' 替换目标位置的模型(默认 'return'
onOccupiedZone: 'replace'
}, },
transform: { transform: {
position: position, position: position,

View File

@ -123,13 +123,6 @@ export class AppDropZone {
divisionsMap[item.name] = item.divisions; divisionsMap[item.name] = item.divisions;
}); });
// 定义方向关键字映射(支持中英文)
const directionKeywords: Record<string, string[]> = {
'前': ['前', 'front', 'qian'],
'后': ['后', 'back', 'hou'],
'左': ['左', 'left', 'zuo'],
'右': ['右', 'right', 'you']
};
// 匹配墙面名称(精确匹配) // 匹配墙面名称(精确匹配)
const matchWallName = (wallName: string): number | null => { const matchWallName = (wallName: string): number | null => {
@ -166,6 +159,11 @@ export class AppDropZone {
}) })
.filter(wall => wall !== null) as typeof this.dropZoneConfig.walls; // 过滤掉未配置的墙面 .filter(wall => wall !== null) as typeof this.dropZoneConfig.walls; // 过滤掉未配置的墙面
// 更新 wallDivisionsMap重要用于后续的自动排列和拖拽检查
this.dropZoneConfig.walls.forEach(wall => {
this.wallDivisionsMap.set(wall.name, wall.divisions);
});
// 清除旧的放置区域网格(不清除模型) // 清除旧的放置区域网格(不清除模型)
this.clearZones(); this.clearZones();
@ -419,7 +417,7 @@ export class AppDropZone {
if (this.appModel) { if (this.appModel) {
// 计算新位置(从放置区域的中心点加上法线偏移) // 计算新位置(从放置区域的中心点加上法线偏移)
const offsetDistance = 0.05; const offsetDistance = 0; // 增加偏移距离,让模型更往外
const targetPosition = targetZone.center.add(targetZone.normal.scale(offsetDistance)); const targetPosition = targetZone.center.add(targetZone.normal.scale(offsetDistance));
// 计算旋转角度(根据法线方向) // 计算旋转角度(根据法线方向)
@ -510,6 +508,14 @@ export class AppDropZone {
this.placementWall.show(); this.placementWall.show();
} }
/**
* 只显示指定墙面的放置区域
* @param wallName 墙面名称
*/
showWall(wallName: string): void {
this.placementWall.showWall(wallName);
}
/** /**
* 隐藏所有放置区域 * 隐藏所有放置区域
*/ */

View File

@ -729,9 +729,10 @@ export class AppModel extends Monobehiver {
* 将模型放置到指定的放置区域 * 将模型放置到指定的放置区域
* @param modelId 模型ID * @param modelId 模型ID
* @param zoneInfo 放置区域信息 * @param zoneInfo 放置区域信息
* @param offsetDistance 距离墙面的偏移距离默认0.1,正数向外) * @param offsetDistance 距离墙面的偏移距离默认0正数向外
*/ */
placeToZone(modelId: string, zoneInfo: any, offsetDistance: number = 0): void { placeToZone(modelId: string, zoneInfo: any, offsetDistance: number = 0): void {
console.log(zoneInfo);
const meshes = this.modelDic.Get(modelId); const meshes = this.modelDic.Get(modelId);
if (!meshes?.length) { if (!meshes?.length) {
console.warn(`Model not found: ${modelId}`); console.warn(`Model not found: ${modelId}`);

View File

@ -11,6 +11,9 @@ export interface DragConfig {
enable: boolean; enable: boolean;
axis?: 'x' | 'y' | 'z' | 'xy' | 'xz' | 'yz' | 'xyz'; axis?: 'x' | 'y' | 'z' | 'xy' | 'xz' | 'yz' | 'xyz';
step?: number; step?: number;
boundaryConstraint?: boolean; // 边界限制:拖拽不得超出当前墙面的放置区域
snapToZone?: boolean; // 拖拽吸附:松开时自动吸附到最近的分割区域
onOccupiedZone?: 'return' | 'replace'; // 拖拽到已占用区域时的行为:'return' 返回原位置,'replace' 替换目标位置的模型(默认 'return'
} }
/** /**
@ -115,15 +118,33 @@ export class AppModelDrag extends Monobehiver {
pointerDragBehavior.onDragStartObservable.add(() => { pointerDragBehavior.onDragStartObservable.add(() => {
// 禁用相机控制 // 禁用相机控制
this.disableCameraControl(); 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(() => { pointerDragBehavior.onDragEndObservable.add(() => {
// 恢复相机控制 // 恢复相机控制
this.enableCameraControl(); this.enableCameraControl();
// 拖拽结束后,检测模型所属的分割区域并更新映射 // 如果启用了拖拽吸附,隐藏分割区域并吸附到最近区域
this.updateModelZoneMapping(modelId); if (dragInfo.config.snapToZone) {
this.hideZonesForModel(modelId);
this.snapModelToZone(modelId);
} else {
// 否则只更新映射关系
this.updateModelZoneMapping(modelId);
}
}); });
return pointerDragBehavior; 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 * @param modelId 模型ID
@ -332,11 +638,31 @@ export class AppModelDrag extends Monobehiver {
const existingModelId = appDropZone['zoneModelMap']?.get(newKey); const existingModelId = appDropZone['zoneModelMap']?.get(newKey);
if (existingModelId && existingModelId !== modelId) { if (existingModelId && existingModelId !== modelId) {
console.log(`[边界检测] 目标区域 ${closestZoneIndex} 已有模型 ${existingModelId},交换位置`); console.log(`[边界检测] 目标区域 ${closestZoneIndex} 已有模型 ${existingModelId},交换位置`);
// 将原有模型移动到旧位置 // 将原有模型移动到旧位置
if (currentZoneIndex !== -1) { if (currentZoneIndex !== -1) {
const swapKey = `${originalWallName}[${currentZoneIndex}]`; const swapKey = `${originalWallName}[${currentZoneIndex}]`;
appDropZone['zoneModelMap']?.set(swapKey, existingModelId); appDropZone['zoneModelMap']?.set(swapKey, existingModelId);
console.log(`[边界检测] 模型 ${existingModelId} 移动到区域 ${currentZoneIndex}`); 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}`);
}
}
} }
} }

View File

@ -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;
}
});
}
/** /**
* 隐藏所有放置区域 * 隐藏所有放置区域
*/ */

View File

@ -93,7 +93,7 @@ class AppRay extends Monobehiver {
const clickedZone = zones.find(zone => zone.mesh === pickInfo.pickedMesh); const clickedZone = zones.find(zone => zone.mesh === pickInfo.pickedMesh);
if (clickedZone) { if (clickedZone) {
// 计算该放置区域的目标位置和旋转 // 计算该放置区域的目标位置和旋转
const offsetDistance = 0.05; // 增加偏移距离,让模型更往外 const offsetDistance = 0; // 增加偏移距离,让模型更往外
const targetPosition = clickedZone.center.add(clickedZone.normal.scale(offsetDistance)); const targetPosition = clickedZone.center.add(clickedZone.normal.scale(offsetDistance));
const targetDirection = clickedZone.normal.scale(-1); const targetDirection = clickedZone.normal.scale(-1);
const angle = Math.atan2(targetDirection.x, targetDirection.z); const angle = Math.atan2(targetDirection.x, targetDirection.z);