删除console.log
This commit is contained in:
394
DRAG_SNAP_BUG_ANALYSIS.md
Normal file
394
DRAG_SNAP_BUG_ANALYSIS.md
Normal file
@ -0,0 +1,394 @@
|
||||
# 拖拽吸附功能 - 潜在Bug分析报告
|
||||
|
||||
## 🔴 严重问题(可能导致间歇性失败)
|
||||
|
||||
### Bug #1: **竞态条件 - dragStartPosition 可能为 null**
|
||||
**位置:** `AppModelDrag.ts:156-159`
|
||||
|
||||
```typescript
|
||||
let hasMoved = false;
|
||||
if (meshes && meshes.length > 0 && dragStartPosition) {
|
||||
const distance = Vector3.Distance(dragStartPosition, meshes[0].position);
|
||||
hasMoved = distance > 0.01;
|
||||
}
|
||||
```
|
||||
|
||||
**问题:**
|
||||
- `dragStartPosition` 是闭包变量,在多次快速拖拽时可能在 `onDragEndObservable` 触发前被清空
|
||||
- 如果用户**快速点击-拖动-松开**(<50ms),`dragStartPosition` 可能还未设置就被读取
|
||||
|
||||
**触发条件:** 快速拖拽,或高延迟场景下事件顺序错乱
|
||||
|
||||
**修复方案:**
|
||||
```typescript
|
||||
// 在 onDragStartObservable 中保存到 dragInfo
|
||||
pointerDragBehavior.onDragStartObservable.add(() => {
|
||||
const meshes = this.mainApp.appModel?.modelDic?.Get(modelId);
|
||||
if (meshes && meshes.length > 0) {
|
||||
if (!dragInfo.startPosition) {
|
||||
dragInfo.startPosition = meshes[0].position.clone();
|
||||
}
|
||||
}
|
||||
// ...
|
||||
});
|
||||
|
||||
// 在 onDragEndObservable 中使用持久化的位置
|
||||
if (meshes && meshes.length > 0 && dragInfo.startPosition) {
|
||||
const distance = Vector3.Distance(dragInfo.startPosition, meshes[0].position);
|
||||
hasMoved = distance > 0.01;
|
||||
dragInfo.startPosition = null; // 清除
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Bug #2: **映射更新时机问题 - 可能丢失映射**
|
||||
**位置:** `AppModelDrag.ts:165-174`
|
||||
|
||||
```typescript
|
||||
if (hasMoved) {
|
||||
if (dragInfo.config.snapToZone && hasShownZones) {
|
||||
this.hideZonesForModel(modelId);
|
||||
this.snapModelToZone(modelId); // ← 这里会 return,不更新映射
|
||||
} else {
|
||||
this.updateModelZoneMapping(modelId);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**问题:**
|
||||
- `snapModelToZone()` 内部有多个 `return` 语句(444、452、483行)
|
||||
- 当返回原位置时,会 `return` 并**注释说"不更新映射,保持原映射"**
|
||||
- 但如果 `zoneModelMap` 在此之前被意外清空(如替换模式交换),映射就丢失了
|
||||
|
||||
**触发条件:**
|
||||
1. 拖拽到已占用区域
|
||||
2. 配置为返回原位置
|
||||
3. 但原映射已被其他操作修改
|
||||
|
||||
**修复方案:**
|
||||
```typescript
|
||||
// snapModelToZone 内部返回前,确保映射存在
|
||||
if (originalZoneIndex !== -1) {
|
||||
const originalKey = `${wallName}[${originalZoneIndex}]`;
|
||||
// 强制恢复映射
|
||||
appDropZone['zoneModelMap']?.set(originalKey, modelId);
|
||||
console.log(`[拖拽吸附] 恢复映射: ${originalKey} -> ${modelId}`);
|
||||
return;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Bug #3: **边界检测逻辑缺陷 - Y轴未检查**
|
||||
**位置:** `AppModelDrag.ts:390-423`
|
||||
|
||||
```typescript
|
||||
// 计算墙面的边界
|
||||
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) {
|
||||
// 左右墙面
|
||||
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) {
|
||||
// 前后墙面
|
||||
minX = Math.min(minX, zone.center.x - halfWidth);
|
||||
maxX = Math.max(maxX, zone.center.x + halfWidth);
|
||||
}
|
||||
});
|
||||
|
||||
// ❌ 问题:Y轴完全未检查!
|
||||
const currentPos = rootMesh.position;
|
||||
if (minX !== Number.POSITIVE_INFINITY && maxX !== Number.NEGATIVE_INFINITY) {
|
||||
if (currentPos.x < minX || currentPos.x > maxX) {
|
||||
isOutOfBounds = true;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**问题:**
|
||||
- 只检查 X 和 Z 轴,**Y轴完全不检查**
|
||||
- 如果用户切换到 Y 轴拖拽,配件可以无限上下移动而不会触发"超出边界"
|
||||
- 配件可能飞到天上或地下,但系统认为"在边界内"
|
||||
|
||||
**触发条件:**
|
||||
1. 启用 Y 轴拖拽 (`axis: 'y'` 或 `'xyz'`)
|
||||
2. 向上或向下拖拽
|
||||
3. 边界检测失效
|
||||
|
||||
**修复方案:**
|
||||
```typescript
|
||||
// 添加 Y 轴边界检查
|
||||
let minY = Number.POSITIVE_INFINITY;
|
||||
let maxY = Number.NEGATIVE_INFINITY;
|
||||
|
||||
wallZones.forEach(zone => {
|
||||
const halfHeight = zone.height / 2;
|
||||
minY = Math.min(minY, zone.center.y - halfHeight);
|
||||
maxY = Math.max(maxY, zone.center.y + halfHeight);
|
||||
// ... 原有 X/Z 检查
|
||||
});
|
||||
|
||||
// 检查 Y 轴
|
||||
if (minY !== Number.POSITIVE_INFINITY && maxY !== Number.NEGATIVE_INFINITY) {
|
||||
if (currentPos.y < minY || currentPos.y > maxY) {
|
||||
isOutOfBounds = true;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🟡 中等问题(可能导致不一致)
|
||||
|
||||
### Bug #4: **旋转角度计算不完整**
|
||||
**位置:** `AppModelDrag.ts:439-441, 478-480, 504-506`
|
||||
|
||||
```typescript
|
||||
const targetDirection = originalZone.normal.scale(-1);
|
||||
const angle = Math.atan2(targetDirection.x, targetDirection.z);
|
||||
rootMesh.rotation.y = angle;
|
||||
```
|
||||
|
||||
**问题:**
|
||||
- 只更新 Y 轴旋转,假设配件永远直立
|
||||
- 如果配件初始有 X 或 Z 轴旋转(倾斜),吸附后会**丢失这些旋转**
|
||||
- 对于可旋转的配件(如斜挂的装饰),会出现"吸附后变歪"的问题
|
||||
|
||||
**触发条件:** 配件有非零的 rotation.x 或 rotation.z
|
||||
|
||||
**修复方案:**
|
||||
```typescript
|
||||
// 保存并恢复其他轴的旋转
|
||||
const originalRotationX = rootMesh.rotation.x;
|
||||
const originalRotationZ = rootMesh.rotation.z;
|
||||
|
||||
const targetDirection = originalZone.normal.scale(-1);
|
||||
const angle = Math.atan2(targetDirection.x, targetDirection.z);
|
||||
rootMesh.rotation.y = angle;
|
||||
|
||||
// 恢复其他轴
|
||||
rootMesh.rotation.x = originalRotationX;
|
||||
rootMesh.rotation.z = originalRotationZ;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Bug #5: **替换模式下的映射覆盖冲突**
|
||||
**位置:** `AppModelDrag.ts:612-640 + AppDropZone.ts:229-234`
|
||||
|
||||
**问题:**
|
||||
`AppModelDrag.updateModelZoneMapping()` 和 `AppDropZone.onModelPlaced()` 都会操作 `zoneModelMap`,可能产生竞态:
|
||||
|
||||
**时序问题:**
|
||||
```
|
||||
1. 用户拖拽模型A到模型B所在位置
|
||||
2. AppModelDrag 删除 oldKey[A]
|
||||
3. AppModelDrag 检测到 newKey 已有模型B
|
||||
4. AppModelDrag 设置 swapKey[B] = B (交换)
|
||||
5. AppModelDrag 设置 newKey = A
|
||||
6. ❌ 此时如果 AppDropZone.onModelPlaced() 也触发...
|
||||
7. AppDropZone 检测到 newKey 已有A
|
||||
8. AppDropZone 删除模型A(认为是旧模型!)
|
||||
9. 结果:模型A和B都消失了
|
||||
```
|
||||
|
||||
**触发条件:**
|
||||
- 替换模式 + 快速连续拖拽
|
||||
- 或者某个事件触发了 `onModelPlaced()`
|
||||
|
||||
**修复方案:**
|
||||
```typescript
|
||||
// 在 updateModelZoneMapping 中添加锁
|
||||
private isUpdatingMapping = false;
|
||||
|
||||
private updateModelZoneMapping(modelId: string): void {
|
||||
if (this.isUpdatingMapping) {
|
||||
console.warn(`[映射更新] 正在更新中,跳过 ${modelId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.isUpdatingMapping = true;
|
||||
try {
|
||||
// ... 原有逻辑
|
||||
} finally {
|
||||
this.isUpdatingMapping = false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Bug #6: **最近区域查找可能不准确**
|
||||
**位置:** `AppModelDrag.ts:367-377, 559-568`
|
||||
|
||||
```typescript
|
||||
wallZones.forEach((zone, index) => {
|
||||
const distance = rootMesh.position.subtract(zone.center).length();
|
||||
if (distance < minDistance) {
|
||||
minDistance = distance;
|
||||
closestZoneIndex = index;
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
**问题:**
|
||||
- 使用欧几里得距离(3D空间直线距离)
|
||||
- 但配件应该吸附到**墙面上的投影点**,而不是空间距离
|
||||
- 如果配件被拖到离墙面很远的地方,可能吸附到错误的区域
|
||||
|
||||
**示例:**
|
||||
```
|
||||
墙面:Z=0
|
||||
区域0: center=(0, 1, 0)
|
||||
区域1: center=(5, 1, 0)
|
||||
|
||||
配件位置: (2.5, 10, 0) ← 离墙面很远,但在正中间
|
||||
|
||||
空间距离:
|
||||
- 到区域0: √((2.5-0)² + (10-1)² + 0²) = √87.25 ≈ 9.34
|
||||
- 到区域1: √((2.5-5)² + (10-1)² + 0²) = √87.25 ≈ 9.34
|
||||
|
||||
结果:可能选中任意一个(取决于遍历顺序)
|
||||
```
|
||||
|
||||
**正确做法:** 应该先投影到墙面,再计算2D距离
|
||||
|
||||
**修复方案:**
|
||||
```typescript
|
||||
wallZones.forEach((zone, index) => {
|
||||
// 计算到墙面的投影点
|
||||
const toModel = rootMesh.position.subtract(zone.center);
|
||||
const distanceToPlane = Vector3.Dot(toModel, zone.normal);
|
||||
const projectedPoint = rootMesh.position.subtract(zone.normal.scale(distanceToPlane));
|
||||
|
||||
// 使用投影点到区域中心的距离
|
||||
const distance = projectedPoint.subtract(zone.center).length();
|
||||
|
||||
if (distance < minDistance) {
|
||||
minDistance = distance;
|
||||
closestZoneIndex = index;
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🟢 轻微问题(边缘情况)
|
||||
|
||||
### Bug #7: **闭包变量状态泄漏**
|
||||
**位置:** `AppModelDrag.ts:118-119, 176-178`
|
||||
|
||||
```typescript
|
||||
let dragStartPosition: Vector3 | null = null;
|
||||
let hasShownZones = false;
|
||||
|
||||
// onDragEndObservable 结束时清除
|
||||
dragStartPosition = null;
|
||||
hasShownZones = false;
|
||||
```
|
||||
|
||||
**问题:**
|
||||
- 如果 `onDragEndObservable` 因异常未触发,这些变量永远不会清除
|
||||
- 下次拖拽会使用**上次的脏数据**
|
||||
|
||||
**修复方案:**
|
||||
```typescript
|
||||
// 在 onDragStartObservable 开始时强制重置
|
||||
pointerDragBehavior.onDragStartObservable.add(() => {
|
||||
// 强制清除旧状态(防止异常导致未清除)
|
||||
dragStartPosition = null;
|
||||
hasShownZones = false;
|
||||
|
||||
// 然后记录新状态
|
||||
const meshes = this.mainApp.appModel?.modelDic?.Get(modelId);
|
||||
if (meshes && meshes.length > 0) {
|
||||
dragStartPosition = meshes[0].position.clone();
|
||||
}
|
||||
// ...
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📋 修复优先级建议
|
||||
|
||||
### P0 - 立即修复
|
||||
1. **Bug #1** - 竞态条件(可能导致吸附完全失效)
|
||||
2. **Bug #2** - 映射丢失(配件消失)
|
||||
3. **Bug #5** - 替换冲突(配件重复删除)
|
||||
|
||||
### P1 - 本周修复
|
||||
4. **Bug #3** - Y轴边界检测(安全问题)
|
||||
5. **Bug #6** - 吸附不准确(用户体验)
|
||||
|
||||
### P2 - 下周修复
|
||||
6. **Bug #4** - 旋转丢失(视觉问题)
|
||||
7. **Bug #7** - 状态泄漏(稳定性)
|
||||
|
||||
---
|
||||
|
||||
## 🧪 测试场景(复现间歇性Bug)
|
||||
|
||||
### 场景1:快速连续拖拽
|
||||
```javascript
|
||||
// 模拟用户快速拖拽
|
||||
for (let i = 0; i < 10; i++) {
|
||||
setTimeout(() => {
|
||||
// 快速拖动配件A
|
||||
dragModel('accessory_A', randomPosition());
|
||||
}, i * 100); // 每100ms一次
|
||||
}
|
||||
```
|
||||
|
||||
### 场景2:替换模式压力测试
|
||||
```javascript
|
||||
// 两个配件互相替换
|
||||
setInterval(() => {
|
||||
dragModel('accessory_A', positionB);
|
||||
setTimeout(() => {
|
||||
dragModel('accessory_B', positionA);
|
||||
}, 50);
|
||||
}, 500);
|
||||
```
|
||||
|
||||
### 场景3:边界外拖拽
|
||||
```javascript
|
||||
// 拖到墙外
|
||||
dragModel('accessory_A', { x: 1000, y: 0, z: 0 });
|
||||
// 预期:返回原位置
|
||||
// 实际:可能映射丢失
|
||||
```
|
||||
|
||||
### 场景4:Y轴拖拽
|
||||
```javascript
|
||||
// 启用Y轴拖拽
|
||||
kernel.drag.configure('accessory_A', {
|
||||
enable: true,
|
||||
axis: 'xyz',
|
||||
snapToZone: true
|
||||
});
|
||||
// 向上拖100单位
|
||||
dragModel('accessory_A', { x: 0, y: 100, z: 0 });
|
||||
// 预期:触发边界检测
|
||||
// 实际:边界检测失效
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 💡 总结
|
||||
|
||||
拖拽吸附功能的间歇性问题主要来自:
|
||||
1. **异步状态管理不当**(Bug #1, #7)
|
||||
2. **映射更新时序冲突**(Bug #2, #5)
|
||||
3. **边界检测不完整**(Bug #3)
|
||||
4. **算法不够精确**(Bug #6)
|
||||
|
||||
建议优先修复 Bug #1、#2、#5,这些会导致明显的功能失效。
|
||||
Reference in New Issue
Block a user