From c6257883e5a4c835ee0f1248b09f7b56fd31cf5d Mon Sep 17 00:00:00 2001 From: zguiy <1415466602@qq.com> Date: Mon, 25 May 2026 12:35:19 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E9=85=8D=E4=BB=B6=E5=90=B8?= =?UTF-8?q?=E9=99=84=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- index.js | 4 + src/babylonjs/AppDropZone.ts | 24 ++- src/babylonjs/AppModel.ts | 3 +- src/babylonjs/AppModelDrag.ts | 330 +++++++++++++++++++++++++++++- src/babylonjs/AppPlacementWall.ts | 24 +++ src/babylonjs/AppRay.ts | 2 +- 6 files changed, 374 insertions(+), 13 deletions(-) diff --git a/index.js b/index.js index 43a6938..ac27d9c 100644 --- a/index.js +++ b/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, diff --git a/src/babylonjs/AppDropZone.ts b/src/babylonjs/AppDropZone.ts index 938fee8..4602473 100644 --- a/src/babylonjs/AppDropZone.ts +++ b/src/babylonjs/AppDropZone.ts @@ -123,14 +123,7 @@ export class AppDropZone { divisionsMap[item.name] = item.divisions; }); - // 定义方向关键字映射(支持中英文) - const directionKeywords: Record = { - '前': ['前', '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); + } + /** * 隐藏所有放置区域 */ diff --git a/src/babylonjs/AppModel.ts b/src/babylonjs/AppModel.ts index 3429ed0..f3abb48 100644 --- a/src/babylonjs/AppModel.ts +++ b/src/babylonjs/AppModel.ts @@ -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}`); diff --git a/src/babylonjs/AppModelDrag.ts b/src/babylonjs/AppModelDrag.ts index 8b29607..4977510 100644 --- a/src/babylonjs/AppModelDrag.ts +++ b/src/babylonjs/AppModelDrag.ts @@ -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}`); + } + } } } diff --git a/src/babylonjs/AppPlacementWall.ts b/src/babylonjs/AppPlacementWall.ts index 3a0fab2..c76fd75 100644 --- a/src/babylonjs/AppPlacementWall.ts +++ b/src/babylonjs/AppPlacementWall.ts @@ -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; + } + }); + } + /** * 隐藏所有放置区域 */ diff --git a/src/babylonjs/AppRay.ts b/src/babylonjs/AppRay.ts index 7c30a15..2f63fc8 100644 --- a/src/babylonjs/AppRay.ts +++ b/src/babylonjs/AppRay.ts @@ -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);