fix: RFID duplicate validation and correct magnet direction calculation

- Add real-time RFID duplicate validation in map editor with automatic rollback
- Remove RFID auto-assignment to maintain data consistency between editor and simulator
- Fix magnet direction calculation to use actual forward direction angles instead of arbitrary assignment
- Add node names to simulator combo boxes for better identification
- Improve UI layout by drawing connection lines before text for better visibility

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
ChiKyun Kim
2025-09-15 16:31:40 +09:00
parent 1add9ed59a
commit 7f48253770
41 changed files with 4827 additions and 3649 deletions

View File

@@ -5,7 +5,7 @@
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{B2C3D4E5-0000-0000-0000-000000000000}</ProjectGuid>
<OutputType>WinExe</OutputType>
<OutputType>Exe</OutputType>
<RootNamespace>AGVSimulator</RootNamespace>
<AssemblyName>AGVSimulator</AssemblyName>
<TargetFrameworkVersion>v4.8</TargetFrameworkVersion>
@@ -31,6 +31,9 @@
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup>
<StartupObject />
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Core" />

View File

@@ -1,52 +0,0 @@
namespace AGVSimulator.Controls
{
partial class SimulatorCanvas
{
/// <summary>
/// 필수 디자이너 변수입니다.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// 사용 중인 모든 리소스를 정리합니다.
/// </summary>
/// <param name="disposing">관리되는 리소스를 삭제해야 하면 true이고, 그렇지 않으면 false입니다.</param>
protected override void Dispose(bool disposing)
{
if (disposing)
{
if (components != null)
{
components.Dispose();
}
// 커스텀 리소스 정리
CleanupResources();
}
base.Dispose(disposing);
}
#region
/// <summary>
/// 디자이너 지원에 필요한 메서드입니다.
/// 이 메서드의 내용을 코드 편집기로 수정하지 마세요.
/// </summary>
private void InitializeComponent()
{
this.SuspendLayout();
//
// SimulatorCanvas
//
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 12F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.BackColor = System.Drawing.Color.White;
this.Name = "SimulatorCanvas";
this.Size = new System.Drawing.Size(800, 600);
this.ResumeLayout(false);
}
#endregion
}
}

View File

