fix: Add motor direction parameter to magnet direction calculation in pathfinding

- 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>
This commit is contained in:
backuppc
2025-10-24 15:46:16 +09:00
parent 3ddecf63ed
commit d932b8d332
47 changed files with 7473 additions and 1088 deletions

View File

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
@@ -46,7 +46,6 @@
</ItemGroup>
<ItemGroup>
<Compile Include="Models\SimulatorConfig.cs" />
<Compile Include="Models\VirtualAGV.cs" />
<Compile Include="Models\SimulationState.cs" />
<Compile Include="Forms\SimulatorForm.cs">
<SubType>Form</SubType>

View File

@@ -93,6 +93,7 @@ namespace AGVSimulator.Forms
this._startNodeCombo = new System.Windows.Forms.ComboBox();
this.startNodeLabel = new System.Windows.Forms.Label();
this._agvControlGroup = new System.Windows.Forms.GroupBox();
this.btNextNode = new System.Windows.Forms.Button();
this._setPositionButton = new System.Windows.Forms.Button();
this._rfidTextBox = new System.Windows.Forms.TextBox();
this._rfidLabel = new System.Windows.Forms.Label();
@@ -108,7 +109,7 @@ namespace AGVSimulator.Forms
this._agvInfoTitleLabel = new System.Windows.Forms.Label();
this._liftDirectionLabel = new System.Windows.Forms.Label();
this._motorDirectionLabel = new System.Windows.Forms.Label();
this._pathDebugLabel = new System.Windows.Forms.Label();
this._pathDebugLabel = new System.Windows.Forms.TextBox();
this._menuStrip.SuspendLayout();
this._toolStrip.SuspendLayout();
this._statusStrip.SuspendLayout();
@@ -495,7 +496,6 @@ namespace AGVSimulator.Forms
this._calculatePathButton.UseVisualStyleBackColor = true;
this._calculatePathButton.Click += new System.EventHandler(this.OnCalculatePath_Click);
//
//
// _targetCalcButton
//
this._targetCalcButton.Location = new System.Drawing.Point(10, 148);
@@ -552,6 +552,7 @@ namespace AGVSimulator.Forms
//
// _agvControlGroup
//
this._agvControlGroup.Controls.Add(this.btNextNode);
this._agvControlGroup.Controls.Add(this._setPositionButton);
this._agvControlGroup.Controls.Add(this._rfidTextBox);
this._agvControlGroup.Controls.Add(this._rfidLabel);
@@ -570,11 +571,21 @@ namespace AGVSimulator.Forms
this._agvControlGroup.TabStop = false;
this._agvControlGroup.Text = "AGV 제어";
//
// btNextNode
//
this.btNextNode.Location = new System.Drawing.Point(160, 183);
this.btNextNode.Name = "btNextNode";
this.btNextNode.Size = new System.Drawing.Size(60, 21);
this.btNextNode.TabIndex = 10;
this.btNextNode.Text = "다음";
this.btNextNode.UseVisualStyleBackColor = true;
this.btNextNode.Click += new System.EventHandler(this.btNextNode_Click);
//
// _setPositionButton
//
this._setPositionButton.Location = new System.Drawing.Point(160, 138);
this._setPositionButton.Name = "_setPositionButton";
this._setPositionButton.Size = new System.Drawing.Size(60, 67);
this._setPositionButton.Size = new System.Drawing.Size(60, 39);
this._setPositionButton.TabIndex = 7;
this._setPositionButton.Text = "위치설정";
this._setPositionButton.UseVisualStyleBackColor = true;
@@ -667,30 +678,30 @@ namespace AGVSimulator.Forms
// _canvasPanel
//
this._canvasPanel.Dock = System.Windows.Forms.DockStyle.Fill;
this._canvasPanel.Location = new System.Drawing.Point(0, 109);
this._canvasPanel.Location = new System.Drawing.Point(0, 129);
this._canvasPanel.Name = "_canvasPanel";
this._canvasPanel.Size = new System.Drawing.Size(967, 669);
this._canvasPanel.Size = new System.Drawing.Size(967, 649);
this._canvasPanel.TabIndex = 4;
//
// _agvInfoPanel
//
this._agvInfoPanel.BackColor = System.Drawing.Color.LightBlue;
this._agvInfoPanel.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
this._agvInfoPanel.Controls.Add(this._pathDebugLabel);
this._agvInfoPanel.Controls.Add(this._agvInfoTitleLabel);
this._agvInfoPanel.Controls.Add(this._liftDirectionLabel);
this._agvInfoPanel.Controls.Add(this._motorDirectionLabel);
this._agvInfoPanel.Controls.Add(this._pathDebugLabel);
this._agvInfoPanel.Dock = System.Windows.Forms.DockStyle.Top;
this._agvInfoPanel.Location = new System.Drawing.Point(0, 49);
this._agvInfoPanel.Name = "_agvInfoPanel";
this._agvInfoPanel.Size = new System.Drawing.Size(967, 60);
this._agvInfoPanel.Size = new System.Drawing.Size(967, 80);
this._agvInfoPanel.TabIndex = 5;
//
// _agvInfoTitleLabel
//
this._agvInfoTitleLabel.AutoSize = true;
this._agvInfoTitleLabel.Font = new System.Drawing.Font("맑은 고딕", 9F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(129)));
this._agvInfoTitleLabel.Location = new System.Drawing.Point(10, 12);
this._agvInfoTitleLabel.Location = new System.Drawing.Point(10, 8);
this._agvInfoTitleLabel.Name = "_agvInfoTitleLabel";
this._agvInfoTitleLabel.Size = new System.Drawing.Size(91, 15);
this._agvInfoTitleLabel.TabIndex = 0;
@@ -700,7 +711,7 @@ namespace AGVSimulator.Forms
//
this._liftDirectionLabel.AutoSize = true;
this._liftDirectionLabel.Font = new System.Drawing.Font("맑은 고딕", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(129)));
this._liftDirectionLabel.Location = new System.Drawing.Point(120, 12);
this._liftDirectionLabel.Location = new System.Drawing.Point(120, 8);
this._liftDirectionLabel.Name = "_liftDirectionLabel";
this._liftDirectionLabel.Size = new System.Drawing.Size(83, 15);
this._liftDirectionLabel.TabIndex = 1;
@@ -710,7 +721,7 @@ namespace AGVSimulator.Forms
//
this._motorDirectionLabel.AutoSize = true;
this._motorDirectionLabel.Font = new System.Drawing.Font("맑은 고딕", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(129)));
this._motorDirectionLabel.Location = new System.Drawing.Point(250, 12);
this._motorDirectionLabel.Location = new System.Drawing.Point(250, 8);
this._motorDirectionLabel.Name = "_motorDirectionLabel";
this._motorDirectionLabel.Size = new System.Drawing.Size(71, 15);
this._motorDirectionLabel.TabIndex = 2;
@@ -718,13 +729,14 @@ namespace AGVSimulator.Forms
//
// _pathDebugLabel
//
this._pathDebugLabel.AutoSize = true;
this._pathDebugLabel.Font = new System.Drawing.Font("맑은 고딕", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(129)));
this._pathDebugLabel.ForeColor = System.Drawing.Color.DarkBlue;
this._pathDebugLabel.BackColor = System.Drawing.Color.LightBlue;
this._pathDebugLabel.BorderStyle = System.Windows.Forms.BorderStyle.None;
this._pathDebugLabel.Font = new System.Drawing.Font("굴림", 8.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(129)));
this._pathDebugLabel.Location = new System.Drawing.Point(10, 30);
this._pathDebugLabel.Multiline = true;
this._pathDebugLabel.Name = "_pathDebugLabel";
this._pathDebugLabel.Size = new System.Drawing.Size(114, 15);
this._pathDebugLabel.TabIndex = 3;
this._pathDebugLabel.Size = new System.Drawing.Size(947, 43);
this._pathDebugLabel.TabIndex = 4;
this._pathDebugLabel.Text = "경로: 설정되지 않음";
//
// SimulatorForm
@@ -828,6 +840,7 @@ namespace AGVSimulator.Forms
private System.Windows.Forms.Label _liftDirectionLabel;
private System.Windows.Forms.Label _motorDirectionLabel;
private System.Windows.Forms.Label _agvInfoTitleLabel;
private System.Windows.Forms.Label _pathDebugLabel;
private System.Windows.Forms.Button btNextNode;
private System.Windows.Forms.TextBox _pathDebugLabel;
}
}

