feat: Add comprehensive path prediction test with ProgressLogForm

- Add ProgressLogForm.cs for test result logging with ListView
- Implement real UI workflow simulation in path prediction test
- Test all connected node pairs to all docking targets
- Support CSV export for test results
- Keep List<string> ConnectedNodes structure (reverted List<MapNode> changes)
- Display RFID values in log for better readability

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
backuppc
2025-10-29 14:14:33 +09:00
parent 24a14fbd48
commit 0b59479d34
9 changed files with 884 additions and 366 deletions

View File

@@ -559,253 +559,7 @@ namespace AGVNavigationCore.Models
#endregion
#region Directional Navigation
/// <summary>
/// 현재 이전/현재 위치와 이동 방향을 기반으로 다음 노드 ID를 반환
///
/// 사용 예시:
/// - 001에서 002로 이동 후, Forward 선택 → 003 반환
/// - 003에서 004로 이동 후, Right 선택 → 030 반환
/// - 004에서 003으로 Backward 이동 중, GetNextNodeId(Backward) → 002 반환
///
/// 전제조건: SetPosition이 최소 2번 이상 호출되어 _prevPosition이 설정되어야 함
/// </summary>
/// <param name="direction">이동 방향 (Forward/Backward/Left/Right)</param>
/// <param name="allNodes">맵의 모든 노드</param>
/// <returns>다음 노드 ID (또는 null)</returns>
public MapNode GetNextNodeId(AgvDirection direction, List<MapNode> allNodes)
{
// 전제조건 검증: 2개 위치 히스토리 필요
if ( _prevNode == null)
{
return null;
}
if (_currentNode == null || allNodes == null || allNodes.Count == 0)
{
return null;
}
// 현재 노드에 연결된 노드들 가져오기
var connectedNodeIds = _currentNode.ConnectedNodes;
if (connectedNodeIds == null || connectedNodeIds.Count == 0)
{
return null;
}
// 연결된 노드 중 현재 노드가 아닌 것들만 필터링
var candidateNodes = allNodes.Where(n =>
connectedNodeIds.Contains(n.NodeId) && n.NodeId != _currentNode.NodeId
).ToList();
if (candidateNodes.Count == 0)
{
return null;
}
// 이전→현재 벡터 계산 (진행 방향 벡터)
var movementVector = new PointF(
_currentPosition.X - _prevPosition.X,
_currentPosition.Y - _prevPosition.Y
);
// 벡터 정규화
var movementLength = (float)Math.Sqrt(
movementVector.X * movementVector.X +
movementVector.Y * movementVector.Y
);
if (movementLength < 0.001f) // 거의 이동하지 않음
{
return candidateNodes[0]; // 첫 번째 연결 노드 반환
}
var normalizedMovement = new PointF(
movementVector.X / movementLength,
movementVector.Y / movementLength
);
// 각 후보 노드에 대해 방향 점수 계산
var bestCandidate = (node: (MapNode)null, score: 0.0f);
foreach (var candidate in candidateNodes)
{
var toNextVector = new PointF(
candidate.Position.X - _currentPosition.X,
candidate.Position.Y - _currentPosition.Y
);
var toNextLength = (float)Math.Sqrt(
toNextVector.X * toNextVector.X +
toNextVector.Y * toNextVector.Y
);
if (toNextLength < 0.001f)
{
continue;
}
var normalizedToNext = new PointF(
toNextVector.X / toNextLength,
toNextVector.Y / toNextLength
);
// 진행 방향 기반 점수 계산
float score = CalculateDirectionalScore(
normalizedMovement,
normalizedToNext,
direction
);
if (score > bestCandidate.score)
{
bestCandidate = (candidate, score);
}
}
return bestCandidate.node;
}
/// <summary>
/// 이동 방향을 기반으로 방향 점수를 계산
/// 높은 점수 = 더 나은 선택지
/// </summary>
private float CalculateDirectionalScore(
PointF movementDirection, // 정규화된 이전→현재 벡터
PointF nextDirection, // 정규화된 현재→다음 벡터
AgvDirection requestedDir) // 요청된 이동 방향
{
// 벡터 간 내적 계산 (유사도: -1 ~ 1)
float dotProduct = (movementDirection.X * nextDirection.X) +
(movementDirection.Y * nextDirection.Y);
// 벡터 간 외적 계산 (좌우 판별: Z 성분)
// 양수 = 좌측, 음수 = 우측
float crossProduct = (movementDirection.X * nextDirection.Y) -
(movementDirection.Y * nextDirection.X);
float baseScore = 0;
switch (requestedDir)
{
case AgvDirection.Forward:
// Forward: 현재 모터 상태에 따라 다름
// 1) 현재 Forward 모터 상태라면 → 같은 경로 (계속 전진)
// 2) 현재 Backward 모터 상태라면 → 반대 경로 (모터 방향 전환)
if (_currentDirection == AgvDirection.Forward)
{
// 이미 Forward 상태, 계속 Forward → 같은 경로
if (dotProduct > 0.9f)
baseScore = 100.0f;
else if (dotProduct > 0.5f)
baseScore = 80.0f;
else if (dotProduct > 0.0f)
baseScore = 50.0f;
else if (dotProduct > -0.5f)
baseScore = 20.0f;
}
else
{
// Backward 상태에서 Forward로 → 반대 경로
if (dotProduct < -0.9f)
baseScore = 100.0f;
else if (dotProduct < -0.5f)
baseScore = 80.0f;
else if (dotProduct < 0.0f)
baseScore = 50.0f;
else if (dotProduct < 0.5f)
baseScore = 20.0f;
}
break;
case AgvDirection.Backward:
// Backward: 현재 모터 상태에 따라 다름
// 1) 현재 Backward 모터 상태라면 → 같은 경로 (Forward와 동일)
// 2) 현재 Forward 모터 상태라면 → 반대 경로 (현재의 Backward와 반대)
if (_currentDirection == AgvDirection.Backward)
{
// 이미 Backward 상태, 계속 Backward → 같은 경로
if (dotProduct > 0.9f)
baseScore = 100.0f;
else if (dotProduct > 0.5f)
baseScore = 80.0f;
else if (dotProduct > 0.0f)
baseScore = 50.0f;
else if (dotProduct > -0.5f)
baseScore = 20.0f;
}
else
{
// Forward 상태에서 Backward로 → 반대 경로
if (dotProduct < -0.9f)
baseScore = 100.0f;
else if (dotProduct < -0.5f)
baseScore = 80.0f;
else if (dotProduct < 0.0f)
baseScore = 50.0f;
else if (dotProduct < 0.5f)
baseScore = 20.0f;
}
break;
case AgvDirection.Left:
// Left: 좌측 방향 선호
if (dotProduct > 0.0f) // Forward 상태
{
if (crossProduct > 0.5f)
baseScore = 100.0f;
else if (crossProduct > 0.0f)
baseScore = 70.0f;
else if (crossProduct > -0.5f)
baseScore = 50.0f;
else
baseScore = 30.0f;
}
else // Backward 상태 - 좌우 반전
{
if (crossProduct < -0.5f)
baseScore = 100.0f;
else if (crossProduct < 0.0f)
baseScore = 70.0f;
else if (crossProduct < 0.5f)
baseScore = 50.0f;
else
baseScore = 30.0f;
}
break;
case AgvDirection.Right:
// Right: 우측 방향 선호
if (dotProduct > 0.0f) // Forward 상태
{
if (crossProduct < -0.5f)
baseScore = 100.0f;
else if (crossProduct < 0.0f)
baseScore = 70.0f;
else if (crossProduct < 0.5f)
baseScore = 50.0f;
else
baseScore = 30.0f;
}
else // Backward 상태 - 좌우 반전
{
if (crossProduct > 0.5f)
baseScore = 100.0f;
else if (crossProduct > 0.0f)
baseScore = 70.0f;
else if (crossProduct > -0.5f)
baseScore = 50.0f;
else
baseScore = 30.0f;
}
break;
}
return baseScore;
}
#endregion
#region Cleanup