@@ -1,622 +0,0 @@
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Windows.Forms;
using AGVMapEditor.Models;
using AGVNavigationCore.Models;
using AGVNavigationCore.PathFinding;
using AGVSimulator.Models;
namespace AGVSimulator.Controls
{
/// <summary>
/// AGV 시뮬레이션 시각화 캔버스
/// </summary>
public partial class SimulatorCanvas : UserControl
{
#region Fields
private List<MapNode> _mapNodes;
private List<VirtualAGV> _agvList;
private PathResult _currentPath;
// 그래픽 설정
private float _zoom = 1.0f;
private Point _panOffset = Point.Empty;
private bool _isPanning = false;
private Point _lastMousePos = Point.Empty;
// 색상 설정
private readonly Brush _normalNodeBrush = Brushes.LightBlue;
private readonly Brush _rotationNodeBrush = Brushes.Yellow;
private readonly Brush _dockingNodeBrush = Brushes.Orange;
private readonly Brush _chargingNodeBrush = Brushes.Green;
private readonly Brush _agvBrush = Brushes.Red;
private readonly Brush _pathBrush = Brushes.Purple;
private readonly Pen _connectionPen = new Pen(Color.Gray, 2);
private readonly Pen _pathPen = new Pen(Color.Purple, 3);
private readonly Pen _agvPen = new Pen(Color.Red, 3);
// 크기 설정
private const int NODE_SIZE = 20;
private const int AGV_SIZE = 30;
private const int CONNECTION_ARROW_SIZE = 8;
#endregion
#region Properties
/// <summary>
/// 맵 노드 목록
/// </summary>
public List<MapNode> MapNodes
{
get => _mapNodes;
set
{
_mapNodes = value;
Invalidate();
}
}
/// <summary>
/// AGV 목록
/// </summary>
public List<VirtualAGV> AGVList
{
get => _agvList;
set
{
_agvList = value;
Invalidate();
}
}
/// <summary>
/// 현재 경로
/// </summary>
public PathResult CurrentPath
{
get => _currentPath;
set
{
_currentPath = value;
Invalidate();
}
}
#endregion
#region Constructor
public SimulatorCanvas()
{
InitializeComponent();
InitializeCanvas();
}
#endregion
#region Initialization
private void InitializeCanvas()
{
_mapNodes = new List<MapNode>();
_agvList = new List<VirtualAGV>();
SetStyle(ControlStyles.AllPaintingInWmPaint |
ControlStyles.UserPaint |
ControlStyles.DoubleBuffer |
ControlStyles.ResizeRedraw, true);
BackColor = Color.White;
// 마우스 이벤트 연결
MouseDown += OnMouseDown;
MouseMove += OnMouseMove;
MouseUp += OnMouseUp;
MouseWheel += OnMouseWheel;
}
#endregion
#region Public Methods
/// <summary>
/// AGV 추가
/// </summary>
public void AddAGV(VirtualAGV agv)
{
if (_agvList == null)
_agvList = new List<VirtualAGV>();
_agvList.Add(agv);
// AGV 이벤트 연결
agv.PositionChanged += OnAGVPositionChanged;
agv.StateChanged += OnAGVStateChanged;
Invalidate();
}
/// <summary>
/// AGV 제거
/// </summary>
public void RemoveAGV(string agvId)
{
var agv = _agvList?.FirstOrDefault(a => a.AgvId == agvId);
if (agv != null)
{
// 이벤트 연결 해제
agv.PositionChanged -= OnAGVPositionChanged;
agv.StateChanged -= OnAGVStateChanged;
_agvList.Remove(agv);
Invalidate();
}
}
/// <summary>
/// 모든 AGV 제거
/// </summary>
public void ClearAGVs()
{
if (_agvList != null)
{
foreach (var agv in _agvList)
{
agv.PositionChanged -= OnAGVPositionChanged;
agv.StateChanged -= OnAGVStateChanged;
}
_agvList.Clear();
Invalidate();
}
}
/// <summary>
/// 확대/축소 초기화
/// </summary>
public void ResetZoom()
{
_zoom = 1.0f;
_panOffset = Point.Empty;
Invalidate();
}
/// <summary>
/// 맵 전체 맞춤
/// </summary>
public void FitToMap()
{
if (_mapNodes == null || _mapNodes.Count == 0)
return;
var minX = _mapNodes.Min(n => n.Position.X);
var maxX = _mapNodes.Max(n => n.Position.X);
var minY = _mapNodes.Min(n => n.Position.Y);
var maxY = _mapNodes.Max(n => n.Position.Y);
var mapWidth = maxX - minX + 100; // 여백 추가
var mapHeight = maxY - minY + 100;
var zoomX = (float)Width / mapWidth;
var zoomY = (float)Height / mapHeight;
_zoom = Math.Min(zoomX, zoomY) * 0.9f; // 약간의 여백
_panOffset = new Point(
(int)((Width - mapWidth * _zoom) / 2 - minX * _zoom),
(int)((Height - mapHeight * _zoom) / 2 - minY * _zoom)
);
Invalidate();
}
#endregion
#region Event Handlers
private void OnAGVPositionChanged(object sender, Point newPosition)
{
Invalidate(); // AGV 위치 변경시 화면 갱신
}
private void OnAGVStateChanged(object sender, AGVState newState)
{
Invalidate(); // AGV 상태 변경시 화면 갱신
}
private void OnMouseDown(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Right)
{
_isPanning = true;
_lastMousePos = e.Location;
Cursor = Cursors.Hand;
}
}
private void OnMouseMove(object sender, MouseEventArgs e)
{
if (_isPanning)
{
var deltaX = e.X - _lastMousePos.X;
var deltaY = e.Y - _lastMousePos.Y;
_panOffset = new Point(
_panOffset.X + deltaX,
_panOffset.Y + deltaY
);
_lastMousePos = e.Location;
Invalidate();
}
}
private void OnMouseUp(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Right)
{
_isPanning = false;
Cursor = Cursors.Default;
}
}
private void OnMouseWheel(object sender, MouseEventArgs e)
{
var zoomFactor = e.Delta > 0 ? 1.1f : 0.9f;
var newZoom = _zoom * zoomFactor;
if (newZoom >= 0.1f && newZoom <= 10.0f)
{
// 마우스 위치 기준으로 줌
var mouseX = e.X - _panOffset.X;
var mouseY = e.Y - _panOffset.Y;
_panOffset = new Point(
(int)(_panOffset.X - mouseX * (zoomFactor - 1)),
(int)(_panOffset.Y - mouseY * (zoomFactor - 1))
);
_zoom = newZoom;
Invalidate();
}
}
#endregion
#region Painting
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
var g = e.Graphics;
g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
// 변환 행렬 설정
g.TranslateTransform(_panOffset.X, _panOffset.Y);
g.ScaleTransform(_zoom, _zoom);
// 배경 그리드 그리기
DrawGrid(g);
// 맵 노드 연결선 그리기
DrawNodeConnections(g);
// 경로 그리기
if (_currentPath != null && _currentPath.Success)
{
DrawPath(g);
}
// 맵 노드 그리기
DrawMapNodes(g);
// AGV 그리기
DrawAGVs(g);
// 정보 표시 (변환 해제)
g.ResetTransform();
DrawInfo(g);
}
private void DrawGrid(Graphics g)
{
var gridSize = 50;
var pen = new Pen(Color.LightGray, 1);
var startX = -(int)(_panOffset.X / _zoom / gridSize) * gridSize;
var startY = -(int)(_panOffset.Y / _zoom / gridSize) * gridSize;
var endX = startX + (int)(Width / _zoom) + gridSize;
var endY = startY + (int)(Height / _zoom) + gridSize;
for (int x = startX; x <= endX; x += gridSize)
{
g.DrawLine(pen, x, startY, x, endY);
}
for (int y = startY; y <= endY; y += gridSize)
{
g.DrawLine(pen, startX, y, endX, y);
}
pen.Dispose();
}
private void DrawNodeConnections(Graphics g)
{
if (_mapNodes == null) return;
foreach (var node in _mapNodes)
{
if (node.ConnectedNodes != null)
{
foreach (var connectedNodeId in node.ConnectedNodes)
{
var connectedNode = _mapNodes.FirstOrDefault(n => n.NodeId == connectedNodeId);
if (connectedNode != null)
{
DrawConnection(g, node.Position, connectedNode.Position);
}
}
}
}
}
private void DrawConnection(Graphics g, Point from, Point to)
{
g.DrawLine(_connectionPen, from, to);
// 방향 화살표 그리기
var angle = Math.Atan2(to.Y - from.Y, to.X - from.X);
var arrowX = to.X - CONNECTION_ARROW_SIZE * Math.Cos(angle);
var arrowY = to.Y - CONNECTION_ARROW_SIZE * Math.Sin(angle);
var arrowPoint1 = new PointF(
(float)(arrowX - CONNECTION_ARROW_SIZE * Math.Cos(angle - Math.PI / 6)),
(float)(arrowY - CONNECTION_ARROW_SIZE * Math.Sin(angle - Math.PI / 6))
);
var arrowPoint2 = new PointF(
(float)(arrowX - CONNECTION_ARROW_SIZE * Math.Cos(angle + Math.PI / 6)),
(float)(arrowY - CONNECTION_ARROW_SIZE * Math.Sin(angle + Math.PI / 6))
);
g.DrawLine(_connectionPen, to, arrowPoint1);
g.DrawLine(_connectionPen, to, arrowPoint2);
}
private void DrawPath(Graphics g)
{
if (_currentPath?.Path == null || _currentPath.Path.Count < 2)
return;
for (int i = 0; i < _currentPath.Path.Count - 1; i++)
{
var currentNodeId = _currentPath.Path[i];
var nextNodeId = _currentPath.Path[i + 1];
var currentNode = _mapNodes?.FirstOrDefault(n => n.NodeId == currentNodeId);
var nextNode = _mapNodes?.FirstOrDefault(n => n.NodeId == nextNodeId);
if (currentNode != null && nextNode != null)
{
g.DrawLine(_pathPen, currentNode.Position, nextNode.Position);
}
}
}
private void DrawMapNodes(Graphics g)
{
if (_mapNodes == null) return;
foreach (var node in _mapNodes)
{
DrawMapNode(g, node);
}
}
private void DrawMapNode(Graphics g, MapNode node)
{
var brush = GetNodeBrush(node.Type);
var rect = new Rectangle(
node.Position.X - NODE_SIZE / 2,
node.Position.Y - NODE_SIZE / 2,
NODE_SIZE,
NODE_SIZE
);
// 노드 그리기
if (node.Type == NodeType.Rotation)
{
g.FillEllipse(brush, rect); // 회전 노드는 원형
}
else
{
g.FillRectangle(brush, rect); // 일반 노드는 사각형
}
g.DrawRectangle(Pens.Black, rect);
// 노드 ID 표시
var font = new Font("Arial", 8);
var textSize = g.MeasureString(node.NodeId, font);
var textPos = new PointF(
node.Position.X - textSize.Width / 2,
node.Position.Y + NODE_SIZE / 2 + 2
);
g.DrawString(node.NodeId, font, Brushes.Black, textPos);
font.Dispose();
}
private Brush GetNodeBrush(NodeType nodeType)
{
switch (nodeType)
{
case NodeType.Rotation: return _rotationNodeBrush;
case NodeType.Docking: return _dockingNodeBrush;
case NodeType.Charging: return _chargingNodeBrush;
default: return _normalNodeBrush;
}
}
private void DrawAGVs(Graphics g)
{
if (_agvList == null) return;
foreach (var agv in _agvList)
{
DrawAGV(g, agv);
}
}
private void DrawAGV(Graphics g, VirtualAGV agv)
{
var position = agv.CurrentPosition;
var rect = new Rectangle(
position.X - AGV_SIZE / 2,
position.Y - AGV_SIZE / 2,
AGV_SIZE,
AGV_SIZE
);
// AGV 상태에 따른 색상 변경
var brush = GetAGVBrush(agv.CurrentState);
// AGV 본체 그리기
g.FillEllipse(brush, rect);
g.DrawEllipse(_agvPen, rect);
// 방향 표시
DrawAGVDirection(g, position, agv.CurrentDirection);
// AGV ID 표시
var font = new Font("Arial", 10, FontStyle.Bold);
var textSize = g.MeasureString(agv.AgvId, font);
var textPos = new PointF(
position.X - textSize.Width / 2,
position.Y + AGV_SIZE / 2 + 5
);
g.DrawString(agv.AgvId, font, Brushes.Black, textPos);
font.Dispose();
}
private Brush GetAGVBrush(AGVState state)
{
switch (state)
{
case AGVState.Moving: return Brushes.Blue;
case AGVState.Rotating: return Brushes.Yellow;
case AGVState.Docking: return Brushes.Orange;
case AGVState.Charging: return Brushes.Green;
case AGVState.Error: return Brushes.Red;
default: return Brushes.Gray; // Idle
}
}
private void DrawAGVDirection(Graphics g, Point position, AgvDirection direction)
{
var arrowSize = 10;
var pen = new Pen(Color.White, 2);
switch (direction)
{
case AgvDirection.Forward:
// 위쪽 화살표
g.DrawLine(pen, position.X, position.Y - arrowSize, position.X, position.Y + arrowSize);
g.DrawLine(pen, position.X, position.Y - arrowSize, position.X - 5, position.Y - arrowSize + 5);
g.DrawLine(pen, position.X, position.Y - arrowSize, position.X + 5, position.Y - arrowSize + 5);
break;
case AgvDirection.Backward:
// 아래쪽 화살표
g.DrawLine(pen, position.X, position.Y - arrowSize, position.X, position.Y + arrowSize);
g.DrawLine(pen, position.X, position.Y + arrowSize, position.X - 5, position.Y + arrowSize - 5);
g.DrawLine(pen, position.X, position.Y + arrowSize, position.X + 5, position.Y + arrowSize - 5);
break;
case AgvDirection.Left:
// 왼쪽 화살표
g.DrawLine(pen, position.X - arrowSize, position.Y, position.X + arrowSize, position.Y);
g.DrawLine(pen, position.X - arrowSize, position.Y, position.X - arrowSize + 5, position.Y - 5);
g.DrawLine(pen, position.X - arrowSize, position.Y, position.X - arrowSize + 5, position.Y + 5);
break;
case AgvDirection.Right:
// 오른쪽 화살표
g.DrawLine(pen, position.X - arrowSize, position.Y, position.X + arrowSize, position.Y);
g.DrawLine(pen, position.X + arrowSize, position.Y, position.X + arrowSize - 5, position.Y - 5);
g.DrawLine(pen, position.X + arrowSize, position.Y, position.X + arrowSize - 5, position.Y + 5);
break;
}
pen.Dispose();
}
private void DrawInfo(Graphics g)
{
var font = new Font("Arial", 10);
var brush = Brushes.Black;
var y = 10;
// 줌 레벨 표시
g.DrawString($"줌: {_zoom:P0}", font, brush, new PointF(10, y));
y += 20;
// AGV 정보 표시
if (_agvList != null)
{
g.DrawString($"AGV 수: {_agvList.Count}", font, brush, new PointF(10, y));
y += 20;
foreach (var agv in _agvList)
{
var info = $"{agv.AgvId}: {agv.CurrentState} ({agv.CurrentPosition.X},{agv.CurrentPosition.Y})";
g.DrawString(info, font, brush, new PointF(10, y));
y += 15;
}
}
// 경로 정보 표시
if (_currentPath != null && _currentPath.Success)
{
y += 10;
g.DrawString($"경로: {_currentPath.Path.Count}개 노드", font, brush, new PointF(10, y));
y += 15;
g.DrawString($"거리: {_currentPath.TotalDistance:F1}", font, brush, new PointF(10, y));
y += 15;
g.DrawString($"계산시간: {_currentPath.CalculationTimeMs}ms", font, brush, new PointF(10, y));
}
font.Dispose();
}
#endregion
#region Cleanup
private void CleanupResources()
{
// AGV 이벤트 연결 해제
if (_agvList != null)
{
foreach (var agv in _agvList)
{
agv.PositionChanged -= OnAGVPositionChanged;
agv.StateChanged -= OnAGVStateChanged;
}
}
// 리소스 정리
_connectionPen?.Dispose();
_pathPen?.Dispose();
_agvPen?.Dispose();
}
#endregion
}
}