View File

@@ -303,28 +303,17 @@ namespace AGVSimulator.Forms
var currentDirection = selectedAGV?.CurrentDirection ?? AgvDirection.Forward;
// AGV의 이전 위치에서 가장 가까운 노드 찾기
MapNode prevNode = startNode; // 기본값으로 시작 노드 사용
if (selectedAGV != null && _mapNodes != null && _mapNodes.Count > 0)
{
// AGV 현재 위치에서 가장 가까운 노드 찾기
var agvPos = selectedAGV.CurrentPosition;
prevNode = _mapNodes.OrderBy(n =>
Math.Sqrt(Math.Pow(n.Position.X - agvPos.X, 2) +
Math.Pow(n.Position.Y - agvPos.Y, 2))).FirstOrDefault();
if (prevNode == null)
prevNode = startNode;
}
var prevNode = selectedAGV?.PrevNode;
// 고급 경로 계획 사용 (노드 객체 직접 전달)
var advancedResult = _advancedPathfinder.FindPath(startNode, targetNode, prevNode, currentDirection);
var advancedResult = _advancedPathfinder.FindPath_test(startNode, targetNode, prevNode, currentDirection);
if (advancedResult.Success)
{
// 도킹 검증이 없는 경우 추가 검증 수행
if (advancedResult.DockingValidation == null || !advancedResult.DockingValidation.IsValidationRequired)
{
advancedResult.DockingValidation = DockingValidator.ValidateDockingDirection(advancedResult, _mapNodes, currentDirection);
advancedResult.DockingValidation = DockingValidator.ValidateDockingDirection(advancedResult, _mapNodes);
}
_simulatorCanvas.CurrentPath = advancedResult;
@@ -582,45 +571,77 @@ namespace AGVSimulator.Forms
var selectedDirectionItem = _directionCombo.SelectedItem as DirectionItem;
var selectedDirection = selectedDirectionItem?.Direction ?? AgvDirection.Forward;
// 콘솔 출력 (상세한 리프트 방향 계산 과정)
//이전위치와 동일한지 체크한다.
if(selectedAGV.CurrentNodeId == targetNode.NodeId && selectedAGV.CurrentDirection == selectedDirection)
{
Program.WriteLine($"이전 노드위치와 모터의 방향이 동일하여 현재 위치 변경이 취소됩니다(NODE:{targetNode.NodeId},RFID:{targetNode.RfidId},DIR:{selectedDirection})");
return;
}
// 콘솔 출력 (상세한 리프트 방향 계산 과정)
Program.WriteLine($"[AGV-{selectedAGV.AgvId}] 위치 설정:");
Program.WriteLine($" RFID: {rfidId} → 노드: {targetNode.NodeId}");
Program.WriteLine($" 새로운 위치: ({targetNode.Position.X}, {targetNode.Position.Y})");
Program.WriteLine($" 모터 방향: {selectedDirectionItem?.DisplayText ?? ""} ({selectedDirection})");
Program.WriteLine($" 위치: ({targetNode.Position.X}, {targetNode.Position.Y})");
Program.WriteLine($" 방향: {selectedDirectionItem?.DisplayText ?? ""} ({selectedDirection})");
// SetPosition 호출 전 상태
var oldTargetPos = selectedAGV.TargetPosition;
var oldCurrentPos = selectedAGV.CurrentPosition;
Program.WriteLine($" [BEFORE] 현재 CurrentPosition: ({oldCurrentPos.X}, {oldCurrentPos.Y})");
Program.WriteLine($" [BEFORE] 이전 TargetPosition: {(oldTargetPos.HasValue ? $"({oldTargetPos.Value.X}, {oldTargetPos.Value.Y})" : "None")}");
var PrevNodeID = selectedAGV.CurrentNodeId;
var PrevDir = selectedAGV.CurrentDirection;
var PrevPosition = selectedAGV.CurrentPosition;
Program.WriteLine($" [BEFORE] Node:{PrevNodeID}, Dir:{PrevDir},Pos X:{PrevPosition.X},{PrevPosition.Y}");
// AGV 위치 및 방향 설정
_simulatorCanvas.SetAGVPosition(selectedAGV.AgvId, targetNode.Position);
_simulatorCanvas.SetAGVPosition(selectedAGV.AgvId, targetNode, selectedDirection);
_simulatorCanvas.UpdateAGVDirection(selectedAGV.AgvId, selectedDirection);
// VirtualAGV 객체의 위치와 방향 업데이트
selectedAGV.SetPosition(targetNode, targetNode.Position, selectedDirection); // 이전 위치 기억하도록
selectedAGV.SetPosition(targetNode, selectedDirection); // 이전 위치 기억하도록
// SetPosition 호출 후 상태 확인 및 리프트 계산
var newTargetPos = selectedAGV.TargetPosition;
var newPrevPos = selectedAGV.PrevPosition;
var newCurrentPos = selectedAGV.CurrentPosition;
Program.WriteLine($" [AFTER] 새로운 CurrentPosition: ({newCurrentPos.X}, {newCurrentPos.Y})");
Program.WriteLine($" [AFTER] 새로운 TargetPosition: {(newTargetPos.HasValue ? $"({newTargetPos.Value.X}, {newTargetPos.Value.Y})" : "None")}");
Program.WriteLine($" [AFTER] 새로운 PrevPosition: {(newPrevPos.HasValue ? $"({newPrevPos.Value.X}, {newPrevPos.Value.Y})" : "None")}");
// 리프트 방향 계산 과정 상세 출력
Program.WriteLine($" [LIFT] 리프트 방향 계산:");
CalculateLiftDirectionDetailed(selectedAGV);
Program.WriteLine("");
_statusLabel.Text = $"{selectedAGV.AgvId} 위치를 RFID '{rfidId}' (노드: {targetNode.NodeId}), 방향: {selectedDirectionItem?.DisplayText ?? ""}로 설정했습니다.";
_rfidTextBox.Text = ""; // 입력 필드 초기화
// 시뮬레이터 캔버스의 해당 노드로 이동
_simulatorCanvas.PanToNode(targetNode.NodeId);
// 시작 노드 콤보박스를 현재 위치로 자동 선택
SetStartNodeToCombo(targetNode.NodeId);
}
/// <summary>
/// 시작 노드 콤보박스에 노드를 설정
/// </summary>
private void SetStartNodeToCombo(string nodeId)
{
try
{
for (int i = 0; i < _startNodeCombo.Items.Count; i++)
{
var item = _startNodeCombo.Items[i].ToString();
if (item.Contains($"[{nodeId}]"))
{
_startNodeCombo.SelectedIndex = i;
Program.WriteLine($"[SYSTEM] 시작 노드를 '{nodeId}'로 자동 선택했습니다.");
break;
}
}
}
catch (Exception ex)
{
Program.WriteLine($"[ERROR] 시작 노드 자동 선택 실패: {ex.Message}");
}
}
private string GetAvailableRfidList()
@@ -833,22 +854,22 @@ namespace AGVSimulator.Forms
private void CalculateLiftDirectionDetailed(VirtualAGV agv)
{
var currentPos = agv.CurrentPosition;
var targetPos = agv.TargetPosition;
var prevPos = agv.PrevPosition;
var dockingDirection = agv.DockingDirection;
Program.WriteLine($" 입력값: CurrentPos=({currentPos.X}, {currentPos.Y})");
Program.WriteLine($" 입력값: TargetPos={(!targetPos.HasValue ? "None" : $"({targetPos.Value.X}, {targetPos.Value.Y})")}");
Program.WriteLine($" 입력값: prevPos={(!prevPos.HasValue ? "None" : $"({prevPos.Value.X}, {prevPos.Value.Y})")}");
Program.WriteLine($" 입력값: DockingDirection={dockingDirection}");
if (!targetPos.HasValue || targetPos.Value == currentPos)
if (!prevPos.HasValue || prevPos.Value == currentPos)
{
Program.WriteLine($" 결과: 방향을 알 수 없음 (TargetPos 없음 또는 같은 위치)");
Program.WriteLine($" 결과: 방향을 알 수 없음 (이전 위치값 없음 또는 같은 위치)");
return;
}
// 이동 방향 계산 (이전 → 현재 = TargetPos → CurrentPos)
var dx = currentPos.X - targetPos.Value.X;
var dy = currentPos.Y - targetPos.Value.Y;
var dx = currentPos.X - prevPos.Value.X;
var dy = currentPos.Y - prevPos.Value.Y;
Program.WriteLine($" 이동 벡터: dx={dx}, dy={dy}");
if (Math.Abs(dx) < 1 && Math.Abs(dy) < 1)
@@ -859,7 +880,7 @@ namespace AGVSimulator.Forms
// 경로 예측 기반 LiftCalculator를 사용하여 리프트 방향 계산
var liftInfo = AGVNavigationCore.Utils.LiftCalculator.CalculateLiftInfoWithPathPrediction(
currentPos, targetPos.Value, agv.CurrentDirection, _mapNodes);
currentPos, prevPos.Value, agv.CurrentDirection, _mapNodes);
// 이동 각도 계산 (표시용)
var moveAngleRad = Math.Atan2(dy, dx);
@@ -883,7 +904,7 @@ namespace AGVSimulator.Forms
private string CalculateLiftDirection(VirtualAGV agv)
{
var currentPos = agv.CurrentPosition;
var targetPos = agv.TargetPosition;
var targetPos = agv.PrevPosition;
var dockingDirection = agv.DockingDirection;
if (!targetPos.HasValue || targetPos.Value == currentPos)
@@ -1063,14 +1084,15 @@ namespace AGVSimulator.Forms
{
var motorInfo = advancedResult.DetailedPath[i];
var rfidId = GetRfidByNodeId(motorInfo.NodeId);
string motorSymbol = motorInfo.MotorDirection == AgvDirection.Forward ? "[전진]" : "[후진]";
string motorSymbol = motorInfo.MotorDirection == AgvDirection.Forward ? "[F]" : "[B]";
// 마그넷 방향 표시
if (motorInfo.MagnetDirection != MagnetDirection.Straight)
{
string magnetSymbol = motorInfo.MagnetDirection == MagnetDirection.Left ? "[]" : "[]";
string magnetSymbol = motorInfo.MagnetDirection == MagnetDirection.Left ? "[L]" : "[R]";
motorSymbol += magnetSymbol;
}
else motorSymbol += "[S]";
// 특수 동작 표시
if (motorInfo.RequiresSpecialAction)
@@ -1084,10 +1106,10 @@ namespace AGVSimulator.Forms
string pathString = string.Join(" → ", pathWithDetails);
// UI에 표시 (길이 제한)
if (pathString.Length > 100)
{
pathString = pathString.Substring(0, 97) + "...";
}
//if (pathString.Length > 100)
//{
// pathString = pathString.Substring(0, 97) + "...";
//}
// 통계 정보
var forwardCount = advancedResult.DetailedPath.Count(m => m.MotorDirection == AgvDirection.Forward);
@@ -1101,89 +1123,7 @@ namespace AGVSimulator.Forms
_pathDebugLabel.Text = $"고급경로: {pathString} (총 {advancedResult.DetailedPath.Count}개 노드, {advancedResult.TotalDistance:F1}px, {stats})";
}
/// <summary>
/// 경로 디버깅 정보 업데이트 (RFID 값 표시, 모터방향 정보 포함)
/// </summary>
private void UpdatePathDebugInfo(AGVPathResult agvResult)
{
if (agvResult == null || !agvResult.Success)
{
_pathDebugLabel.Text = "경로: 설정되지 않음";
return;
}
// 노드 ID를 RFID로 변환한 경로 생성
var pathWithRfid = agvResult.Path.Select(nodeId => GetRfidByNodeId(nodeId)).ToList();
// 콘솔 디버그 정보 출력 (RFID 기준)
Program.WriteLine($"[DEBUG] 경로 계산 완료:");
Program.WriteLine($" 전체 경로 (RFID): [{string.Join(" ", pathWithRfid)}]");
Program.WriteLine($" 전체 경로 (NodeID): [{string.Join(" ", agvResult.Path)}]");
Program.WriteLine($" 경로 노드 수: {agvResult.Path.Count}");
if (agvResult.NodeMotorInfos != null)
{
Program.WriteLine($" 모터정보 수: {agvResult.NodeMotorInfos.Count}");
for (int i = 0; i < agvResult.NodeMotorInfos.Count; i++)
{
var info = agvResult.NodeMotorInfos[i];
var rfidId = GetRfidByNodeId(info.NodeId);
var nextRfidId = info.NextNodeId != null ? GetRfidByNodeId(info.NextNodeId) : "END";
var flags = new List<string>();
if (info.CanRotate) flags.Add("회전가능");
if (info.IsDirectionChangePoint) flags.Add("방향전환");
if (info.RequiresSpecialAction) flags.Add($"특수동작:{info.SpecialActionDescription}");
var flagsStr = flags.Count > 0 ? $" [{string.Join(", ", flags)}]" : "";
Program.WriteLine($" {i}: {rfidId}({info.NodeId}) → {info.MotorDirection} → {nextRfidId}{flagsStr}");
}
}
// 모터방향 정보가 있다면 이를 포함하여 경로 문자열 구성 (RFID 기준)
string pathString;
if (agvResult.NodeMotorInfos != null && agvResult.NodeMotorInfos.Count > 0)
{
// RFID별 모터방향 정보를 포함한 경로 문자열 생성
var pathWithMotorInfo = new List<string>();
for (int i = 0; i < agvResult.NodeMotorInfos.Count; i++)
{
var motorInfo = agvResult.NodeMotorInfos[i];
var rfidId = GetRfidByNodeId(motorInfo.NodeId);
string motorSymbol = motorInfo.MotorDirection == AgvDirection.Forward ? "[전진]" : "[후진]";
// 특수 동작 표시 추가
if (motorInfo.RequiresSpecialAction)
motorSymbol += "[🔄]";
else if (motorInfo.IsDirectionChangePoint && motorInfo.CanRotate)
motorSymbol += "[↻]";
pathWithMotorInfo.Add($"{rfidId}{motorSymbol}");
}
pathString = string.Join(" → ", pathWithMotorInfo);
}
else
{
// 기본 경로 정보만 표시 (RFID 기준)
pathString = string.Join(" → ", pathWithRfid);
}
// UI에 표시 (길이 제한)
if (pathString.Length > 120)
{
pathString = pathString.Substring(0, 117) + "...";
}
// 모터방향 통계 추가
string motorStats = "";
if (agvResult.NodeMotorInfos != null && agvResult.NodeMotorInfos.Count > 0)
{
var forwardCount = agvResult.NodeMotorInfos.Count(m => m.MotorDirection == AgvDirection.Forward);
var backwardCount = agvResult.NodeMotorInfos.Count(m => m.MotorDirection == AgvDirection.Backward);
motorStats = $", 전진: {forwardCount}, 후진: {backwardCount}";
}
_pathDebugLabel.Text = $"경로: {pathString} (총 {agvResult.Path.Count}개 노드, {agvResult.TotalDistance:F1}px{motorStats})";
}
private void OnReloadMap_Click(object sender, EventArgs e)
{
@@ -1294,6 +1234,27 @@ namespace AGVSimulator.Forms
_statusLabel.Text = "초기화 완료";
}
private void btNextNode_Click(object sender, EventArgs e)
{
//get next node
// 선택된 AGV 확인
var selectedAGV = _agvListCombo.SelectedItem as VirtualAGV;
if (selectedAGV == null)
{
MessageBox.Show("먼저 AGV를 선택해주세요.", "알림", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
// 선택된 방향 확인
var selectedDirectionItem = _directionCombo.SelectedItem as DirectionItem;
var selectedDirection = selectedDirectionItem?.Direction ?? AgvDirection.Forward;
var nextNode = selectedAGV.GetNextNodeId(selectedDirection, this._mapNodes);
MessageBox.Show($"Node:{nextNode.NodeId},RFID:{nextNode.RfidId}");
}
}
/// <summary>

View File

@@ -1,561 +0,0 @@
//using System;
//using System.Collections.Generic;
//using System.Drawing;
//using System.Linq;
//using AGVMapEditor.Models;
//using AGVNavigationCore.Models;
//using AGVNavigationCore.PathFinding;
//using AGVNavigationCore.PathFinding.Core;
//using AGVNavigationCore.Controls;
//namespace AGVSimulator.Models
//{
// /// <summary>
// /// 가상 AGV 클래스
// /// 실제 AGV의 동작을 시뮬레이션
// /// </summary>
// public class VirtualAGV : IAGV
// {
// #region Events
// /// <summary>
// /// AGV 상태 변경 이벤트
// /// </summary>
// public event EventHandler<AGVState> StateChanged;
// /// <summary>
// /// 위치 변경 이벤트
// /// </summary>
// public event EventHandler<(Point, AgvDirection, MapNode)> PositionChanged;
// /// <summary>
// /// RFID 감지 이벤트
// /// </summary>
// public event EventHandler<string> RfidDetected;
// /// <summary>
// /// 경로 완료 이벤트
// /// </summary>
// public event EventHandler<AGVPathResult> PathCompleted;
// /// <summary>
// /// 오류 발생 이벤트
// /// </summary>
// public event EventHandler<string> ErrorOccurred;
// #endregion
// #region Fields
// private string _agvId;
// private Point _currentPosition;
// private Point _targetPosition;
// private string _targetId;
// private string _currentId;
// private AgvDirection _currentDirection;
// private AgvDirection _targetDirection;
// private AGVState _currentState;
// private float _currentSpeed;
// // 경로 관련
// private AGVPathResult _currentPath;
// private List<string> _remainingNodes;
// private int _currentNodeIndex;
// private MapNode _currentNode;
// private MapNode _targetNode;
// // 이동 관련
// private System.Windows.Forms.Timer _moveTimer;
// private DateTime _lastMoveTime;
// private Point _moveStartPosition;
// private Point _moveTargetPosition;
// private float _moveProgress;
// // 도킹 관련
// private DockingDirection _dockingDirection;
// // 시뮬레이션 설정
// private readonly float _moveSpeed = 50.0f; // 픽셀/초
// private readonly float _rotationSpeed = 90.0f; // 도/초
// private readonly int _updateInterval = 50; // ms
// #endregion
// #region Properties
// /// <summary>
// /// AGV ID
// /// </summary>
// public string AgvId => _agvId;
// /// <summary>
// /// 현재 위치
// /// </summary>
// public Point CurrentPosition
// {
// get => _currentPosition;
// set => _currentPosition = value;
// }
// /// <summary>
// /// 현재 방향
// /// 모터의 동작 방향
// /// </summary>
// public AgvDirection CurrentDirection
// {
// get => _currentDirection;
// set => _currentDirection = value;
// }
// /// <summary>
// /// 현재 상태
// /// </summary>
// public AGVState CurrentState
// {
// get => _currentState;
// set => _currentState = value;
// }
// /// <summary>
// /// 현재 속도
// /// </summary>
// public float CurrentSpeed => _currentSpeed;
// /// <summary>
// /// 현재 경로
// /// </summary>
// public AGVPathResult CurrentPath => _currentPath;
// /// <summary>
// /// 현재 노드 ID
// /// </summary>
// public string CurrentNodeId => _currentNode.NodeId;
// /// <summary>
// /// 목표 위치
// /// </summary>
// public Point? TargetPosition => _targetPosition;
// /// <summary>
// /// 배터리 레벨 (시뮬레이션)
// /// </summary>
// public float BatteryLevel { get; set; } = 100.0f;
// /// <summary>
// /// 목표 노드 ID
// /// </summary>
// public string TargetNodeId => _targetNode.NodeId;
// /// <summary>
// /// 도킹 방향
// /// </summary>
// public DockingDirection DockingDirection => _dockingDirection;
// #endregion
// #region Constructor
// /// <summary>
// /// 생성자
// /// </summary>
// /// <param name="agvId">AGV ID</param>
// /// <param name="startPosition">시작 위치</param>
// /// <param name="startDirection">시작 방향</param>
// public VirtualAGV(string agvId, Point startPosition, AgvDirection startDirection = AgvDirection.Forward)
// {
// _agvId = agvId;
// _currentPosition = startPosition;
// _currentDirection = startDirection;
// _currentState = AGVState.Idle;
// _currentSpeed = 0;
// _dockingDirection = DockingDirection.Forward; // 기본값: 전진 도킹
// _currentNode = null; // = string.Empty;
// _targetNode = null;// string.Empty;
// InitializeTimer();
// }
// #endregion
// #region Initialization
// private void InitializeTimer()
// {
// _moveTimer = new System.Windows.Forms.Timer();
// _moveTimer.Interval = _updateInterval;
// _moveTimer.Tick += OnMoveTimer_Tick;
// _lastMoveTime = DateTime.Now;
// }
// #endregion
// #region Public Methods
// /// <summary>
// /// 경로 실행 시작
// /// </summary>
// /// <param name="path">실행할 경로</param>
// /// <param name="mapNodes">맵 노드 목록</param>
// public void StartPath(AGVPathResult path, List<MapNode> mapNodes)
// {
// if (path == null || !path.Success)
// {
// OnError("유효하지 않은 경로입니다.");
// return;
// }
// _currentPath = path;
// _remainingNodes = new List<string>(path.Path);
// _currentNodeIndex = 0;
// // 시작 노드와 목표 노드 설정
// if (_remainingNodes.Count > 0)
// {
// var startNode = mapNodes.FirstOrDefault(n => n.NodeId == _remainingNodes[0]);
// if (startNode != null)
// {
// _currentNode = startNode;
// // 목표 노드 설정 (경로의 마지막 노드)
// if (_remainingNodes.Count > 1)
// {
// var _targetNodeId = _remainingNodes[_remainingNodes.Count - 1];
// var targetNode = mapNodes.FirstOrDefault(n => n.NodeId == _targetNodeId);
// // 목표 노드의 타입에 따라 도킹 방향 결정
// if (targetNode != null)
// {
// _dockingDirection = GetDockingDirection(targetNode.Type);
// }
// }
// StartMovement();
// }
// else
// {
// OnError($"시작 노드를 찾을 수 없습니다: {_remainingNodes[0]}");
// }
// }
// }
// /// <summary>
// /// 경로 정지
// /// </summary>
// public void StopPath()
// {
// _moveTimer.Stop();
// _currentPath = null;
// _remainingNodes?.Clear();
// SetState(AGVState.Idle);
// _currentSpeed = 0;
// }
// /// <summary>
// /// 긴급 정지
// /// </summary>
// public void EmergencyStop()
// {
// StopPath();
// OnError("긴급 정지가 실행되었습니다.");
// }
// /// <summary>
// /// 수동 이동 (테스트용)
// /// </summary>
// /// <param name="targetPosition">목표 위치</param>
// public void MoveTo(Point targetPosition)
// {
// _targetPosition = targetPosition;
// _moveStartPosition = _currentPosition;
// _moveTargetPosition = targetPosition;
// _moveProgress = 0;
// SetState(AGVState.Moving);
// _moveTimer.Start();
// }
// /// <summary>
// /// 수동 회전 (테스트용)
// /// </summary>
// /// <param name="direction">회전 방향</param>
// public void Rotate(AgvDirection direction)
// {
// if (_currentState != AGVState.Idle)
// return;
// SetState(AGVState.Rotating);
// // 시뮬레이션: 즉시 방향 변경 (실제로는 시간이 걸림)
// _currentDirection = direction;
// System.Threading.Thread.Sleep(500); // 회전 시간 시뮬레이션
// SetState(AGVState.Idle);
// }
// /// <summary>
// /// AGV 위치 직접 설정 (시뮬레이터용)
// /// TargetPosition을 이전 위치로 저장하여 리프트 방향 계산이 가능하도록 함
// /// </summary>
// /// <param name="newPosition">새로운 위치</param>
// /// <param name="motorDirection">모터이동방향</param>
// public void SetPosition(MapNode node, Point newPosition, AgvDirection motorDirection)
// {
// // 현재 위치를 이전 위치로 저장 (리프트 방향 계산용)
// if (_currentPosition != Point.Empty)
// {
// _targetPosition = _currentPosition; // 이전 위치 (previousPos 역할)
// _targetDirection = _currentDirection;
// _targetNode = node;
// }
// // 새로운 위치 설정
// _currentPosition = newPosition;
// _currentDirection = motorDirection;
// _currentNode = node;
// // 위치 변경 이벤트 발생
// PositionChanged?.Invoke(this, (_currentPosition, _currentDirection, _currentNode));
// }
// /// <summary>
// /// 충전 시작 (시뮬레이션)
// /// </summary>
// public void StartCharging()
// {
// if (_currentState == AGVState.Idle)
// {
// SetState(AGVState.Charging);
// // 충전 시뮬레이션 시작
// }
// }
// /// <summary>
// /// 충전 종료
// /// </summary>
// public void StopCharging()
// {
// if (_currentState == AGVState.Charging)
// {
// SetState(AGVState.Idle);
// }
// }
// /// <summary>
// /// AGV 정보 조회
// /// </summary>
// public string GetStatus()
// {
// return $"AGV[{_agvId}] 위치:({_currentPosition.X},{_currentPosition.Y}) " +
// $"방향:{_currentDirection} 상태:{_currentState} " +
// $"속도:{_currentSpeed:F1} 배터리:{BatteryLevel:F1}%";
// }
// /// <summary>
// /// 현재 RFID 시뮬레이션 (현재 위치 기준)
// /// </summary>
// public string SimulateRfidReading(List<MapNode> mapNodes)
// {
// // 현재 위치에서 가장 가까운 노드 찾기
// var closestNode = FindClosestNode(_currentPosition, mapNodes);
// if (closestNode == null)
// return null;
// // 해당 노드의 RFID 정보 반환 (MapNode에 RFID 정보 포함)
// return closestNode.HasRfid() ? closestNode.RfidId : null;
// }
// #endregion
// #region Private Methods
// private void StartMovement()
// {
// SetState(AGVState.Moving);
// _moveTimer.Start();
// _lastMoveTime = DateTime.Now;
// }
// private void OnMoveTimer_Tick(object sender, EventArgs e)
// {
// var now = DateTime.Now;
// var deltaTime = (float)(now - _lastMoveTime).TotalSeconds;
// _lastMoveTime = now;
// UpdateMovement(deltaTime);
// UpdateBattery(deltaTime);
// // 위치 변경 이벤트 발생
// PositionChanged?.Invoke(this, (_currentPosition, _currentDirection, _currentNode));
// }
// private void UpdateMovement(float deltaTime)
// {
// if (_currentState != AGVState.Moving)
// return;
// // 목표 위치까지의 거리 계산
// var distance = CalculateDistance(_currentPosition, _moveTargetPosition);
// if (distance < 5.0f) // 도달 임계값
// {
// // 목표 도달
// _currentPosition = _moveTargetPosition;
// _currentSpeed = 0;
// // 다음 노드로 이동
// ProcessNextNode();
// }
// else
// {
// // 계속 이동
// var moveDistance = _moveSpeed * deltaTime;
// var direction = new PointF(
// _moveTargetPosition.X - _currentPosition.X,
// _moveTargetPosition.Y - _currentPosition.Y
// );
// // 정규화
// var length = (float)Math.Sqrt(direction.X * direction.X + direction.Y * direction.Y);
// if (length > 0)
// {
// direction.X /= length;
// direction.Y /= length;
// }
// // 새 위치 계산
// _currentPosition = new Point(
// (int)(_currentPosition.X + direction.X * moveDistance),
// (int)(_currentPosition.Y + direction.Y * moveDistance)
// );
// _currentSpeed = _moveSpeed;
// }
// }
// private void UpdateBattery(float deltaTime)
// {
// // 배터리 소모 시뮬레이션
// if (_currentState == AGVState.Moving)
// {
// BatteryLevel -= 0.1f * deltaTime; // 이동시 소모
// }
// else if (_currentState == AGVState.Charging)
// {
// BatteryLevel += 5.0f * deltaTime; // 충전
// BatteryLevel = Math.Min(100.0f, BatteryLevel);
// }
// BatteryLevel = Math.Max(0, BatteryLevel);
// // 배터리 부족 경고
// if (BatteryLevel < 20.0f && _currentState != AGVState.Charging)
// {
// OnError($"배터리 부족: {BatteryLevel:F1}%");
// }
// }
// private void ProcessNextNode()
// {
// if (_remainingNodes == null || _currentNodeIndex >= _remainingNodes.Count - 1)
// {
// // 경로 완료
// _moveTimer.Stop();
// SetState(AGVState.Idle);
// PathCompleted?.Invoke(this, _currentPath);
// return;
// }
// // 다음 노드로 이동
// _currentNodeIndex++;
// var nextNodeId = _remainingNodes[_currentNodeIndex];
// // RFID 감지 시뮬레이션
// RfidDetected?.Invoke(this, $"RFID_{nextNodeId}");
// //_currentNodeId = nextNodeId;
// // 다음 목표 위치 설정 (실제로는 맵에서 좌표 가져와야 함)
// // 여기서는 간단히 현재 위치에서 랜덤 오프셋으로 설정
// var random = new Random();
// _moveTargetPosition = new Point(
// _currentPosition.X + random.Next(-100, 100),
// _currentPosition.Y + random.Next(-100, 100)
// );
// }
// private MapNode FindClosestNode(Point position, List<MapNode> mapNodes)
// {
// if (mapNodes == null || mapNodes.Count == 0)
// return null;
// MapNode closestNode = null;
// float closestDistance = float.MaxValue;
// foreach (var node in mapNodes)
// {
// var distance = CalculateDistance(position, node.Position);
// if (distance < closestDistance)
// {
// closestDistance = distance;
// closestNode = node;
// }
// }
// // 일정 거리 내에 있는 노드만 반환
// return closestDistance < 50.0f ? closestNode : null;
// }
// private float CalculateDistance(Point from, Point to)
// {
// var dx = to.X - from.X;
// var dy = to.Y - from.Y;
// return (float)Math.Sqrt(dx * dx + dy * dy);
// }
// private void SetState(AGVState newState)
// {
// if (_currentState != newState)
// {
// _currentState = newState;
// StateChanged?.Invoke(this, newState);
// }
// }
// private DockingDirection GetDockingDirection(NodeType nodeType)
// {
// switch (nodeType)
// {
// case NodeType.Charging:
// return DockingDirection.Forward; // 충전기: 전진 도킹
// case NodeType.Docking:
// return DockingDirection.Backward; // 장비 (로더, 클리너, 오프로더, 버퍼): 후진 도킹
// default:
// return DockingDirection.Forward; // 기본값: 전진
// }
// }
// private void OnError(string message)
// {
// SetState(AGVState.Error);
// ErrorOccurred?.Invoke(this, message);
// }
// #endregion
// #region Cleanup
// /// <summary>
// /// 리소스 정리
// /// </summary>
// public void Dispose()
// {
// _moveTimer?.Stop();
// _moveTimer?.Dispose();
// }
// #endregion
// }
//}