feat: Add comprehensive path prediction test with ProgressLogForm
- Add ProgressLogForm.cs for test result logging with ListView - Implement real UI workflow simulation in path prediction test - Test all connected node pairs to all docking targets - Support CSV export for test results - Keep List<string> ConnectedNodes structure (reverted List<MapNode> changes) - Display RFID values in log for better readability 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -559,253 +559,7 @@ namespace AGVNavigationCore.Models
|
||||
|
||||
#endregion
|
||||
|
||||
#region Directional Navigation
|
||||
|
||||
/// <summary>
|
||||
/// 현재 이전/현재 위치와 이동 방향을 기반으로 다음 노드 ID를 반환
|
||||
///
|
||||
/// 사용 예시:
|
||||
/// - 001에서 002로 이동 후, Forward 선택 → 003 반환
|
||||
/// - 003에서 004로 이동 후, Right 선택 → 030 반환
|
||||
/// - 004에서 003으로 Backward 이동 중, GetNextNodeId(Backward) → 002 반환
|
||||
///
|
||||
/// 전제조건: SetPosition이 최소 2번 이상 호출되어 _prevPosition이 설정되어야 함
|
||||
/// </summary>
|
||||
/// <param name="direction">이동 방향 (Forward/Backward/Left/Right)</param>
|
||||
/// <param name="allNodes">맵의 모든 노드</param>
|
||||
/// <returns>다음 노드 ID (또는 null)</returns>
|
||||
public MapNode GetNextNodeId(AgvDirection direction, List<MapNode> allNodes)
|
||||
{
|
||||
// 전제조건 검증: 2개 위치 히스토리 필요
|
||||
if ( _prevNode == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (_currentNode == null || allNodes == null || allNodes.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// 현재 노드에 연결된 노드들 가져오기
|
||||
var connectedNodeIds = _currentNode.ConnectedNodes;
|
||||
if (connectedNodeIds == null || connectedNodeIds.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// 연결된 노드 중 현재 노드가 아닌 것들만 필터링
|
||||
var candidateNodes = allNodes.Where(n =>
|
||||
connectedNodeIds.Contains(n.NodeId) && n.NodeId != _currentNode.NodeId
|
||||
).ToList();
|
||||
|
||||
if (candidateNodes.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// 이전→현재 벡터 계산 (진행 방향 벡터)
|
||||
var movementVector = new PointF(
|
||||
_currentPosition.X - _prevPosition.X,
|
||||
_currentPosition.Y - _prevPosition.Y
|
||||
);
|
||||
|
||||
// 벡터 정규화
|
||||
var movementLength = (float)Math.Sqrt(
|
||||
movementVector.X * movementVector.X +
|
||||
movementVector.Y * movementVector.Y
|
||||
);
|
||||
|
||||
if (movementLength < 0.001f) // 거의 이동하지 않음
|
||||
{
|
||||
return candidateNodes[0]; // 첫 번째 연결 노드 반환
|
||||
}
|
||||
|
||||
var normalizedMovement = new PointF(
|
||||
movementVector.X / movementLength,
|
||||
movementVector.Y / movementLength
|
||||
);
|
||||
|
||||
// 각 후보 노드에 대해 방향 점수 계산
|
||||
var bestCandidate = (node: (MapNode)null, score: 0.0f);
|
||||
|
||||
foreach (var candidate in candidateNodes)
|
||||
{
|
||||
var toNextVector = new PointF(
|
||||
candidate.Position.X - _currentPosition.X,
|
||||
candidate.Position.Y - _currentPosition.Y
|
||||
);
|
||||
|
||||
var toNextLength = (float)Math.Sqrt(
|
||||
toNextVector.X * toNextVector.X +
|
||||
toNextVector.Y * toNextVector.Y
|
||||
);
|
||||
|
||||
if (toNextLength < 0.001f)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var normalizedToNext = new PointF(
|
||||
toNextVector.X / toNextLength,
|
||||
toNextVector.Y / toNextLength
|
||||
);
|
||||
|
||||
// 진행 방향 기반 점수 계산
|
||||
float score = CalculateDirectionalScore(
|
||||
normalizedMovement,
|
||||
normalizedToNext,
|
||||
direction
|
||||
);
|
||||
|
||||
if (score > bestCandidate.score)
|
||||
{
|
||||
bestCandidate = (candidate, score);
|
||||
}
|
||||
}
|
||||
|
||||
return bestCandidate.node;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 이동 방향을 기반으로 방향 점수를 계산
|
||||
/// 높은 점수 = 더 나은 선택지
|
||||
/// </summary>
|
||||
private float CalculateDirectionalScore(
|
||||
PointF movementDirection, // 정규화된 이전→현재 벡터
|
||||
PointF nextDirection, // 정규화된 현재→다음 벡터
|
||||
AgvDirection requestedDir) // 요청된 이동 방향
|
||||
{
|
||||
// 벡터 간 내적 계산 (유사도: -1 ~ 1)
|
||||
float dotProduct = (movementDirection.X * nextDirection.X) +
|
||||
(movementDirection.Y * nextDirection.Y);
|
||||
|
||||
// 벡터 간 외적 계산 (좌우 판별: Z 성분)
|
||||
// 양수 = 좌측, 음수 = 우측
|
||||
float crossProduct = (movementDirection.X * nextDirection.Y) -
|
||||
(movementDirection.Y * nextDirection.X);
|
||||
|
||||
float baseScore = 0;
|
||||
|
||||
switch (requestedDir)
|
||||
{
|
||||
case AgvDirection.Forward:
|
||||
// Forward: 현재 모터 상태에 따라 다름
|
||||
// 1) 현재 Forward 모터 상태라면 → 같은 경로 (계속 전진)
|
||||
// 2) 현재 Backward 모터 상태라면 → 반대 경로 (모터 방향 전환)
|
||||
if (_currentDirection == AgvDirection.Forward)
|
||||
{
|
||||
// 이미 Forward 상태, 계속 Forward → 같은 경로
|
||||
if (dotProduct > 0.9f)
|
||||
baseScore = 100.0f;
|
||||
else if (dotProduct > 0.5f)
|
||||
baseScore = 80.0f;
|
||||
else if (dotProduct > 0.0f)
|
||||
baseScore = 50.0f;
|
||||
else if (dotProduct > -0.5f)
|
||||
baseScore = 20.0f;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Backward 상태에서 Forward로 → 반대 경로
|
||||
if (dotProduct < -0.9f)
|
||||
baseScore = 100.0f;
|
||||
else if (dotProduct < -0.5f)
|
||||
baseScore = 80.0f;
|
||||
else if (dotProduct < 0.0f)
|
||||
baseScore = 50.0f;
|
||||
else if (dotProduct < 0.5f)
|
||||
baseScore = 20.0f;
|
||||
}
|
||||
break;
|
||||
|
||||
case AgvDirection.Backward:
|
||||
// Backward: 현재 모터 상태에 따라 다름
|
||||
// 1) 현재 Backward 모터 상태라면 → 같은 경로 (Forward와 동일)
|
||||
// 2) 현재 Forward 모터 상태라면 → 반대 경로 (현재의 Backward와 반대)
|
||||
if (_currentDirection == AgvDirection.Backward)
|
||||
{
|
||||
// 이미 Backward 상태, 계속 Backward → 같은 경로
|
||||
if (dotProduct > 0.9f)
|
||||
baseScore = 100.0f;
|
||||
else if (dotProduct > 0.5f)
|
||||
baseScore = 80.0f;
|
||||
else if (dotProduct > 0.0f)
|
||||
baseScore = 50.0f;
|
||||
else if (dotProduct > -0.5f)
|
||||
baseScore = 20.0f;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Forward 상태에서 Backward로 → 반대 경로
|
||||
if (dotProduct < -0.9f)
|
||||
baseScore = 100.0f;
|
||||
else if (dotProduct < -0.5f)
|
||||
baseScore = 80.0f;
|
||||
else if (dotProduct < 0.0f)
|
||||
baseScore = 50.0f;
|
||||
else if (dotProduct < 0.5f)
|
||||
baseScore = 20.0f;
|
||||
}
|
||||
break;
|
||||
|
||||
case AgvDirection.Left:
|
||||
// Left: 좌측 방향 선호
|
||||
if (dotProduct > 0.0f) // Forward 상태
|
||||
{
|
||||
if (crossProduct > 0.5f)
|
||||
baseScore = 100.0f;
|
||||
else if (crossProduct > 0.0f)
|
||||
baseScore = 70.0f;
|
||||
else if (crossProduct > -0.5f)
|
||||
baseScore = 50.0f;
|
||||
else
|
||||
baseScore = 30.0f;
|
||||
}
|
||||
else // Backward 상태 - 좌우 반전
|
||||
{
|
||||
if (crossProduct < -0.5f)
|
||||
baseScore = 100.0f;
|
||||
else if (crossProduct < 0.0f)
|
||||
baseScore = 70.0f;
|
||||
else if (crossProduct < 0.5f)
|
||||
baseScore = 50.0f;
|
||||
else
|
||||
baseScore = 30.0f;
|
||||
}
|
||||
break;
|
||||
|
||||
case AgvDirection.Right:
|
||||
// Right: 우측 방향 선호
|
||||
if (dotProduct > 0.0f) // Forward 상태
|
||||
{
|
||||
if (crossProduct < -0.5f)
|
||||
baseScore = 100.0f;
|
||||
else if (crossProduct < 0.0f)
|
||||
baseScore = 70.0f;
|
||||
else if (crossProduct < 0.5f)
|
||||
baseScore = 50.0f;
|
||||
else
|
||||
baseScore = 30.0f;
|
||||
}
|
||||
else // Backward 상태 - 좌우 반전
|
||||
{
|
||||
if (crossProduct > 0.5f)
|
||||
baseScore = 100.0f;
|
||||
else if (crossProduct > 0.0f)
|
||||
baseScore = 70.0f;
|
||||
else if (crossProduct > -0.5f)
|
||||
baseScore = 50.0f;
|
||||
else
|
||||
baseScore = 30.0f;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return baseScore;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
#region Cleanup
|
||||
|
||||
|
||||
Reference in New Issue
Block a user