fix: Add motor direction parameter to magnet direction calculation in pathfinding

- Fixed critical issue in ConvertToDetailedPath where motor direction was not passed to GetRequiredMagnetDirection
- Motor direction is essential for backward movement as Left/Right directions must be inverted
- Modified AGVPathfinder.cs line 280 to pass currentDirection parameter
- Ensures backward motor direction properly inverts magnet sensor directions

feat: Add waypoint support to pathfinding system

- Added FindPath overload with params string[] waypointNodeIds in AStarPathfinder
- Supports sequential traversal through multiple intermediate nodes
- Validates waypoints and prevents duplicates in sequence
- Returns combined path result with aggregated metrics

feat: Implement path result merging with DetailedPath preservation

- Added CombineResults method in AStarPathfinder for intelligent path merging
- Automatically deduplicates nodes when last of previous path equals first of current
- Preserves DetailedPath information including motor and magnet directions
- Essential for multi-segment path operations

feat: Integrate magnet direction with motor direction awareness

- Modified JunctionAnalyzer.GetRequiredMagnetDirection to accept AgvDirection parameter
- Inverts Left/Right magnet directions when moving Backward
- Properly handles motor direction context throughout pathfinding

feat: Add automatic start node selection in simulator

- Added SetStartNodeToCombo method to SimulatorForm
- Automatically selects start node combo box when AGV position is set via RFID
- Improves UI usability and workflow efficiency

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
backuppc
2025-10-24 15:46:16 +09:00
parent 3ddecf63ed
commit d932b8d332
47 changed files with 7473 additions and 1088 deletions

View File

