using System; using System.Collections.Generic; using System.Drawing; using System.Linq; using AGVNavigationCore.Models; using AGVNavigationCore.PathFinding.Analysis; using AGVNavigationCore.PathFinding.Planning; namespace AGVNavigationCore.Utils { /// /// AGV 방향 계산 헬퍼 유틸리티 /// 현재 위치에서 주어진 모터 방향과 마그넷 방향으로 이동할 때 다음 노드를 계산 /// 이전 이동 방향과 마그넷 방향을 고려하여 더 정확한 경로 예측 /// public static class DirectionalHelper { /// /// AGV방향과 일치하는지 확인한다. 단 원본위치에서 dock 위치가 Don't Care 라면 true가 반환 됩니다. /// /// /// /// public static bool MatchAGVDirection(this DockingDirection dock, AgvDirection agvdirection) { if (dock == DockingDirection.DontCare) return true; if (dock == DockingDirection.Forward && agvdirection == AgvDirection.Forward) return true; if (dock == DockingDirection.Backward && agvdirection == AgvDirection.Backward) return true; return false; } private static JunctionAnalyzer _junctionAnalyzer; /// /// JunctionAnalyzer 초기화 (첫 호출 시) /// private static void InitializeJunctionAnalyzer(List allNodes) { if (_junctionAnalyzer == null && allNodes != null) { _junctionAnalyzer = new JunctionAnalyzer(allNodes); } } /// /// 현재 노드에서 주어진 모터 방향과 마그넷 방향으로 이동할 때 다음 노드를 반환 /// 이전 모터 방향과 마그넷 방향을 고려하여 더 정확한 경로 예측 /// /// 현재 노드 /// 이전 노드 (진행 방향 기준점) /// 이전 구간의 모터 방향 /// 현재 모터 방향 (Forward 또는 Backward) /// 현재 마그넷 방향 (Straight/Left/Right) /// 모든 맵 노드 /// 다음 노드 (또는 null) public static MapNode GetNextNodeByDirection( MapNode currentNode, MapNode prevNode, AgvDirection prevDirection, AgvDirection direction, MagnetDirection magnetDirection, List allNodes) { if (currentNode == null || prevNode == null || allNodes == null) return null; // JunctionAnalyzer 초기화 InitializeJunctionAnalyzer(allNodes); // 현재 노드에 연결된 노드들 중 이전 노드가 아닌 노드들만 필터링 var connectedMapNodes = currentNode.ConnectedMapNodes; if (connectedMapNodes == null || connectedMapNodes.Count == 0) return null; List candidateNodes = new List(); if (prevDirection == direction) { candidateNodes = connectedMapNodes.Where(n => n.Id != prevNode.Id).ToList(); } else { candidateNodes = connectedMapNodes.ToList(); } if (candidateNodes.Count == 0) return null; // 이전→현재 이동 벡터 var movementVector = new PointF( currentNode.Position.X - prevNode.Position.X, currentNode.Position.Y - prevNode.Position.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 ); // 각 후보 노드에 대해 점수 계산 MapNode bestNode = null; float bestScore = float.MinValue; Console.WriteLine( $"\n[GetNextNodeByDirection] ========== 다음 노드 선택 시작 =========="); Console.WriteLine( $" 현재노드: {currentNode.RfidId}[{currentNode.Id}]({currentNode.Position.X:F1}, {currentNode.Position.Y:F1})"); Console.WriteLine( $" 이전노드: {prevNode.RfidId}[{prevNode.Id}]({prevNode.Position.X:F1}, {prevNode.Position.Y:F1})"); Console.WriteLine( $" 이동벡터: ({movementVector.X:F2}, {movementVector.Y:F2}) → 정규화: ({normalizedMovement.X:F3}, {normalizedMovement.Y:F3})"); Console.WriteLine( $" 현재방향: {direction}, 이전방향: {prevDirection}, 마그넷방향: {magnetDirection}"); Console.WriteLine( $" 후보노드 개수: {candidateNodes.Count}"); foreach (var candidate in candidateNodes) { var toNextVector = new PointF( candidate.Position.X - currentNode.Position.X, candidate.Position.Y - currentNode.Position.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 ); // 내적 계산 (유사도: -1 ~ 1) float dotProduct = (normalizedMovement.X * normalizedToNext.X) + (normalizedMovement.Y * normalizedToNext.Y); float score; if (direction == prevDirection) { // Forward: 진행 방향과 유사한 방향 선택 (높은 내적 = 좋음) score = dotProduct; } else // Backward { // Backward: 진행 방향과 반대인 방향 선택 (낮은 내적 = 좋음) score = -dotProduct; } Console.WriteLine( $"\n [후보] {candidate.RfidId}[{candidate.Id}]({candidate.Position.X:F1}, {candidate.Position.Y:F1})"); Console.WriteLine( $" 벡터: ({toNextVector.X:F2}, {toNextVector.Y:F2}), 길이: {toNextLength:F2}"); Console.WriteLine( $" 정규화벡터: ({normalizedToNext.X:F3}, {normalizedToNext.Y:F3})"); Console.WriteLine( $" 내적(dotProduct): {dotProduct:F4}"); Console.WriteLine( $" 기본점수 ({(direction == prevDirection ? "방향유지" : "방향변경")}): {score:F4}"); // 이전 모터 방향이 제공된 경우: 방향 일관성 보너스 추가 var scoreBeforeMotor = score; score = ApplyMotorDirectionConsistencyBonus( score, direction, prevDirection, dotProduct ); Console.WriteLine( $" 모터방향 적용 후: {scoreBeforeMotor:F4} → {score:F4}"); // 마그넷 방향을 고려한 점수 조정 var scoreBeforeMagnet = score; score = ApplyMagnetDirectionBonus( score, magnetDirection, normalizedMovement, normalizedToNext, currentNode, candidate, direction ); Console.WriteLine( $" 마그넷방향 적용 후: {scoreBeforeMagnet:F4} → {score:F4}"); if (score > bestScore) { bestScore = score; bestNode = candidate; Console.WriteLine( $" ⭐ 현재 최고점수 선택됨!"); } } Console.WriteLine( $"\n 최종선택: {bestNode?.RfidId ?? 0}[{bestNode?.Id ?? "null"}] (점수: {bestScore:F4})"); Console.WriteLine( $"[GetNextNodeByDirection] ========== 다음 노드 선택 종료 ==========\n"); return bestNode; } /// /// 모터 방향 일관성을 고려한 점수 보정 /// 같은 방향으로 계속 이동하는 경우 보너스 점수 부여 /// /// 기본 점수 /// 현재 모터 방향 /// 이전 모터 방향 /// 벡터 내적값 /// 조정된 점수 private static float ApplyMotorDirectionConsistencyBonus( float baseScore, AgvDirection currentDirection, AgvDirection prevMotorDirection, float dotProduct) { float adjustedScore = baseScore; // 모터 방향이 변경되지 않은 경우: 일관성 보너스 if (currentDirection == prevMotorDirection) { // Forward 지속: 직진 방향으로의 이동 선호 // Backward 지속: 반대 방향으로의 이동 선호 const float CONSISTENCY_BONUS = 0.2f; adjustedScore += CONSISTENCY_BONUS; System.Diagnostics.Debug.WriteLine( $"[DirectionalHelper] 모터 방향 일관성 보너스: {currentDirection} → {currentDirection} " + $"(점수: {baseScore:F3} → {adjustedScore:F3})"); } else { // 모터 방향이 변경된 경우: 방향 변경 페널티 const float DIRECTION_CHANGE_PENALTY = 0.15f; adjustedScore -= DIRECTION_CHANGE_PENALTY; System.Diagnostics.Debug.WriteLine( $"[DirectionalHelper] 모터 방향 변경 페널티: {prevMotorDirection} → {currentDirection} " + $"(점수: {baseScore:F3} → {adjustedScore:F3})"); } return adjustedScore; } /// /// 마그넷 방향을 고려한 점수 보정 /// Straight/Left/Right 마그넷 방향에 따라 후보 노드를 평가 /// /// 기본 점수 /// 마그넷 방향 (Straight/Left/Right) /// 정규화된 이동 벡터 /// 정규화된 다음 이동 벡터 /// 현재 노드 /// 후보 노드 /// 조정된 점수 private static float ApplyMagnetDirectionBonus( float baseScore, MagnetDirection magnetDirection, PointF normalizedMovement, PointF normalizedToNext, MapNode currentNode, MapNode candidate, AgvDirection direction) { float adjustedScore = baseScore; // Straight: 일직선 방향 (높은 내적 보너스) if (magnetDirection == MagnetDirection.Straight) { const float STRAIGHT_BONUS = 0.5f; adjustedScore += STRAIGHT_BONUS; Console.WriteLine( $" [마그넷 판정] Straight 보너스 +0.5: {baseScore:F4} → {adjustedScore:F4}"); } // Left 또는 Right: 모터 위치에 따른 회전 방향 판단 else if (magnetDirection == MagnetDirection.Left || magnetDirection == MagnetDirection.Right) { // 2D 외적: movement × toNext = movement.X * toNext.Y - movement.Y * toNext.X float crossProduct = (normalizedMovement.X * normalizedToNext.Y) - (normalizedMovement.Y * normalizedToNext.X); bool isLeftMotorMatch = false; bool isRightMotorMatch = false; // ===== 정방향(Forward) 이동 ===== if (direction == AgvDirection.Forward) { // Forward 이동 시 외적 판정: // - 외적 < 0 (음수) = 반시계 회전 = Left 모터 멈춤 // - 외적 > 0 (양수) = 시계 회전 = Right 모터 멈춤 // // 예: 004 → 012 → 016 (Left 모터) // 외적 = -0.9407 (음수) → 반시계 → Left 일치 ✅ isLeftMotorMatch = crossProduct < 0; // 음수 = 반시계 = Left 멈춤 isRightMotorMatch = crossProduct > 0; // 양수 = 시계 = Right 멈춤 } // ===== 역방향(Backward) 이동 ===== else // Backward { // Backward 이동 시 외적 판정: // - 외적 < 0 (음수) = 시계 회전 = Left 모터 멈춤 // - 외적 > 0 (양수) = 반시계 회전 = Right 모터 멈춤 // // 예: 012 → 004 → 003 (Left 모터) // 외적 = 0.9334 (양수) → 반시계(역방향 기준 시계) → Left 일치 ✅ isLeftMotorMatch = crossProduct > 0; // 양수 = 시계(역) = Left 멈춤 isRightMotorMatch = crossProduct < 0; // 음수 = 반시계(역) = Right 멈춤 } Console.WriteLine( $" [마그넷 판정] 외적(Cross): {crossProduct:F4}, Left모터일치: {isLeftMotorMatch}, Right모터일치: {isRightMotorMatch} [{direction}]"); // 외적의 절대값으로 회전 강도 판단 (0에 가까우면 약함, 1에 가까우면 강함) float rotationStrength = Math.Abs(crossProduct); if ((magnetDirection == MagnetDirection.Left && isLeftMotorMatch) || (magnetDirection == MagnetDirection.Right && isRightMotorMatch)) { // 올바른 모터 방향: 회전 강도에 비례한 보너스 // 강한 회전(|외적| ≈ 1): +2.0 // 약한 회전(|외적| ≈ 0.2): +0.4 float magnetBonus = rotationStrength * 2.0f; adjustedScore += magnetBonus; Console.WriteLine( $" [마그넷 판정] ✅ {magnetDirection} 모터 일치 (회전강도: {rotationStrength:F4}, 보너스 +{magnetBonus:F4}): {baseScore:F4} → {adjustedScore:F4}"); } else { // 잘못된 모터 방향: 회전 강도에 비례한 페널티 // 강한 회전(|외적| ≈ 1): -2.0 // 약한 회전(|외적| ≈ 0.2): -0.4 float magnetPenalty = rotationStrength * 2.0f; adjustedScore -= magnetPenalty; string actualMotor = crossProduct > 0 ? "Left" : "Right"; Console.WriteLine( $" [마그넷 판정] ❌ {magnetDirection} 모터 불일치 (실제: {actualMotor}, 회전강도: {rotationStrength:F4}, 페널티 -{magnetPenalty:F4}): {baseScore:F4} → {adjustedScore:F4}"); } } return adjustedScore; } /// /// 모터 방향을 고려한 다음 노드 선택 (디버깅/분석용) /// public static (MapNode node, float score, string reason) GetNextNodeByDirectionWithDetails( MapNode currentNode, MapNode prevNode, AgvDirection direction, List allNodes, AgvDirection? prevMotorDirection) { if (currentNode == null || prevNode == null || allNodes == null) return (null, 0, "입력 파라미터가 null입니다"); var connectedMapNodes = currentNode.ConnectedMapNodes; if (connectedMapNodes == null || connectedMapNodes.Count == 0) return (null, 0, "연결된 노드가 없습니다"); var candidateNodes = connectedMapNodes.ToList(); if (candidateNodes.Count == 0) return (null, 0, "후보 노드가 없습니다"); var movementVector = new PointF( currentNode.Position.X - prevNode.Position.X, currentNode.Position.Y - prevNode.Position.Y ); var movementLength = (float)Math.Sqrt( movementVector.X * movementVector.X + movementVector.Y * movementVector.Y ); if (movementLength < 0.001f) return (candidateNodes[0], 1.0f, "움직임이 거의 없음"); var normalizedMovement = new PointF( movementVector.X / movementLength, movementVector.Y / movementLength ); MapNode bestNode = null; float bestScore = float.MinValue; string reason = ""; foreach (var candidate in candidateNodes) { var toNextVector = new PointF( candidate.Position.X - currentNode.Position.X, candidate.Position.Y - currentNode.Position.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 dotProduct = (normalizedMovement.X * normalizedToNext.X) + (normalizedMovement.Y * normalizedToNext.Y); float score = (direction == AgvDirection.Forward) ? dotProduct : -dotProduct; if (prevMotorDirection.HasValue) { score = ApplyMotorDirectionConsistencyBonus( score, direction, prevMotorDirection.Value, dotProduct ); } if (score > bestScore) { bestScore = score; bestNode = candidate; // 선택 이유 생성 if (prevMotorDirection.HasValue && direction == prevMotorDirection) { reason = $"모터 방향 일관성 유지 ({direction}) → {candidate.Id}"; } else if (prevMotorDirection.HasValue) { reason = $"모터 방향 변경 ({prevMotorDirection} → {direction}) → {candidate.Id}"; } else { reason = $"방향 기반 선택 ({direction}) → {candidate.Id}"; } } } return (bestNode, bestScore, reason); } } }