View File

@@ -66,17 +66,17 @@ namespace AGVNavigationCore.PathFinding.Core
{
var pathNode = _nodeMap[mapNode.NodeId];
foreach (var connectedNodeId in mapNode.ConnectedNodes)
foreach (var connectedNode in mapNode.ConnectedNodes)
{
if (_nodeMap.ContainsKey(connectedNodeId))
if (_nodeMap.ContainsKey(connectedNode))
{
// 양방향 연결 생성 (단일 연결이 양방향을 의미)
if (!pathNode.ConnectedNodes.Contains(connectedNodeId))
if (!pathNode.ConnectedNodes.Contains(connectedNode))
{
pathNode.ConnectedNodes.Add(connectedNodeId);
pathNode.ConnectedNodes.Add(connectedNode);
}
var connectedPathNode = _nodeMap[connectedNodeId];
var connectedPathNode = _nodeMap[connectedNode];
if (!connectedPathNode.ConnectedNodes.Contains(mapNode.NodeId))
{
connectedPathNode.ConnectedNodes.Add(mapNode.NodeId);
@@ -594,13 +594,13 @@ namespace AGVNavigationCore.PathFinding.Core
foreach (var connectedNodeId in junctionNode.ConnectedNodes)
{
if (connectedNodeId == null) continue;
// 조건 1: 왔던 길이 아님
if (connectedNodeId == previousNodeId)
continue;
if (connectedNodeId == previousNodeId) continue;
// 조건 2: 갈 길이 아님
if (connectedNodeId == targetNodeId)
continue;
if (connectedNodeId == targetNodeId) continue;
// 조건 3, 4, 5: 존재하고, 활성 상태이고, 네비게이션 가능
var connectedNode = _mapNodes.FirstOrDefault(n => n.NodeId == connectedNodeId);

View File

@@ -22,6 +22,8 @@ namespace AGVNavigationCore.PathFinding.Planning
private readonly JunctionAnalyzer _junctionAnalyzer;
private readonly DirectionChangePlanner _directionChangePlanner;
public AGVPathfinder(List<MapNode> mapNodes)
{
_mapNodes = mapNodes ?? new List<MapNode>();
@@ -114,7 +116,7 @@ namespace AGVNavigationCore.PathFinding.Planning
return AGVPathResult.CreateFailure("목적지 노드가 null입니다.", 0, 0);
if (prevNode == null)
return AGVPathResult.CreateFailure("이전위치 노드가 null입니다.", 0, 0);
if (startNode == targetNode)
if (startNode.NodeId == targetNode.NodeId && targetNode.DockDirection.MatchAGVDirection(prevDirection))
return AGVPathResult.CreateFailure("목적지와 현재위치가 동일합니다.", 0, 0);
var ReverseDirection = (currentDirection == AgvDirection.Forward ? AgvDirection.Backward : AgvDirection.Forward);
@@ -258,8 +260,11 @@ namespace AGVNavigationCore.PathFinding.Planning
MakeDetailData(path2, currentDirection);
}
MapNode tempNode = null;
//3.방향전환을 위환 대체 노드찾기
var tempNode = _basicPathfinder.FindAlternateNodeForDirectionChange(JunctionInPath.NodeId,
tempNode = _basicPathfinder.FindAlternateNodeForDirectionChange(JunctionInPath.NodeId,
path1.Path[path1.Path.Count - 2].NodeId,
path2.Path[1].NodeId);
@@ -268,9 +273,13 @@ namespace AGVNavigationCore.PathFinding.Planning
return AGVPathResult.CreateFailure("방향 전환을 위한 대체 노드를 찾을 수 없습니다.", 0, 0);
// path1 (시작 → 교차로)
var combinedResult = path1;
//교차로 대체노드를 사용한 경우
//if (tempNode != null)
{
// 교차로 → 대체노드 경로 계산
var pathToTemp = _basicPathfinder.FindPath(JunctionInPath.NodeId, tempNode.NodeId);
pathToTemp.PrevNode = JunctionInPath;
@@ -300,9 +309,25 @@ namespace AGVNavigationCore.PathFinding.Planning
// (path1 + pathToTemp) + pathFromTemp 합치기
combinedResult = _basicPathfinder.CombineResults(combinedResult, pathFromTemp);
//현재까지 노드에서 목적지까지의 방향이 일치하면 그대로 사용한다.
bool temp3ok = false;
var TempCheck3 = _basicPathfinder.FindPath(combinedResult.Path.Last().NodeId, targetNode.NodeId);
if (TempCheck3.Path.First().NodeId.Equals(combinedResult.Path.Last().NodeId))
{
if (targetNode.DockDirection == DockingDirection.Forward && combinedResult.DetailedPath.Last().MotorDirection == AgvDirection.Forward)
{
temp3ok = true;
}
else if (targetNode.DockDirection == DockingDirection.Backward && combinedResult.DetailedPath.Last().MotorDirection == AgvDirection.Backward)
{
temp3ok = true;
}
}
//대체노드에서 최종 목적지를 다시 확인한다.
if ((currentDirection == AgvDirection.Forward && targetNode.DockDirection != DockingDirection.Forward) ||
(currentDirection == AgvDirection.Backward && targetNode.DockDirection != DockingDirection.Backward))
if (temp3ok == false)
{
//목적지와 방향이 맞지 않다. 그러므로 대체노드를 추가로 더 찾아야한다.
var tempNode2 = _basicPathfinder.FindAlternateNodeForDirectionChange(JunctionInPath.NodeId,
@@ -330,6 +355,7 @@ namespace AGVNavigationCore.PathFinding.Planning
combinedResult = _basicPathfinder.CombineResults(combinedResult, pathToTemp3);
}
}
// (path1 + pathToTemp + pathFromTemp) + path2 합치기
combinedResult = _basicPathfinder.CombineResults(combinedResult, path2);

View File

@@ -15,6 +15,21 @@ namespace AGVNavigationCore.Utils
/// </summary>
public static class DirectionalHelper
{
/// <summary>
/// AGV방향과 일치하는지 확인한다. 단 원본위치에서 dock 위치가 Don't Care 라면 true가 반환 됩니다.
/// </summary>
/// <param name="dock"></param>
/// <param name="agvdirection"></param>
/// <returns></returns>
public static bool MatchAGVDirection(this DockingDirection dock, AgvDirection agvdirection)
{
if (dock == DockingDirection.DontCare) return true;
if (dock == DockingDirection.Forward && agvdirection == AgvDirection.Forward) return true;
if (dock == DockingDirection.Backward && agvdirection == AgvDirection.Backward) return true;
return false;
}
private static JunctionAnalyzer _junctionAnalyzer;
/// <summary>

View File

@@ -45,6 +45,7 @@
</Reference>
</ItemGroup>
<ItemGroup>
<Compile Include="Forms\ProgressLogForm.cs" />
<Compile Include="Models\SimulatorConfig.cs" />
<Compile Include="Models\SimulationState.cs" />
<Compile Include="Forms\SimulatorForm.cs">

View File

@@ -0,0 +1,356 @@
using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Text;
using System.Windows.Forms;
namespace AGVSimulator.Forms
{
/// <summary>
/// 경로 예측 테스트 결과 로그 항목
/// </summary>
public class PathTestLogItem
{
public string PreviousPosition { get; set; }
public string MotorDirection { get; set; } // 정방향 or 역방향
public string CurrentPosition { get; set; }
public string TargetPosition { get; set; }
public string DockingPosition { get; set; } // 도킹위치
public bool Success { get; set; }
public string Message { get; set; }
public string DetailedPath { get; set; }
public DateTime Timestamp { get; set; }
public PathTestLogItem()
{
Timestamp = DateTime.Now;
}
}
/// <summary>
/// 경로 예측 테스트 진행 상황 로그 표시 폼
/// </summary>
public partial class ProgressLogForm : Form
{
private ListView _logListView;
private Button _closeButton;
private Button _cancelButton;
private Button _saveCSVButton;
private ProgressBar _progressBar;
private Label _statusLabel;
private List<PathTestLogItem> _logItems;
/// <summary>
/// 취소 요청 여부
/// </summary>
public bool CancelRequested { get; private set; }
public ProgressLogForm()
{
InitializeComponent();
CancelRequested = false;
_logItems = new List<PathTestLogItem>();
}
private void InitializeComponent()
{
this.Text = "경로 예측 테스트 진행 상황";
this.Size = new Size(1200, 600);
this.StartPosition = FormStartPosition.CenterParent;
this.FormBorderStyle = FormBorderStyle.Sizable;
this.MinimumSize = new Size(800, 400);
// 상태 레이블
_statusLabel = new Label
{
Text = "준비 중...",
Dock = DockStyle.Top,
Height = 30,
TextAlign = ContentAlignment.MiddleLeft,
Padding = new Padding(10, 5, 10, 5),
Font = new Font("맑은 고딕", 10F, FontStyle.Bold)
};
// 프로그레스바
_progressBar = new ProgressBar
{
Dock = DockStyle.Top,
Height = 25,
Style = ProgressBarStyle.Continuous,
Minimum = 0,
Maximum = 100
};
// 로그 리스트뷰
_logListView = new ListView
{
Dock = DockStyle.Fill,
View = View.Details,
FullRowSelect = true,
GridLines = true,
Font = new Font("맑은 고딕", 9F),
BackColor = Color.White
};
// 컬럼 추가
_logListView.Columns.Add("이전위치", 80);
_logListView.Columns.Add("모터방향", 80);
_logListView.Columns.Add("현재위치", 80);
_logListView.Columns.Add("대상위치", 80);
_logListView.Columns.Add("도킹위치", 80);
_logListView.Columns.Add("성공", 50);
_logListView.Columns.Add("메세지", 180);
_logListView.Columns.Add("상세경로", 350);
_logListView.Columns.Add("시간", 90);
// 버튼 패널
var buttonPanel = new Panel
{
Dock = DockStyle.Bottom,
Height = 50
};
_cancelButton = new Button
{
Text = "취소",
Size = new Size(100, 30),
Location = new Point(10, 10),
Enabled = true
};
_cancelButton.Click += OnCancel_Click;
_closeButton = new Button
{
Text = "닫기",
Size = new Size(100, 30),
Location = new Point(120, 10),
Enabled = false
};
_closeButton.Click += OnClose_Click;
_saveCSVButton = new Button
{
Text = "CSV 저장",
Size = new Size(100, 30),
Location = new Point(230, 10),
Enabled = true
};
_saveCSVButton.Click += OnSaveCSV_Click;
buttonPanel.Controls.Add(_cancelButton);
buttonPanel.Controls.Add(_closeButton);
buttonPanel.Controls.Add(_saveCSVButton);
this.Controls.Add(_logListView);
this.Controls.Add(_progressBar);
this.Controls.Add(_statusLabel);
this.Controls.Add(buttonPanel);
}
/// <summary>
/// 로그 추가 (PathTestLogItem)
/// </summary>
public void AddLogItem(PathTestLogItem item)
{
if (InvokeRequired)
{
Invoke(new Action<PathTestLogItem>(AddLogItem), item);
return;
}
_logItems.Add(item);
var listItem = new ListViewItem(item.PreviousPosition ?? "-");
listItem.SubItems.Add(item.MotorDirection ?? "-");
listItem.SubItems.Add(item.CurrentPosition ?? "-");
listItem.SubItems.Add(item.TargetPosition ?? "-");
listItem.SubItems.Add(item.DockingPosition ?? "-");
listItem.SubItems.Add(item.Success ? "O" : "X");
listItem.SubItems.Add(item.Message ?? "-");
listItem.SubItems.Add(item.DetailedPath ?? "-");
listItem.SubItems.Add(item.Timestamp.ToString("HH:mm:ss"));
// 성공 여부에 따라 색상 설정
if (!item.Success)
{
listItem.BackColor = Color.LightPink;
}
_logListView.Items.Add(listItem);
_logListView.EnsureVisible(_logListView.Items.Count - 1);
}
/// <summary>
/// 간단한 텍스트 로그 추가 (상태 메시지용)
/// </summary>
public void AppendLog(string message)
{
var item = new PathTestLogItem
{
Message = message,
Success = true
};
AddLogItem(item);
}
/// <summary>
/// 상태 메시지 업데이트
/// </summary>
public void UpdateStatus(string status)
{
if (InvokeRequired)
{
Invoke(new Action<string>(UpdateStatus), status);
return;
}
_statusLabel.Text = status;
}
/// <summary>
/// 프로그레스바 업데이트
/// </summary>
public void UpdateProgress(int value, int maximum)
{
if (InvokeRequired)
{
Invoke(new Action<int, int>(UpdateProgress), value, maximum);
return;
}
_progressBar.Maximum = maximum;
_progressBar.Value = Math.Min(value, maximum);
}
/// <summary>
/// 작업 완료 시 호출
/// </summary>
public void SetCompleted()
{
if (InvokeRequired)
{
Invoke(new Action(SetCompleted));
return;
}
_cancelButton.Enabled = false;
_closeButton.Enabled = true;
UpdateStatus("작업 완료");
}
/// <summary>
/// 작업 취소 시 호출
/// </summary>
public void SetCancelled()
{
if (InvokeRequired)
{
Invoke(new Action(SetCancelled));
return;
}
_cancelButton.Enabled = false;
_closeButton.Enabled = true;
UpdateStatus("작업 취소됨");
}
private void OnCancel_Click(object sender, EventArgs e)
{
var result = MessageBox.Show(
"진행 중인 작업을 취소하시겠습니까?",
"취소 확인",
MessageBoxButtons.YesNo,
MessageBoxIcon.Question);
if (result == DialogResult.Yes)
{
CancelRequested = true;
_cancelButton.Enabled = false;
UpdateStatus("취소 요청됨...");
AppendLog("사용자가 취소를 요청했습니다.");
}
}
private void OnSaveCSV_Click(object sender, EventArgs e)
{
if (_logItems.Count == 0)
{
MessageBox.Show("저장할 데이터가 없습니다.", "알림", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
using (var saveDialog = new SaveFileDialog())
{
saveDialog.Filter = "CSV 파일 (*.csv)|*.csv|모든 파일 (*.*)|*.*";
saveDialog.DefaultExt = "csv";
saveDialog.FileName = $"경로예측테스트_{DateTime.Now:yyyyMMdd_HHmmss}.csv";
if (saveDialog.ShowDialog() == DialogResult.OK)
{
try
{
SaveToCSV(saveDialog.FileName);
MessageBox.Show($"CSV 파일이 저장되었습니다.\n{saveDialog.FileName}",
"저장 완료", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
catch (Exception ex)
{
MessageBox.Show($"CSV 저장 중 오류가 발생했습니다:\n{ex.Message}",
"오류", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
}
}
/// <summary>
/// CSV 파일로 저장
/// </summary>
private void SaveToCSV(string filePath)
{
using (var writer = new StreamWriter(filePath, false, Encoding.UTF8))
{
// 헤더 작성
writer.WriteLine("이전위치,모터방향,현재위치,대상위치,도킹위치,성공,메세지,상세경로,시간");
// 데이터 작성
foreach (var item in _logItems)
{
var line = $"{EscapeCSV(item.PreviousPosition)}," +
$"{EscapeCSV(item.MotorDirection)}," +
$"{EscapeCSV(item.CurrentPosition)}," +
$"{EscapeCSV(item.TargetPosition)}," +
$"{EscapeCSV(item.DockingPosition)}," +
$"{(item.Success ? "O" : "X")}," +
$"{EscapeCSV(item.Message)}," +
$"{EscapeCSV(item.DetailedPath)}," +
$"{item.Timestamp:yyyy-MM-dd HH:mm:ss}";
writer.WriteLine(line);
}
}
}
/// <summary>
/// CSV 셀 데이터 이스케이프 처리
/// </summary>
private string EscapeCSV(string value)
{
if (string.IsNullOrEmpty(value))
return "";
// 쉼표, 큰따옴표, 줄바꿈이 있으면 큰따옴표로 감싸고 내부 큰따옴표는 두 개로
if (value.Contains(",") || value.Contains("\"") || value.Contains("\n") || value.Contains("\r"))
{
return "\"" + value.Replace("\"", "\"\"") + "\"";
}
return value;
}
private void OnClose_Click(object sender, EventArgs e)
{
this.Close();
}
}
}

View File

@@ -45,6 +45,7 @@ namespace AGVSimulator.Forms
/// </summary>
private void InitializeComponent()
{
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(SimulatorForm));
this._menuStrip = new System.Windows.Forms.MenuStrip();
this.fileToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.openMapToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
@@ -74,9 +75,12 @@ namespace AGVSimulator.Forms
this.toolStripSeparator3 = new System.Windows.Forms.ToolStripSeparator();
this.fitToMapToolStripButton = new System.Windows.Forms.ToolStripButton();
this.resetZoomToolStripButton = new System.Windows.Forms.ToolStripButton();
this.toolStripSeparator5 = new System.Windows.Forms.ToolStripSeparator();
this.toolStripButton1 = new System.Windows.Forms.ToolStripButton();
this._statusStrip = new System.Windows.Forms.StatusStrip();
this._statusLabel = new System.Windows.Forms.ToolStripStatusLabel();
this._coordLabel = new System.Windows.Forms.ToolStripStatusLabel();
this.prb1 = new System.Windows.Forms.ToolStripProgressBar();
this._controlPanel = new System.Windows.Forms.Panel();
this._statusGroup = new System.Windows.Forms.GroupBox();
this._pathLengthLabel = new System.Windows.Forms.Label();
@@ -92,7 +96,6 @@ 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();
@@ -105,10 +108,10 @@ namespace AGVSimulator.Forms
this._agvListCombo = new System.Windows.Forms.ComboBox();
this._canvasPanel = new System.Windows.Forms.Panel();
this._agvInfoPanel = new System.Windows.Forms.Panel();
this._pathDebugLabel = new System.Windows.Forms.TextBox();
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.TextBox();
this._menuStrip.SuspendLayout();
this._toolStrip.SuspendLayout();
this._statusStrip.SuspendLayout();
@@ -274,7 +277,9 @@ namespace AGVSimulator.Forms
this.btAllReset,
this.toolStripSeparator3,
this.fitToMapToolStripButton,
this.resetZoomToolStripButton});
this.resetZoomToolStripButton,
this.toolStripSeparator5,
this.toolStripButton1});
this._toolStrip.Location = new System.Drawing.Point(0, 24);
this._toolStrip.Name = "_toolStrip";
this._toolStrip.Size = new System.Drawing.Size(1200, 25);
@@ -372,11 +377,26 @@ namespace AGVSimulator.Forms
this.resetZoomToolStripButton.ToolTipText = "줌을 초기화합니다";
this.resetZoomToolStripButton.Click += new System.EventHandler(this.OnResetZoom_Click);
//
// toolStripSeparator5
//
this.toolStripSeparator5.Name = "toolStripSeparator5";
this.toolStripSeparator5.Size = new System.Drawing.Size(6, 25);
//
// toolStripButton1
//
this.toolStripButton1.Image = ((System.Drawing.Image)(resources.GetObject("toolStripButton1.Image")));
this.toolStripButton1.ImageTransparentColor = System.Drawing.Color.Magenta;
this.toolStripButton1.Name = "toolStripButton1";
this.toolStripButton1.Size = new System.Drawing.Size(75, 22);
this.toolStripButton1.Text = "경로예측";
this.toolStripButton1.Click += new System.EventHandler(this.toolStripButton1_Click);
//
// _statusStrip
//
this._statusStrip.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
this._statusLabel,
this._coordLabel});
this._coordLabel,
this.prb1});
this._statusStrip.Location = new System.Drawing.Point(0, 778);
this._statusStrip.Name = "_statusStrip";
this._statusStrip.Size = new System.Drawing.Size(1200, 22);
@@ -394,6 +414,11 @@ namespace AGVSimulator.Forms
this._coordLabel.Name = "_coordLabel";
this._coordLabel.Size = new System.Drawing.Size(0, 17);
//
// prb1
//
this.prb1.Name = "prb1";
this.prb1.Size = new System.Drawing.Size(200, 16);
//
// _controlPanel
//
this._controlPanel.BackColor = System.Drawing.SystemColors.Control;
@@ -540,7 +565,6 @@ 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);
@@ -559,16 +583,6 @@ 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);
@@ -685,6 +699,18 @@ namespace AGVSimulator.Forms
this._agvInfoPanel.Size = new System.Drawing.Size(967, 80);
this._agvInfoPanel.TabIndex = 5;
//
// _pathDebugLabel
//
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(947, 43);
this._pathDebugLabel.TabIndex = 4;
this._pathDebugLabel.Text = "경로: 설정되지 않음";
//
// _agvInfoTitleLabel
//
this._agvInfoTitleLabel.AutoSize = true;
@@ -715,18 +741,6 @@ namespace AGVSimulator.Forms
this._motorDirectionLabel.TabIndex = 2;
this._motorDirectionLabel.Text = "모터 방향: -";
//
// _pathDebugLabel
//
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(947, 43);
this._pathDebugLabel.TabIndex = 4;
this._pathDebugLabel.Text = "경로: 설정되지 않음";
//
// SimulatorForm
//
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 12F);
@@ -827,7 +841,9 @@ 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.Button btNextNode;
private System.Windows.Forms.TextBox _pathDebugLabel;
private System.Windows.Forms.ToolStripSeparator toolStripSeparator5;
private System.Windows.Forms.ToolStripButton toolStripButton1;
private System.Windows.Forms.ToolStripProgressBar prb1;
}
}

View File

@@ -4,6 +4,8 @@ using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using AGVNavigationCore.Models;
using AGVNavigationCore.Controls;
@@ -28,7 +30,7 @@ namespace AGVSimulator.Forms
private AGVPathfinder _advancedPathfinder;
private List<VirtualAGV> _agvList;
private SimulationState _simulationState;
private Timer _simulationTimer;
private System.Windows.Forms.Timer _simulationTimer;
private SimulatorConfig _config;
private string _currentMapFilePath;
private bool _isTargetCalcMode; // 타겟계산 모드 상태
@@ -76,7 +78,7 @@ namespace AGVSimulator.Forms
CreateSimulatorCanvas();
// 타이머 초기화
_simulationTimer = new Timer();
_simulationTimer = new System.Windows.Forms.Timer();
_simulationTimer.Interval = 100; // 100ms 간격
_simulationTimer.Tick += OnSimulationTimer_Tick;
@@ -268,6 +270,12 @@ namespace AGVSimulator.Forms
}
private void OnCalculatePath_Click(object sender, EventArgs e)
{
var rlt = CalcPath();
if (rlt.result == false) MessageBox.Show(rlt.message, "알림", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
(bool result, string message) CalcPath()
{
// 시작 RFID가 없으면 AGV 현재 위치로 설정
if (_startNodeCombo.SelectedItem == null || _startNodeCombo.Text == "선택하세요")
@@ -277,8 +285,7 @@ namespace AGVSimulator.Forms
if (_startNodeCombo.SelectedItem == null || _targetNodeCombo.SelectedItem == null)
{
MessageBox.Show("시작 RFID와 목표 RFID를 선택해주세요.", "알림", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
return (false, "시작 RFID와 목표 RFID를 선택해주세요.");
}
var startItem = _startNodeCombo.SelectedItem as ComboBoxItem<MapNode>;
@@ -288,8 +295,7 @@ namespace AGVSimulator.Forms
if (startNode == null || targetNode == null)
{
MessageBox.Show("선택한 노드 정보가 올바르지 않습니다.", "오류", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
return (false, "선택한 노드 정보가 올바르지 않습니다.");
}
@@ -300,10 +306,9 @@ namespace AGVSimulator.Forms
// 현재 AGV 방향 가져오기
var selectedAGV = _agvListCombo.SelectedItem as VirtualAGV;
if(selectedAGV == null)
if (selectedAGV == null)
{
MessageBox.Show("Virtual AGV 가 없습니다", "오류", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
return (false, "Virtual AGV 가 없습니다");
}
var currentDirection = selectedAGV.CurrentDirection;
@@ -331,14 +336,14 @@ namespace AGVSimulator.Forms
// 고급 경로 디버깅 정보 표시
UpdateAdvancedPathDebugInfo(advancedResult);
return (true, string.Empty);
}
else
{
// 경로 실패시 디버깅 정보 초기화
_pathDebugLabel.Text = $"경로: 실패 - {advancedResult.ErrorMessage}";
MessageBox.Show($"경로를 찾을 수 없습니다:\n{advancedResult.ErrorMessage}", "경로 계산 실패",
MessageBoxButtons.OK, MessageBoxIcon.Warning);
return (false, $"경로를 찾을 수 없습니다:\n{advancedResult.ErrorMessage}");
}
}
@@ -559,7 +564,7 @@ namespace AGVSimulator.Forms
var selectedDirection = selectedDirectionItem?.Direction ?? AgvDirection.Forward;
//이전위치와 동일한지 체크한다.
if(selectedAGV.CurrentNodeId == targetNode.NodeId && selectedAGV.CurrentDirection == selectedDirection)
if (selectedAGV.CurrentNodeId == targetNode.NodeId && selectedAGV.CurrentDirection == selectedDirection)
{
Program.WriteLine($"이전 노드위치와 모터의 방향이 동일하여 현재 위치 변경이 취소됩니다(NODE:{targetNode.NodeId},RFID:{targetNode.RfidId},DIR:{selectedDirection})");
return;
@@ -1218,26 +1223,355 @@ 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)
private async void toolStripButton1_Click(object sender, EventArgs e)
{
MessageBox.Show("먼저 AGV를 선택해주세요.", "알림", MessageBoxButtons.OK, MessageBoxIcon.Information);
// 맵과 AGV 확인
if (_mapNodes == null || _mapNodes.Count == 0)
{
MessageBox.Show("맵 데이터가 없습니다. 먼저 맵을 로드해주세요.", "알림",
MessageBoxButtons.OK, MessageBoxIcon.Warning);
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}");
var selectedAGV = _agvListCombo.SelectedItem as VirtualAGV;
if (selectedAGV == null)
{
MessageBox.Show("테스트할 AGV를 선택해주세요.", "알림",
MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
// 도킹 타겟 노드 찾기
var dockingTargets = _mapNodes.Where(n =>
n.Type == NodeType.Charging || n.Type == NodeType.Docking).ToList();
if (dockingTargets.Count == 0)
{
MessageBox.Show("도킹 타겟(충전기 또는 장비)이 없습니다.", "알림",
MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
// 연결된 노드 쌍 찾기 (사전 계산)
var nodePairs = GetConnectedNodePairs();
var testCount = nodePairs.Count * dockingTargets.Count * 2; // 노드 쌍 x 도킹 타겟 x 방향(2)
// 테스트 시작 확인
var result = MessageBox.Show(
$"경로 예측 테스트를 시작합니다.\n\n" +
$"※ 실제 사용자 시나리오 재현 방식:\n" +
$" 연결된 노드 쌍을 따라 AGV를 2번 이동시켜\n" +
$" 방향을 확정한 후 각 도킹 타겟으로 경로 계산\n\n" +
$"• 연결된 노드 쌍: {nodePairs.Count}개\n" +
$"• 도킹 타겟: {dockingTargets.Count}개 (충전기/장비)\n" +
$"• AGV 방향: 2가지 (정방향/역방향)\n" +
$"• 총 테스트 케이스: {testCount}개\n\n" +
$"※ UI가 실시간으로 업데이트됩니다.\n" +
$"계속하시겠습니까?",
"경로 예측 테스트 확인",
MessageBoxButtons.YesNo,
MessageBoxIcon.Question);
if (result != DialogResult.Yes)
return;
// 로그 폼 생성 및 표시
var logForm = new ProgressLogForm();
logForm.Show(this);
logForm.UpdateProgress(0, testCount);
logForm.UpdateStatus("테스트 준비 중...");
// 비동기 테스트 시작
await Task.Run(() => RunPathPredictionTest(selectedAGV, dockingTargets, logForm));
// 완료
if (logForm.CancelRequested)
{
logForm.SetCancelled();
MessageBox.Show("테스트가 취소되었습니다.", "알림",
MessageBoxButtons.OK, MessageBoxIcon.Information);
}
else
{
logForm.SetCompleted();
MessageBox.Show("경로 예측 테스트가 완료되었습니다.", "완료",
MessageBoxButtons.OK, MessageBoxIcon.Information);
}
}
/// <summary>
/// 노드의 표시 이름 가져오기 (RFID 우선, 없으면 (NodeId))
/// </summary>
private string GetNodeDisplayName(MapNode node)
{
if (node == null)
return "-";
if (!string.IsNullOrEmpty(node.RfidId))
return node.RfidId;
return $"({node.NodeId})";
}
/// <summary>
/// 방향 콤보박스 선택 (테스트용)
/// </summary>
private void SetDirectionComboBox(AgvDirection direction)
{
for (int i = 0; i < _directionCombo.Items.Count; i++)
{
var item = _directionCombo.Items[i] as DirectionItem;
if (item != null && item.Direction == direction)
{
_directionCombo.SelectedIndex = i;
return;
}
}
}
/// <summary>
/// 목표 노드 콤보박스 선택 (테스트용)
/// </summary>
private void SetTargetNodeComboBox(string nodeId)
{
for (int i = 0; i < _targetNodeCombo.Items.Count; i++)
{
var item = _targetNodeCombo.Items[i] as ComboBoxItem<MapNode>;
if (item?.Value?.NodeId == nodeId)
{
_targetNodeCombo.SelectedIndex = i;
return;
}
}
}
/// <summary>
/// UI 상태로부터 테스트 결과 생성 (테스트용)
/// </summary>
private PathTestLogItem CreateTestResultFromUI(MapNode prevNode, MapNode targetNode,
string directionName, (bool result, string message) calcResult)
{
var currentNode = _mapNodes.FirstOrDefault(n => n.NodeId ==
(_agvListCombo.SelectedItem as VirtualAGV)?.CurrentNodeId);
var logItem = new PathTestLogItem
{
PreviousPosition = GetNodeDisplayName(prevNode),
MotorDirection = directionName,
CurrentPosition = GetNodeDisplayName(currentNode),
TargetPosition = GetNodeDisplayName(targetNode),
DockingPosition = targetNode.Type == NodeType.Charging ? "충전기" : "장비"
};
if (calcResult.result)
{
// 경로 계산 성공 - 현재 화면에 표시된 경로 정보 사용
var currentPath = _simulatorCanvas.CurrentPath;
if (currentPath != null && currentPath.Success)
{
// 도킹 검증
var dockingValidation = DockingValidator.ValidateDockingDirection(currentPath, _mapNodes);
if (dockingValidation.IsValid)
{
logItem.Success = true;
logItem.Message = "성공";
logItem.DetailedPath = string.Join(" → ", currentPath.GetDetailedInfo());
}
else
{
logItem.Success = false;
logItem.Message = $"도킹 검증 실패: {dockingValidation.ValidationError}";
logItem.DetailedPath = string.Join(" → ", currentPath.GetDetailedInfo());
}
}
else
{
logItem.Success = true;
logItem.Message = "경로 계산 성공";
logItem.DetailedPath = "-";
}
}
else
{
// 경로 계산 실패
logItem.Success = false;
logItem.Message = calcResult.message;
logItem.DetailedPath = "-";
}
return logItem;
}
/// <summary>
/// 연결된 노드 쌍 찾기 (A→B 형태)
/// </summary>
private List<(MapNode nodeA, MapNode nodeB)> GetConnectedNodePairs()
{
var pairs = new List<(MapNode, MapNode)>();
var processedPairs = new HashSet<string>();
foreach (var nodeA in _mapNodes)
{
if (nodeA.ConnectedNodes == null || nodeA.ConnectedNodes.Count == 0)
continue;
// 연결된 노드 ID를 실제 MapNode 객체로 변환
foreach (var connectedNodeId in nodeA.ConnectedNodes)
{
var nodeB = _mapNodes.FirstOrDefault(n => n.NodeId == connectedNodeId);
if (nodeB == null)
continue;
// 중복 방지 (A→B와 B→A를 같은 것으로 간주)
var pairKey1 = $"{nodeA.NodeId}→{nodeB.NodeId}";
var pairKey2 = $"{nodeB.NodeId}→{nodeA.NodeId}";
if (!processedPairs.Contains(pairKey1) && !processedPairs.Contains(pairKey2))
{
pairs.Add((nodeA, nodeB));
processedPairs.Add(pairKey1);
processedPairs.Add(pairKey2);
}
}
}
return pairs;
}
/// <summary>
/// 경로 예측 테스트 실행 (실제 사용자 시나리오 재현)
/// </summary>
private void RunPathPredictionTest(VirtualAGV agv, List<MapNode> dockingTargets, ProgressLogForm logForm)
{
var directions = new[]
{
(AgvDirection.Forward, "정방향"),
(AgvDirection.Backward, "역방향")
};
// 연결된 노드 쌍 찾기
var nodePairs = GetConnectedNodePairs();
int totalTests = nodePairs.Count * dockingTargets.Count * 2;
int currentTest = 0;
int successCount = 0;
int failCount = 0;
logForm.UpdateStatus("경로 예측 테스트 진행 중...");
logForm.AppendLog($"테스트 시작: 총 {totalTests}개 케이스");
logForm.AppendLog($"연결된 노드 쌍: {nodePairs.Count}개");
logForm.AppendLog($"도킹 타겟: {dockingTargets.Count}개");
logForm.AppendLog("---");
// 각 연결된 노드 쌍에 대해 테스트
foreach (var (nodeA, nodeB) in nodePairs)
{
foreach (var (direction, directionName) in directions)
{
// 취소 확인
if (logForm.CancelRequested)
{
logForm.AppendLog($"테스트 취소됨 - {currentTest}/{totalTests} 완료");
return;
}
// === 실제 사용자 워크플로우 재현 ===
// 1단계: AGV를 nodeA로 이동 (실제 UI 조작)
this.Invoke((MethodInvoker)delegate
{
// RFID 텍스트박스에 값 입력
_rfidTextBox.Text = nodeA.RfidId;
// 방향 콤보박스 선택
SetDirectionComboBox(direction);
// 위치설정 버튼 클릭 (실제 사용자 동작)
SetAGVPositionByRfid();
Application.DoEvents(); // UI 업데이트
});
Thread.Sleep(100); // 시각적 효과
// 2단계: AGV를 nodeB로 이동 (방향 확정됨)
this.Invoke((MethodInvoker)delegate
{
// RFID 텍스트박스에 값 입력
_rfidTextBox.Text = nodeB.RfidId;
// 방향 콤보박스 선택
SetDirectionComboBox(direction);
// 위치설정 버튼 클릭 (실제 사용자 동작)
SetAGVPositionByRfid();
Application.DoEvents(); // UI 업데이트
});
Thread.Sleep(100); // 시각적 효과
// 3단계: nodeB 위치에서 모든 도킹 타겟으로 경로 예측
foreach (var dockingTarget in dockingTargets)
{
// 취소 확인
if (logForm.CancelRequested)
{
logForm.AppendLog($"테스트 취소됨 - {currentTest}/{totalTests} 완료");
return;
}
currentTest++;
// UI 스레드에서 경로 계산 및 테스트 (실제 UI 사용)
PathTestLogItem testResult = null;
this.Invoke((MethodInvoker)delegate
{
// 진행상황 업데이트
logForm.UpdateProgress(currentTest, totalTests);
logForm.UpdateStatus($"테스트 진행 중... ({currentTest}/{totalTests}) [{GetNodeDisplayName(nodeA)}→{GetNodeDisplayName(nodeB)}→{GetNodeDisplayName(dockingTarget)}]");
prb1.Value = (int)((double)currentTest / totalTests * 100);
// 목표 노드 콤보박스 선택
SetTargetNodeComboBox(dockingTarget.NodeId);
// 경로 계산 버튼 클릭 (실제 사용자 동작)
var calcResult = CalcPath();
// 테스트 결과 생성
testResult = CreateTestResultFromUI(nodeA, dockingTarget, directionName, calcResult);
// 로그 추가
logForm.AddLogItem(testResult);
// 실패한 경우에만 경로를 화면에 표시 (시각적 확인)
if (!testResult.Success && _simulatorCanvas.CurrentPath != null)
{
_simulatorCanvas.Invalidate();
}
Application.DoEvents();
});
if (testResult.Success)
successCount++;
else
failCount++;
// UI 반응성을 위한 짧은 대기
Thread.Sleep(50);
}
}
}
// 최종 결과
logForm.AppendLog($"");
logForm.AppendLog($"=== 테스트 완료 ===");
logForm.AppendLog($"총 테스트: {totalTests}");
logForm.AppendLog($"성공: {successCount}");
logForm.AppendLog($"실패: {failCount}");
logForm.AppendLog($"성공률: {(double)successCount / totalTests * 100:F1}%");
}
}
/// <summary>

View File

@@ -123,6 +123,22 @@
<metadata name="_toolStrip.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>132, 17</value>
</metadata>
<assembly alias="System.Drawing" name="System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
<data name="toolStripButton1.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>
iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8
YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAIFSURBVDhPpZLtS1NhGMbPPxJmmlYSgqHiKzGU1EDxg4iK
YKyG2WBogqMYJQOtCEVRFBGdTBCJfRnkS4VaaWNT5sqx1BUxRXxDHYxAJLvkusEeBaPAB+5z4Jzn+t3X
/aLhnEfjo8m+dCoa+7/C3O2Hqe0zDC+8KG+cRZHZhdzaaWTVTCLDMIY0vfM04Nfh77/G/sEhwpEDbO3t
I7TxE8urEVy99fT/AL5gWDLrTB/hnF4XsW0khCu5ln8DmJliT2AXrcNBsU1gj/MH4nMeKwBrPktM28xM
cX79DFKrHHD5d9D26hvicx4pABt2lpg10zYzU0zr7+e3xXGcrkEB2O2TNec9nJFwB3alZn5jZorfeDZh
6Q3g8s06BeCoKF4MRURoH1+BY2oNCbeb0TIclIYxOhzf8frTOuo7FxCbbVIAzpni0iceEc8vhzEwGkJD
lx83ymxifejdKjRNk/8PWnyIyTQqAJek0jqHwfEVscu31baIu8+90sTE4nY025dQ2/5FIPpnXlzKuK8A
HBUzHot52djqQ6HZhfR7IwK4mKpHtvEDMqvfCiQ6zaAAXM8x94aIWTNrLLG4kVUzgaTSPlzLtyJOZxbb
1wtfyg4Q+AfA3aZlButjSfxGcUJBk4g5tuP3haQKRKXcUQDOmbvNTpPOJeFFjordZmbWTNvMTHFUcpUC
nOccAdABIDXXE1nzAAAAAElFTkSuQmCC
</value>
</data>
<metadata name="_statusStrip.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>237, 17</value>
</metadata>