- 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>
8.2 KiB
8.2 KiB
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
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
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)
🎯 동작 원리
- 이동 벡터: "AGV가 이동한 방향"을 나타냄
- Forward: 같은 방향으로 계속 진행
- Backward: 반대 방향으로 돌아감
Forward 논리:
001→002 이동 벡터 방향으로
→ 002에서 Forward 선택
→ 같은 방향인 003 선택 ✓
Backward 논리:
001→002 이동 벡터 방향으로
→ 002에서 Backward 선택
→ 반대 방향인 001 선택 ✓
🧪 테스트 클래스
파일: GetNextNodeIdTest.cs
실행하면:
- 각 시나리오별 벡터 계산 출력
- 내적/외적 값 표시
- 후보 노드별 점수 계산
- 선택된 노드 및 검증 결과
분석 완료: 2025-10-23 상태: 🟢 로직 정확 검증 완료