View File

@@ -1,119 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion. Classes that don't support this are
serialized and stored with the mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

View File

@@ -70,6 +70,7 @@ namespace AGVSimulator.Forms
this.startSimulationToolStripButton = new System.Windows.Forms.ToolStripButton();
this.stopSimulationToolStripButton = new System.Windows.Forms.ToolStripButton();
this.resetToolStripButton = new System.Windows.Forms.ToolStripButton();
this.btAllReset = new System.Windows.Forms.ToolStripButton();
this.toolStripSeparator3 = new System.Windows.Forms.ToolStripSeparator();
this.fitToMapToolStripButton = new System.Windows.Forms.ToolStripButton();
this.resetZoomToolStripButton = new System.Windows.Forms.ToolStripButton();
@@ -85,6 +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._avoidRotationCheckBox = new System.Windows.Forms.CheckBox();
this._targetNodeCombo = new System.Windows.Forms.ComboBox();
this.targetNodeLabel = new System.Windows.Forms.Label();
this._startNodeCombo = new System.Windows.Forms.ComboBox();
@@ -93,13 +95,19 @@ namespace AGVSimulator.Forms
this._setPositionButton = new System.Windows.Forms.Button();
this._rfidTextBox = new System.Windows.Forms.TextBox();
this._rfidLabel = new System.Windows.Forms.Label();
this._directionCombo = new System.Windows.Forms.ComboBox();
this._directionLabel = new System.Windows.Forms.Label();
this._stopSimulationButton = new System.Windows.Forms.Button();
this._startSimulationButton = new System.Windows.Forms.Button();
this._removeAgvButton = new System.Windows.Forms.Button();
this._addAgvButton = new System.Windows.Forms.Button();
this._agvListCombo = new System.Windows.Forms.ComboBox();
this._canvasPanel = new System.Windows.Forms.Panel();
this.btAllReset = new System.Windows.Forms.ToolStripButton();
this._agvInfoPanel = new System.Windows.Forms.Panel();
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._menuStrip.SuspendLayout();
this._toolStrip.SuspendLayout();
this._statusStrip.SuspendLayout();
@@ -107,6 +115,7 @@ namespace AGVSimulator.Forms
this._statusGroup.SuspendLayout();
this._pathGroup.SuspendLayout();
this._agvControlGroup.SuspendLayout();
this._agvInfoPanel.SuspendLayout();
this.SuspendLayout();
//
// _menuStrip
@@ -139,7 +148,7 @@ namespace AGVSimulator.Forms
//
this.openMapToolStripMenuItem.Name = "openMapToolStripMenuItem";
this.openMapToolStripMenuItem.ShortcutKeys = ((System.Windows.Forms.Keys)((System.Windows.Forms.Keys.Control | System.Windows.Forms.Keys.O)));
this.openMapToolStripMenuItem.Size = new System.Drawing.Size(183, 22);
this.openMapToolStripMenuItem.Size = new System.Drawing.Size(221, 22);
this.openMapToolStripMenuItem.Text = "맵 열기(&O)...";
this.openMapToolStripMenuItem.Click += new System.EventHandler(this.OnOpenMap_Click);
//
@@ -147,33 +156,33 @@ namespace AGVSimulator.Forms
//
this.reloadMapToolStripMenuItem.Name = "reloadMapToolStripMenuItem";
this.reloadMapToolStripMenuItem.ShortcutKeys = ((System.Windows.Forms.Keys)((System.Windows.Forms.Keys.Control | System.Windows.Forms.Keys.R)));
this.reloadMapToolStripMenuItem.Size = new System.Drawing.Size(183, 22);
this.reloadMapToolStripMenuItem.Size = new System.Drawing.Size(221, 22);
this.reloadMapToolStripMenuItem.Text = "맵 다시열기(&R)";
this.reloadMapToolStripMenuItem.Click += new System.EventHandler(this.OnReloadMap_Click);
//
// toolStripSeparator1
//
this.toolStripSeparator1.Name = "toolStripSeparator1";
this.toolStripSeparator1.Size = new System.Drawing.Size(180, 6);
this.toolStripSeparator1.Size = new System.Drawing.Size(218, 6);
//
// launchMapEditorToolStripMenuItem
//
this.launchMapEditorToolStripMenuItem.Name = "launchMapEditorToolStripMenuItem";
this.launchMapEditorToolStripMenuItem.ShortcutKeys = ((System.Windows.Forms.Keys)((System.Windows.Forms.Keys.Control | System.Windows.Forms.Keys.M)));
this.launchMapEditorToolStripMenuItem.Size = new System.Drawing.Size(183, 22);
this.launchMapEditorToolStripMenuItem.Size = new System.Drawing.Size(221, 22);
this.launchMapEditorToolStripMenuItem.Text = "MapEditor 실행(&M)";
this.launchMapEditorToolStripMenuItem.Click += new System.EventHandler(this.OnLaunchMapEditor_Click);
//
// toolStripSeparator4
//
this.toolStripSeparator4.Name = "toolStripSeparator4";
this.toolStripSeparator4.Size = new System.Drawing.Size(180, 6);
this.toolStripSeparator4.Size = new System.Drawing.Size(218, 6);
//
// exitToolStripMenuItem
//
this.exitToolStripMenuItem.Name = "exitToolStripMenuItem";
this.exitToolStripMenuItem.ShortcutKeys = ((System.Windows.Forms.Keys)((System.Windows.Forms.Keys.Alt | System.Windows.Forms.Keys.F4)));
this.exitToolStripMenuItem.Size = new System.Drawing.Size(183, 22);
this.exitToolStripMenuItem.Size = new System.Drawing.Size(221, 22);
this.exitToolStripMenuItem.Text = "종료(&X)";
this.exitToolStripMenuItem.Click += new System.EventHandler(this.OnExit_Click);
//
@@ -284,7 +293,7 @@ namespace AGVSimulator.Forms
//
this.reloadMapToolStripButton.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
this.reloadMapToolStripButton.Name = "reloadMapToolStripButton";
this.reloadMapToolStripButton.Size = new System.Drawing.Size(63, 22);
this.reloadMapToolStripButton.Size = new System.Drawing.Size(59, 22);
this.reloadMapToolStripButton.Text = "다시열기";
this.reloadMapToolStripButton.ToolTipText = "현재 맵을 다시 로드합니다";
this.reloadMapToolStripButton.Click += new System.EventHandler(this.OnReloadMap_Click);
@@ -293,7 +302,7 @@ namespace AGVSimulator.Forms
//
this.launchMapEditorToolStripButton.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
this.launchMapEditorToolStripButton.Name = "launchMapEditorToolStripButton";
this.launchMapEditorToolStripButton.Size = new System.Drawing.Size(71, 22);
this.launchMapEditorToolStripButton.Size = new System.Drawing.Size(66, 22);
this.launchMapEditorToolStripButton.Text = "MapEditor";
this.launchMapEditorToolStripButton.ToolTipText = "MapEditor를 실행합니다";
this.launchMapEditorToolStripButton.Click += new System.EventHandler(this.OnLaunchMapEditor_Click);
@@ -330,6 +339,15 @@ namespace AGVSimulator.Forms
this.resetToolStripButton.ToolTipText = "시뮬레이션을 초기화합니다";
this.resetToolStripButton.Click += new System.EventHandler(this.OnReset_Click);
//
// btAllReset
//
this.btAllReset.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
this.btAllReset.Name = "btAllReset";
this.btAllReset.Size = new System.Drawing.Size(71, 22);
this.btAllReset.Text = "전체초기화";
this.btAllReset.ToolTipText = "시뮬레이션을 초기화합니다";
this.btAllReset.Click += new System.EventHandler(this.btAllReset_Click);
//
// toolStripSeparator3
//
this.toolStripSeparator3.Name = "toolStripSeparator3";
@@ -382,9 +400,9 @@ namespace AGVSimulator.Forms
this._controlPanel.Controls.Add(this._pathGroup);
this._controlPanel.Controls.Add(this._agvControlGroup);
this._controlPanel.Dock = System.Windows.Forms.DockStyle.Right;
this._controlPanel.Location = new System.Drawing.Point(950, 49);
this._controlPanel.Location = new System.Drawing.Point(967, 49);
this._controlPanel.Name = "_controlPanel";
this._controlPanel.Size = new System.Drawing.Size(250, 729);
this._controlPanel.Size = new System.Drawing.Size(233, 729);
this._controlPanel.TabIndex = 3;
//
// _statusGroup
@@ -392,9 +410,10 @@ namespace AGVSimulator.Forms
this._statusGroup.Controls.Add(this._pathLengthLabel);
this._statusGroup.Controls.Add(this._agvCountLabel);
this._statusGroup.Controls.Add(this._simulationStatusLabel);
this._statusGroup.Location = new System.Drawing.Point(10, 356);
this._statusGroup.Dock = System.Windows.Forms.DockStyle.Top;
this._statusGroup.Location = new System.Drawing.Point(0, 404);
this._statusGroup.Name = "_statusGroup";
this._statusGroup.Size = new System.Drawing.Size(230, 100);
this._statusGroup.Size = new System.Drawing.Size(233, 100);
this._statusGroup.TabIndex = 3;
this._statusGroup.TabStop = false;
this._statusGroup.Text = "상태 정보";
@@ -431,20 +450,22 @@ 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._avoidRotationCheckBox);
this._pathGroup.Controls.Add(this._targetNodeCombo);
this._pathGroup.Controls.Add(this.targetNodeLabel);
this._pathGroup.Controls.Add(this._startNodeCombo);
this._pathGroup.Controls.Add(this.startNodeLabel);
this._pathGroup.Location = new System.Drawing.Point(10, 200);
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(230, 150);
this._pathGroup.Size = new System.Drawing.Size(233, 190);
this._pathGroup.TabIndex = 1;
this._pathGroup.TabStop = false;
this._pathGroup.Text = "경로 제어";
//
// _clearPathButton
//
this._clearPathButton.Location = new System.Drawing.Point(150, 120);
this._clearPathButton.Location = new System.Drawing.Point(150, 148);
this._clearPathButton.Name = "_clearPathButton";
this._clearPathButton.Size = new System.Drawing.Size(70, 25);
this._clearPathButton.TabIndex = 6;
@@ -454,7 +475,7 @@ namespace AGVSimulator.Forms
//
// _startPathButton
//
this._startPathButton.Location = new System.Drawing.Point(80, 120);
this._startPathButton.Location = new System.Drawing.Point(80, 148);
this._startPathButton.Name = "_startPathButton";
this._startPathButton.Size = new System.Drawing.Size(65, 25);
this._startPathButton.TabIndex = 5;
@@ -464,7 +485,7 @@ namespace AGVSimulator.Forms
//
// _calculatePathButton
//
this._calculatePathButton.Location = new System.Drawing.Point(10, 120);
this._calculatePathButton.Location = new System.Drawing.Point(10, 148);
this._calculatePathButton.Name = "_calculatePathButton";
this._calculatePathButton.Size = new System.Drawing.Size(65, 25);
this._calculatePathButton.TabIndex = 4;
@@ -472,10 +493,20 @@ namespace AGVSimulator.Forms
this._calculatePathButton.UseVisualStyleBackColor = true;
this._calculatePathButton.Click += new System.EventHandler(this.OnCalculatePath_Click);
//
// _avoidRotationCheckBox
//
this._avoidRotationCheckBox.AutoSize = true;
this._avoidRotationCheckBox.Location = new System.Drawing.Point(10, 126);
this._avoidRotationCheckBox.Name = "_avoidRotationCheckBox";
this._avoidRotationCheckBox.Size = new System.Drawing.Size(104, 16);
this._avoidRotationCheckBox.TabIndex = 7;
this._avoidRotationCheckBox.Text = "회전 구간 회피";
this._avoidRotationCheckBox.UseVisualStyleBackColor = true;
//
// _targetNodeCombo
//
this._targetNodeCombo.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
this._targetNodeCombo.Location = new System.Drawing.Point(10, 95);
this._targetNodeCombo.Location = new System.Drawing.Point(10, 97);
this._targetNodeCombo.Name = "_targetNodeCombo";
this._targetNodeCombo.Size = new System.Drawing.Size(210, 20);
this._targetNodeCombo.TabIndex = 3;
@@ -511,14 +542,17 @@ namespace AGVSimulator.Forms
this._agvControlGroup.Controls.Add(this._setPositionButton);
this._agvControlGroup.Controls.Add(this._rfidTextBox);
this._agvControlGroup.Controls.Add(this._rfidLabel);
this._agvControlGroup.Controls.Add(this._directionCombo);
this._agvControlGroup.Controls.Add(this._directionLabel);
this._agvControlGroup.Controls.Add(this._stopSimulationButton);
this._agvControlGroup.Controls.Add(this._startSimulationButton);
this._agvControlGroup.Controls.Add(this._removeAgvButton);
this._agvControlGroup.Controls.Add(this._addAgvButton);
this._agvControlGroup.Controls.Add(this._agvListCombo);
this._agvControlGroup.Location = new System.Drawing.Point(10, 10);
this._agvControlGroup.Dock = System.Windows.Forms.DockStyle.Top;
this._agvControlGroup.Location = new System.Drawing.Point(0, 0);
this._agvControlGroup.Name = "_agvControlGroup";
this._agvControlGroup.Size = new System.Drawing.Size(230, 180);
this._agvControlGroup.Size = new System.Drawing.Size(233, 214);
this._agvControlGroup.TabIndex = 0;
this._agvControlGroup.TabStop = false;
this._agvControlGroup.Text = "AGV 제어";
@@ -527,7 +561,7 @@ namespace AGVSimulator.Forms
//
this._setPositionButton.Location = new System.Drawing.Point(160, 138);
this._setPositionButton.Name = "_setPositionButton";
this._setPositionButton.Size = new System.Drawing.Size(60, 25);
this._setPositionButton.Size = new System.Drawing.Size(60, 67);
this._setPositionButton.TabIndex = 7;
this._setPositionButton.Text = "위치설정";
this._setPositionButton.UseVisualStyleBackColor = true;
@@ -550,6 +584,24 @@ namespace AGVSimulator.Forms
this._rfidLabel.TabIndex = 5;
this._rfidLabel.Text = "RFID 현재위치:";
//
// _directionCombo
//
this._directionCombo.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
this._directionCombo.FormattingEnabled = true;
this._directionCombo.Location = new System.Drawing.Point(10, 185);
this._directionCombo.Name = "_directionCombo";
this._directionCombo.Size = new System.Drawing.Size(140, 20);
this._directionCombo.TabIndex = 8;
//
// _directionLabel
//
this._directionLabel.AutoSize = true;
this._directionLabel.Location = new System.Drawing.Point(10, 165);
this._directionLabel.Name = "_directionLabel";
this._directionLabel.Size = new System.Drawing.Size(85, 12);
this._directionLabel.TabIndex = 9;
this._directionLabel.Text = "모터 구동방향:";
//
// _stopSimulationButton
//
this._stopSimulationButton.Location = new System.Drawing.Point(120, 85);
@@ -602,19 +654,65 @@ namespace AGVSimulator.Forms
// _canvasPanel
//
this._canvasPanel.Dock = System.Windows.Forms.DockStyle.Fill;
this._canvasPanel.Location = new System.Drawing.Point(0, 49);
this._canvasPanel.Location = new System.Drawing.Point(0, 109);
this._canvasPanel.Name = "_canvasPanel";
this._canvasPanel.Size = new System.Drawing.Size(950, 729);
this._canvasPanel.Size = new System.Drawing.Size(967, 669);
this._canvasPanel.TabIndex = 4;
//
// btAllReset
// _agvInfoPanel
//
this.btAllReset.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
this.btAllReset.Name = "btAllReset";
this.btAllReset.Size = new System.Drawing.Size(71, 22);
this.btAllReset.Text = "전체초기화";
this.btAllReset.ToolTipText = "시뮬레이션을 초기화합니다";
this.btAllReset.Click += new System.EventHandler(this.btAllReset_Click);
this._agvInfoPanel.BackColor = System.Drawing.Color.LightBlue;
this._agvInfoPanel.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
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.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.Name = "_agvInfoTitleLabel";
this._agvInfoTitleLabel.Size = new System.Drawing.Size(91, 15);
this._agvInfoTitleLabel.TabIndex = 0;
this._agvInfoTitleLabel.Text = "AGV 상태 정보:";
//
// _liftDirectionLabel
//
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.Name = "_liftDirectionLabel";
this._liftDirectionLabel.Size = new System.Drawing.Size(83, 15);
this._liftDirectionLabel.TabIndex = 1;
this._liftDirectionLabel.Text = "리프트 방향: -";
//
// _motorDirectionLabel
//
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.Name = "_motorDirectionLabel";
this._motorDirectionLabel.Size = new System.Drawing.Size(71, 15);
this._motorDirectionLabel.TabIndex = 2;
this._motorDirectionLabel.Text = "모터 방향: -";
//
// _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.Location = new System.Drawing.Point(10, 30);
this._pathDebugLabel.Name = "_pathDebugLabel";
this._pathDebugLabel.Size = new System.Drawing.Size(114, 15);
this._pathDebugLabel.TabIndex = 3;
this._pathDebugLabel.Text = "경로: 설정되지 않음";
//
// SimulatorForm
//
@@ -622,6 +720,7 @@ namespace AGVSimulator.Forms
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(1200, 800);
this.Controls.Add(this._canvasPanel);
this.Controls.Add(this._agvInfoPanel);
this.Controls.Add(this._controlPanel);
this.Controls.Add(this._statusStrip);
this.Controls.Add(this._toolStrip);
@@ -644,6 +743,8 @@ namespace AGVSimulator.Forms
this._pathGroup.PerformLayout();
this._agvControlGroup.ResumeLayout(false);
this._agvControlGroup.PerformLayout();
this._agvInfoPanel.ResumeLayout(false);
this._agvInfoPanel.PerformLayout();
this.ResumeLayout(false);
this.PerformLayout();
@@ -692,6 +793,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.CheckBox _avoidRotationCheckBox;
private System.Windows.Forms.GroupBox _statusGroup;
private System.Windows.Forms.Label _simulationStatusLabel;
private System.Windows.Forms.Label _agvCountLabel;
@@ -700,11 +802,18 @@ namespace AGVSimulator.Forms
private System.Windows.Forms.Label _rfidLabel;
private System.Windows.Forms.TextBox _rfidTextBox;
private System.Windows.Forms.Button _setPositionButton;
private System.Windows.Forms.ComboBox _directionCombo;
private System.Windows.Forms.Label _directionLabel;
private System.Windows.Forms.ToolStripButton btAllReset;
private System.Windows.Forms.ToolStripMenuItem reloadMapToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem launchMapEditorToolStripMenuItem;
private System.Windows.Forms.ToolStripSeparator toolStripSeparator4;
private System.Windows.Forms.ToolStripButton reloadMapToolStripButton;
private System.Windows.Forms.ToolStripButton launchMapEditorToolStripButton;
private System.Windows.Forms.Panel _agvInfoPanel;
private System.Windows.Forms.Label _liftDirectionLabel;
private System.Windows.Forms.Label _motorDirectionLabel;
private System.Windows.Forms.Label _agvInfoTitleLabel;
private System.Windows.Forms.Label _pathDebugLabel;
}
}

