refactor: PathFinding 폴더 구조 개선 및 도킹 에러 기능 추가

- PathFinding 폴더를 Core, Validation, Planning, Analysis로 세분화
- 네임스페이스 정리 및 using 문 업데이트
- UnifiedAGVCanvas에 SetDockingError 메서드 추가
- 도킹 검증 시스템 인프라 구축
- DockingValidator 유틸리티 클래스 추가
- 빌드 오류 수정 및 안정성 개선

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
ChiKyun Kim
2025-09-16 14:46:53 +09:00
parent debbf712d4
commit ef72b77f1c
37 changed files with 1085 additions and 2796 deletions

View File

@@ -86,7 +86,7 @@ namespace AGVSimulator.Forms
this._clearPathButton = new System.Windows.Forms.Button();
this._startPathButton = new System.Windows.Forms.Button();
this._calculatePathButton = new System.Windows.Forms.Button();
this._selectTargetButton = new System.Windows.Forms.Button();
this._targetCalcButton = new System.Windows.Forms.Button();
this._avoidRotationCheckBox = new System.Windows.Forms.CheckBox();
this._targetNodeCombo = new System.Windows.Forms.ComboBox();
this.targetNodeLabel = new System.Windows.Forms.Label();
@@ -412,7 +412,7 @@ namespace AGVSimulator.Forms
this._statusGroup.Controls.Add(this._agvCountLabel);
this._statusGroup.Controls.Add(this._simulationStatusLabel);
this._statusGroup.Dock = System.Windows.Forms.DockStyle.Top;
this._statusGroup.Location = new System.Drawing.Point(0, 404);
this._statusGroup.Location = new System.Drawing.Point(0, 446);
this._statusGroup.Name = "_statusGroup";
this._statusGroup.Size = new System.Drawing.Size(233, 100);
this._statusGroup.TabIndex = 3;
@@ -451,7 +451,7 @@ namespace AGVSimulator.Forms
this._pathGroup.Controls.Add(this._clearPathButton);
this._pathGroup.Controls.Add(this._startPathButton);
this._pathGroup.Controls.Add(this._calculatePathButton);
this._pathGroup.Controls.Add(this._selectTargetButton);
this._pathGroup.Controls.Add(this._targetCalcButton);
this._pathGroup.Controls.Add(this._avoidRotationCheckBox);
this._pathGroup.Controls.Add(this._targetNodeCombo);
this._pathGroup.Controls.Add(this.targetNodeLabel);
@@ -460,14 +460,14 @@ namespace AGVSimulator.Forms
this._pathGroup.Dock = System.Windows.Forms.DockStyle.Top;
this._pathGroup.Location = new System.Drawing.Point(0, 214);
this._pathGroup.Name = "_pathGroup";
this._pathGroup.Size = new System.Drawing.Size(233, 190);
this._pathGroup.Size = new System.Drawing.Size(233, 232);
this._pathGroup.TabIndex = 1;
this._pathGroup.TabStop = false;
this._pathGroup.Text = "경로 제어";
//
// _clearPathButton
//
this._clearPathButton.Location = new System.Drawing.Point(150, 148);
this._clearPathButton.Location = new System.Drawing.Point(150, 177);
this._clearPathButton.Name = "_clearPathButton";
this._clearPathButton.Size = new System.Drawing.Size(70, 25);
this._clearPathButton.TabIndex = 6;
@@ -477,7 +477,7 @@ namespace AGVSimulator.Forms
//
// _startPathButton
//
this._startPathButton.Location = new System.Drawing.Point(80, 148);
this._startPathButton.Location = new System.Drawing.Point(80, 177);
this._startPathButton.Name = "_startPathButton";
this._startPathButton.Size = new System.Drawing.Size(65, 25);
this._startPathButton.TabIndex = 5;
@@ -487,24 +487,25 @@ namespace AGVSimulator.Forms
//
// _calculatePathButton
//
this._calculatePathButton.Location = new System.Drawing.Point(10, 148);
this._calculatePathButton.Location = new System.Drawing.Point(10, 177);
this._calculatePathButton.Name = "_calculatePathButton";
this._calculatePathButton.Size = new System.Drawing.Size(65, 25);
this._calculatePathButton.TabIndex = 4;
this._calculatePathButton.Text = "경로 계산";
this._calculatePathButton.UseVisualStyleBackColor = true;
this._calculatePathButton.Click += new System.EventHandler(this.OnCalculatePath_Click);
//
//
// _selectTargetButton
//
this._selectTargetButton.Location = new System.Drawing.Point(80, 148);
this._selectTargetButton.Name = "_selectTargetButton";
this._selectTargetButton.Size = new System.Drawing.Size(70, 25);
this._selectTargetButton.TabIndex = 8;
this._selectTargetButton.Text = "목적지 선택";
this._selectTargetButton.UseVisualStyleBackColor = true;
this._selectTargetButton.Click += new System.EventHandler(this.OnSelectTarget_Click);
//
// _targetCalcButton
//
this._targetCalcButton.Location = new System.Drawing.Point(10, 148);
this._targetCalcButton.Name = "_targetCalcButton";
this._targetCalcButton.Size = new System.Drawing.Size(70, 25);
this._targetCalcButton.TabIndex = 9;
this._targetCalcButton.Text = "타겟계산";
this._targetCalcButton.UseVisualStyleBackColor = true;
this._targetCalcButton.Click += new System.EventHandler(this.OnTargetCalc_Click);
//
// _avoidRotationCheckBox
//
this._avoidRotationCheckBox.AutoSize = true;
@@ -805,7 +806,7 @@ namespace AGVSimulator.Forms
private System.Windows.Forms.Button _calculatePathButton;
private System.Windows.Forms.Button _startPathButton;
private System.Windows.Forms.Button _clearPathButton;
private System.Windows.Forms.Button _selectTargetButton;
private System.Windows.Forms.Button _targetCalcButton;
private System.Windows.Forms.CheckBox _avoidRotationCheckBox;
private System.Windows.Forms.GroupBox _statusGroup;
private System.Windows.Forms.Label _simulationStatusLabel;