@@ -0,0 +1,367 @@
# GetNextNodeId() 로직 분석 및 검증
## 🎯 검증 대상
사용자 요구사항:
```
001 (65, 229) → 002 (206, 244) → Forward → 003 ✓
001 (65, 229) → 002 (206, 244) → Backward → 001 ✓
002 (206, 244) → 003 (278, 278) → Forward → 004 ✓
002 (206, 244) → 003 (278, 278) → Backward → 002 ✓
```
---
## 📐 벡터 계산 논리
### 기본 개념
```
이전 위치: prevPos = (x1, y1)
현재 위치: currentPos = (x2, y2)
이동 벡터: v_movement = (x2-x1, y2-y1)
→ 이 벡터의 방향이 "AGV가 이동한 방향"
다음 노드 위치: nextPos = (x3, y3)
다음 벡터: v_next = (x3-x2, y3-y2)
→ 이 벡터의 방향이 "다음 노드로 가는 방향"
```
### 내적 (Dot Product)
```
dot = v_movement · v_next = v_m.x * v_n.x + v_m.y * v_n.y
의미:
dot ≈ 1 : 거의 같은 방향 (0°) → Forward에 적합
dot ≈ 0 : 직각 (90°) → Left/Right
dot ≈ -1 : 거의 반대 방향 (180°) → Backward에 적합
```
### 외적 (Cross Product)
```
cross = v_movement × v_next (Z 성분) = v_m.x * v_n.y - v_m.y * v_n.x
의미:
cross > 0 : 반시계 방향 (좌측)
cross < 0 : 시계 방향 (우측)
```
---
## 🧪 실제 시나리오 계산
### 시나리오 1: 001 → 002 → Forward → ?
#### 초기 조건
```
001: (65, 229)
002: (206, 244)
003: (278, 278)
이동 벡터: v_m = (206-65, 244-229) = (141, 15)
정규화: n_m = (141/142.79, 15/142.79) ≈ (0.987, 0.105)
```
#### 002의 ConnectedNodes: [N001, N003]
**후보 1: N001**
```
다음 벡터: v_n = (65-206, 229-244) = (-141, -15)
정규화: n_n = (-141/142.79, -15/142.79) ≈ (-0.987, -0.105)
내적: dot = 0.987*(-0.987) + 0.105*(-0.105)
= -0.974 - 0.011
≈ -0.985 (매우 반대 방향)
외적: cross = 0.987*(-0.105) - 0.105*(-0.987)
= -0.104 + 0.104
≈ 0
Forward 모드에서:
dotProduct < -0.5 → baseScore = 20.0 (낮은 점수)
```
**후보 2: N003**
```
다음 벡터: v_n = (278-206, 278-244) = (72, 34)
정규화: n_n = (72/79.88, 34/79.88) ≈ (0.901, 0.426)
내적: dot = 0.987*0.901 + 0.105*0.426
= 0.889 + 0.045
≈ 0.934 (거의 같은 방향)
외적: cross = 0.987*0.426 - 0.105*0.901
= 0.421 - 0.095
≈ 0.326
Forward 모드에서:
dotProduct > 0.9 → baseScore = 100.0 ✓ (최고 점수!)
```
#### 결과: N003 선택 ✅
---
### 시나리오 2: 001 → 002 → Backward → ?
#### 초기 조건
```
001: (65, 229)
002: (206, 244)
이동 벡터: v_m = (141, 15) (같음)
정규화: n_m = (0.987, 0.105) (같음)
```
#### 002의 ConnectedNodes: [N001, N003]
**후보 1: N001**
```
다음 벡터: v_n = (-141, -15) (같음)
정규화: n_n = (-0.987, -0.105) (같음)
내적: dot ≈ -0.985 (매우 반대 방향)
Backward 모드에서:
dotProduct < -0.9 → baseScore = 100.0 ✓ (최고 점수!)
```
**후보 2: N003**
```
다음 벡터: v_n = (72, 34) (같음)
정규화: n_n = (0.901, 0.426) (같음)
내적: dot ≈ 0.934 (거의 같은 방향)
Backward 모드에서:
dotProduct > 0.5 → baseScore = 0 (점수 없음)
```
#### 결과: N001 선택 ✅
---
### 시나리오 3: 002 → 003 → Forward → ?
#### 초기 조건
```
002: (206, 244)
003: (278, 278)
004: (380, 340)
이동 벡터: v_m = (278-206, 278-244) = (72, 34)
정규화: n_m ≈ (0.901, 0.426)
```
#### 003의 ConnectedNodes: [N002, N004]
**후보 1: N002**
```
다음 벡터: v_n = (206-278, 244-278) = (-72, -34)
정규화: n_n ≈ (-0.901, -0.426)
내적: dot ≈ -0.934 (거의 반대)
Forward 모드에서:
dotProduct < 0 → baseScore ≤ 50.0
```
**후보 2: N004**
```
다음 벡터: v_n = (380-278, 340-278) = (102, 62)
정규화: n_n = (102/119.54, 62/119.54) ≈ (0.853, 0.519)
내적: dot = 0.901*0.853 + 0.426*0.519
= 0.768 + 0.221
≈ 0.989 (거의 같은 방향)
Forward 모드에서:
dotProduct > 0.9 → baseScore = 100.0 ✓
```
#### 결과: N004 선택 ✅
---
### 시나리오 4: 002 → 003 → Backward → ?
#### 초기 조건
```
002: (206, 244)
003: (278, 278)
이동 벡터: v_m = (72, 34)
정규화: n_m = (0.901, 0.426)
```
#### 003의 ConnectedNodes: [N002, N004]
**후보 1: N002**
```
다음 벡터: v_n = (-72, -34)
정규화: n_n = (-0.901, -0.426)
내적: dot ≈ -0.934 (거의 반대)
Backward 모드에서:
dotProduct < -0.9 → baseScore = 100.0 ✓
```
**후보 2: N004**
```
다음 벡터: v_n = (102, 62)
정규화: n_n = (0.853, 0.519)
내적: dot ≈ 0.989 (거의 같은)
Backward 모드에서:
dotProduct > 0 → baseScore = 0 (점수 없음)
```
#### 결과: N002 선택 ✅
---
## ✅ 검증 결과
| 시나리오 | 이전→현재 | 방향 | 예상 | 계산 결과 | 검증 |
|---------|----------|------|------|----------|------|
| 1 | 001→002 | Forward | 003 | 003 (100.0) | ✅ |
| 2 | 001→002 | Backward | 001 | 001 (100.0) | ✅ |
| 3 | 002→003 | Forward | 004 | 004 (100.0) | ✅ |
| 4 | 002→003 | Backward | 002 | 002 (100.0) | ✅ |
---
## 🔍 핵심 로직 검토
### VirtualAGV.GetNextNodeId() - 라인 628-719
```csharp
public string GetNextNodeId(AgvDirection direction, List<MapNode> allNodes)
{
// 1⃣ 히스토리 검증
if (_prevPosition == Point.Empty || _currentPosition == Point.Empty)
return null; // ← 2개 위치 필수
// 2⃣ 연결된 노드 필터링
var candidateNodes = allNodes.Where(n =>
_currentNode.ConnectedNodes.Contains(n.NodeId)
).ToList();
// 3⃣ 이동 벡터 계산
var movementVector = new PointF(
_currentPosition.X - _prevPosition.X,
_currentPosition.Y - _prevPosition.Y
);
// 4⃣ 정규화
var normalizedMovement = new PointF(
movementVector.X / movementLength,
movementVector.Y / movementLength
);
// 5⃣ 각 후보에 대해 점수 계산
foreach (var candidate in candidateNodes)
{
float score = CalculateDirectionalScore(
normalizedMovement,
normalizedToNext,
direction // ← Forward/Backward/Left/Right
);
if (score > bestCandidate.score)
bestCandidate = (candidate, score);
}
return bestCandidate.node?.NodeId;
}
```
### CalculateDirectionalScore() - 라인 721-821
```csharp
private float CalculateDirectionalScore(
PointF movementDirection, // 정규화된 이동 벡터
PointF nextDirection, // 정규화된 다음 벡터
AgvDirection requestedDir) // 요청된 방향
{
// 내적: 유사도 계산
float dotProduct = (movementDirection.X * nextDirection.X) +
(movementDirection.Y * nextDirection.Y);
// 외적: 좌우 판별
float crossProduct = (movementDirection.X * nextDirection.Y) -
(movementDirection.Y * nextDirection.X);
// 방향에 따라 점수 계산
switch (requestedDir)
{
case AgvDirection.Forward:
// Forward: dotProduct > 0.9 → 100점 ✓
break;
case AgvDirection.Backward:
// Backward: dotProduct < -0.9 → 100점 ✓
break;
case AgvDirection.Left:
// Left: crossProduct 양수 선호 ✓
break;
case AgvDirection.Right:
// Right: crossProduct 음수 선호 ✓
break;
}
return baseScore;
}
```
---
## 📊 최종 결론
### ✅ 로직이 정확함
모든 시나리오에서:
- **Forward 이동**: 이동 벡터와 방향이 거의 같은 노드 선택 (dotProduct > 0.9)
- **Backward 이동**: 이동 벡터와 반대 방향인 노드 선택 (dotProduct < -0.9)
### 🎯 동작 원리
1. **이동 벡터**: "AGV가 이동한 방향"을 나타냄
2. **Forward**: 같은 방향으로 계속 진행
3. **Backward**: 반대 방향으로 돌아감
```
Forward 논리:
001→002 이동 벡터 방향으로
→ 002에서 Forward 선택
→ 같은 방향인 003 선택 ✓
Backward 논리:
001→002 이동 벡터 방향으로
→ 002에서 Backward 선택
→ 반대 방향인 001 선택 ✓
```
---
## 🧪 테스트 클래스
**파일**: `GetNextNodeIdTest.cs`
실행하면:
1. 각 시나리오별 벡터 계산 출력
2. 내적/외적 값 표시
3. 후보 노드별 점수 계산
4. 선택된 노드 및 검증 결과
---
**분석 완료**: 2025-10-23
**상태**: 🟢 로직 정확 검증 완료