diff --git a/Cs_HMI/AGVMapEditor/Models/NodePropertyWrapper.cs b/Cs_HMI/AGVMapEditor/Models/NodePropertyWrapper.cs index ab48a7a..0191e0a 100644 --- a/Cs_HMI/AGVMapEditor/Models/NodePropertyWrapper.cs +++ b/Cs_HMI/AGVMapEditor/Models/NodePropertyWrapper.cs @@ -405,6 +405,19 @@ namespace AGVMapEditor.Models } } + [Category("기본 정보")] + [DisplayName("도킹 방향")] + [Description("도킹이 필요한 노드의 경우 AGV 진입 방향 (DontCare: 방향 무관, Forward: 전진 도킹, Backward: 후진 도킹)")] + public DockingDirection DockDirection + { + get => _node.DockDirection; + set + { + _node.DockDirection = value; + _node.ModifiedDate = DateTime.Now; + } + } + diff --git a/Cs_HMI/AGVNavigationCore/AGVNavigationCore.csproj b/Cs_HMI/AGVNavigationCore/AGVNavigationCore.csproj index 57102aa..bfea9ae 100644 --- a/Cs_HMI/AGVNavigationCore/AGVNavigationCore.csproj +++ b/Cs_HMI/AGVNavigationCore/AGVNavigationCore.csproj @@ -65,6 +65,11 @@ + UnifiedAGVCanvas.cs + UserControl + + + UnifiedAGVCanvas.cs UserControl @@ -73,6 +78,7 @@ + @@ -84,10 +90,6 @@ UnifiedAGVCanvas.cs - - UnifiedAGVCanvas.cs - UserControl - diff --git a/Cs_HMI/AGVNavigationCore/Controls/IAGV.cs b/Cs_HMI/AGVNavigationCore/Controls/IAGV.cs index 8c2a20e..8bfba21 100644 --- a/Cs_HMI/AGVNavigationCore/Controls/IAGV.cs +++ b/Cs_HMI/AGVNavigationCore/Controls/IAGV.cs @@ -11,16 +11,17 @@ namespace AGVNavigationCore.Controls public interface IAGV { string AgvId { get; } - Point CurrentPosition { get; } - AgvDirection CurrentDirection { get; } - AGVState CurrentState { get; } + Point CurrentPosition { get; set; } + AgvDirection CurrentDirection { get; set; } + AGVState CurrentState { get; set; } float BatteryLevel { get; } - + // 이동 경로 정보 추가 Point? TargetPosition { get; } string CurrentNodeId { get; } string TargetNodeId { get; } DockingDirection DockingDirection { get; } + } diff --git a/Cs_HMI/AGVNavigationCore/Controls/UnifiedAGVCanvas.cs b/Cs_HMI/AGVNavigationCore/Controls/UnifiedAGVCanvas.cs index 78c3f8e..136fefd 100644 --- a/Cs_HMI/AGVNavigationCore/Controls/UnifiedAGVCanvas.cs +++ b/Cs_HMI/AGVNavigationCore/Controls/UnifiedAGVCanvas.cs @@ -35,7 +35,6 @@ namespace AGVNavigationCore.Controls /// public enum CanvasMode { - ViewOnly, // 읽기 전용 (시뮬레이터, 모니터링) Edit // 편집 가능 (맵 에디터) } @@ -52,7 +51,6 @@ namespace AGVNavigationCore.Controls DeleteConnection, // 연결 삭제 모드 AddLabel, // 라벨 추가 모드 AddImage, // 이미지 추가 모드 - SelectTarget // 목적지 선택 모드 (시뮬레이터 전용) } #endregion @@ -60,7 +58,7 @@ namespace AGVNavigationCore.Controls #region Fields // 캔버스 모드 - private CanvasMode _canvasMode = CanvasMode.ViewOnly; + private CanvasMode _canvasMode = CanvasMode.Edit; private EditMode _editMode = EditMode.Select; // 맵 데이터 @@ -121,7 +119,7 @@ namespace AGVNavigationCore.Controls private Brush _gridBrush; private Brush _agvBrush; private Brush _pathBrush; - + private Pen _connectionPen; private Pen _gridPen; private Pen _tempConnectionPen; @@ -150,9 +148,6 @@ namespace AGVNavigationCore.Controls public event EventHandler AGVSelected; public event EventHandler AGVStateChanged; - // 시뮬레이터 이벤트 - public event EventHandler TargetNodeSelected; - #endregion #region Properties @@ -180,7 +175,7 @@ namespace AGVNavigationCore.Controls set { if (_canvasMode != CanvasMode.Edit) return; - + _editMode = value; if (_editMode != EditMode.Connect) { @@ -365,8 +360,8 @@ namespace AGVNavigationCore.Controls private void InitializeCanvas() { - SetStyle(ControlStyles.AllPaintingInWmPaint | - ControlStyles.UserPaint | + SetStyle(ControlStyles.AllPaintingInWmPaint | + ControlStyles.UserPaint | ControlStyles.DoubleBuffer | ControlStyles.ResizeRedraw, true); @@ -392,7 +387,7 @@ namespace AGVNavigationCore.Controls _selectedNodeBrush = new SolidBrush(Color.Red); _hoveredNodeBrush = new SolidBrush(Color.LightCyan); _destinationNodeBrush = new SolidBrush(Color.Gold); - + // AGV 및 경로 브러쉬 _agvBrush = new SolidBrush(Color.Red); _pathBrush = new SolidBrush(Color.Purple); @@ -403,7 +398,7 @@ namespace AGVNavigationCore.Controls // 펜 _connectionPen = new Pen(Color.DarkBlue, CONNECTION_WIDTH); _connectionPen.EndCap = LineCap.ArrowAnchor; - + _gridPen = new Pen(Color.LightGray, 1); _tempConnectionPen = new Pen(Color.Orange, 2) { DashStyle = DashStyle.Dash }; _selectedNodePen = new Pen(Color.Red, 3); @@ -422,16 +417,8 @@ namespace AGVNavigationCore.Controls private void UpdateModeUI() { // 모드에 따른 UI 업데이트 - if (_canvasMode == CanvasMode.ViewOnly) - { - Cursor = Cursors.Default; - _contextMenu.Enabled = false; - } - else - { - _contextMenu.Enabled = true; - Cursor = GetCursorForMode(_editMode); - } + _contextMenu.Enabled = true; + Cursor = GetCursorForMode(_editMode); } #endregion @@ -447,7 +434,7 @@ namespace AGVNavigationCore.Controls _agvPositions[agvId] = position; else _agvPositions.Add(agvId, position); - + Invalidate(); } @@ -460,7 +447,7 @@ namespace AGVNavigationCore.Controls _agvDirections[agvId] = direction; else _agvDirections.Add(agvId, direction); - + Invalidate(); } @@ -473,7 +460,7 @@ namespace AGVNavigationCore.Controls _agvStates[agvId] = state; else _agvStates.Add(agvId, state); - + Invalidate(); } @@ -537,12 +524,12 @@ namespace AGVNavigationCore.Controls private void UpdateDestinationNode() { _destinationNode = null; - + if (_currentPath != null && _currentPath.Success && _currentPath.Path != null && _currentPath.Path.Count > 0) { // 경로의 마지막 노드가 목적지 string destinationNodeId = _currentPath.Path[_currentPath.Path.Count - 1]; - + // 노드 목록에서 해당 노드 찾기 _destinationNode = _nodes?.FirstOrDefault(n => n.NodeId == destinationNodeId); } diff --git a/Cs_HMI/AGVNavigationCore/Models/Enums.cs b/Cs_HMI/AGVNavigationCore/Models/Enums.cs index fb37ed2..15c9ecd 100644 --- a/Cs_HMI/AGVNavigationCore/Models/Enums.cs +++ b/Cs_HMI/AGVNavigationCore/Models/Enums.cs @@ -28,6 +28,8 @@ namespace AGVNavigationCore.Models /// public enum DockingDirection { + /// 도킹 방향 상관없음 (일반 경로 노드) + DontCare, /// 전진 도킹 (충전기) Forward, /// 후진 도킹 (로더, 클리너, 오프로더, 버퍼) diff --git a/Cs_HMI/AGVNavigationCore/Models/MapLoader.cs b/Cs_HMI/AGVNavigationCore/Models/MapLoader.cs index 5b89d49..edb1fed 100644 --- a/Cs_HMI/AGVNavigationCore/Models/MapLoader.cs +++ b/Cs_HMI/AGVNavigationCore/Models/MapLoader.cs @@ -52,7 +52,16 @@ namespace AGVNavigationCore.Models } var json = File.ReadAllText(filePath); - var mapData = JsonConvert.DeserializeObject(json); + + // JSON 역직렬화 설정: 누락된 속성 무시, 안전한 처리 + var settings = new JsonSerializerSettings + { + MissingMemberHandling = MissingMemberHandling.Ignore, + NullValueHandling = NullValueHandling.Ignore, + DefaultValueHandling = DefaultValueHandling.Populate + }; + + var mapData = JsonConvert.DeserializeObject(json, settings); if (mapData != null) { @@ -63,6 +72,9 @@ namespace AGVNavigationCore.Models // 기존 Description 데이터를 Name으로 마이그레이션 MigrateDescriptionToName(result.Nodes); + // DockingDirection 마이그레이션 (기존 NodeType 기반으로 설정) + MigrateDockingDirection(result.Nodes); + // 중복된 NodeId 정리 FixDuplicateNodeIds(result.Nodes); @@ -143,6 +155,36 @@ namespace AGVNavigationCore.Models // 기존 파일들은 다시 저장될 때 Description 없이 저장됨 } + /// + /// 기존 맵 파일의 DockingDirection을 NodeType 기반으로 마이그레이션 + /// + /// 맵 노드 목록 + private static void MigrateDockingDirection(List mapNodes) + { + if (mapNodes == null || mapNodes.Count == 0) return; + + foreach (var node in mapNodes) + { + // 기존 파일에서 DockingDirection이 기본값(DontCare)인 경우에만 마이그레이션 + if (node.DockDirection == DockingDirection.DontCare) + { + switch (node.Type) + { + case NodeType.Charging: + node.DockDirection = DockingDirection.Forward; + break; + case NodeType.Docking: + node.DockDirection = DockingDirection.Backward; + break; + default: + // Normal, Rotation, Label, Image는 DontCare 유지 + node.DockDirection = DockingDirection.DontCare; + break; + } + } + } + } + /// /// 중복된 NodeId를 가진 노드들을 고유한 NodeId로 수정 /// diff --git a/Cs_HMI/AGVNavigationCore/Models/MapNode.cs b/Cs_HMI/AGVNavigationCore/Models/MapNode.cs index a4ed4be..497b29c 100644 --- a/Cs_HMI/AGVNavigationCore/Models/MapNode.cs +++ b/Cs_HMI/AGVNavigationCore/Models/MapNode.cs @@ -34,9 +34,9 @@ namespace AGVNavigationCore.Models public NodeType Type { get; set; } = NodeType.Normal; /// - /// 도킹 방향 (도킹/충전 노드인 경우만 사용) + /// 도킹 방향 (도킹/충전 노드인 경우에만 Forward/Backward, 일반 노드는 DontCare) /// - public DockingDirection? DockDirection { get; set; } = null; + public DockingDirection DockDirection { get; set; } = DockingDirection.DontCare; /// /// 연결된 노드 ID 목록 (경로 정보) diff --git a/Cs_HMI/AGVNavigationCore/PathFinding/Planning/AGVPathfinder.cs b/Cs_HMI/AGVNavigationCore/PathFinding/Planning/AGVPathfinder.cs index 0636805..0b13b02 100644 --- a/Cs_HMI/AGVNavigationCore/PathFinding/Planning/AGVPathfinder.cs +++ b/Cs_HMI/AGVNavigationCore/PathFinding/Planning/AGVPathfinder.cs @@ -32,29 +32,23 @@ namespace AGVNavigationCore.PathFinding.Planning /// /// AGV 경로 계산 /// - public AGVPathResult FindPath(string startNodeId, string targetNodeId, AgvDirection currentDirection = AgvDirection.Forward) + public AGVPathResult FindPath(MapNode startNode, MapNode targetNode, AgvDirection currentDirection = AgvDirection.Forward) { var stopwatch = System.Diagnostics.Stopwatch.StartNew(); try { - // 1. 목적지 도킹 방향 요구사항 확인 - var requiredDirection = _directionChangePlanner.GetRequiredDockingDirection(targetNodeId); + // 입력 검증 + if (startNode == null) + return AGVPathResult.CreateFailure("시작 노드가 null입니다.", 0, 0); + if (targetNode == null) + return AGVPathResult.CreateFailure("목적지 노드가 null입니다.", 0, 0); - // 2. 방향 전환이 필요한지 확인 - bool needDirectionChange = (currentDirection != requiredDirection); + // 1. 목적지 도킹 방향 요구사항 확인 (노드의 도킹방향 속성에서 확인) + var requiredDirection = GetRequiredDockingDirection(targetNode.DockDirection); - AGVPathResult result; - if (needDirectionChange) - { - // 방향 전환이 필요한 경우 - result = PlanPathWithDirectionChange(startNodeId, targetNodeId, currentDirection, requiredDirection); - } - else - { - // 직접 경로 계산 - result = PlanDirectPath(startNodeId, targetNodeId, currentDirection); - } + // 통합된 경로 계획 함수 사용 + AGVPathResult result = PlanPath(startNode, targetNode, currentDirection, requiredDirection); result.CalculationTimeMs = stopwatch.ElapsedMilliseconds; @@ -73,58 +67,76 @@ namespace AGVNavigationCore.PathFinding.Planning } /// - /// 직접 경로 계획 + /// 노드 도킹 방향에 따른 필요한 AGV 방향 반환 /// - private AGVPathResult PlanDirectPath(string startNodeId, string targetNodeId, AgvDirection currentDirection) + private AgvDirection? GetRequiredDockingDirection(DockingDirection dockDirection) { - var basicResult = _basicPathfinder.FindPath(startNodeId, targetNodeId); - - if (!basicResult.Success) + switch (dockDirection) { - return AGVPathResult.CreateFailure(basicResult.ErrorMessage, basicResult.CalculationTimeMs, basicResult.ExploredNodeCount); + case DockingDirection.Forward: + return AgvDirection.Forward; // 전진 도킹 + case DockingDirection.Backward: + return AgvDirection.Backward; // 후진 도킹 + case DockingDirection.DontCare: + default: + return null; // 도킹 방향 상관없음 } - - // 기본 경로를 상세 경로로 변환 - var detailedPath = ConvertToDetailedPath(basicResult.Path, currentDirection); - - return AGVPathResult.CreateSuccess( - detailedPath, - basicResult.TotalDistance, - basicResult.CalculationTimeMs, - basicResult.ExploredNodeCount, - "직접 경로 - 방향 전환 불필요" - ); } /// - /// 방향 전환을 포함한 경로 계획 + /// 통합 경로 계획 (직접 경로 또는 방향 전환 경로) /// - private AGVPathResult PlanPathWithDirectionChange(string startNodeId, string targetNodeId, AgvDirection currentDirection, AgvDirection requiredDirection) + private AGVPathResult PlanPath(MapNode startNode, MapNode targetNode, AgvDirection currentDirection, AgvDirection? requiredDirection = null) { - var directionChangePlan = _directionChangePlanner.PlanDirectionChange(startNodeId, targetNodeId, currentDirection, requiredDirection); + bool needDirectionChange = requiredDirection.HasValue && (currentDirection != requiredDirection.Value); - if (!directionChangePlan.Success) + if (needDirectionChange) { - return AGVPathResult.CreateFailure(directionChangePlan.ErrorMessage, 0, 0); + // 방향 전환 경로 계획 + var directionChangePlan = _directionChangePlanner.PlanDirectionChange( + startNode.NodeId, targetNode.NodeId, currentDirection, requiredDirection.Value); + + if (!directionChangePlan.Success) + { + return AGVPathResult.CreateFailure(directionChangePlan.ErrorMessage, 0, 0); + } + + var detailedPath = ConvertDirectionChangePath(directionChangePlan, currentDirection, requiredDirection.Value); + float totalDistance = CalculatePathDistance(detailedPath); + + return AGVPathResult.CreateSuccess( + detailedPath, + totalDistance, + 0, + 0, + directionChangePlan.PlanDescription, + true, + directionChangePlan.DirectionChangeNode + ); } + else + { + // 직접 경로 계획 + var basicResult = _basicPathfinder.FindPath(startNode.NodeId, targetNode.NodeId); - // 방향 전환 경로를 상세 경로로 변환 - var detailedPath = ConvertDirectionChangePath(directionChangePlan, currentDirection, requiredDirection); + if (!basicResult.Success) + { + return AGVPathResult.CreateFailure(basicResult.ErrorMessage, basicResult.CalculationTimeMs, basicResult.ExploredNodeCount); + } - // 거리 계산 - float totalDistance = CalculatePathDistance(detailedPath); + var detailedPath = ConvertToDetailedPath(basicResult.Path, currentDirection); - return AGVPathResult.CreateSuccess( - detailedPath, - totalDistance, - 0, - 0, - directionChangePlan.PlanDescription, - true, - directionChangePlan.DirectionChangeNode - ); + return AGVPathResult.CreateSuccess( + detailedPath, + basicResult.TotalDistance, + basicResult.CalculationTimeMs, + basicResult.ExploredNodeCount, + "직접 경로 - 방향 전환 불필요" + ); + } } + /// /// 기본 경로를 상세 경로로 변환 /// diff --git a/Cs_HMI/AGVNavigationCore/PathFinding/Planning/DirectionChangePlanner.cs b/Cs_HMI/AGVNavigationCore/PathFinding/Planning/DirectionChangePlanner.cs index 2135abc..33a2940 100644 --- a/Cs_HMI/AGVNavigationCore/PathFinding/Planning/DirectionChangePlanner.cs +++ b/Cs_HMI/AGVNavigationCore/PathFinding/Planning/DirectionChangePlanner.cs @@ -4,6 +4,7 @@ using System.Linq; using AGVNavigationCore.Models; using AGVNavigationCore.PathFinding.Core; using AGVNavigationCore.PathFinding.Analysis; +using AGVNavigationCore.PathFinding.Validation; namespace AGVNavigationCore.PathFinding.Planning { @@ -114,37 +115,50 @@ namespace AGVNavigationCore.PathFinding.Planning } /// - /// 방향 전환에 적합한 갈림길 검색 + /// 방향 전환에 적합한 갈림길 검색 (인근 우회 경로 우선) /// private List FindSuitableChangeJunctions(string startNodeId, string targetNodeId, AgvDirection currentDirection, AgvDirection requiredDirection) { var suitableJunctions = new List(); - // 시작점과 목표점 사이의 경로에 있는 갈림길들 우선 검색 - var directPath = _pathfinder.FindPath(startNodeId, targetNodeId); - if (directPath.Success) + // 1. 시작점 인근의 갈림길들을 우선 검색 (경로 진행 중 우회용) + var nearbyJunctions = FindNearbyJunctions(startNodeId, 2); // 2단계 내의 갈림길 + foreach (var junction in nearbyJunctions) { - foreach (var nodeId in directPath.Path) + if (junction == startNodeId) continue; // 시작점 제외 + + var junctionInfo = _junctionAnalyzer.GetJunctionInfo(junction); + if (junctionInfo != null && junctionInfo.IsJunction) { - var junctionInfo = _junctionAnalyzer.GetJunctionInfo(nodeId); - if (junctionInfo != null && junctionInfo.IsJunction) + // 이 갈림길을 통해 목적지로 갈 수 있는지 확인 + if (CanReachTargetViaJunction(junction, targetNodeId) && + HasSuitableDetourOptions(junction, startNodeId)) { - suitableJunctions.Add(nodeId); + suitableJunctions.Add(junction); } } } - // 추가로 시작점 주변의 갈림길들도 검색 - var nearbyJunctions = FindNearbyJunctions(startNodeId, 3); // 3단계 내의 갈림길 - foreach (var junction in nearbyJunctions) + // 2. 직진 경로상의 갈림길들도 검색 (단, 되돌아가기 방지) + var directPath = _pathfinder.FindPath(startNodeId, targetNodeId); + if (directPath.Success) { - if (!suitableJunctions.Contains(junction)) + foreach (var nodeId in directPath.Path.Skip(2)) // 시작점과 다음 노드는 제외 { - suitableJunctions.Add(junction); + var junctionInfo = _junctionAnalyzer.GetJunctionInfo(nodeId); + if (junctionInfo != null && junctionInfo.IsJunction) + { + // 직진 경로상에서는 더 엄격한 조건 적용 + if (!suitableJunctions.Contains(nodeId) && + HasMultipleExitOptions(nodeId)) + { + suitableJunctions.Add(nodeId); + } + } } } - // 거리순으로 정렬 (시작점에서 가까운 순) + // 거리순으로 정렬 (가까운 갈림길 우선 - 인근 우회용) return SortJunctionsByDistance(startNodeId, suitableJunctions); } @@ -244,10 +258,19 @@ namespace AGVNavigationCore.PathFinding.Planning if (changePath.Count > 0) { + // **VALIDATION**: 되돌아가기 패턴 검증 + var validationResult = ValidateDirectionChangePath(changePath, startNodeId, junctionNodeId); + if (!validationResult.IsValid) + { + System.Diagnostics.Debug.WriteLine($"[DirectionChangePlanner] ❌ 갈림길 {junctionNodeId} 경로 검증 실패: {validationResult.ValidationError}"); + return DirectionChangePlan.CreateFailure($"갈림길 {junctionNodeId} 검증 실패: {validationResult.ValidationError}"); + } + // 실제 방향 전환 노드 찾기 (우회 노드) string actualDirectionChangeNode = FindActualDirectionChangeNode(changePath, junctionNodeId); string description = $"갈림길 {GetDisplayName(junctionNodeId)}를 통해 {GetDisplayName(actualDirectionChangeNode)}에서 방향 전환: {currentDirection} → {requiredDirection}"; + System.Diagnostics.Debug.WriteLine($"[DirectionChangePlanner] ✅ 유효한 방향전환 경로: {string.Join(" → ", changePath)}"); return DirectionChangePlan.CreateSuccess(changePath, actualDirectionChangeNode, description); } @@ -260,7 +283,7 @@ namespace AGVNavigationCore.PathFinding.Planning } /// - /// 방향 전환 경로 생성 + /// 방향 전환 경로 생성 (인근 갈림길 우회 방식) /// private List GenerateDirectionChangePath(string startNodeId, string targetNodeId, string junctionNodeId, AgvDirection currentDirection, AgvDirection requiredDirection) { @@ -271,17 +294,69 @@ namespace AGVNavigationCore.PathFinding.Planning if (!toJunctionPath.Success) return fullPath; + // 2. 인근 갈림길을 통한 우회인지, 직진 경로상 갈림길인지 판단 + var directPath = _pathfinder.FindPath(startNodeId, targetNodeId); + bool isNearbyDetour = !directPath.Success || !directPath.Path.Contains(junctionNodeId); + + if (isNearbyDetour) + { + // 인근 갈림길 우회: 직진하다가 마그넷으로 방향 전환 + return GenerateNearbyDetourPath(startNodeId, targetNodeId, junctionNodeId, currentDirection, requiredDirection); + } + else + { + // 직진 경로상 갈림길: 기존 방식으로 처리 (단, 되돌아가기 방지) + return GenerateDirectPathChangeRoute(startNodeId, targetNodeId, junctionNodeId, currentDirection, requiredDirection); + } + } + + /// + /// 인근 갈림길을 통한 우회 경로 생성 (예: 012 → 013 → 마그넷으로 016 방향) + /// + private List GenerateNearbyDetourPath(string startNodeId, string targetNodeId, string junctionNodeId, AgvDirection currentDirection, AgvDirection requiredDirection) + { + var fullPath = new List(); + + // 1. 시작점에서 갈림길까지 직진 (현재 방향 유지) + var toJunctionPath = _pathfinder.FindPath(startNodeId, junctionNodeId); + if (!toJunctionPath.Success) + return fullPath; + fullPath.AddRange(toJunctionPath.Path); - // 2. 갈림길에서 방향 전환 처리 + // 2. 갈림길에서 방향 전환 후 목적지로 + // 이때 마그넷 센서를 이용해 목적지 방향으로 진입 + var fromJunctionPath = _pathfinder.FindPath(junctionNodeId, targetNodeId); + if (fromJunctionPath.Success && fromJunctionPath.Path.Count > 1) + { + fullPath.AddRange(fromJunctionPath.Path.Skip(1)); // 중복 노드 제거 + } + + return fullPath; + } + + /// + /// 직진 경로상 갈림길에서 방향 전환 경로 생성 (기존 방식 개선) + /// + private List GenerateDirectPathChangeRoute(string startNodeId, string targetNodeId, string junctionNodeId, AgvDirection currentDirection, AgvDirection requiredDirection) + { + var fullPath = new List(); + + // 1. 시작점에서 갈림길까지의 경로 + var toJunctionPath = _pathfinder.FindPath(startNodeId, junctionNodeId); + if (!toJunctionPath.Success) + return fullPath; + + fullPath.AddRange(toJunctionPath.Path); + + // 2. 갈림길에서 방향 전환 처리 (되돌아가기 방지) if (currentDirection != requiredDirection) { - // AGV가 어느 노드에서 갈림길로 왔는지 파악 string fromNodeId = toJunctionPath.Path.Count >= 2 ? toJunctionPath.Path[toJunctionPath.Path.Count - 2] : startNodeId; var changeSequence = GenerateDirectionChangeSequence(junctionNodeId, fromNodeId, currentDirection, requiredDirection); - if (changeSequence.Count > 1) // 첫 번째는 갈림길 자체이므로 제외 + if (changeSequence.Count > 1) { fullPath.AddRange(changeSequence.Skip(1)); } @@ -292,7 +367,7 @@ namespace AGVNavigationCore.PathFinding.Planning var fromJunctionPath = _pathfinder.FindPath(lastNode, targetNodeId); if (fromJunctionPath.Success && fromJunctionPath.Path.Count > 1) { - fullPath.AddRange(fromJunctionPath.Path.Skip(1)); // 중복 노드 제거 + fullPath.AddRange(fromJunctionPath.Path.Skip(1)); } return fullPath; @@ -361,7 +436,7 @@ namespace AGVNavigationCore.PathFinding.Planning private string FindBestDetourNode(string junctionNodeId, List availableNodes, string excludeNodeId) { // 왔던 길(excludeNodeId)를 제외한 노드 중에서 최적의 우회 노드 선택 - // 우선순위: 1) 직진방향 2) 가장 작은 각도 변화 3) 막다른 길이 아닌 노드 + // 우선순위: 1) 막다른 길이 아닌 노드 (우회 후 복귀 가능) 2) 직진방향 3) 목적지 방향 var junctionNode = _mapNodes.FirstOrDefault(n => n.NodeId == junctionNodeId); var fromNode = _mapNodes.FirstOrDefault(n => n.NodeId == excludeNodeId); @@ -478,6 +553,163 @@ namespace AGVNavigationCore.PathFinding.Planning return junctionNodeId; } + /// + /// 갈림길에서 적절한 우회 옵션이 있는지 확인 + /// + private bool HasSuitableDetourOptions(string junctionNodeId, string excludeNodeId) + { + var junctionInfo = _junctionAnalyzer.GetJunctionInfo(junctionNodeId); + if (junctionInfo == null || !junctionInfo.IsJunction) + return false; + + // 제외할 노드(직전 노드)를 뺀 연결된 노드가 2개 이상이어야 적절한 우회 가능 + var availableConnections = junctionInfo.ConnectedNodes + .Where(nodeId => nodeId != excludeNodeId) + .ToList(); + + // 최소 2개의 우회 옵션이 있어야 함 (갈림길에서 방향전환 후 다시 나갈 수 있어야 함) + return availableConnections.Count >= 2; + } + + /// + /// 갈림길을 통해 목적지에 도달할 수 있는지 확인 + /// + private bool CanReachTargetViaJunction(string junctionNodeId, string targetNodeId) + { + // 갈림길에서 목적지까지의 경로가 존재하는지 확인 + var pathToTarget = _pathfinder.FindPath(junctionNodeId, targetNodeId); + return pathToTarget.Success; + } + + /// + /// 갈림길에서 여러 출구 옵션이 있는지 확인 (직진 경로상 갈림길용) + /// + private bool HasMultipleExitOptions(string junctionNodeId) + { + var junctionInfo = _junctionAnalyzer.GetJunctionInfo(junctionNodeId); + if (junctionInfo == null || !junctionInfo.IsJunction) + return false; + + // 최소 3개 이상의 연결 노드가 있어야 적절한 방향전환 가능 + return junctionInfo.ConnectedNodes.Count >= 3; + } + + /// + /// 방향전환 경로 검증 - 되돌아가기 패턴 및 물리적 실현성 검증 + /// + private PathValidationResult ValidateDirectionChangePath(List path, string startNodeId, string junctionNodeId) + { + if (path == null || path.Count == 0) + { + return PathValidationResult.CreateInvalid(startNodeId, "", "경로가 비어있습니다."); + } + + // 1. 되돌아가기 패턴 검증 (A → B → A) + var backtrackingPatterns = DetectBacktrackingPatterns(path); + if (backtrackingPatterns.Count > 0) + { + var issues = new List(); + foreach (var pattern in backtrackingPatterns) + { + issues.Add($"되돌아가기 패턴 발견: {pattern}"); + } + + string errorMessage = $"되돌아가기 패턴 검출 ({backtrackingPatterns.Count}개): {string.Join(", ", issues)}"; + System.Diagnostics.Debug.WriteLine($"[PathValidation] ❌ 경로: {string.Join(" → ", path)}"); + System.Diagnostics.Debug.WriteLine($"[PathValidation] ❌ 되돌아가기 패턴: {errorMessage}"); + + return PathValidationResult.CreateInvalidWithBacktracking( + path, backtrackingPatterns, startNodeId, "", junctionNodeId, errorMessage); + } + + // 2. 연속된 중복 노드 검증 + var duplicates = DetectConsecutiveDuplicates(path); + if (duplicates.Count > 0) + { + string errorMessage = $"연속된 중복 노드 발견: {string.Join(", ", duplicates)}"; + return PathValidationResult.CreateInvalid(startNodeId, "", errorMessage); + } + + // 3. 경로 연결성 검증 + var connectivity = ValidatePathConnectivity(path); + if (!connectivity.IsValid) + { + return PathValidationResult.CreateInvalid(startNodeId, "", $"경로 연결성 오류: {connectivity.ValidationError}"); + } + + // 4. 갈림길 포함 여부 검증 + if (!path.Contains(junctionNodeId)) + { + return PathValidationResult.CreateInvalid(startNodeId, "", $"갈림길 {junctionNodeId}이 경로에 포함되지 않음"); + } + + System.Diagnostics.Debug.WriteLine($"[PathValidation] ✅ 유효한 경로: {string.Join(" → ", path)}"); + return PathValidationResult.CreateValid(path, startNodeId, "", junctionNodeId); + } + + /// + /// 되돌아가기 패턴 검출 (A → B → A) + /// + private List DetectBacktrackingPatterns(List path) + { + var patterns = new List(); + + for (int i = 0; i < path.Count - 2; i++) + { + string nodeA = path[i]; + string nodeB = path[i + 1]; + string nodeC = path[i + 2]; + + // A → B → A 패턴 검출 + if (nodeA == nodeC && nodeA != nodeB) + { + var pattern = BacktrackingPattern.Create(nodeA, nodeB, nodeA, i, i + 2); + patterns.Add(pattern); + } + } + + return patterns; + } + + /// + /// 연속된 중복 노드 검출 + /// + private List DetectConsecutiveDuplicates(List path) + { + var duplicates = new List(); + + for (int i = 0; i < path.Count - 1; i++) + { + if (path[i] == path[i + 1]) + { + duplicates.Add(path[i]); + } + } + + return duplicates; + } + + /// + /// 경로 연결성 검증 + /// + private PathValidationResult ValidatePathConnectivity(List path) + { + for (int i = 0; i < path.Count - 1; i++) + { + string currentNode = path[i]; + string nextNode = path[i + 1]; + + // 두 노드간 직접 연결성 확인 (맵 노드의 ConnectedNodes 리스트 사용) + var currentMapNode = _mapNodes.FirstOrDefault(n => n.NodeId == currentNode); + if (currentMapNode == null || !currentMapNode.ConnectedNodes.Contains(nextNode)) + { + return PathValidationResult.CreateInvalid(currentNode, nextNode, $"노드 {currentNode}와 {nextNode} 사이에 연결이 없음"); + } + } + + return PathValidationResult.CreateNotRequired(); + } + /// /// 두 점 사이의 거리 계산 /// @@ -488,25 +720,6 @@ namespace AGVNavigationCore.PathFinding.Planning return (float)Math.Sqrt(dx * dx + dy * dy); } - /// - /// 목적지 도킹 방향 요구사항 확인 - /// - public AgvDirection GetRequiredDockingDirection(string targetNodeId) - { - var targetNode = _mapNodes.FirstOrDefault(n => n.NodeId == targetNodeId); - if (targetNode == null) - return AgvDirection.Forward; - - switch (targetNode.Type) - { - case NodeType.Charging: - return AgvDirection.Forward; // 충전기는 전진 도킹 - case NodeType.Docking: - return AgvDirection.Backward; // 일반 도킹은 후진 도킹 - default: - return AgvDirection.Forward; // 기본은 전진 - } - } /// /// 경로 계획 요약 정보 diff --git a/Cs_HMI/AGVNavigationCore/PathFinding/Validation/PathValidationResult.cs b/Cs_HMI/AGVNavigationCore/PathFinding/Validation/PathValidationResult.cs new file mode 100644 index 0000000..885cf9e --- /dev/null +++ b/Cs_HMI/AGVNavigationCore/PathFinding/Validation/PathValidationResult.cs @@ -0,0 +1,205 @@ +using System.Collections.Generic; +using AGVNavigationCore.Models; + +namespace AGVNavigationCore.PathFinding.Validation +{ + /// + /// 경로 검증 결과 (되돌아가기 패턴 검증 포함) + /// + public class PathValidationResult + { + /// + /// 경로 검증이 필요한지 여부 + /// + public bool IsValidationRequired { get; set; } + + /// + /// 경로 검증 통과 여부 + /// + public bool IsValid { get; set; } + + /// + /// 검증된 경로 + /// + public List ValidatedPath { get; set; } + + /// + /// 검출된 되돌아가기 패턴 목록 (A → B → A 형태) + /// + public List BacktrackingPatterns { get; set; } + + /// + /// 갈림길 노드 목록 + /// + public List JunctionNodes { get; set; } + + /// + /// 시작 노드 ID + /// + public string StartNodeId { get; set; } + + /// + /// 목표 노드 ID + /// + public string TargetNodeId { get; set; } + + /// + /// 갈림길 노드 ID (방향 전환용) + /// + public string JunctionNodeId { get; set; } + + /// + /// 검증 오류 메시지 (실패시) + /// + public string ValidationError { get; set; } + + /// + /// 기본 생성자 + /// + public PathValidationResult() + { + IsValidationRequired = false; + IsValid = true; + ValidatedPath = new List(); + BacktrackingPatterns = new List(); + JunctionNodes = new List(); + StartNodeId = string.Empty; + TargetNodeId = string.Empty; + JunctionNodeId = string.Empty; + ValidationError = string.Empty; + } + + /// + /// 검증 불필요한 경우 생성 + /// + public static PathValidationResult CreateNotRequired() + { + return new PathValidationResult + { + IsValidationRequired = false, + IsValid = true + }; + } + + /// + /// 검증 성공 결과 생성 + /// + public static PathValidationResult CreateValid(List path, string startNodeId, string targetNodeId, string junctionNodeId = "") + { + return new PathValidationResult + { + IsValidationRequired = true, + IsValid = true, + ValidatedPath = new List(path), + StartNodeId = startNodeId, + TargetNodeId = targetNodeId, + JunctionNodeId = junctionNodeId + }; + } + + /// + /// 검증 실패 결과 생성 (되돌아가기 패턴 검출) + /// + public static PathValidationResult CreateInvalidWithBacktracking( + List path, + List backtrackingPatterns, + string startNodeId, + string targetNodeId, + string junctionNodeId, + string error) + { + return new PathValidationResult + { + IsValidationRequired = true, + IsValid = false, + ValidatedPath = new List(path), + BacktrackingPatterns = new List(backtrackingPatterns), + StartNodeId = startNodeId, + TargetNodeId = targetNodeId, + JunctionNodeId = junctionNodeId, + ValidationError = error + }; + } + + /// + /// 일반 검증 실패 결과 생성 + /// + public static PathValidationResult CreateInvalid(string startNodeId, string targetNodeId, string error) + { + return new PathValidationResult + { + IsValidationRequired = true, + IsValid = false, + StartNodeId = startNodeId, + TargetNodeId = targetNodeId, + ValidationError = error + }; + } + } + + /// + /// 되돌아가기 패턴 정보 (A → B → A) + /// + public class BacktrackingPattern + { + /// + /// 시작 노드 (A) + /// + public string StartNode { get; set; } + + /// + /// 중간 노드 (B) + /// + public string MiddleNode { get; set; } + + /// + /// 되돌아간 노드 (다시 A) + /// + public string ReturnNode { get; set; } + + /// + /// 경로에서의 시작 인덱스 + /// + public int StartIndex { get; set; } + + /// + /// 경로에서의 종료 인덱스 + /// + public int EndIndex { get; set; } + + /// + /// 기본 생성자 + /// + public BacktrackingPattern() + { + StartNode = string.Empty; + MiddleNode = string.Empty; + ReturnNode = string.Empty; + StartIndex = -1; + EndIndex = -1; + } + + /// + /// 되돌아가기 패턴 생성 + /// + public static BacktrackingPattern Create(string startNode, string middleNode, string returnNode, int startIndex, int endIndex) + { + return new BacktrackingPattern + { + StartNode = startNode, + MiddleNode = middleNode, + ReturnNode = returnNode, + StartIndex = startIndex, + EndIndex = endIndex + }; + } + + /// + /// 패턴 설명 문자열 + /// + public override string ToString() + { + return $"{StartNode} → {MiddleNode} → {ReturnNode} (인덱스: {StartIndex}-{EndIndex})"; + } + } +} \ No newline at end of file diff --git a/Cs_HMI/AGVNavigationCore/Utils/DockingValidator.cs b/Cs_HMI/AGVNavigationCore/Utils/DockingValidator.cs index 82c10f8..a20fecd 100644 --- a/Cs_HMI/AGVNavigationCore/Utils/DockingValidator.cs +++ b/Cs_HMI/AGVNavigationCore/Utils/DockingValidator.cs @@ -25,6 +25,7 @@ namespace AGVNavigationCore.Utils // 경로가 없거나 실패한 경우 if (pathResult == null || !pathResult.Success || pathResult.Path == null || pathResult.Path.Count == 0) { + System.Diagnostics.Debug.WriteLine($"[DockingValidator] 도킹 검증 불필요: 경로 없음"); return DockingValidationResult.CreateNotRequired(); } @@ -32,26 +33,36 @@ namespace AGVNavigationCore.Utils string targetNodeId = pathResult.Path[pathResult.Path.Count - 1]; var targetNode = mapNodes?.FirstOrDefault(n => n.NodeId == targetNodeId); + System.Diagnostics.Debug.WriteLine($"[DockingValidator] 목적지 노드: {targetNodeId}"); + if (targetNode == null) { + System.Diagnostics.Debug.WriteLine($"[DockingValidator] 목적지 노드 찾을 수 없음: {targetNodeId}"); return DockingValidationResult.CreateNotRequired(); } - // 도킹이 필요한 노드 타입인지 확인 - if (!IsDockingRequired(targetNode.Type)) + System.Diagnostics.Debug.WriteLine($"[DockingValidator] 목적지 노드 타입: {targetNode.Type} ({(int)targetNode.Type})"); + + // 도킹이 필요한 노드인지 확인 (DockDirection이 DontCare가 아닌 경우) + if (!IsDockingRequired(targetNode.DockDirection)) { + System.Diagnostics.Debug.WriteLine($"[DockingValidator] 도킹 불필요: {targetNode.DockDirection}"); return DockingValidationResult.CreateNotRequired(); } // 필요한 도킹 방향 확인 - var requiredDirection = GetRequiredDockingDirection(targetNode.Type); + var requiredDirection = GetRequiredDockingDirection(targetNode.DockDirection); + System.Diagnostics.Debug.WriteLine($"[DockingValidator] 필요한 도킹 방향: {requiredDirection}"); // 경로 기반 최종 방향 계산 var calculatedDirection = CalculateFinalDirection(pathResult.Path, mapNodes, currentDirection); + System.Diagnostics.Debug.WriteLine($"[DockingValidator] 계산된 최종 방향: {calculatedDirection}"); + System.Diagnostics.Debug.WriteLine($"[DockingValidator] AGV 현재 방향: {currentDirection}"); // 검증 수행 if (calculatedDirection == requiredDirection) { + System.Diagnostics.Debug.WriteLine($"[DockingValidator] ✅ 도킹 검증 성공"); return DockingValidationResult.CreateValid( targetNodeId, targetNode.Type, @@ -61,6 +72,7 @@ namespace AGVNavigationCore.Utils else { string error = $"도킹 방향 불일치: 필요={GetDirectionText(requiredDirection)}, 계산됨={GetDirectionText(calculatedDirection)}"; + System.Diagnostics.Debug.WriteLine($"[DockingValidator] ❌ 도킹 검증 실패: {error}"); return DockingValidationResult.CreateInvalid( targetNodeId, targetNode.Type, @@ -71,66 +83,97 @@ namespace AGVNavigationCore.Utils } /// - /// 도킹이 필요한 노드 타입인지 확인 + /// 도킹이 필요한 노드인지 확인 (도킹방향이 DontCare가 아닌 경우) /// - private static bool IsDockingRequired(NodeType nodeType) + private static bool IsDockingRequired(DockingDirection dockDirection) { - return nodeType == NodeType.Charging || nodeType == NodeType.Docking; + return dockDirection != DockingDirection.DontCare; } /// - /// 노드 타입에 따른 필요한 도킹 방향 반환 + /// 노드 도킹 방향에 따른 필요한 AGV 방향 반환 /// - private static AgvDirection GetRequiredDockingDirection(NodeType nodeType) + private static AgvDirection GetRequiredDockingDirection(DockingDirection dockDirection) { - switch (nodeType) + switch (dockDirection) { - case NodeType.Charging: - return AgvDirection.Forward; // 충전기는 전진 도킹 - case NodeType.Docking: - return AgvDirection.Backward; // 일반 도킹은 후진 도킹 + case DockingDirection.Forward: + return AgvDirection.Forward; // 전진 도킹 + case DockingDirection.Backward: + return AgvDirection.Backward; // 후진 도킹 + case DockingDirection.DontCare: default: - return AgvDirection.Forward; // 기본값 + return AgvDirection.Forward; // 기본값 (사실상 사용되지 않음) } } /// /// 경로 기반 최종 방향 계산 - /// 현재 구현: 간단한 추정 (향후 고도화 가능) + /// 개선된 구현: 경로 진행 방향과 목적지 노드 타입을 고려 /// private static AgvDirection CalculateFinalDirection(List path, List mapNodes, AgvDirection currentDirection) { - // 경로가 2개 이상일 때만 방향 변화 추정 + System.Diagnostics.Debug.WriteLine($"[CalculateFinalDirection] 입력 - 경로 수: {path?.Count}, 현재 방향: {currentDirection}"); + + // 경로가 1개 이하면 현재 방향 유지 if (path.Count < 2) { + System.Diagnostics.Debug.WriteLine($"[CalculateFinalDirection] 경로가 짧음, 현재 방향 유지: {currentDirection}"); return currentDirection; } - // 마지막 구간의 노드들 찾기 - var secondLastNodeId = path[path.Count - 2]; + // 목적지 노드 확인 var lastNodeId = path[path.Count - 1]; - - var secondLastNode = mapNodes?.FirstOrDefault(n => n.NodeId == secondLastNodeId); var lastNode = mapNodes?.FirstOrDefault(n => n.NodeId == lastNodeId); - if (secondLastNode == null || lastNode == null) + if (lastNode == null) { + System.Diagnostics.Debug.WriteLine($"[CalculateFinalDirection] 목적지 노드 찾을 수 없음: {lastNodeId}"); return currentDirection; } - // 마지막 구간의 이동 방향 분석 + // 도킹 노드인 경우, 필요한 도킹 방향으로 설정 + if (IsDockingRequired(lastNode.DockDirection)) + { + var requiredDockingDirection = GetRequiredDockingDirection(lastNode.DockDirection); + System.Diagnostics.Debug.WriteLine($"[CalculateFinalDirection] 도킹 노드(DockDirection={lastNode.DockDirection}) 감지, 필요 방향: {requiredDockingDirection}"); + + // 현재 방향이 필요한 도킹 방향과 다르면 경고 로그 + if (currentDirection != requiredDockingDirection) + { + System.Diagnostics.Debug.WriteLine($"[CalculateFinalDirection] ⚠️ 현재 방향({currentDirection})과 필요 도킹 방향({requiredDockingDirection}) 불일치"); + } + + // 도킹 노드의 경우 항상 필요한 도킹 방향 반환 + return requiredDockingDirection; + } + + // 일반 노드인 경우 마지막 구간의 이동 방향 분석 + var secondLastNodeId = path[path.Count - 2]; + var secondLastNode = mapNodes?.FirstOrDefault(n => n.NodeId == secondLastNodeId); + + if (secondLastNode == null) + { + System.Diagnostics.Debug.WriteLine($"[CalculateFinalDirection] 이전 노드 찾을 수 없음: {secondLastNodeId}"); + return currentDirection; + } + + // 마지막 구간의 이동 벡터 계산 var deltaX = lastNode.Position.X - secondLastNode.Position.X; var deltaY = lastNode.Position.Y - secondLastNode.Position.Y; + var distance = Math.Sqrt(deltaX * deltaX + deltaY * deltaY); + + System.Diagnostics.Debug.WriteLine($"[CalculateFinalDirection] 마지막 구간: {secondLastNodeId} → {lastNodeId}, 벡터: ({deltaX}, {deltaY}), 거리: {distance:F2}"); // 이동 거리가 매우 작으면 현재 방향 유지 - var distance = Math.Sqrt(deltaX * deltaX + deltaY * deltaY); if (distance < 1.0) { + System.Diagnostics.Debug.WriteLine($"[CalculateFinalDirection] 이동 거리 너무 짧음, 현재 방향 유지: {currentDirection}"); return currentDirection; } - // 간단한 방향 추정 (향후 더 정교한 로직으로 개선 가능) - // 현재는 현재 방향을 유지한다고 가정 + // 일반 노드의 경우 현재 방향 유지 (방향 전환은 회전 노드에서만 발생) + System.Diagnostics.Debug.WriteLine($"[CalculateFinalDirection] 일반 노드, 현재 방향 유지: {currentDirection}"); return currentDirection; } diff --git a/Cs_HMI/AGVSimulator/Forms/SimulatorForm.cs b/Cs_HMI/AGVSimulator/Forms/SimulatorForm.cs index c89716c..0103f68 100644 --- a/Cs_HMI/AGVSimulator/Forms/SimulatorForm.cs +++ b/Cs_HMI/AGVSimulator/Forms/SimulatorForm.cs @@ -95,7 +95,7 @@ namespace AGVSimulator.Forms _simulatorCanvas.Dock = DockStyle.Fill; // 목적지 선택 이벤트 구독 - _simulatorCanvas.TargetNodeSelected += OnTargetNodeSelected; + _simulatorCanvas.NodeSelected += OnTargetNodeSelected; _canvasPanel.Controls.Add(_simulatorCanvas); } @@ -302,20 +302,23 @@ namespace AGVSimulator.Forms var selectedAGV = _agvListCombo.SelectedItem as VirtualAGV; var currentDirection = selectedAGV?.CurrentDirection ?? AgvDirection.Forward; - // 고급 경로 계획 사용 (단일 경로 계산 방식) - var advancedResult = _advancedPathfinder.FindPath(startNode.NodeId, targetNode.NodeId, currentDirection); + // 고급 경로 계획 사용 (노드 객체 직접 전달) + var advancedResult = _advancedPathfinder.FindPath(startNode, targetNode, currentDirection); if (advancedResult.Success) { - // 고급 경로 결과를 AGVPathResult 형태로 변환 (도킹 검증 포함) - var agvResult = ConvertToAGVPathResult(advancedResult, currentDirection); + // 도킹 검증이 없는 경우 추가 검증 수행 + if (advancedResult.DockingValidation == null || !advancedResult.DockingValidation.IsValidationRequired) + { + advancedResult.DockingValidation = DockingValidator.ValidateDockingDirection(advancedResult, _mapNodes, currentDirection); + } - _simulatorCanvas.CurrentPath = agvResult; + _simulatorCanvas.CurrentPath = advancedResult; _pathLengthLabel.Text = $"경로 길이: {advancedResult.TotalDistance:F1}"; _statusLabel.Text = $"경로 계산 완료 ({advancedResult.CalculationTimeMs}ms)"; // 도킹 검증 결과 확인 및 UI 표시 - CheckAndDisplayDockingValidation(agvResult); + CheckAndDisplayDockingValidation(advancedResult); // 고급 경로 디버깅 정보 표시 UpdateAdvancedPathDebugInfo(advancedResult); @@ -364,7 +367,6 @@ namespace AGVSimulator.Forms _isTargetCalcMode = false; _targetCalcButton.Text = "타겟계산"; _targetCalcButton.BackColor = SystemColors.Control; - _simulatorCanvas.CurrentEditMode = UnifiedAGVCanvas.EditMode.Select; _statusLabel.Text = "타겟계산 모드 해제"; } else @@ -373,7 +375,6 @@ namespace AGVSimulator.Forms _isTargetCalcMode = true; _targetCalcButton.Text = "계산 취소"; _targetCalcButton.BackColor = Color.LightGreen; - _simulatorCanvas.CurrentEditMode = UnifiedAGVCanvas.EditMode.SelectTarget; _statusLabel.Text = "목적지 노드를 클릭하세요 (자동으로 경로 계산됨)"; } } @@ -390,6 +391,7 @@ namespace AGVSimulator.Forms //_targetCalcButton.Text = "타겟계산"; //_targetCalcButton.BackColor = SystemColors.Control; //_simulatorCanvas.CurrentEditMode = UnifiedAGVCanvas.EditMode.Select; + if (selectedNode == null) return; // 목적지를 선택된 노드로 설정 SetTargetNodeInCombo(selectedNode.NodeId); @@ -985,32 +987,6 @@ namespace AGVSimulator.Forms } } - /// - /// 고급 경로 결과를 기존 AGVPathResult 형태로 변환 (도킹 검증 포함) - /// - private AGVPathResult ConvertToAGVPathResult(AGVPathResult advancedResult, AgvDirection? currentDirection = null) - { - var agvResult = new AGVPathResult(); - agvResult.Success = advancedResult.Success; - agvResult.Path = advancedResult.GetSimplePath(); - agvResult.NodeMotorInfos = advancedResult.DetailedPath; - agvResult.TotalDistance = advancedResult.TotalDistance; - agvResult.CalculationTimeMs = advancedResult.CalculationTimeMs; - agvResult.ExploredNodes = advancedResult.ExploredNodeCount; - agvResult.ErrorMessage = advancedResult.ErrorMessage; - - // 도킹 검증 수행 (AdvancedPathResult에서 이미 수행되었다면 그 결과 사용) - if (advancedResult.DockingValidation != null && advancedResult.DockingValidation.IsValidationRequired) - { - agvResult.DockingValidation = advancedResult.DockingValidation; - } - else if (agvResult.Success && _mapNodes != null && currentDirection.HasValue) - { - agvResult.DockingValidation = DockingValidator.ValidateDockingDirection(agvResult, _mapNodes, currentDirection.Value); - } - - return agvResult; - } /// diff --git a/Cs_HMI/AGVSimulator/Models/VirtualAGV.cs b/Cs_HMI/AGVSimulator/Models/VirtualAGV.cs index 50dc579..11c3fa7 100644 --- a/Cs_HMI/AGVSimulator/Models/VirtualAGV.cs +++ b/Cs_HMI/AGVSimulator/Models/VirtualAGV.cs @@ -89,17 +89,29 @@ namespace AGVSimulator.Models /// /// 현재 위치 /// - public Point CurrentPosition => _currentPosition; + public Point CurrentPosition + { + get => _currentPosition; + set => _currentPosition = value; + } /// /// 현재 방향 /// - public AgvDirection CurrentDirection => _currentDirection; + public AgvDirection CurrentDirection + { + get => _currentDirection; + set => _currentDirection = value; + } /// /// 현재 상태 /// - public AGVState CurrentState => _currentState; + public AGVState CurrentState + { + get => _currentState; + set => _currentState = value; + } /// /// 현재 속도 diff --git a/Cs_HMI/CLAUDE.md b/Cs_HMI/CLAUDE.md index 1b0d3e0..6d5b480 100644 --- a/Cs_HMI/CLAUDE.md +++ b/Cs_HMI/CLAUDE.md @@ -297,6 +297,50 @@ AGV가 후진 상태로 006 → 005 → 004 이동 중, 037(버퍼)에 도킹 - **에러 처리**: 사용자 확인 다이얼로그와 상태바 메시지 활용 - **코드 재사용**: UnifiedAGVCanvas를 맵에디터와 시뮬레이터에서 공통 사용 +## AGVNavigationCore 프로젝트 구조 (함수 및 클래스 배치 가이드) + +### 📁 AGVNavigationCore 폴더 구조 +``` +AGVNavigationCore/ +├── Controls/ # UI 컨트롤 및 캔버스 +├── Models/ # 데이터 모델 및 Enum 정의 +├── Utils/ # 유틸리티 클래스 +└── PathFinding/ # 경로 탐색 관련 모든 기능 + ├── Analysis/ # 경로 분석 관련 클래스 + ├── Core/ # 핵심 경로 탐색 알고리즘 + ├── Planning/ # 경로 계획 및 방향 변경 로직 + └── Validation/ # 검증 관련 클래스 (DockingValidationResult, PathValidationResult 등) +``` + +### 🎯 클래스 배치 원칙 + +#### PathFinding/Validation/ +- **검증 결과 클래스**: `*ValidationResult.cs` 패턴 사용 +- **검증 로직**: 결과를 반환하는 검증 메서드 포함 +- **네임스페이스**: `AGVNavigationCore.PathFinding.Validation` +- **패턴**: 정적 팩토리 메서드 (CreateValid, CreateInvalid, CreateNotRequired) +- **속성**: IsValid, ValidationError, 관련 상세 정보 + +#### PathFinding/Planning/ +- **경로 계획 클래스**: 실제 경로 탐색 및 계획 로직 +- **방향 변경 로직**: DirectionChangePlanner.cs 등 +- **경로 최적화**: 경로 생성과 관련된 전략 패턴 + +#### PathFinding/Core/ +- **핵심 알고리즘**: A* 알고리즘 등 기본 경로 탐색 +- **기본 경로 탐색**: 단순한 점-to-점 경로 계산 + +#### PathFinding/Analysis/ +- **경로 분석**: 생성된 경로의 품질 및 특성 분석 +- **성능 분석**: 경로 효율성 및 최적화 분석 + +### 📋 새 클래스 생성 시 체크리스트 +1. **기능별 분류**: 검증(Validation), 계획(Planning), 분석(Analysis), 핵심(Core) +2. **네임스페이스 일치**: 폴더 구조와 네임스페이스 일치 확인 +3. **명명 규칙**: 기능을 명확히 나타내는 클래스명 사용 +4. **의존성**: 순환 참조 방지 및 계층적 의존성 유지 +5. **테스트 가능성**: 단위 테스트가 가능한 구조로 설계 + ### 🚨 알려진 이슈 - **빌드 환경**: MSBuild 2022가 설치되지 않은 환경에서 빌드 불가 - **좌표 시스템**: 줌/팬 상태에서 좌표 변환 정확성 지속 모니터링 필요 \ No newline at end of file diff --git a/Cs_HMI/Data/NewMap.agvmap b/Cs_HMI/Data/NewMap.agvmap index 63b78fb..7c82aab 100644 --- a/Cs_HMI/Data/NewMap.agvmap +++ b/Cs_HMI/Data/NewMap.agvmap @@ -5,7 +5,7 @@ "Name": "UNLOADER", "Position": "65, 229", "Type": 2, - "DockDirection": null, + "DockDirection": 2, "ConnectedNodes": [], "CanRotate": false, "StationId": "", @@ -35,7 +35,7 @@ "Name": "N002", "Position": "206, 244", "Type": 0, - "DockDirection": null, + "DockDirection": 0, "ConnectedNodes": [ "N001" ], @@ -67,7 +67,7 @@ "Name": "N003", "Position": "278, 278", "Type": 0, - "DockDirection": null, + "DockDirection": 0, "ConnectedNodes": [ "N002" ], @@ -99,7 +99,7 @@ "Name": "N004", "Position": "380, 340", "Type": 1, - "DockDirection": null, + "DockDirection": 0, "ConnectedNodes": [ "N003", "N022", @@ -133,7 +133,7 @@ "Name": "N006", "Position": "520, 220", "Type": 0, - "DockDirection": null, + "DockDirection": 0, "ConnectedNodes": [ "N007" ], @@ -165,7 +165,7 @@ "Name": "N007", "Position": "600, 180", "Type": 0, - "DockDirection": null, + "DockDirection": 0, "ConnectedNodes": [], "CanRotate": false, "StationId": "", @@ -195,7 +195,7 @@ "Name": "N008", "Position": "299, 456", "Type": 0, - "DockDirection": null, + "DockDirection": 0, "ConnectedNodes": [ "N009", "N031" @@ -228,7 +228,7 @@ "Name": "N009", "Position": "193, 477", "Type": 0, - "DockDirection": null, + "DockDirection": 0, "ConnectedNodes": [ "N010" ], @@ -260,7 +260,7 @@ "Name": "TOPS", "Position": "52, 466", "Type": 2, - "DockDirection": null, + "DockDirection": 2, "ConnectedNodes": [], "CanRotate": false, "StationId": "", @@ -290,7 +290,7 @@ "Name": "N011", "Position": "460, 420", "Type": 0, - "DockDirection": null, + "DockDirection": 0, "ConnectedNodes": [ "N012", "N004", @@ -324,7 +324,7 @@ "Name": "N012", "Position": "540, 480", "Type": 0, - "DockDirection": null, + "DockDirection": 0, "ConnectedNodes": [ "N013" ], @@ -356,7 +356,7 @@ "Name": "N013", "Position": "620, 520", "Type": 0, - "DockDirection": null, + "DockDirection": 0, "ConnectedNodes": [ "N014" ], @@ -388,7 +388,7 @@ "Name": "LOADER", "Position": "720, 580", "Type": 2, - "DockDirection": null, + "DockDirection": 2, "ConnectedNodes": [], "CanRotate": false, "StationId": "", @@ -418,7 +418,7 @@ "Name": "CHARGER #2", "Position": "679, 199", "Type": 3, - "DockDirection": null, + "DockDirection": 1, "ConnectedNodes": [ "N007" ], @@ -450,7 +450,7 @@ "Name": "N022", "Position": "459, 279", "Type": 0, - "DockDirection": null, + "DockDirection": 0, "ConnectedNodes": [ "N023", "N006" @@ -483,7 +483,7 @@ "Name": "N023", "Position": "440, 220", "Type": 0, - "DockDirection": null, + "DockDirection": 0, "ConnectedNodes": [ "N024" ], @@ -515,7 +515,7 @@ "Name": "N024", "Position": "500, 160", "Type": 0, - "DockDirection": null, + "DockDirection": 0, "ConnectedNodes": [ "N025" ], @@ -547,7 +547,7 @@ "Name": "N025", "Position": "600, 120", "Type": 0, - "DockDirection": null, + "DockDirection": 0, "ConnectedNodes": [ "N026" ], @@ -579,7 +579,7 @@ "Name": "CHARGER #1", "Position": "660, 100", "Type": 3, - "DockDirection": null, + "DockDirection": 1, "ConnectedNodes": [], "CanRotate": false, "StationId": "", @@ -609,7 +609,7 @@ "Name": "Amkor Technology Korea", "Position": "58, 64", "Type": 4, - "DockDirection": null, + "DockDirection": 0, "ConnectedNodes": [], "CanRotate": false, "StationId": "", @@ -639,7 +639,7 @@ "Name": "logo", "Position": "700, 320", "Type": 5, - "DockDirection": null, + "DockDirection": 0, "ConnectedNodes": [], "CanRotate": false, "StationId": "", @@ -669,7 +669,7 @@ "Name": "", "Position": "436, 485", "Type": 0, - "DockDirection": null, + "DockDirection": 0, "ConnectedNodes": [ "N016" ], @@ -701,7 +701,7 @@ "Name": "", "Position": "425, 524", "Type": 0, - "DockDirection": null, + "DockDirection": 0, "ConnectedNodes": [ "N017" ], @@ -733,7 +733,7 @@ "Name": "", "Position": "387, 557", "Type": 0, - "DockDirection": null, + "DockDirection": 0, "ConnectedNodes": [ "N018" ], @@ -765,7 +765,7 @@ "Name": "", "Position": "314, 549", "Type": 0, - "DockDirection": null, + "DockDirection": 0, "ConnectedNodes": [ "N005" ], @@ -797,7 +797,7 @@ "Name": "", "Position": "229, 553", "Type": 0, - "DockDirection": null, + "DockDirection": 0, "ConnectedNodes": [ "N020" ], @@ -829,7 +829,7 @@ "Name": "", "Position": "148, 545", "Type": 0, - "DockDirection": null, + "DockDirection": 0, "ConnectedNodes": [], "CanRotate": false, "StationId": "", @@ -859,7 +859,7 @@ "Name": "", "Position": "66, 547", "Type": 0, - "DockDirection": null, + "DockDirection": 0, "ConnectedNodes": [ "N020" ], @@ -890,8 +890,8 @@ "NodeId": "N027", "Name": "BUF1", "Position": "65, 644", - "Type": 0, - "DockDirection": null, + "Type": 2, + "DockDirection": 2, "ConnectedNodes": [ "N021" ], @@ -899,9 +899,9 @@ "StationId": "", "StationType": null, "CreatedDate": "2025-09-12T17:22:54.7345704+09:00", - "ModifiedDate": "2025-09-15T15:40:45.5634178+09:00", + "ModifiedDate": "2025-09-16T16:25:24.8062758+09:00", "IsActive": true, - "DisplayColor": "Blue", + "DisplayColor": "Green", "RfidId": "041", "RfidStatus": "정상", "RfidDescription": "", @@ -922,8 +922,8 @@ "NodeId": "N028", "Name": "BUF2", "Position": "149, 645", - "Type": 0, - "DockDirection": null, + "Type": 2, + "DockDirection": 2, "ConnectedNodes": [ "N020" ], @@ -931,9 +931,9 @@ "StationId": "", "StationType": null, "CreatedDate": "2025-09-12T17:22:55.5263512+09:00", - "ModifiedDate": "2025-09-15T15:40:43.9434181+09:00", + "ModifiedDate": "2025-09-16T16:25:28.6358219+09:00", "IsActive": true, - "DisplayColor": "Blue", + "DisplayColor": "Green", "RfidId": "040", "RfidStatus": "정상", "RfidDescription": "", @@ -954,8 +954,8 @@ "NodeId": "N029", "Name": "BUF3", "Position": "231, 639", - "Type": 0, - "DockDirection": null, + "Type": 2, + "DockDirection": 2, "ConnectedNodes": [ "N005" ], @@ -963,9 +963,9 @@ "StationId": "", "StationType": null, "CreatedDate": "2025-09-12T17:22:56.6623294+09:00", - "ModifiedDate": "2025-09-15T15:40:42.4726909+09:00", + "ModifiedDate": "2025-09-16T16:25:34.5699894+09:00", "IsActive": true, - "DisplayColor": "Blue", + "DisplayColor": "Green", "RfidId": "039", "RfidStatus": "정상", "RfidDescription": "", @@ -986,8 +986,8 @@ "NodeId": "N030", "Name": "BUF4", "Position": "314, 639", - "Type": 0, - "DockDirection": null, + "Type": 2, + "DockDirection": 2, "ConnectedNodes": [ "N018" ], @@ -995,9 +995,9 @@ "StationId": "", "StationType": null, "CreatedDate": "2025-09-12T17:22:57.5510908+09:00", - "ModifiedDate": "2025-09-15T15:40:40.8445282+09:00", + "ModifiedDate": "2025-09-16T16:25:40.3838199+09:00", "IsActive": true, - "DisplayColor": "Blue", + "DisplayColor": "Green", "RfidId": "038", "RfidStatus": "정상", "RfidDescription": "", @@ -1019,7 +1019,7 @@ "Name": "", "Position": "337, 397", "Type": 0, - "DockDirection": null, + "DockDirection": 0, "ConnectedNodes": [], "CanRotate": false, "StationId": "", @@ -1045,6 +1045,6 @@ "DisplayText": "N031 - [030]" } ], - "CreatedDate": "2025-09-15T15:40:48.3450265+09:00", + "CreatedDate": "2025-09-16T17:25:55.1597433+09:00", "Version": "1.0" } \ No newline at end of file