View File

@@ -25,6 +25,7 @@ namespace AGVSimulator.Forms
private List<MapNode> _mapNodes;
private NodeResolver _nodeResolver;
private PathCalculator _pathCalculator;
private AdvancedAGVPathfinder _advancedPathfinder;
private List<VirtualAGV> _agvList;
private SimulationState _simulationState;
private Timer _simulationTimer;
@@ -50,6 +51,9 @@ namespace AGVSimulator.Forms
{
InitializeComponent();
InitializeForm();
// Load 이벤트 연결
this.Load += SimulatorForm_Load;
}
#endregion
@@ -80,6 +84,8 @@ namespace AGVSimulator.Forms
// 초기 상태 설정
UpdateUI();
// 마지막 맵 파일 자동 로드 확인은 Form_Load에서 수행
}
@@ -121,6 +127,12 @@ namespace AGVSimulator.Forms
#region Event Handlers
private void SimulatorForm_Load(object sender, EventArgs e)
{
// 폼이 완전히 로드된 후 마지막 맵 파일 자동 로드 확인
CheckAndLoadLastMapFile();
}
private void OnOpenMap_Click(object sender, EventArgs e)
{
using (var openDialog = new OpenFileDialog())
@@ -279,7 +291,39 @@ namespace AGVSimulator.Forms
_pathCalculator.SetMapData(_mapNodes);
}
var agvResult = _pathCalculator.FindAGVPath(startNode.NodeId, targetNode.NodeId);
if (_advancedPathfinder == null)
{
_advancedPathfinder = new AdvancedAGVPathfinder(_mapNodes);
}
// 현재 AGV 방향 가져오기
var selectedAGV = _agvListCombo.SelectedItem as VirtualAGV;
var currentDirection = selectedAGV?.CurrentDirection ?? AgvDirection.Forward;
// 고급 경로 계획 사용
var advancedResult = _advancedPathfinder.FindAdvancedPath(startNode.NodeId, targetNode.NodeId, currentDirection);
if (advancedResult.Success)
{
// 고급 경로 결과를 기존 AGVPathResult 형태로 변환
var agvResult1 = ConvertToAGVPathResult(advancedResult);
_simulatorCanvas.CurrentPath = agvResult1.ToPathResult();
_pathLengthLabel.Text = $"경로 길이: {advancedResult.TotalDistance:F1}";
_statusLabel.Text = $"고급 경로 계산 완료 ({advancedResult.CalculationTimeMs}ms)";
// 고급 경로 디버깅 정보 표시
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)
{
@@ -434,8 +478,12 @@ namespace AGVSimulator.Forms
if (nodesWithRfid.Count == 0)
return "RFID가 할당된 노드가 없습니다.";
// 처음 10개의 RFID만 표시
var rfidList = nodesWithRfid.Take(10).Select(n => $"- {n.RfidId} → {n.NodeId}");
// 처음 10개의 RFID만 표시 (노드 이름 포함)
var rfidList = nodesWithRfid.Take(10).Select(n =>
{
var nodeNamePart = !string.IsNullOrEmpty(n.Name) ? $" {n.Name}" : "";
return $"- {n.RfidId} → {n.NodeId}{nodeNamePart}";
});
var result = string.Join("\n", rfidList);
if (nodesWithRfid.Count > 10)
@@ -457,8 +505,7 @@ namespace AGVSimulator.Forms
_mapNodes = result.Nodes;
_currentMapFilePath = filePath;
// RFID가 없는 노드들에 자동 할당
MapLoader.AssignAutoRfidIds(_mapNodes);
// RFID 자동 할당 제거 - 에디터에서 설정한 값 그대로 사용
// 시뮬레이터 캔버스에 맵 설정
_simulatorCanvas.Nodes = _mapNodes;
@@ -488,6 +535,35 @@ namespace AGVSimulator.Forms
}
}
/// <summary>
/// 마지막 맵 파일이 있는지 확인하고 사용자에게 로드할지 물어봄
/// </summary>
private void CheckAndLoadLastMapFile()
{
if (_config.AutoLoadLastMapFile && _config.HasValidLastMapFile())
{
string fileName = Path.GetFileName(_config.LastMapFilePath);
var result = MessageBox.Show(
$"마지막으로 사용한 맵 파일을 찾았습니다:\n\n{fileName}\n\n이 파일을 열까요?",
"마지막 맵 파일 로드",
MessageBoxButtons.YesNo,
MessageBoxIcon.Question);
if (result == DialogResult.Yes)
{
try
{
LoadMapFile(_config.LastMapFilePath);
}
catch (Exception ex)
{
MessageBox.Show($"맵 파일 로드 중 오류가 발생했습니다:\n{ex.Message}",
"맵 파일 로드 오류", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
}
}
private void UpdateNodeComboBoxes()
{
_startNodeCombo.Items.Clear();
@@ -499,8 +575,9 @@ namespace AGVSimulator.Forms
{
if (node.IsActive && node.HasRfid())
{
// {rfid} - [{node}] 형식으로 ComboBoxItem 생성
var displayText = $"{node.RfidId} - [{node.NodeId}]";
// {rfid} - [{node}] {name} 형식으로 ComboBoxItem 생성
var nodeNamePart = !string.IsNullOrEmpty(node.Name) ? $" {node.Name}" : "";
var displayText = $"{node.RfidId} - [{node.NodeId}]{nodeNamePart}";
var item = new ComboBoxItem<MapNode>(node, displayText);
_startNodeCombo.Items.Add(item);
@@ -709,6 +786,111 @@ namespace AGVSimulator.Forms
return node?.HasRfid() == true ? node.RfidId : nodeId;
}
/// <summary>
/// 고급 경로 결과를 기존 AGVPathResult 형태로 변환
/// </summary>
private AGVPathResult ConvertToAGVPathResult(AdvancedAGVPathfinder.AdvancedPathResult advancedResult)
{
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;
return agvResult;
}
/// <summary>
/// 고급 경로 디버깅 정보 업데이트
/// </summary>
private void UpdateAdvancedPathDebugInfo(AdvancedAGVPathfinder.AdvancedPathResult advancedResult)
{
if (advancedResult == null || !advancedResult.Success)
{
_pathDebugLabel.Text = "고급 경로: 설정되지 않음";
return;
}
// 노드 ID를 RFID로 변환한 경로 생성
var pathWithRfid = advancedResult.GetSimplePath().Select(nodeId => GetRfidByNodeId(nodeId)).ToList();
// 콘솔 디버그 정보 출력
Program.WriteLine($"[ADVANCED DEBUG] 고급 경로 계산 완료:");
Program.WriteLine($" 전체 경로 (RFID): [{string.Join(" ", pathWithRfid)}]");
Program.WriteLine($" 전체 경로 (NodeID): [{string.Join(" ", advancedResult.GetSimplePath())}]");
Program.WriteLine($" 경로 노드 수: {advancedResult.DetailedPath.Count}");
Program.WriteLine($" 방향 전환 필요: {advancedResult.RequiredDirectionChange}");
if (advancedResult.RequiredDirectionChange && !string.IsNullOrEmpty(advancedResult.DirectionChangeNode))
{
Program.WriteLine($" 방향 전환 노드: {advancedResult.DirectionChangeNode}");
}
Program.WriteLine($" 설명: {advancedResult.PlanDescription}");
// 상세 경로 정보 출력
for (int i = 0; i < advancedResult.DetailedPath.Count; i++)
{
var info = advancedResult.DetailedPath[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}");
if (info.MagnetDirection != MagnetDirection.Straight) flags.Add($"마그넷:{info.MagnetDirection}");
var flagsStr = flags.Count > 0 ? $" [{string.Join(", ", flags)}]" : "";
Program.WriteLine($" {i}: {rfidId}({info.NodeId}) → {info.MotorDirection} → {nextRfidId}{flagsStr}");
}
// 경로 문자열 구성 (마그넷 방향 포함)
var pathWithDetails = new List<string>();
for (int i = 0; i < advancedResult.DetailedPath.Count; i++)
{
var motorInfo = advancedResult.DetailedPath[i];
var rfidId = GetRfidByNodeId(motorInfo.NodeId);
string motorSymbol = motorInfo.MotorDirection == AgvDirection.Forward ? "[전진]" : "[후진]";
// 마그넷 방향 표시
if (motorInfo.MagnetDirection != MagnetDirection.Straight)
{
string magnetSymbol = motorInfo.MagnetDirection == MagnetDirection.Left ? "[←]" : "[→]";
motorSymbol += magnetSymbol;
}
// 특수 동작 표시
if (motorInfo.RequiresSpecialAction)
motorSymbol += "[🔄]";
else if (motorInfo.IsDirectionChangePoint && motorInfo.CanRotate)
motorSymbol += "[↻]";
pathWithDetails.Add($"{rfidId}{motorSymbol}");
}
string pathString = string.Join(" → ", pathWithDetails);
// UI에 표시 (길이 제한)
if (pathString.Length > 100)
{
pathString = pathString.Substring(0, 97) + "...";
}
// 통계 정보
var forwardCount = advancedResult.DetailedPath.Count(m => m.MotorDirection == AgvDirection.Forward);
var backwardCount = advancedResult.DetailedPath.Count(m => m.MotorDirection == AgvDirection.Backward);
var magnetDirectionChanges = advancedResult.DetailedPath.Count(m => m.MagnetDirection != MagnetDirection.Straight);
string stats = $"전진: {forwardCount}, 후진: {backwardCount}";
if (magnetDirectionChanges > 0)
stats += $", 마그넷제어: {magnetDirectionChanges}";
_pathDebugLabel.Text = $"고급경로: {pathString} (총 {advancedResult.DetailedPath.Count}개 노드, {advancedResult.TotalDistance:F1}px, {stats})";
}
/// <summary>
/// 경로 디버깅 정보 업데이트 (RFID 값 표시, 모터방향 정보 포함)
/// </summary>
@@ -736,7 +918,14 @@ namespace AGVSimulator.Forms
var info = agvResult.NodeMotorInfos[i];
var rfidId = GetRfidByNodeId(info.NodeId);
var nextRfidId = info.NextNodeId != null ? GetRfidByNodeId(info.NextNodeId) : "END";
Program.WriteLine($" {i}: {rfidId}({info.NodeId}) → {info.MotorDirection} → {nextRfidId}");
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}");
}
}
@@ -751,6 +940,13 @@ namespace AGVSimulator.Forms
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);

View File

@@ -26,6 +26,11 @@ namespace AGVSimulator.Models
/// </summary>
public bool AutoSave { get; set; } = true;
/// <summary>
/// 프로그램 시작시 마지막 맵 파일을 자동으로 로드할지 여부
/// </summary>
public bool AutoLoadLastMapFile { get; set; } = true;
#endregion
#region Static Methods
@@ -105,10 +110,19 @@ namespace AGVSimulator.Models
/// <returns>유효한 경로인지 여부</returns>
public bool IsMapEditorPathValid()
{
return !string.IsNullOrEmpty(MapEditorExecutablePath) &&
return !string.IsNullOrEmpty(MapEditorExecutablePath) &&
File.Exists(MapEditorExecutablePath);
}
/// <summary>
/// 마지막 맵 파일이 존재하는지 확인
/// </summary>
/// <returns>마지막 맵 파일이 유효한지 여부</returns>
public bool HasValidLastMapFile()
{
return !string.IsNullOrEmpty(LastMapFilePath) && File.Exists(LastMapFilePath);
}
#endregion
}
}

View File

@@ -9,21 +9,33 @@ namespace AGVSimulator
/// </summary>
static class Program
{
/// <summary>
/// 콘솔 출력 (타임스탬프 포함)
/// </summary>
public static void WriteLine(string message)
{
string timestampedMessage = $"[{DateTime.Now:HH:mm:ss.fff}] {message}";
Console.WriteLine(timestampedMessage);
}
/// <summary>
/// 애플리케이션의 주 진입점입니다.
/// </summary>
/// <param name="args">명령줄 인수</param>
[STAThread]
static void Main()
static void Main(string[] args)
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
try
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new SimulatorForm());
}
catch (Exception ex)
{
Console.WriteLine($"[ERROR] 시뮬레이터 실행 중 오류: {ex.Message}");
Console.WriteLine($"[ERROR] 스택 트레이스: {ex.StackTrace}");
MessageBox.Show($"시뮬레이터 실행 중 오류가 발생했습니다:\n{ex.Message}",
"시스템 오류", MessageBoxButtons.OK, MessageBoxIcon.Error);
}