View File

@@ -5,12 +5,14 @@ using System.Drawing;
using System.IO;
using System.Linq;
using System.Windows.Forms;
using AGVMapEditor.Models;
using AGVNavigationCore.Models;
using AGVNavigationCore.Controls;
using AGVNavigationCore.PathFinding;
using AGVNavigationCore.Utils;
using AGVSimulator.Models;
using Newtonsoft.Json;
using AGVNavigationCore.PathFinding.Planning;
using AGVNavigationCore.PathFinding.Core;
namespace AGVSimulator.Forms
{
@@ -23,15 +25,13 @@ namespace AGVSimulator.Forms
private UnifiedAGVCanvas _simulatorCanvas;
private List<MapNode> _mapNodes;
private NodeResolver _nodeResolver;
private PathCalculator _pathCalculator;
private AdvancedAGVPathfinder _advancedPathfinder;
private AGVPathfinder _advancedPathfinder;
private List<VirtualAGV> _agvList;
private SimulationState _simulationState;
private Timer _simulationTimer;
private SimulatorConfig _config;
private string _currentMapFilePath;
private bool _isSelectingTarget; // 목적지 선택 모드 상태
private bool _isTargetCalcMode; // 타겟계산 모드 상태
// UI Controls - Designer에서 생성됨
@@ -89,13 +89,10 @@ namespace AGVSimulator.Forms
// 마지막 맵 파일 자동 로드 확인은 Form_Load에서 수행
}
private void CreateSimulatorCanvas()
{
_simulatorCanvas = new UnifiedAGVCanvas();
_simulatorCanvas.Dock = DockStyle.Fill;
_simulatorCanvas.Mode = UnifiedAGVCanvas.CanvasMode.ViewOnly;
// 목적지 선택 이벤트 구독
_simulatorCanvas.TargetNodeSelected += OnTargetNodeSelected;
@@ -272,6 +269,12 @@ namespace AGVSimulator.Forms
private void OnCalculatePath_Click(object sender, EventArgs e)
{
// 시작 RFID가 없으면 AGV 현재 위치로 설정
if (_startNodeCombo.SelectedItem == null || _startNodeCombo.Text == "선택하세요")
{
SetStartNodeFromAGVPosition();
}
if (_startNodeCombo.SelectedItem == null || _targetNodeCombo.SelectedItem == null)
{
MessageBox.Show("시작 RFID와 목표 RFID를 선택해주세요.", "알림", MessageBoxButtons.OK, MessageBoxIcon.Information);
@@ -289,61 +292,40 @@ namespace AGVSimulator.Forms
return;
}
if (_pathCalculator == null)
{
_pathCalculator = new PathCalculator();
_pathCalculator.SetMapData(_mapNodes);
}
if (_advancedPathfinder == null)
{
_advancedPathfinder = new AdvancedAGVPathfinder(_mapNodes);
_advancedPathfinder = new AGVPathfinder(_mapNodes);
}
// 현재 AGV 방향 가져오기
var selectedAGV = _agvListCombo.SelectedItem as VirtualAGV;
var currentDirection = selectedAGV?.CurrentDirection ?? AgvDirection.Forward;
// 고급 경로 계획 사용
var advancedResult = _advancedPathfinder.FindAdvancedPath(startNode.NodeId, targetNode.NodeId, currentDirection);
// 고급 경로 계획 사용 (단일 경로 계산 방식)
var advancedResult = _advancedPathfinder.FindPath(startNode.NodeId, targetNode.NodeId, currentDirection);
if (advancedResult.Success)
{
// 고급 경로 결과를 기존 AGVPathResult 형태로 변환
var agvResult1 = ConvertToAGVPathResult(advancedResult);
// 고급 경로 결과를 AGVPathResult 형태로 변환 (도킹 검증 포함)
var agvResult = ConvertToAGVPathResult(advancedResult, currentDirection);
_simulatorCanvas.CurrentPath = agvResult1.ToPathResult();
_simulatorCanvas.CurrentPath = agvResult;
_pathLengthLabel.Text = $"경로 길이: {advancedResult.TotalDistance:F1}";
_statusLabel.Text = $"고급 경로 계산 완료 ({advancedResult.CalculationTimeMs}ms)";
_statusLabel.Text = $"경로 계산 완료 ({advancedResult.CalculationTimeMs}ms)";
// 도킹 검증 결과 확인 및 UI 표시
CheckAndDisplayDockingValidation(agvResult);
// 고급 경로 디버깅 정보 표시
UpdateAdvancedPathDebugInfo(advancedResult);
return;
}
// 고급 경로 실패시 기존 방식으로 fallback
// 회전 회피 옵션 설정
var options = _avoidRotationCheckBox.Checked
? PathfindingOptions.AvoidRotation
: PathfindingOptions.Default;
var agvResult = _pathCalculator.FindAGVPath(startNode.NodeId, targetNode.NodeId, null, options);
if (agvResult.Success)
{
_simulatorCanvas.CurrentPath = agvResult.ToPathResult();
_pathLengthLabel.Text = $"경로 길이: {agvResult.TotalDistance:F1}";
_statusLabel.Text = $"경로 계산 완료 ({agvResult.CalculationTimeMs}ms)";
// 경로 디버깅 정보 표시
UpdatePathDebugInfo(agvResult);
}
else
{
// 경로 실패시 디버깅 정보 초기화
_pathDebugLabel.Text = $"경로: 실패 - {agvResult.ErrorMessage}";
MessageBox.Show($"경로를 찾을 수 없습니다:\n{agvResult.ErrorMessage}", "경로 계산 실패",
_pathDebugLabel.Text = $"경로: 실패 - {advancedResult.ErrorMessage}";
MessageBox.Show($"경로를 찾을 수 없습니다:\n{advancedResult.ErrorMessage}", "경로 계산 실패",
MessageBoxButtons.OK, MessageBoxIcon.Warning);
}
}
@@ -374,25 +356,25 @@ namespace AGVSimulator.Forms
_statusLabel.Text = "경로 지움";
}
private void OnSelectTarget_Click(object sender, EventArgs e)
private void OnTargetCalc_Click(object sender, EventArgs e)
{
if (_isSelectingTarget)
if (_isTargetCalcMode)
{
// 목적지 선택 모드 해제
_isSelectingTarget = false;
_selectTargetButton.Text = "목적지 선택";
_selectTargetButton.BackColor = SystemColors.Control;
// 타겟계산 모드 해제
_isTargetCalcMode = false;
_targetCalcButton.Text = "타겟계산";
_targetCalcButton.BackColor = SystemColors.Control;
_simulatorCanvas.CurrentEditMode = UnifiedAGVCanvas.EditMode.Select;
_statusLabel.Text = "목적지 선택 모드 해제";
_statusLabel.Text = "타겟계산 모드 해제";
}
else
{
// 목적지 선택 모드 활성화
_isSelectingTarget = true;
_selectTargetButton.Text = "선택 취소";
_selectTargetButton.BackColor = Color.LightBlue;
// 타겟계산 모드 활성화
_isTargetCalcMode = true;
_targetCalcButton.Text = "계산 취소";
_targetCalcButton.BackColor = Color.LightGreen;
_simulatorCanvas.CurrentEditMode = UnifiedAGVCanvas.EditMode.SelectTarget;
_statusLabel.Text = "맵에서 목적지 노드를 클릭하세요";
_statusLabel.Text = "목적지 노드를 클릭하세요 (자동으로 경로 계산됨)";
}
}
@@ -400,35 +382,123 @@ namespace AGVSimulator.Forms
{
try
{
// 목적지 선택 모드 해제
_isSelectingTarget = false;
_selectTargetButton.Text = "목적지 선택";
_selectTargetButton.BackColor = SystemColors.Control;
_simulatorCanvas.CurrentEditMode = UnifiedAGVCanvas.EditMode.Select;
// 목적지 콤보박스에 선택된 노드 설정
var displayText = GetDisplayName(selectedNode.NodeId);
for (int i = 0; i < _targetNodeCombo.Items.Count; i++)
// 타겟계산 모드에서만 처리
if (_isTargetCalcMode)
{
var item = _targetNodeCombo.Items[i].ToString();
if (item.Contains($"[{selectedNode.NodeId}]"))
{
_targetNodeCombo.SelectedIndex = i;
break;
}
// 타겟계산 모드 해제
//_isTargetCalcMode = false;
//_targetCalcButton.Text = "타겟계산";
//_targetCalcButton.BackColor = SystemColors.Control;
//_simulatorCanvas.CurrentEditMode = UnifiedAGVCanvas.EditMode.Select;
// 목적지를 선택된 노드로 설정
SetTargetNodeInCombo(selectedNode.NodeId);
var displayText = GetDisplayName(selectedNode.NodeId);
_statusLabel.Text = $"타겟계산 - 목적지: {displayText}";
// 자동으로 경로 계산 수행
OnCalculatePath_Click(this, EventArgs.Empty);
}
_statusLabel.Text = $"목적지 설정: {displayText}";
// 자동으로 경로 계산 수행
OnCalculatePath_Click(this, EventArgs.Empty);
}
catch (Exception ex)
{
_statusLabel.Text = $"목적지 선택 오류: {ex.Message}";
_statusLabel.Text = $"노드 선택 오류: {ex.Message}";
}
}
/// <summary>
/// 목적지 콤보박스에 노드 설정
/// </summary>
private void SetTargetNodeInCombo(string nodeId)
{
for (int i = 0; i < _targetNodeCombo.Items.Count; i++)
{
var item = _targetNodeCombo.Items[i].ToString();
if (item.Contains($"[{nodeId}]"))
{
_targetNodeCombo.SelectedIndex = i;
break;
}
}
}
/// <summary>
/// AGV 현재 노드로 시작 노드 설정
/// </summary>
private void SetStartNodeFromAGVPosition()
{
try
{
if (_agvList.Count > 0)
{
var agv = _agvList[0]; // 첫 번째 AGV 사용
var currentNodeId = agv.CurrentNodeId;
// AGV가 현재 노드 정보를 가지고 있는 경우 직접 사용
if (!string.IsNullOrEmpty(currentNodeId))
{
// 시작 노드 콤보박스에 설정
for (int i = 0; i < _startNodeCombo.Items.Count; i++)
{
var item = _startNodeCombo.Items[i].ToString();
if (item.Contains($"[{currentNodeId}]"))
{
_startNodeCombo.SelectedIndex = i;
return; // 성공적으로 설정됨
}
}
}
// CurrentNodeId가 없거나 콤보박스에서 찾지 못한 경우 위치 기반으로 폴백
var currentPos = agv.CurrentPosition;
var closestNode = FindClosestNode(currentPos);
if (closestNode != null)
{
// 시작 노드 콤보박스에 설정
for (int i = 0; i < _startNodeCombo.Items.Count; i++)
{
var item = _startNodeCombo.Items[i].ToString();
if (item.Contains($"[{closestNode.NodeId}]"))
{
_startNodeCombo.SelectedIndex = i;
break;
}
}
}
}
}
catch (Exception ex)
{
_statusLabel.Text = $"시작 노드 설정 오류: {ex.Message}";
}
}
/// <summary>
/// 위치에서 가장 가까운 노드 찾기
/// </summary>
private MapNode FindClosestNode(Point position)
{
if (_mapNodes == null || _mapNodes.Count == 0)
return null;
MapNode closestNode = null;
double closestDistance = double.MaxValue;
foreach (var node in _mapNodes)
{
var distance = Math.Sqrt(Math.Pow(node.Position.X - position.X, 2) +
Math.Pow(node.Position.Y - position.Y, 2));
if (distance < closestDistance)
{
closestDistance = distance;
closestNode = node;
}
}
return closestNode;
}
private void OnSetPosition_Click(object sender, EventArgs e)
{
SetAGVPositionByRfid();
@@ -859,9 +929,66 @@ namespace AGVSimulator.Forms
}
/// <summary>
/// 고급 경로 결과를 기존 AGVPathResult 형태로 변환
/// 도킹 검증 결과 확인 및 UI 표시
/// </summary>
private AGVPathResult ConvertToAGVPathResult(AdvancedAGVPathfinder.AdvancedPathResult advancedResult)
private void CheckAndDisplayDockingValidation(AGVPathResult agvResult)
{
if (agvResult?.DockingValidation == null)
return;
var validation = agvResult.DockingValidation;
// 도킹 검증이 필요하지 않은 경우
if (!validation.IsValidationRequired)
return;
// 도킹 검증 실패시 UI에 표시
if (!validation.IsValid)
{
// 상태바에 경고 메시지 표시
_statusLabel.Text = $"⚠️ 도킹 방향 오류: {validation.ValidationError}";
_statusLabel.ForeColor = Color.Red;
// 경로는 표시하되, 목적지 노드에 X 마크 표시 요청
_simulatorCanvas.SetDockingError(validation.TargetNodeId, true);
// 사용자에게 알림
MessageBox.Show($"도킹 방향 검증 실패!\n\n" +
$"노드: {validation.TargetNodeId} ({validation.TargetNodeType})\n" +
$"필요 방향: {GetDirectionText(validation.RequiredDockingDirection)}\n" +
$"계산 방향: {GetDirectionText(validation.CalculatedFinalDirection)}\n\n" +
$"오류: {validation.ValidationError}",
"도킹 검증 실패", MessageBoxButtons.OK, MessageBoxIcon.Warning);
}
else
{
// 도킹 검증 성공시 정상 표시
if (_statusLabel.ForeColor == Color.Red)
_statusLabel.ForeColor = Color.Black;
_simulatorCanvas.SetDockingError(validation.TargetNodeId, false);
}
}
/// <summary>
/// AGV 방향을 한글 텍스트로 변환
/// </summary>
private string GetDirectionText(AgvDirection direction)
{
switch (direction)
{
case AgvDirection.Forward: return "전진";
case AgvDirection.Backward: return "후진";
case AgvDirection.Left: return "좌회전";
case AgvDirection.Right: return "우회전";
case AgvDirection.Stop: return "정지";
default: return direction.ToString();
}
}
/// <summary>
/// 고급 경로 결과를 기존 AGVPathResult 형태로 변환 (도킹 검증 포함)
/// </summary>
private AGVPathResult ConvertToAGVPathResult(AGVPathResult advancedResult, AgvDirection? currentDirection = null)
{
var agvResult = new AGVPathResult();
agvResult.Success = advancedResult.Success;
@@ -871,13 +998,25 @@ namespace AGVSimulator.Forms
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;
}
/// <summary>
/// 고급 경로 디버깅 정보 업데이트
/// </summary>
private void UpdateAdvancedPathDebugInfo(AdvancedAGVPathfinder.AdvancedPathResult advancedResult)
private void UpdateAdvancedPathDebugInfo(AGVPathResult advancedResult)
{
if (advancedResult == null || !advancedResult.Success)
{

View File

@@ -5,6 +5,7 @@ using System.Linq;
using AGVMapEditor.Models;
using AGVNavigationCore.Models;
using AGVNavigationCore.PathFinding;
using AGVNavigationCore.PathFinding.Core;
using AGVNavigationCore.Controls;
namespace AGVSimulator.Models
@@ -36,7 +37,7 @@ namespace AGVSimulator.Models
/// <summary>
/// 경로 완료 이벤트
/// </summary>
public event EventHandler<PathResult> PathCompleted;
public event EventHandler<AGVPathResult> PathCompleted;
/// <summary>
/// 오류 발생 이벤트
@@ -55,7 +56,7 @@ namespace AGVSimulator.Models
private float _currentSpeed;
// 경로 관련
private PathResult _currentPath;
private AGVPathResult _currentPath;
private List<string> _remainingNodes;
private int _currentNodeIndex;
private string _currentNodeId;
@@ -108,7 +109,7 @@ namespace AGVSimulator.Models
/// <summary>
/// 현재 경로
/// </summary>
public PathResult CurrentPath => _currentPath;
public AGVPathResult CurrentPath => _currentPath;
/// <summary>
/// 현재 노드 ID
@@ -180,7 +181,7 @@ namespace AGVSimulator.Models
/// </summary>
/// <param name="path">실행할 경로</param>
/// <param name="mapNodes">맵 노드 목록</param>
public void StartPath(PathResult path, List<MapNode> mapNodes)
public void StartPath(AGVPathResult path, List<MapNode> mapNodes)
{
if (path == null || !path.Success)
{
@@ -287,7 +288,7 @@ namespace AGVSimulator.Models
/// <summary>
/// AGV 위치 직접 설정 (시뮬레이터용)
/// 이전 위치를 TargetPosition로 저장하여 리프트 방향 계산이 가능하도록 함
/// TargetPosition을 이전 위치로 저장하여 리프트 방향 계산이 가능하도록 함
/// </summary>
/// <param name="newPosition">새로운 위치</param>
public void SetPosition(Point newPosition)
@@ -295,12 +296,12 @@ namespace AGVSimulator.Models
// 현재 위치를 이전 위치로 저장 (리프트 방향 계산용)
if (_currentPosition != Point.Empty)
{
_targetPosition = _currentPosition;
_targetPosition = _currentPosition; // 이전 위치 (previousPos 역할)
}
// 새로운 위치 설정
_currentPosition = newPosition;
// 위치 변경 이벤트 발생
PositionChanged?.Invoke(this, _currentPosition);
}
@@ -341,16 +342,15 @@ namespace AGVSimulator.Models
/// <summary>
/// 현재 RFID 시뮬레이션 (현재 위치 기준)
/// </summary>
public string SimulateRfidReading(List<MapNode> mapNodes, List<RfidMapping> rfidMappings)
public string SimulateRfidReading(List<MapNode> mapNodes)
{
// 현재 위치에서 가장 가까운 노드 찾기
var closestNode = FindClosestNode(_currentPosition, mapNodes);
if (closestNode == null)
return null;
// 해당 노드의 RFID 매핑 찾기
var mapping = rfidMappings.FirstOrDefault(m => m.LogicalNodeId == closestNode.NodeId);
return mapping?.RfidId;
// 해당 노드의 RFID 정보 반환 (MapNode에 RFID 정보 포함)
return closestNode.HasRfid() ? closestNode.RfidId : null;
}
#endregion