diff --git a/Cs_HMI/AGVMapEditor/AGVMapEditor.csproj b/Cs_HMI/AGVMapEditor/AGVMapEditor.csproj
index 3440faf..8c38a9d 100644
--- a/Cs_HMI/AGVMapEditor/AGVMapEditor.csproj
+++ b/Cs_HMI/AGVMapEditor/AGVMapEditor.csproj
@@ -43,12 +43,17 @@
-
-
+
+ {C5F7A8B2-8D3E-4A1B-9C6E-7F4D5E2A9B1C}
+ AGVNavigationCore
+
+
+
+
+
+
+
-
-
-
Form
@@ -56,12 +61,6 @@
MainForm.cs
-
- UserControl
-
-
- MapCanvas.cs
-
@@ -69,11 +68,9 @@
MainForm.cs
-
- MapCanvas.cs
-
+
diff --git a/Cs_HMI/AGVMapEditor/Controls/MapCanvas.Designer.cs b/Cs_HMI/AGVMapEditor/Controls/MapCanvas.Designer.cs
deleted file mode 100644
index da7a559..0000000
--- a/Cs_HMI/AGVMapEditor/Controls/MapCanvas.Designer.cs
+++ /dev/null
@@ -1,59 +0,0 @@
-namespace AGVMapEditor.Controls
-{
- partial class MapCanvas
- {
- ///
- /// 필수 디자이너 변수입니다.
- ///
- private System.ComponentModel.IContainer components = null;
-
- ///
- /// 사용 중인 모든 리소스를 정리합니다.
- ///
- /// 관리되는 리소스를 삭제해야 하면 true이고, 그렇지 않으면 false입니다.
- protected override void Dispose(bool disposing)
- {
- if (disposing)
- {
- if (components != null)
- {
- components.Dispose();
- }
-
- // MapCanvas의 추가 리소스 정리
- _normalNodeBrush?.Dispose();
- _rotationNodeBrush?.Dispose();
- _dockingNodeBrush?.Dispose();
- _chargingNodeBrush?.Dispose();
- _selectedNodeBrush?.Dispose();
- _hoveredNodeBrush?.Dispose();
- _connectionPen?.Dispose();
- _gridPen?.Dispose();
- }
- base.Dispose(disposing);
- }
-
- #region 구성 요소 디자이너에서 생성한 코드
-
- ///
- /// 디자이너 지원에 필요한 메서드입니다.
- /// 이 메서드의 내용을 코드 편집기로 수정하지 마세요.
- ///
- private void InitializeComponent()
- {
- this.SuspendLayout();
- //
- // MapCanvas
- //
- this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 12F);
- this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
- this.BackColor = System.Drawing.Color.White;
- this.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
- this.Name = "MapCanvas";
- this.ResumeLayout(false);
-
- }
-
- #endregion
- }
-}
\ No newline at end of file
diff --git a/Cs_HMI/AGVMapEditor/Controls/MapCanvas.cs b/Cs_HMI/AGVMapEditor/Controls/MapCanvas.cs
deleted file mode 100644
index 3769f22..0000000
--- a/Cs_HMI/AGVMapEditor/Controls/MapCanvas.cs
+++ /dev/null
@@ -1,608 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Drawing;
-using System.Drawing.Drawing2D;
-using System.Linq;
-using System.Windows.Forms;
-using AGVMapEditor.Models;
-
-namespace AGVMapEditor.Controls
-{
- ///
- /// 맵 편집을 위한 그래픽 캔버스 컨트롤
- ///
- public partial class MapCanvas : UserControl
- {
- #region Constants
-
- private const int NODE_SIZE = 20;
- private const int NODE_RADIUS = NODE_SIZE / 2;
- private const int GRID_SIZE = 20;
- private const float CONNECTION_WIDTH = 2.0f;
-
- #endregion
-
- #region Fields
-
- private List _nodes;
- private MapNode _selectedNode;
- private MapNode _hoveredNode;
- private bool _isDragging;
- private Point _dragOffset;
- private Point _lastMousePosition;
-
- // 그리드 및 줌 관련
- private bool _showGrid = true;
- private float _zoomFactor = 1.0f;
- private Point _panOffset = Point.Empty;
-
- // 브러쉬 및 펜
- private Brush _normalNodeBrush;
- private Brush _rotationNodeBrush;
- private Brush _dockingNodeBrush;
- private Brush _chargingNodeBrush;
- private Brush _selectedNodeBrush;
- private Brush _hoveredNodeBrush;
- private Pen _connectionPen;
- private Pen _gridPen;
-
- #endregion
-
- #region Events
-
- ///
- /// 노드가 선택되었을 때 발생하는 이벤트
- ///
- public event EventHandler NodeSelected;
-
- ///
- /// 노드가 이동되었을 때 발생하는 이벤트
- ///
- public event EventHandler NodeMoved;
-
- ///
- /// 배경이 클릭되었을 때 발생하는 이벤트
- ///
- public event EventHandler BackgroundClicked;
-
- #endregion
-
- #region Constructor
-
- public MapCanvas() : this(new List())
- {
- }
-
- public MapCanvas(List nodes)
- {
- InitializeComponent();
- InitializeGraphics();
- SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint | ControlStyles.DoubleBuffer | ControlStyles.ResizeRedraw, true);
-
- _nodes = nodes ?? new List();
-
- // 이벤트 연결
- this.MouseDown += MapCanvas_MouseDown;
- this.MouseMove += MapCanvas_MouseMove;
- this.MouseUp += MapCanvas_MouseUp;
- this.MouseWheel += MapCanvas_MouseWheel;
- this.KeyDown += MapCanvas_KeyDown;
-
- // 포커스를 받을 수 있도록 설정
- this.TabStop = true;
- }
-
- #endregion
-
- #region Graphics Initialization
-
- private void InitializeGraphics()
- {
- _normalNodeBrush = new SolidBrush(Color.Blue);
- _rotationNodeBrush = new SolidBrush(Color.Orange);
- _dockingNodeBrush = new SolidBrush(Color.Green);
- _chargingNodeBrush = new SolidBrush(Color.Red);
- _selectedNodeBrush = new SolidBrush(Color.Yellow);
- _hoveredNodeBrush = new SolidBrush(Color.LightBlue);
-
- _connectionPen = new Pen(Color.Gray, CONNECTION_WIDTH);
- _gridPen = new Pen(Color.LightGray, 1.0f) { DashStyle = DashStyle.Dot };
- }
-
- #endregion
-
- #region Properties
-
- ///
- /// 그리드 표시 여부
- ///
- public bool ShowGrid
- {
- get { return _showGrid; }
- set
- {
- _showGrid = value;
- Invalidate();
- }
- }
-
- ///
- /// 줌 팩터
- ///
- public float ZoomFactor
- {
- get { return _zoomFactor; }
- set
- {
- _zoomFactor = Math.Max(0.1f, Math.Min(5.0f, value));
- Invalidate();
- }
- }
-
- ///
- /// 선택된 노드
- ///
- public MapNode SelectedNode
- {
- get { return _selectedNode; }
- set
- {
- if (_selectedNode != value)
- {
- _selectedNode = value;
- Invalidate();
- }
- }
- }
-
- #endregion
-
- #region Mouse Events
-
- private void MapCanvas_MouseDown(object sender, MouseEventArgs e)
- {
- this.Focus(); // 키보드 이벤트를 받기 위해 포커스 설정
-
- var worldPoint = ScreenToWorld(e.Location);
- var hitNode = GetNodeAt(worldPoint);
-
- if (e.Button == MouseButtons.Left)
- {
- if (hitNode != null)
- {
- // 노드 선택 및 드래그 시작
- SelectNode(hitNode);
- _isDragging = true;
- _dragOffset = new Point(worldPoint.X - hitNode.Position.X, worldPoint.Y - hitNode.Position.Y);
- }
- else
- {
- // 배경 클릭
- SelectNode(null);
- BackgroundClicked?.Invoke(this, worldPoint);
- }
- }
- else if (e.Button == MouseButtons.Right)
- {
- // 우클릭 메뉴 (추후 구현)
- ShowContextMenu(worldPoint, hitNode);
- }
-
- _lastMousePosition = e.Location;
- }
-
- private void MapCanvas_MouseMove(object sender, MouseEventArgs e)
- {
- var worldPoint = ScreenToWorld(e.Location);
-
- if (_isDragging && _selectedNode != null)
- {
- // 노드 드래그
- var newPosition = new Point(worldPoint.X - _dragOffset.X, worldPoint.Y - _dragOffset.Y);
-
- // 그리드에 맞춤 (Ctrl 키를 누르지 않은 경우)
- if (!ModifierKeys.HasFlag(Keys.Control))
- {
- newPosition.X = (newPosition.X / GRID_SIZE) * GRID_SIZE;
- newPosition.Y = (newPosition.Y / GRID_SIZE) * GRID_SIZE;
- }
-
- _selectedNode.Position = newPosition;
- Invalidate();
- }
- else if (e.Button == MouseButtons.Middle || (e.Button == MouseButtons.Left && ModifierKeys.HasFlag(Keys.Space)))
- {
- // 팬 (화면 이동)
- var deltaX = e.X - _lastMousePosition.X;
- var deltaY = e.Y - _lastMousePosition.Y;
- _panOffset.X += deltaX;
- _panOffset.Y += deltaY;
- Invalidate();
- }
- else
- {
- // 호버 효과
- var hitNode = GetNodeAt(worldPoint);
- if (_hoveredNode != hitNode)
- {
- _hoveredNode = hitNode;
- Invalidate();
- }
- }
-
- _lastMousePosition = e.Location;
- }
-
- private void MapCanvas_MouseUp(object sender, MouseEventArgs e)
- {
- if (_isDragging && _selectedNode != null)
- {
- NodeMoved?.Invoke(this, _selectedNode);
- }
-
- _isDragging = false;
- }
-
- private void MapCanvas_MouseWheel(object sender, MouseEventArgs e)
- {
- // 줌
- var delta = e.Delta > 0 ? 1.1f : 0.9f;
- ZoomFactor *= delta;
- }
-
- #endregion
-
- #region Keyboard Events
-
- private void MapCanvas_KeyDown(object sender, KeyEventArgs e)
- {
- switch (e.KeyCode)
- {
- case Keys.Delete:
- // 선택된 노드 삭제 (메인 폼에서 처리하도록 이벤트 발생)
- if (_selectedNode != null)
- {
- // 삭제 확인 후 처리는 메인 폼에서
- }
- break;
-
- case Keys.G:
- // 그리드 토글
- ShowGrid = !ShowGrid;
- break;
-
- case Keys.Home:
- // 뷰 리셋
- ZoomFactor = 1.0f;
- _panOffset = Point.Empty;
- Invalidate();
- break;
- }
- }
-
- #endregion
-
- #region Coordinate Conversion
-
- ///
- /// 스크린 좌표를 월드 좌표로 변환
- ///
- private Point ScreenToWorld(Point screenPoint)
- {
- var worldX = (int)((screenPoint.X - _panOffset.X) / _zoomFactor);
- var worldY = (int)((screenPoint.Y - _panOffset.Y) / _zoomFactor);
- return new Point(worldX, worldY);
- }
-
- ///
- /// 월드 좌표를 스크린 좌표로 변환
- ///
- private Point WorldToScreen(Point worldPoint)
- {
- var screenX = (int)(worldPoint.X * _zoomFactor + _panOffset.X);
- var screenY = (int)(worldPoint.Y * _zoomFactor + _panOffset.Y);
- return new Point(screenX, screenY);
- }
-
- #endregion
-
- #region Node Management
-
- ///
- /// 지정된 위치의 노드 검색
- ///
- private MapNode GetNodeAt(Point worldPoint)
- {
- foreach (var node in _nodes)
- {
- var distance = Math.Sqrt(Math.Pow(worldPoint.X - node.Position.X, 2) + Math.Pow(worldPoint.Y - node.Position.Y, 2));
- if (distance <= NODE_RADIUS)
- {
- return node;
- }
- }
- return null;
- }
-
- ///
- /// 노드 선택
- ///
- private void SelectNode(MapNode node)
- {
- if (_selectedNode != node)
- {
- _selectedNode = node;
- NodeSelected?.Invoke(this, node);
- Invalidate();
- }
- }
-
- ///
- /// 우클릭 컨텍스트 메뉴 표시
- ///
- private void ShowContextMenu(Point worldPoint, MapNode node)
- {
- var contextMenu = new ContextMenuStrip();
-
- if (node != null)
- {
- contextMenu.Items.Add("노드 삭제", null, (s, e) => { /* 삭제 처리 */ });
- contextMenu.Items.Add(new ToolStripSeparator());
- contextMenu.Items.Add("일반 노드로 변경", null, (s, e) => ChangeNodeType(node, NodeType.Normal));
- contextMenu.Items.Add("회전 지점으로 변경", null, (s, e) => ChangeNodeType(node, NodeType.Rotation));
- contextMenu.Items.Add("도킹 스테이션으로 변경", null, (s, e) => ChangeNodeType(node, NodeType.Docking));
- contextMenu.Items.Add("충전 스테이션으로 변경", null, (s, e) => ChangeNodeType(node, NodeType.Charging));
- }
- else
- {
- contextMenu.Items.Add("새 노드 추가", null, (s, e) => { /* 노드 추가 처리 */ });
- }
-
- var screenPoint = WorldToScreen(worldPoint);
- contextMenu.Show(this, screenPoint);
- }
-
- ///
- /// 노드 타입 변경
- ///
- private void ChangeNodeType(MapNode node, NodeType newType)
- {
- node.Type = newType;
- node.SetDefaultColorByType(newType);
- Invalidate();
- }
-
- #endregion
-
- #region Painting
-
- protected override void OnPaint(PaintEventArgs e)
- {
- base.OnPaint(e);
-
- var g = e.Graphics;
- g.SmoothingMode = SmoothingMode.AntiAlias;
-
- // 변환 매트릭스 적용
- g.ResetTransform();
- g.TranslateTransform(_panOffset.X, _panOffset.Y);
- g.ScaleTransform(_zoomFactor, _zoomFactor);
-
- // 배경 그리드 그리기
- if (_showGrid)
- {
- DrawGrid(g);
- }
-
- // 연결선 그리기
- DrawConnections(g);
-
- // 노드 그리기
- DrawNodes(g);
-
- // 선택된 노드의 연결 정보 강조 표시
- if (_selectedNode != null)
- {
- DrawSelectedNodeConnections(g);
- }
- }
-
- private void DrawGrid(Graphics g)
- {
- var bounds = GetVisibleWorldBounds();
-
- // 수직선
- for (int x = (bounds.Left / GRID_SIZE) * GRID_SIZE; x <= bounds.Right; x += GRID_SIZE)
- {
- g.DrawLine(_gridPen, x, bounds.Top, x, bounds.Bottom);
- }
-
- // 수평선
- for (int y = (bounds.Top / GRID_SIZE) * GRID_SIZE; y <= bounds.Bottom; y += GRID_SIZE)
- {
- g.DrawLine(_gridPen, bounds.Left, y, bounds.Right, y);
- }
- }
-
- private void DrawConnections(Graphics g)
- {
- foreach (var node in _nodes)
- {
- foreach (var connectedNodeId in node.ConnectedNodes)
- {
- var connectedNode = _nodes.FirstOrDefault(n => n.NodeId == connectedNodeId);
- if (connectedNode != null)
- {
- g.DrawLine(_connectionPen, node.Position, connectedNode.Position);
- }
- }
- }
- }
-
- private void DrawNodes(Graphics g)
- {
- foreach (var node in _nodes)
- {
- DrawNode(g, node);
- }
- }
-
- private void DrawNode(Graphics g, MapNode node)
- {
- var brush = GetNodeBrush(node);
- var rect = new Rectangle(
- node.Position.X - NODE_RADIUS,
- node.Position.Y - NODE_RADIUS,
- NODE_SIZE,
- NODE_SIZE);
-
- // 노드 그리기
- switch (node.Type)
- {
- case NodeType.Normal:
- g.FillEllipse(brush, rect);
- break;
- case NodeType.Rotation:
- g.FillRectangle(brush, rect);
- break;
- case NodeType.Docking:
- g.FillRectangle(brush, rect);
- // 도킹 방향 표시
- DrawDockingDirection(g, node);
- break;
- case NodeType.Charging:
- g.FillEllipse(brush, rect);
- // 충전 표시 (+)
- DrawChargingSymbol(g, node);
- break;
- }
-
- // 노드 테두리
- g.DrawEllipse(Pens.Black, rect);
-
- // 노드 이름 표시
- if (_zoomFactor > 0.5f) // 줌이 충분히 큰 경우만 텍스트 표시
- {
- var font = new Font("Arial", 8 * _zoomFactor);
- var textRect = new RectangleF(
- node.Position.X - 30,
- node.Position.Y + NODE_RADIUS + 2,
- 60, 15);
-
- var format = new StringFormat
- {
- Alignment = StringAlignment.Center,
- LineAlignment = StringAlignment.Near
- };
-
- g.DrawString(node.Name, font, Brushes.Black, textRect, format);
- }
- }
-
- private Brush GetNodeBrush(MapNode node)
- {
- if (node == _selectedNode)
- return _selectedNodeBrush;
-
- if (node == _hoveredNode)
- return _hoveredNodeBrush;
-
- switch (node.Type)
- {
- case NodeType.Normal:
- return _normalNodeBrush;
- case NodeType.Rotation:
- return _rotationNodeBrush;
- case NodeType.Docking:
- return _dockingNodeBrush;
- case NodeType.Charging:
- return _chargingNodeBrush;
- default:
- return _normalNodeBrush;
- }
- }
-
- private void DrawDockingDirection(Graphics g, MapNode node)
- {
- if (node.DockDirection == null)
- return;
-
- var arrowSize = 8;
- var arrowPen = new Pen(Color.White, 2);
-
- Point arrowStart, arrowEnd;
-
- if (node.DockDirection == DockingDirection.Forward)
- {
- arrowStart = new Point(node.Position.X - arrowSize/2, node.Position.Y);
- arrowEnd = new Point(node.Position.X + arrowSize/2, node.Position.Y);
- }
- else // Backward
- {
- arrowStart = new Point(node.Position.X + arrowSize/2, node.Position.Y);
- arrowEnd = new Point(node.Position.X - arrowSize/2, node.Position.Y);
- }
-
- g.DrawLine(arrowPen, arrowStart, arrowEnd);
-
- // 화살표 머리
- var headSize = 3;
- var headPoints = new Point[]
- {
- arrowEnd,
- new Point(arrowEnd.X - headSize, arrowEnd.Y - headSize),
- new Point(arrowEnd.X - headSize, arrowEnd.Y + headSize)
- };
- g.FillPolygon(Brushes.White, headPoints);
- }
-
- private void DrawChargingSymbol(Graphics g, MapNode node)
- {
- var symbolSize = 6;
- var symbolPen = new Pen(Color.White, 2);
-
- // + 모양
- g.DrawLine(symbolPen,
- node.Position.X - symbolSize/2, node.Position.Y,
- node.Position.X + symbolSize/2, node.Position.Y);
- g.DrawLine(symbolPen,
- node.Position.X, node.Position.Y - symbolSize/2,
- node.Position.X, node.Position.Y + symbolSize/2);
- }
-
- private void DrawSelectedNodeConnections(Graphics g)
- {
- var highlightPen = new Pen(Color.Yellow, CONNECTION_WIDTH + 2);
-
- foreach (var connectedNodeId in _selectedNode.ConnectedNodes)
- {
- var connectedNode = _nodes.FirstOrDefault(n => n.NodeId == connectedNodeId);
- if (connectedNode != null)
- {
- g.DrawLine(highlightPen, _selectedNode.Position, connectedNode.Position);
- }
- }
- }
-
- #endregion
-
- #region Helper Methods
-
- ///
- /// 현재 보이는 월드 영역 계산
- ///
- private Rectangle GetVisibleWorldBounds()
- {
- var topLeft = ScreenToWorld(new Point(0, 0));
- var bottomRight = ScreenToWorld(new Point(Width, Height));
-
- return new Rectangle(
- topLeft.X, topLeft.Y,
- bottomRight.X - topLeft.X,
- bottomRight.Y - topLeft.Y);
- }
-
- #endregion
-
- }
-}
\ No newline at end of file
diff --git a/Cs_HMI/AGVMapEditor/Controls/MapCanvas.resx b/Cs_HMI/AGVMapEditor/Controls/MapCanvas.resx
deleted file mode 100644
index b45c916..0000000
--- a/Cs_HMI/AGVMapEditor/Controls/MapCanvas.resx
+++ /dev/null
@@ -1,61 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- text/microsoft-resx
-
-
- 2.0
-
-
- System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
-
-
- System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
-
-
\ No newline at end of file
diff --git a/Cs_HMI/AGVMapEditor/Controls/MapCanvas_Interactive.Designer.cs b/Cs_HMI/AGVMapEditor/Controls/MapCanvas_Interactive.Designer.cs
new file mode 100644
index 0000000..ce5ed68
--- /dev/null
+++ b/Cs_HMI/AGVMapEditor/Controls/MapCanvas_Interactive.Designer.cs
@@ -0,0 +1,34 @@
+namespace AGVMapEditor.Controls
+{
+ partial class MapCanvasInteractive
+ {
+ ///
+ /// 필수 디자이너 변수입니다.
+ ///
+ private System.ComponentModel.IContainer components = null;
+
+
+ #region 구성 요소 디자이너에서 생성한 코드
+
+ ///
+ /// 디자이너 지원에 필요한 메서드입니다.
+ /// 이 메서드의 내용을 코드 편집기로 수정하지 마세요.
+ ///
+ private void InitializeComponent()
+ {
+ this.SuspendLayout();
+ //
+ // MapCanvasInteractive
+ //
+ this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 12F);
+ this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
+ this.BackColor = System.Drawing.Color.White;
+ this.Name = "MapCanvasInteractive";
+ this.Size = new System.Drawing.Size(800, 600);
+ this.ResumeLayout(false);
+
+ }
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/Cs_HMI/AGVMapEditor/Controls/MapCanvas_Interactive.cs b/Cs_HMI/AGVMapEditor/Controls/MapCanvas_Interactive.cs
new file mode 100644
index 0000000..a3b9b3a
--- /dev/null
+++ b/Cs_HMI/AGVMapEditor/Controls/MapCanvas_Interactive.cs
@@ -0,0 +1,1358 @@
+using System;
+using System.Collections.Generic;
+using System.Drawing;
+using System.Drawing.Drawing2D;
+using System.Linq;
+using System.Windows.Forms;
+using AGVMapEditor.Models;
+using AGVNavigationCore.Models;
+
+namespace AGVMapEditor.Controls
+{
+ ///
+ /// 대화형 맵 편집 캔버스 컨트롤
+ /// 마우스로 노드 추가, 드래그 이동, 연결 등의 기능 제공
+ ///
+ public partial class MapCanvasInteractive : UserControl
+ {
+ #region Constants
+
+ private const int NODE_SIZE = 24;
+ private const int NODE_RADIUS = NODE_SIZE / 2;
+ private const int GRID_SIZE = 20;
+ private const float CONNECTION_WIDTH = 2.0f;
+ private const int SNAP_DISTANCE = 10;
+
+ #endregion
+
+ #region Enums
+
+ ///
+ /// 편집 모드 열거형
+ ///
+ public enum EditMode
+ {
+ Select, // 선택 모드
+ Move, // 이동 모드
+ AddNode, // 노드 추가 모드
+ Connect, // 연결 모드
+ Delete, // 삭제 모드
+ AddLabel, // 라벨 추가 모드
+ AddImage // 이미지 추가 모드
+ }
+
+ #endregion
+
+ #region Fields
+
+ private List _nodes;
+ private MapNode _selectedNode;
+
+ // UI 요소들
+ private Image _companyLogo;
+ private string _companyLogoPath = string.Empty;
+ private string _measurementInfo = "스케일: 1:100\n면적: 1000㎡\n최종 수정: " + DateTime.Now.ToString("yyyy-MM-dd");
+
+ private MapNode _hoveredNode;
+ private bool _isDragging;
+ private Point _dragOffset;
+ private Point _lastMousePosition;
+
+ // 연결 모드 관련
+ private bool _isConnectionMode;
+ private MapNode _connectionStartNode;
+ private Point _connectionEndPoint;
+
+ // 편집 모드
+ private EditMode _editMode = EditMode.Select;
+
+ // 그리드 및 줌 관련
+ private bool _showGrid = true;
+ private float _zoomFactor = 1.0f;
+ private Point _panOffset = Point.Empty;
+ private bool _isPanning;
+
+ // 자동 증가 카운터
+ private int _nodeCounter = 1;
+
+ // 브러쉬 및 펜
+ private Brush _normalNodeBrush;
+ private Brush _rotationNodeBrush;
+ private Brush _dockingNodeBrush;
+ private Brush _chargingNodeBrush;
+ private Brush _selectedNodeBrush;
+ private Brush _hoveredNodeBrush;
+ private Brush _gridBrush;
+ private Pen _connectionPen;
+ private Pen _gridPen;
+ private Pen _tempConnectionPen;
+ private Pen _selectedNodePen;
+
+ // 컨텍스트 메뉴
+ private ContextMenuStrip _contextMenu;
+
+ #endregion
+
+ #region Properties
+
+ ///
+ /// 현재 편집 모드
+ ///
+ public EditMode CurrentEditMode
+ {
+ get => _editMode;
+ set
+ {
+ _editMode = value;
+ // 모드 변경시 연결 모드 해제
+ if (_editMode != EditMode.Connect)
+ {
+ CancelConnection();
+ }
+ Cursor = GetCursorForMode(_editMode);
+ Invalidate();
+ }
+ }
+
+ ///
+ /// 그리드 표시 여부
+ ///
+ public bool ShowGrid
+ {
+ get => _showGrid;
+ set
+ {
+ _showGrid = value;
+ Invalidate();
+ }
+ }
+
+ ///
+ /// 줌 팩터
+ ///
+ public float ZoomFactor
+ {
+ get => _zoomFactor;
+ set
+ {
+ _zoomFactor = Math.Max(0.1f, Math.Min(5.0f, value));
+ Invalidate();
+ }
+ }
+
+
+ ///
+ /// 선택된 노드
+ ///
+ public MapNode SelectedNode => _selectedNode;
+
+ ///
+ /// 노드 목록
+ ///
+ public List Nodes
+ {
+ get => _nodes;
+ set
+ {
+ _nodes = value ?? new List();
+ UpdateNodeCounter();
+ Invalidate();
+ }
+ }
+
+ #endregion
+
+ #region Events
+
+ ///
+ /// 노드가 추가되었을 때 발생하는 이벤트
+ ///
+ public event EventHandler NodeAdded;
+
+ ///
+ /// 노드가 선택되었을 때 발생하는 이벤트
+ ///
+ public event EventHandler NodeSelected;
+
+ ///
+ /// 노드가 이동되었을 때 발생하는 이벤트
+ ///
+ public event EventHandler NodeMoved;
+
+ ///
+ /// 노드가 삭제되었을 때 발생하는 이벤트
+ ///
+ public event EventHandler NodeDeleted;
+
+
+ ///
+ /// 노드 연결이 생성되었을 때 발생하는 이벤트
+ ///
+ public event EventHandler<(MapNode From, MapNode To)> ConnectionCreated;
+
+ ///
+ /// 맵이 변경되었을 때 발생하는 이벤트
+ ///
+ public event EventHandler MapChanged;
+
+ #endregion
+
+ #region Constructor
+
+ public MapCanvasInteractive()
+ {
+ InitializeComponent();
+
+
+ // Set Optimized Double Buffer to reduce flickering
+ this.SetStyle(ControlStyles.UserPaint, true);
+ this.SetStyle(ControlStyles.AllPaintingInWmPaint, true);
+ this.SetStyle(ControlStyles.OptimizedDoubleBuffer, true);
+ this.SetStyle(ControlStyles.SupportsTransparentBackColor, true);
+
+ // Redraw when resized
+ this.SetStyle(ControlStyles.ResizeRedraw, true);
+ this.Resize += arLabel_Resize;
+
+ InitializeCanvas();
+ }
+ void arLabel_Resize(object sender, EventArgs e)
+ {
+ Invalidate();
+ }
+
+ private void InitializeCanvas()
+ {
+ _nodes = new List();
+
+ // 더블 버퍼링 및 기타 스타일 설정
+ SetStyle(ControlStyles.AllPaintingInWmPaint |
+ ControlStyles.UserPaint |
+ ControlStyles.DoubleBuffer |
+ ControlStyles.ResizeRedraw, true);
+
+ // 포커스를 받을 수 있도록 설정
+ TabStop = true;
+
+ InitializeGraphics();
+ InitializeContextMenu();
+
+ // 이벤트 연결
+ MouseDown += OnMouseDown;
+ MouseMove += OnMouseMove;
+ MouseUp += OnMouseUp;
+ MouseWheel += OnMouseWheel;
+ KeyDown += OnKeyDown;
+
+ BackColor = Color.White;
+ }
+
+ #endregion
+
+ #region Graphics Initialization
+
+ private void InitializeGraphics()
+ {
+ _normalNodeBrush = new SolidBrush(Color.LightBlue);
+ _rotationNodeBrush = new SolidBrush(Color.Yellow);
+ _dockingNodeBrush = new SolidBrush(Color.Orange);
+ _chargingNodeBrush = new SolidBrush(Color.Green);
+ _selectedNodeBrush = new SolidBrush(Color.Red);
+ _hoveredNodeBrush = new SolidBrush(Color.Pink);
+ _gridBrush = new SolidBrush(Color.LightGray);
+
+ _connectionPen = new Pen(Color.Gray, CONNECTION_WIDTH);
+ _gridPen = new Pen(Color.LightGray, 1.0f) { DashStyle = DashStyle.Dot };
+ _tempConnectionPen = new Pen(Color.Blue, 2.0f) { DashStyle = DashStyle.Dash };
+ _selectedNodePen = new Pen(Color.Black, 2.0f);
+ }
+
+ private void InitializeContextMenu()
+ {
+ _contextMenu = new ContextMenuStrip();
+
+ var addNodeItem = new ToolStripMenuItem("노드 추가");
+ addNodeItem.Click += (s, e) => SetEditMode(EditMode.AddNode);
+
+ var connectItem = new ToolStripMenuItem("노드 연결");
+ connectItem.Click += (s, e) => SetEditMode(EditMode.Connect);
+
+ var deleteItem = new ToolStripMenuItem("삭제");
+ deleteItem.Click += (s, e) => SetEditMode(EditMode.Delete);
+
+ var separator1 = new ToolStripSeparator();
+
+ var normalNodeItem = new ToolStripMenuItem("일반 노드로 변경");
+ normalNodeItem.Click += (s, e) => ChangeSelectedNodeType(NodeType.Normal);
+
+ var rotationNodeItem = new ToolStripMenuItem("회전 노드로 변경");
+ rotationNodeItem.Click += (s, e) => ChangeSelectedNodeType(NodeType.Rotation);
+
+ var dockingNodeItem = new ToolStripMenuItem("도킹 노드로 변경");
+ dockingNodeItem.Click += (s, e) => ChangeSelectedNodeType(NodeType.Docking);
+
+ var chargingNodeItem = new ToolStripMenuItem("충전 노드로 변경");
+ chargingNodeItem.Click += (s, e) => ChangeSelectedNodeType(NodeType.Charging);
+
+ _contextMenu.Items.AddRange(new ToolStripItem[]
+ {
+ addNodeItem, connectItem, deleteItem, separator1,
+ normalNodeItem, rotationNodeItem, dockingNodeItem, chargingNodeItem
+ });
+
+ ContextMenuStrip = _contextMenu;
+ }
+
+ #endregion
+
+ #region Public Methods
+
+ ///
+ /// 편집 모드 설정
+ ///
+ public void SetEditMode(EditMode mode)
+ {
+ CurrentEditMode = mode;
+ }
+
+ ///
+ /// 노드 추가
+ ///
+ public MapNode AddNode(Point location, NodeType nodeType = NodeType.Normal)
+ {
+ var screenLocation = ScreenToWorld(location);
+ var nodeId = $"N{_nodeCounter:D3}";
+ _nodeCounter++;
+
+ var newNode = new MapNode
+ {
+ NodeId = nodeId,
+ Name = nodeId,
+ Position = screenLocation,
+ Type = nodeType,
+ ConnectedNodes = new List(),
+ CanRotate = nodeType == NodeType.Rotation
+ };
+
+ _nodes.Add(newNode);
+ SelectNode(newNode);
+
+ NodeAdded?.Invoke(this, newNode);
+ MapChanged?.Invoke(this, EventArgs.Empty);
+
+ Invalidate();
+ return newNode;
+ }
+
+ ///
+ /// 노드 삭제
+ ///
+ public void DeleteNode(MapNode node)
+ {
+ if (node == null) return;
+
+ // 다른 노드들의 연결에서 이 노드 제거
+ foreach (var otherNode in _nodes)
+ {
+ if (otherNode.ConnectedNodes.Contains(node.NodeId))
+ {
+ otherNode.ConnectedNodes.Remove(node.NodeId);
+ }
+ }
+
+ _nodes.Remove(node);
+
+ if (_selectedNode == node)
+ {
+ _selectedNode = null;
+ }
+
+ NodeDeleted?.Invoke(this, node);
+ MapChanged?.Invoke(this, EventArgs.Empty);
+
+ Invalidate();
+ }
+
+ ///
+ /// 두 노드를 연결
+ ///
+ public void ConnectNodes(MapNode fromNode, MapNode toNode)
+ {
+ if (fromNode == null || toNode == null || fromNode == toNode) return;
+
+ // 라벨이나 이미지 노드는 연결 불가
+ if (fromNode.Type == NodeType.Label || fromNode.Type == NodeType.Image ||
+ toNode.Type == NodeType.Label || toNode.Type == NodeType.Image)
+ {
+ return;
+ }
+
+ if (!fromNode.ConnectedNodes.Contains(toNode.NodeId))
+ {
+ fromNode.ConnectedNodes.Add(toNode.NodeId);
+ ConnectionCreated?.Invoke(this, (fromNode, toNode));
+ MapChanged?.Invoke(this, EventArgs.Empty);
+ Invalidate();
+ }
+ }
+
+ ///
+ /// 화면 좌표를 월드 좌표로 변환
+ ///
+ public Point ScreenToWorld(Point screenPoint)
+ {
+ return new Point(
+ (int)((screenPoint.X - _panOffset.X) / _zoomFactor),
+ (int)((screenPoint.Y - _panOffset.Y) / _zoomFactor)
+ );
+ }
+
+ ///
+ /// 월드 좌표를 화면 좌표로 변환
+ ///
+ public Point WorldToScreen(Point worldPoint)
+ {
+ return new Point(
+ (int)(worldPoint.X * _zoomFactor + _panOffset.X),
+ (int)(worldPoint.Y * _zoomFactor + _panOffset.Y)
+ );
+ }
+
+ ///
+ /// 맵 전체 맞춤
+ ///
+ public void FitToMap()
+ {
+ if (_nodes == null || _nodes.Count == 0) return;
+
+ var minX = _nodes.Min(n => n.Position.X) - 50;
+ var maxX = _nodes.Max(n => n.Position.X) + 50;
+ var minY = _nodes.Min(n => n.Position.Y) - 50;
+ var maxY = _nodes.Max(n => n.Position.Y) + 50;
+
+ var mapWidth = maxX - minX;
+ var mapHeight = maxY - minY;
+
+ var zoomX = (float)Width / mapWidth;
+ var zoomY = (float)Height / mapHeight;
+ _zoomFactor = Math.Min(zoomX, zoomY) * 0.9f;
+
+ _panOffset = new Point(
+ (int)((Width - mapWidth * _zoomFactor) / 2 - minX * _zoomFactor),
+ (int)((Height - mapHeight * _zoomFactor) / 2 - minY * _zoomFactor)
+ );
+
+ Invalidate();
+ }
+
+ #endregion
+
+ #region Mouse Event Handlers
+
+ private void OnMouseDown(object sender, MouseEventArgs e)
+ {
+ Focus();
+
+ var worldPoint = ScreenToWorld(e.Location);
+ var clickedNode = GetNodeAtPoint(worldPoint);
+
+ if (e.Button == MouseButtons.Left)
+ {
+ switch (_editMode)
+ {
+ case EditMode.Select:
+ HandleSelectModeMouseDown(e, worldPoint, clickedNode);
+ break;
+
+ case EditMode.Move:
+ HandleMoveModeMouseDown(e, worldPoint, clickedNode);
+ break;
+
+ case EditMode.AddNode:
+ HandleAddNodeModeMouseDown(e, worldPoint, clickedNode);
+ break;
+
+ case EditMode.Connect:
+ HandleConnectModeMouseDown(e, worldPoint, clickedNode);
+ break;
+
+ case EditMode.Delete:
+ HandleDeleteModeMouseDown(e, worldPoint, clickedNode);
+ break;
+
+ case EditMode.AddLabel:
+ HandleAddLabelModeMouseDown(e, worldPoint);
+ break;
+
+ case EditMode.AddImage:
+ HandleAddImageModeMouseDown(e, worldPoint);
+ break;
+ }
+ }
+
+ _lastMousePosition = e.Location;
+ }
+
+ private void OnMouseMove(object sender, MouseEventArgs e)
+ {
+ var worldPoint = ScreenToWorld(e.Location);
+ var nodeAtPoint = GetNodeAtPoint(worldPoint);
+
+ // 호버 상태 업데이트
+ if (_hoveredNode != nodeAtPoint)
+ {
+ _hoveredNode = nodeAtPoint;
+ Invalidate();
+ }
+
+ switch (_editMode)
+ {
+ case EditMode.Select:
+ HandleSelectModeMouseMove(e, worldPoint);
+ break;
+
+ case EditMode.Move:
+ HandleMoveModeMouseMove(e, worldPoint);
+ break;
+
+ case EditMode.Connect:
+ HandleConnectModeMouseMove(e, worldPoint);
+ break;
+ }
+
+ // 패닝 처리 (가운데 마우스 버튼)
+ if (e.Button == MouseButtons.Middle)
+ {
+ var deltaX = e.X - _lastMousePosition.X;
+ var deltaY = e.Y - _lastMousePosition.Y;
+
+ _panOffset = new Point(_panOffset.X + deltaX, _panOffset.Y + deltaY);
+ Invalidate();
+ }
+
+ _lastMousePosition = e.Location;
+ }
+
+ private void OnMouseUp(object sender, MouseEventArgs e)
+ {
+ _isDragging = false;
+ _isPanning = false;
+ Cursor = GetCursorForMode(_editMode);
+ }
+
+ private void OnMouseWheel(object sender, MouseEventArgs e)
+ {
+ var zoomFactor = e.Delta > 0 ? 1.1f : 0.9f;
+ var newZoom = _zoomFactor * zoomFactor;
+
+ if (newZoom >= 0.1f && newZoom <= 5.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))
+ );
+
+ _zoomFactor = newZoom;
+ Invalidate();
+ }
+ }
+
+ #endregion
+
+ #region Mode-Specific Handlers
+
+ private void HandleSelectModeMouseDown(MouseEventArgs e, Point worldPoint, MapNode clickedNode)
+ {
+ if (clickedNode != null)
+ {
+ SelectNode(clickedNode);
+ }
+ else
+ {
+ SelectNode(null);
+ }
+ }
+
+ private void HandleSelectModeMouseMove(MouseEventArgs e, Point worldPoint)
+ {
+ // 선택 모드에서는 드래그 이동 비활성화
+ }
+
+ private void HandleAddNodeModeMouseDown(MouseEventArgs e, Point worldPoint, MapNode clickedNode)
+ {
+ if (clickedNode == null)
+ {
+ var snappedPoint = _showGrid ? SnapToGrid(worldPoint) : worldPoint;
+ AddNode(WorldToScreen(snappedPoint));
+ }
+ }
+
+ private void HandleConnectModeMouseDown(MouseEventArgs e, Point worldPoint, MapNode clickedNode)
+ {
+ if (clickedNode != null)
+ {
+ // 라벨이나 이미지 노드는 연결 불가
+ if (clickedNode.Type == NodeType.Label || clickedNode.Type == NodeType.Image)
+ {
+ return;
+ }
+
+ if (_connectionStartNode == null)
+ {
+ // 연결 시작
+ _connectionStartNode = clickedNode;
+ _isConnectionMode = true;
+ SelectNode(clickedNode);
+ }
+ else if (_connectionStartNode != clickedNode)
+ {
+ // 연결 완료
+ ConnectNodes(_connectionStartNode, clickedNode);
+ CancelConnection();
+ }
+ else
+ {
+ // 같은 노드 클릭시 연결 취소
+ CancelConnection();
+ }
+ }
+ else
+ {
+ CancelConnection();
+ }
+ }
+
+ private void HandleConnectModeMouseMove(MouseEventArgs e, Point worldPoint)
+ {
+ if (_isConnectionMode)
+ {
+ _connectionEndPoint = worldPoint;
+ Invalidate();
+ }
+ }
+
+ private void HandleMoveModeMouseDown(MouseEventArgs e, Point worldPoint, MapNode clickedNode)
+ {
+ if (clickedNode != null)
+ {
+ SelectNode(clickedNode);
+ _isDragging = true;
+ _dragOffset = new Point(worldPoint.X - clickedNode.Position.X, worldPoint.Y - clickedNode.Position.Y);
+ Cursor = Cursors.SizeAll;
+ }
+ else
+ {
+ SelectNode(null);
+ }
+ }
+
+ private void HandleMoveModeMouseMove(MouseEventArgs e, Point worldPoint)
+ {
+ if (_isDragging && _selectedNode != null)
+ {
+ var newPosition = new Point(worldPoint.X - _dragOffset.X, worldPoint.Y - _dragOffset.Y);
+
+ // 그리드에 스냅
+ if (_showGrid)
+ {
+ newPosition = SnapToGrid(newPosition);
+ }
+
+ _selectedNode.Position = newPosition;
+ NodeMoved?.Invoke(this, _selectedNode);
+ MapChanged?.Invoke(this, EventArgs.Empty);
+ Invalidate();
+ }
+ }
+
+ private void HandleDeleteModeMouseDown(MouseEventArgs e, Point worldPoint, MapNode clickedNode)
+ {
+ if (clickedNode != null)
+ {
+ DeleteNode(clickedNode);
+ }
+ }
+
+ #endregion
+
+ #region Helper Methods
+
+ private MapNode GetNodeAtPoint(Point point)
+ {
+ foreach (var node in _nodes)
+ {
+ var screenPos = WorldToScreen(node.Position);
+ var distance = Math.Sqrt(Math.Pow(point.X - node.Position.X, 2) + Math.Pow(point.Y - node.Position.Y, 2));
+
+ if (distance <= NODE_RADIUS)
+ {
+ return node;
+ }
+ }
+ return null;
+ }
+
+ private void SelectNode(MapNode node)
+ {
+ _selectedNode = node;
+ NodeSelected?.Invoke(this, node);
+ Invalidate();
+ }
+
+ private Point SnapToGrid(Point point)
+ {
+ return new Point(
+ (int)(Math.Round((double)point.X / GRID_SIZE) * GRID_SIZE),
+ (int)(Math.Round((double)point.Y / GRID_SIZE) * GRID_SIZE)
+ );
+ }
+
+ private Cursor GetCursorForMode(EditMode mode)
+ {
+ switch (mode)
+ {
+ case EditMode.Move: return Cursors.SizeAll;
+ case EditMode.AddNode: return Cursors.Cross;
+ case EditMode.Connect: return Cursors.Hand;
+ case EditMode.Delete: return Cursors.No;
+ default: return Cursors.Default;
+ }
+ }
+
+ private void CancelConnection()
+ {
+ _isConnectionMode = false;
+ _connectionStartNode = null;
+ Invalidate();
+ }
+
+ private void ChangeSelectedNodeType(NodeType newType)
+ {
+ if (_selectedNode != null)
+ {
+ _selectedNode.Type = newType;
+ _selectedNode.CanRotate = newType == NodeType.Rotation;
+ MapChanged?.Invoke(this, EventArgs.Empty);
+ Invalidate();
+ }
+ }
+
+ private void HandleAddLabelModeMouseDown(MouseEventArgs e, Point worldPoint)
+ {
+ // 라벨 텍스트 입력 다이얼로그
+ var text = Microsoft.VisualBasic.Interaction.InputBox("라벨 텍스트를 입력하세요:", "라벨 추가", "새 라벨");
+ if (!string.IsNullOrEmpty(text))
+ {
+ var nodeId = GenerateNodeId("LBL");
+ var labelNode = new MapNode(nodeId, text, worldPoint, NodeType.Label);
+ labelNode.LabelText = text;
+
+ _nodes.Add(labelNode);
+ _selectedNode = labelNode;
+
+ NodeAdded?.Invoke(this, labelNode);
+ NodeSelected?.Invoke(this, labelNode);
+ MapChanged?.Invoke(this, EventArgs.Empty);
+
+ // 라벨 추가 후 자동으로 선택 모드로 전환
+ CurrentEditMode = EditMode.Select;
+ Invalidate();
+ }
+ }
+
+ private void HandleAddImageModeMouseDown(MouseEventArgs e, Point worldPoint)
+ {
+ // 이미지 파일 선택 다이얼로그
+ var openFileDialog = new OpenFileDialog
+ {
+ Filter = "Image Files|*.jpg;*.jpeg;*.png;*.bmp;*.gif;*.tiff|All Files|*.*",
+ Title = "이미지 파일 선택"
+ };
+
+ if (openFileDialog.ShowDialog() == DialogResult.OK)
+ {
+ var nodeId = GenerateNodeId("IMG");
+ var imageNode = new MapNode(nodeId, System.IO.Path.GetFileNameWithoutExtension(openFileDialog.FileName), worldPoint, NodeType.Image);
+ imageNode.ImagePath = openFileDialog.FileName;
+
+ if (imageNode.LoadImage())
+ {
+ _nodes.Add(imageNode);
+ _selectedNode = imageNode;
+
+ NodeAdded?.Invoke(this, imageNode);
+ NodeSelected?.Invoke(this, imageNode);
+ MapChanged?.Invoke(this, EventArgs.Empty);
+
+ // 이미지 추가 후 자동으로 선택 모드로 전환
+ CurrentEditMode = EditMode.Select;
+ Invalidate();
+ }
+ else
+ {
+ MessageBox.Show("이미지 로드에 실패했습니다.", "오류", MessageBoxButtons.OK, MessageBoxIcon.Error);
+ }
+ }
+ }
+
+ private string GenerateNodeId(string prefix)
+ {
+ int counter = 1;
+ string nodeId;
+
+ do
+ {
+ nodeId = $"{prefix}{counter:D3}";
+ counter++;
+ } while (_nodes.Any(n => n.NodeId == nodeId));
+
+ return nodeId;
+ }
+
+ private void UpdateNodeCounter()
+ {
+ if (_nodes != null && _nodes.Count > 0)
+ {
+ // 기존 노드 중 가장 큰 번호 찾기
+ var maxNumber = 0;
+ foreach (var node in _nodes)
+ {
+ if (node.NodeId.StartsWith("N") && int.TryParse(node.NodeId.Substring(1), out var number))
+ {
+ maxNumber = Math.Max(maxNumber, number);
+ }
+ }
+ _nodeCounter = maxNumber + 1;
+ }
+ }
+
+ private Brush GetNodeBrush(MapNode node)
+ {
+ if (node == _selectedNode) return _selectedNodeBrush;
+ if (node == _hoveredNode) return _hoveredNodeBrush;
+
+ switch (node.Type)
+ {
+ case NodeType.Rotation: return _rotationNodeBrush;
+ case NodeType.Docking: return _dockingNodeBrush;
+ case NodeType.Charging: return _chargingNodeBrush;
+ default: return _normalNodeBrush;
+ }
+ }
+
+ #endregion
+
+ #region Keyboard Handlers
+
+ private void OnKeyDown(object sender, KeyEventArgs e)
+ {
+ switch (e.KeyCode)
+ {
+ case Keys.Delete:
+ if (_selectedNode != null)
+ {
+ DeleteNode(_selectedNode);
+ }
+ break;
+
+ case Keys.Escape:
+ CancelConnection();
+ SelectNode(null);
+ break;
+
+ case Keys.A:
+ if (e.Control)
+ {
+ SetEditMode(EditMode.AddNode);
+ }
+ break;
+
+ case Keys.C:
+ if (e.Control)
+ {
+ SetEditMode(EditMode.Connect);
+ }
+ break;
+
+ case Keys.D:
+ if (e.Control)
+ {
+ SetEditMode(EditMode.Delete);
+ }
+ break;
+
+ case Keys.S:
+ if (e.Control)
+ {
+ SetEditMode(EditMode.Select);
+ }
+ break;
+
+ case Keys.M:
+ if (e.Control)
+ {
+ SetEditMode(EditMode.Move);
+ }
+ break;
+ }
+ }
+
+ #endregion
+
+ #region Painting
+
+ protected override void OnPaint(PaintEventArgs e)
+ {
+ base.OnPaint(e);
+
+ var g = e.Graphics;
+ g.SmoothingMode = SmoothingMode.AntiAlias;
+
+ // 변환 행렬 설정
+ g.TranslateTransform(_panOffset.X, _panOffset.Y);
+ g.ScaleTransform(_zoomFactor, _zoomFactor);
+
+ // 그리드 그리기
+ if (_showGrid)
+ {
+ DrawGrid(g);
+ }
+
+ // 노드 연결선 그리기
+ DrawConnections(g);
+
+ // 임시 연결선 그리기 (연결 모드일 때)
+ if (_isConnectionMode && _connectionStartNode != null)
+ {
+ DrawTempConnection(g);
+ }
+
+ // 노드 그리기 (라벨, 이미지 포함)
+ DrawNodes(g);
+
+ // 변환 행렬 리셋 후 UI 요소 그리기
+ g.ResetTransform();
+ DrawUI(g);
+ }
+
+ private void DrawGrid(Graphics g)
+ {
+ var startX = -(int)(_panOffset.X / _zoomFactor / GRID_SIZE) * GRID_SIZE;
+ var startY = -(int)(_panOffset.Y / _zoomFactor / GRID_SIZE) * GRID_SIZE;
+ var endX = startX + (int)(Width / _zoomFactor) + GRID_SIZE;
+ var endY = startY + (int)(Height / _zoomFactor) + GRID_SIZE;
+
+ for (int x = startX; x <= endX; x += GRID_SIZE)
+ {
+ g.DrawLine(_gridPen, x, startY, x, endY);
+ }
+
+ for (int y = startY; y <= endY; y += GRID_SIZE)
+ {
+ g.DrawLine(_gridPen, startX, y, endX, y);
+ }
+ }
+
+ private void DrawConnections(Graphics g)
+ {
+ foreach (var node in _nodes)
+ {
+ foreach (var connectedNodeId in node.ConnectedNodes)
+ {
+ var connectedNode = _nodes.FirstOrDefault(n => n.NodeId == connectedNodeId);
+ if (connectedNode != null)
+ {
+ g.DrawLine(_connectionPen, node.Position, connectedNode.Position);
+
+ // 방향 화살표 그리기
+ DrawArrow(g, node.Position, connectedNode.Position);
+ }
+ }
+ }
+ }
+
+ private void DrawTempConnection(Graphics g)
+ {
+ var screenEndPoint = ScreenToWorld(WorldToScreen(_connectionEndPoint));
+ g.DrawLine(_tempConnectionPen, _connectionStartNode.Position, screenEndPoint);
+ }
+
+ private void DrawNodes(Graphics g)
+ {
+ foreach (var node in _nodes)
+ {
+ var brush = GetNodeBrush(node);
+ var rect = new Rectangle(
+ node.Position.X - NODE_RADIUS,
+ node.Position.Y - NODE_RADIUS,
+ NODE_SIZE,
+ NODE_SIZE
+ );
+
+ // 노드 모양에 따른 그리기
+ switch (node.Type)
+ {
+ case NodeType.Label:
+ // 라벨 노드 - 텍스트 렌더링
+ DrawLabelNode(g, node);
+ continue; // 일반 노드 텍스트 렌더링 건너뛰기
+
+ case NodeType.Image:
+ // 이미지 노드 - 이미지 렌더링
+ DrawImageNode(g, node);
+ continue; // 일반 노드 텍스트 렌더링 건너뛰기
+
+ case NodeType.Rotation:
+ // 회전 노드 - 원형
+ g.FillEllipse(brush, rect);
+ g.DrawEllipse(_selectedNodePen, rect);
+ break;
+
+ case NodeType.Docking:
+ // 도킹 노드 - 오각형
+ DrawPentagon(g, brush, _selectedNodePen, node.Position);
+ break;
+
+ case NodeType.Charging:
+ // 충전 노드 - 삼각형
+ DrawTriangle(g, brush, _selectedNodePen, node.Position);
+ break;
+
+ default:
+ // 일반 노드 - 사각형
+ g.FillRectangle(brush, rect);
+ g.DrawRectangle(_selectedNodePen, rect);
+ break;
+ }
+
+ // 선택된 노드는 테두리 강조
+ if (node == _selectedNode)
+ {
+ var selectedPen = new Pen(Color.Red, 3);
+ switch (node.Type)
+ {
+ case NodeType.Rotation:
+ g.DrawEllipse(selectedPen, rect);
+ break;
+ case NodeType.Docking:
+ DrawPentagonOutline(g, selectedPen, node.Position);
+ break;
+ case NodeType.Charging:
+ DrawTriangleOutline(g, selectedPen, node.Position);
+ break;
+ default:
+ g.DrawRectangle(selectedPen, rect);
+ break;
+ }
+ selectedPen.Dispose();
+ }
+
+ // 노드 설명이 있으면 노드 위에 표시
+ if (!string.IsNullOrEmpty(node.Description))
+ {
+ var descFont = new Font("Arial", 6);
+ var descTextSize = g.MeasureString(node.Description, descFont);
+ var descTextPos = new PointF(
+ node.Position.X - descTextSize.Width / 2,
+ node.Position.Y - NODE_RADIUS - descTextSize.Height - 2
+ );
+
+ g.DrawString(node.Description, descFont, Brushes.DarkBlue, descTextPos);
+ descFont.Dispose();
+ }
+
+ // 노드 ID 표시
+ var font = new Font("Arial", 8);
+ var textBrush = Brushes.Black;
+ var textSize = g.MeasureString(node.NodeId, font);
+ var textPos = new PointF(
+ node.Position.X - textSize.Width / 2,
+ node.Position.Y + NODE_RADIUS + 2
+ );
+
+ g.DrawString(node.NodeId, font, textBrush, textPos);
+
+ // RFID 값이 있으면 노드 이름 아래에 작게 표시
+ if (!string.IsNullOrEmpty(node.RfidId))
+ {
+ var rfidFont = new Font("Arial", 6);
+ var rfidTextSize = g.MeasureString(node.RfidId, rfidFont);
+ var rfidTextPos = new PointF(
+ node.Position.X - rfidTextSize.Width / 2,
+ node.Position.Y + NODE_RADIUS + 2 + textSize.Height
+ );
+
+ g.DrawString(node.RfidId, rfidFont, Brushes.Gray, rfidTextPos);
+ rfidFont.Dispose();
+ }
+
+ font.Dispose();
+ }
+ }
+
+ private void DrawArrow(Graphics g, Point start, Point end)
+ {
+ var angle = Math.Atan2(end.Y - start.Y, end.X - start.X);
+ var arrowLength = 8;
+ var arrowAngle = Math.PI / 6;
+
+ var arrowPoint1 = new PointF(
+ (float)(end.X - arrowLength * Math.Cos(angle - arrowAngle)),
+ (float)(end.Y - arrowLength * Math.Sin(angle - arrowAngle))
+ );
+
+ var arrowPoint2 = new PointF(
+ (float)(end.X - arrowLength * Math.Cos(angle + arrowAngle)),
+ (float)(end.Y - arrowLength * Math.Sin(angle + arrowAngle))
+ );
+
+ g.DrawLine(_connectionPen, end, arrowPoint1);
+ g.DrawLine(_connectionPen, end, arrowPoint2);
+ }
+
+ private void DrawUI(Graphics g)
+ {
+ // 현재 모드 표시
+ var modeText = $"모드: {GetModeText(_editMode)}";
+ var font = new Font("Arial", 10, FontStyle.Bold);
+ var textBrush = Brushes.Black;
+ var backgroundBrush = new SolidBrush(Color.FromArgb(200, Color.White));
+
+ var textSize = g.MeasureString(modeText, font);
+ var textRect = new RectangleF(10, 10, textSize.Width + 10, textSize.Height + 5);
+
+ g.FillRectangle(backgroundBrush, textRect);
+ g.DrawString(modeText, font, textBrush, 15, 12);
+
+ font.Dispose();
+ backgroundBrush.Dispose();
+ }
+
+ private string GetModeText(EditMode mode)
+ {
+ switch (mode)
+ {
+ case EditMode.Select: return "선택";
+ case EditMode.Move: return "이동";
+ case EditMode.AddNode: return "노드 추가";
+ case EditMode.Connect: return "노드 연결";
+ case EditMode.Delete: return "삭제";
+ case EditMode.AddLabel: return "라벨 추가";
+ case EditMode.AddImage: return "이미지 추가";
+ default: return "알 수 없음";
+ }
+ }
+
+ private void DrawPentagon(Graphics g, Brush fillBrush, Pen outlinePen, Point center)
+ {
+ var points = GetPentagonPoints(center, NODE_RADIUS);
+ g.FillPolygon(fillBrush, points);
+ g.DrawPolygon(outlinePen, points);
+ }
+
+ private void DrawPentagonOutline(Graphics g, Pen pen, Point center)
+ {
+ var points = GetPentagonPoints(center, NODE_RADIUS);
+ g.DrawPolygon(pen, points);
+ }
+
+ private PointF[] GetPentagonPoints(Point center, int radius)
+ {
+ var points = new PointF[5];
+ var angle = -Math.PI / 2; // 시작 각도 (위쪽부터)
+
+ for (int i = 0; i < 5; i++)
+ {
+ points[i] = new PointF(
+ center.X + (float)(radius * Math.Cos(angle)),
+ center.Y + (float)(radius * Math.Sin(angle))
+ );
+ angle += 2 * Math.PI / 5; // 72도씩 증가
+ }
+
+ return points;
+ }
+
+ private void DrawTriangle(Graphics g, Brush fillBrush, Pen outlinePen, Point center)
+ {
+ var points = GetTrianglePoints(center, NODE_RADIUS);
+ g.FillPolygon(fillBrush, points);
+ g.DrawPolygon(outlinePen, points);
+ }
+
+ private void DrawTriangleOutline(Graphics g, Pen pen, Point center)
+ {
+ var points = GetTrianglePoints(center, NODE_RADIUS);
+ g.DrawPolygon(pen, points);
+ }
+
+ private PointF[] GetTrianglePoints(Point center, int radius)
+ {
+ var points = new PointF[3];
+ var angle = -Math.PI / 2; // 시작 각도 (위쪽부터)
+
+ for (int i = 0; i < 3; i++)
+ {
+ points[i] = new PointF(
+ center.X + (float)(radius * Math.Cos(angle)),
+ center.Y + (float)(radius * Math.Sin(angle))
+ );
+ angle += 2 * Math.PI / 3; // 120도씩 증가
+ }
+
+ return points;
+ }
+
+
+ ///
+ /// 라벨 노드 그리기
+ ///
+ private void DrawLabelNode(Graphics g, MapNode node)
+ {
+ if (string.IsNullOrEmpty(node.LabelText)) return;
+
+ // 폰트 생성
+ var font = new Font(node.FontFamily, node.FontSize, node.FontStyle);
+ var textBrush = new SolidBrush(node.ForeColor);
+
+ // 배경 브러쉬 생성 (필요시)
+ Brush backgroundBrush = null;
+ if (node.ShowBackground)
+ {
+ backgroundBrush = new SolidBrush(node.BackColor);
+ }
+
+ // 텍스트 크기 측정
+ var textSize = g.MeasureString(node.LabelText, font);
+ var textRect = new RectangleF(
+ node.Position.X - textSize.Width / 2,
+ node.Position.Y - textSize.Height / 2,
+ textSize.Width,
+ textSize.Height
+ );
+
+ // 배경 그리기 (필요시)
+ if (backgroundBrush != null)
+ {
+ g.FillRectangle(backgroundBrush, textRect);
+ }
+
+ // 텍스트 그리기
+ g.DrawString(node.LabelText, font, textBrush, textRect.Location);
+
+ // 선택된 노드는 테두리 표시
+ if (node == _selectedNode)
+ {
+ var selectedPen = new Pen(Color.Red, 2);
+ g.DrawRectangle(selectedPen, Rectangle.Round(textRect));
+ selectedPen.Dispose();
+ }
+
+ // 리소스 정리
+ font.Dispose();
+ textBrush.Dispose();
+ backgroundBrush?.Dispose();
+ }
+
+ ///
+ /// 이미지 노드 그리기
+ ///
+ private void DrawImageNode(Graphics g, MapNode node)
+ {
+ // 이미지 로드 (필요시)
+ if (node.LoadedImage == null && !string.IsNullOrEmpty(node.ImagePath))
+ {
+ node.LoadImage();
+ }
+
+ if (node.LoadedImage == null) return;
+
+ // 실제 표시 크기 계산
+ var displaySize = node.GetDisplaySize();
+ if (displaySize.IsEmpty) return;
+
+ var imageRect = new Rectangle(
+ node.Position.X - displaySize.Width / 2,
+ node.Position.Y - displaySize.Height / 2,
+ displaySize.Width,
+ displaySize.Height
+ );
+
+ // 투명도 적용
+ var colorMatrix = new System.Drawing.Imaging.ColorMatrix();
+ colorMatrix.Matrix33 = node.Opacity; // 알파 값 설정
+
+ var imageAttributes = new System.Drawing.Imaging.ImageAttributes();
+ imageAttributes.SetColorMatrix(colorMatrix, System.Drawing.Imaging.ColorMatrixFlag.Default,
+ System.Drawing.Imaging.ColorAdjustType.Bitmap);
+
+ // 회전 변환 적용 (필요시)
+ var originalTransform = g.Transform;
+ if (node.Rotation != 0)
+ {
+ g.TranslateTransform(node.Position.X, node.Position.Y);
+ g.RotateTransform(node.Rotation);
+ g.TranslateTransform(-node.Position.X, -node.Position.Y);
+ }
+
+ // 이미지 그리기
+ g.DrawImage(node.LoadedImage, imageRect, 0, 0, node.LoadedImage.Width, node.LoadedImage.Height,
+ GraphicsUnit.Pixel, imageAttributes);
+
+ // 변환 복원
+ g.Transform = originalTransform;
+
+ // 선택된 노드는 테두리 표시
+ if (node == _selectedNode)
+ {
+ var selectedPen = new Pen(Color.Red, 2);
+ g.DrawRectangle(selectedPen, imageRect);
+ selectedPen.Dispose();
+ }
+
+ // 리소스 정리
+ imageAttributes.Dispose();
+ }
+
+ #endregion
+
+ #region Cleanup
+
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ // 컴포넌트 정리
+ if (components != null)
+ {
+ components.Dispose();
+ }
+
+ // 브러쉬 정리
+ _normalNodeBrush?.Dispose();
+ _rotationNodeBrush?.Dispose();
+ _dockingNodeBrush?.Dispose();
+ _chargingNodeBrush?.Dispose();
+ _selectedNodeBrush?.Dispose();
+ _hoveredNodeBrush?.Dispose();
+ _gridBrush?.Dispose();
+
+ // 펜 정리
+ _connectionPen?.Dispose();
+ _gridPen?.Dispose();
+ _tempConnectionPen?.Dispose();
+ _selectedNodePen?.Dispose();
+
+ // 컨텍스트 메뉴 정리
+ _contextMenu?.Dispose();
+ }
+
+ base.Dispose(disposing);
+ }
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/Cs_HMI/AGVMapEditor/Forms/MainForm.Designer.cs b/Cs_HMI/AGVMapEditor/Forms/MainForm.Designer.cs
index 55ef0af..d0b406c 100644
--- a/Cs_HMI/AGVMapEditor/Forms/MainForm.Designer.cs
+++ b/Cs_HMI/AGVMapEditor/Forms/MainForm.Designer.cs
@@ -35,6 +35,7 @@ namespace AGVMapEditor.Forms
this.toolStripSeparator1 = new System.Windows.Forms.ToolStripSeparator();
this.saveToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.saveAsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
+ this.closeToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.toolStripSeparator2 = new System.Windows.Forms.ToolStripSeparator();
this.exitToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.statusStrip1 = new System.Windows.Forms.StatusStrip();
@@ -42,20 +43,9 @@ namespace AGVMapEditor.Forms
this.splitContainer1 = new System.Windows.Forms.SplitContainer();
this.tabControl1 = new System.Windows.Forms.TabControl();
this.tabPageNodes = new System.Windows.Forms.TabPage();
- this.btnRemoveConnection = new System.Windows.Forms.Button();
- this.btnAddConnection = new System.Windows.Forms.Button();
- this.btnDeleteNode = new System.Windows.Forms.Button();
- this.btnAddNode = new System.Windows.Forms.Button();
this.listBoxNodes = new System.Windows.Forms.ListBox();
+ this._propertyGrid = new System.Windows.Forms.PropertyGrid();
this.label1 = new System.Windows.Forms.Label();
- this.tabPageRfid = new System.Windows.Forms.TabPage();
- this.btnDeleteRfidMapping = new System.Windows.Forms.Button();
- this.btnAddRfidMapping = new System.Windows.Forms.Button();
- this.listBoxRfidMappings = new System.Windows.Forms.ListBox();
- this.label2 = new System.Windows.Forms.Label();
- this.tabPageProperties = new System.Windows.Forms.TabPage();
- this.labelSelectedNode = new System.Windows.Forms.Label();
- this.label3 = new System.Windows.Forms.Label();
this.menuStrip1.SuspendLayout();
this.statusStrip1.SuspendLayout();
((System.ComponentModel.ISupportInitialize)(this.splitContainer1)).BeginInit();
@@ -63,8 +53,6 @@ namespace AGVMapEditor.Forms
this.splitContainer1.SuspendLayout();
this.tabControl1.SuspendLayout();
this.tabPageNodes.SuspendLayout();
- this.tabPageRfid.SuspendLayout();
- this.tabPageProperties.SuspendLayout();
this.SuspendLayout();
//
// menuStrip1
@@ -82,6 +70,7 @@ namespace AGVMapEditor.Forms
this.fileToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.newToolStripMenuItem,
this.openToolStripMenuItem,
+ this.closeToolStripMenuItem,
this.toolStripSeparator1,
this.saveToolStripMenuItem,
this.saveAsToolStripMenuItem,
@@ -95,7 +84,7 @@ namespace AGVMapEditor.Forms
//
this.newToolStripMenuItem.Name = "newToolStripMenuItem";
this.newToolStripMenuItem.ShortcutKeys = ((System.Windows.Forms.Keys)((System.Windows.Forms.Keys.Control | System.Windows.Forms.Keys.N)));
- this.newToolStripMenuItem.Size = new System.Drawing.Size(180, 22);
+ this.newToolStripMenuItem.Size = new System.Drawing.Size(198, 22);
this.newToolStripMenuItem.Text = "새로 만들기(&N)";
this.newToolStripMenuItem.Click += new System.EventHandler(this.newToolStripMenuItem_Click);
//
@@ -103,39 +92,46 @@ namespace AGVMapEditor.Forms
//
this.openToolStripMenuItem.Name = "openToolStripMenuItem";
this.openToolStripMenuItem.ShortcutKeys = ((System.Windows.Forms.Keys)((System.Windows.Forms.Keys.Control | System.Windows.Forms.Keys.O)));
- this.openToolStripMenuItem.Size = new System.Drawing.Size(180, 22);
+ this.openToolStripMenuItem.Size = new System.Drawing.Size(198, 22);
this.openToolStripMenuItem.Text = "열기(&O)";
this.openToolStripMenuItem.Click += new System.EventHandler(this.openToolStripMenuItem_Click);
//
// toolStripSeparator1
//
this.toolStripSeparator1.Name = "toolStripSeparator1";
- this.toolStripSeparator1.Size = new System.Drawing.Size(177, 6);
+ this.toolStripSeparator1.Size = new System.Drawing.Size(195, 6);
//
// saveToolStripMenuItem
//
this.saveToolStripMenuItem.Name = "saveToolStripMenuItem";
this.saveToolStripMenuItem.ShortcutKeys = ((System.Windows.Forms.Keys)((System.Windows.Forms.Keys.Control | System.Windows.Forms.Keys.S)));
- this.saveToolStripMenuItem.Size = new System.Drawing.Size(180, 22);
+ this.saveToolStripMenuItem.Size = new System.Drawing.Size(198, 22);
this.saveToolStripMenuItem.Text = "저장(&S)";
this.saveToolStripMenuItem.Click += new System.EventHandler(this.saveToolStripMenuItem_Click);
//
// saveAsToolStripMenuItem
//
this.saveAsToolStripMenuItem.Name = "saveAsToolStripMenuItem";
- this.saveAsToolStripMenuItem.Size = new System.Drawing.Size(180, 22);
+ this.saveAsToolStripMenuItem.Size = new System.Drawing.Size(198, 22);
this.saveAsToolStripMenuItem.Text = "다른 이름으로 저장(&A)";
this.saveAsToolStripMenuItem.Click += new System.EventHandler(this.saveAsToolStripMenuItem_Click);
//
+ // closeToolStripMenuItem
+ //
+ this.closeToolStripMenuItem.Name = "closeToolStripMenuItem";
+ this.closeToolStripMenuItem.Size = new System.Drawing.Size(198, 22);
+ this.closeToolStripMenuItem.Text = "닫기(&C)";
+ this.closeToolStripMenuItem.Click += new System.EventHandler(this.closeToolStripMenuItem_Click);
+ //
// toolStripSeparator2
//
this.toolStripSeparator2.Name = "toolStripSeparator2";
- this.toolStripSeparator2.Size = new System.Drawing.Size(177, 6);
+ this.toolStripSeparator2.Size = new System.Drawing.Size(195, 6);
//
// exitToolStripMenuItem
//
this.exitToolStripMenuItem.Name = "exitToolStripMenuItem";
- this.exitToolStripMenuItem.Size = new System.Drawing.Size(180, 22);
+ this.exitToolStripMenuItem.Size = new System.Drawing.Size(198, 22);
this.exitToolStripMenuItem.Text = "종료(&X)";
this.exitToolStripMenuItem.Click += new System.EventHandler(this.exitToolStripMenuItem_Click);
//
@@ -172,8 +168,6 @@ namespace AGVMapEditor.Forms
// tabControl1
//
this.tabControl1.Controls.Add(this.tabPageNodes);
- this.tabControl1.Controls.Add(this.tabPageRfid);
- this.tabControl1.Controls.Add(this.tabPageProperties);
this.tabControl1.Dock = System.Windows.Forms.DockStyle.Fill;
this.tabControl1.Location = new System.Drawing.Point(0, 0);
this.tabControl1.Name = "tabControl1";
@@ -183,11 +177,8 @@ namespace AGVMapEditor.Forms
//
// tabPageNodes
//
- this.tabPageNodes.Controls.Add(this.btnRemoveConnection);
- this.tabPageNodes.Controls.Add(this.btnAddConnection);
- this.tabPageNodes.Controls.Add(this.btnDeleteNode);
- this.tabPageNodes.Controls.Add(this.btnAddNode);
this.tabPageNodes.Controls.Add(this.listBoxNodes);
+ this.tabPageNodes.Controls.Add(this._propertyGrid);
this.tabPageNodes.Controls.Add(this.label1);
this.tabPageNodes.Location = new System.Drawing.Point(4, 22);
this.tabPageNodes.Name = "tabPageNodes";
@@ -197,163 +188,33 @@ namespace AGVMapEditor.Forms
this.tabPageNodes.Text = "노드 관리";
this.tabPageNodes.UseVisualStyleBackColor = true;
//
- // btnRemoveConnection
- //
- this.btnRemoveConnection.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)
- | System.Windows.Forms.AnchorStyles.Right)));
- this.btnRemoveConnection.Location = new System.Drawing.Point(6, 637);
- this.btnRemoveConnection.Name = "btnRemoveConnection";
- this.btnRemoveConnection.Size = new System.Drawing.Size(280, 25);
- this.btnRemoveConnection.TabIndex = 5;
- this.btnRemoveConnection.Text = "연결 제거";
- this.btnRemoveConnection.UseVisualStyleBackColor = true;
- this.btnRemoveConnection.Click += new System.EventHandler(this.btnRemoveConnection_Click);
- //
- // btnAddConnection
- //
- this.btnAddConnection.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)
- | System.Windows.Forms.AnchorStyles.Right)));
- this.btnAddConnection.Location = new System.Drawing.Point(6, 606);
- this.btnAddConnection.Name = "btnAddConnection";
- this.btnAddConnection.Size = new System.Drawing.Size(280, 25);
- this.btnAddConnection.TabIndex = 4;
- this.btnAddConnection.Text = "연결 추가";
- this.btnAddConnection.UseVisualStyleBackColor = true;
- this.btnAddConnection.Click += new System.EventHandler(this.btnAddConnection_Click);
- //
- // btnDeleteNode
- //
- this.btnDeleteNode.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)
- | System.Windows.Forms.AnchorStyles.Right)));
- this.btnDeleteNode.Location = new System.Drawing.Point(148, 670);
- this.btnDeleteNode.Name = "btnDeleteNode";
- this.btnDeleteNode.Size = new System.Drawing.Size(138, 25);
- this.btnDeleteNode.TabIndex = 3;
- this.btnDeleteNode.Text = "노드 삭제";
- this.btnDeleteNode.UseVisualStyleBackColor = true;
- this.btnDeleteNode.Click += new System.EventHandler(this.btnDeleteNode_Click);
- //
- // btnAddNode
- //
- this.btnAddNode.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)
- | System.Windows.Forms.AnchorStyles.Right)));
- this.btnAddNode.Location = new System.Drawing.Point(6, 670);
- this.btnAddNode.Name = "btnAddNode";
- this.btnAddNode.Size = new System.Drawing.Size(138, 25);
- this.btnAddNode.TabIndex = 2;
- this.btnAddNode.Text = "노드 추가";
- this.btnAddNode.UseVisualStyleBackColor = true;
- this.btnAddNode.Click += new System.EventHandler(this.btnAddNode_Click);
- //
// listBoxNodes
//
- this.listBoxNodes.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
- | System.Windows.Forms.AnchorStyles.Left)
- | System.Windows.Forms.AnchorStyles.Right)));
+ this.listBoxNodes.Dock = System.Windows.Forms.DockStyle.Fill;
this.listBoxNodes.FormattingEnabled = true;
this.listBoxNodes.ItemHeight = 12;
- this.listBoxNodes.Location = new System.Drawing.Point(6, 25);
+ this.listBoxNodes.Location = new System.Drawing.Point(3, 3);
this.listBoxNodes.Name = "listBoxNodes";
- this.listBoxNodes.Size = new System.Drawing.Size(280, 568);
+ this.listBoxNodes.Size = new System.Drawing.Size(286, 245);
this.listBoxNodes.TabIndex = 1;
//
+ // _propertyGrid
+ //
+ this._propertyGrid.Dock = System.Windows.Forms.DockStyle.Bottom;
+ this._propertyGrid.Location = new System.Drawing.Point(3, 248);
+ this._propertyGrid.Name = "_propertyGrid";
+ this._propertyGrid.Size = new System.Drawing.Size(286, 450);
+ this._propertyGrid.TabIndex = 6;
+ //
// label1
//
this.label1.AutoSize = true;
this.label1.Location = new System.Drawing.Point(6, 6);
this.label1.Name = "label1";
- this.label1.Size = new System.Drawing.Size(53, 12);
+ this.label1.Size = new System.Drawing.Size(57, 12);
this.label1.TabIndex = 0;
this.label1.Text = "노드 목록";
//
- // tabPageRfid
- //
- this.tabPageRfid.Controls.Add(this.btnDeleteRfidMapping);
- this.tabPageRfid.Controls.Add(this.btnAddRfidMapping);
- this.tabPageRfid.Controls.Add(this.listBoxRfidMappings);
- this.tabPageRfid.Controls.Add(this.label2);
- this.tabPageRfid.Location = new System.Drawing.Point(4, 22);
- this.tabPageRfid.Name = "tabPageRfid";
- this.tabPageRfid.Padding = new System.Windows.Forms.Padding(3);
- this.tabPageRfid.Size = new System.Drawing.Size(292, 701);
- this.tabPageRfid.TabIndex = 1;
- this.tabPageRfid.Text = "RFID 매핑";
- this.tabPageRfid.UseVisualStyleBackColor = true;
- //
- // btnDeleteRfidMapping
- //
- this.btnDeleteRfidMapping.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)
- | System.Windows.Forms.AnchorStyles.Right)));
- this.btnDeleteRfidMapping.Location = new System.Drawing.Point(148, 670);
- this.btnDeleteRfidMapping.Name = "btnDeleteRfidMapping";
- this.btnDeleteRfidMapping.Size = new System.Drawing.Size(138, 25);
- this.btnDeleteRfidMapping.TabIndex = 3;
- this.btnDeleteRfidMapping.Text = "매핑 삭제";
- this.btnDeleteRfidMapping.UseVisualStyleBackColor = true;
- this.btnDeleteRfidMapping.Click += new System.EventHandler(this.btnDeleteRfidMapping_Click);
- //
- // btnAddRfidMapping
- //
- this.btnAddRfidMapping.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)
- | System.Windows.Forms.AnchorStyles.Right)));
- this.btnAddRfidMapping.Location = new System.Drawing.Point(6, 670);
- this.btnAddRfidMapping.Name = "btnAddRfidMapping";
- this.btnAddRfidMapping.Size = new System.Drawing.Size(138, 25);
- this.btnAddRfidMapping.TabIndex = 2;
- this.btnAddRfidMapping.Text = "매핑 추가";
- this.btnAddRfidMapping.UseVisualStyleBackColor = true;
- this.btnAddRfidMapping.Click += new System.EventHandler(this.btnAddRfidMapping_Click);
- //
- // listBoxRfidMappings
- //
- this.listBoxRfidMappings.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
- | System.Windows.Forms.AnchorStyles.Left)
- | System.Windows.Forms.AnchorStyles.Right)));
- this.listBoxRfidMappings.FormattingEnabled = true;
- this.listBoxRfidMappings.ItemHeight = 12;
- this.listBoxRfidMappings.Location = new System.Drawing.Point(6, 25);
- this.listBoxRfidMappings.Name = "listBoxRfidMappings";
- this.listBoxRfidMappings.Size = new System.Drawing.Size(280, 628);
- this.listBoxRfidMappings.TabIndex = 1;
- //
- // label2
- //
- this.label2.AutoSize = true;
- this.label2.Location = new System.Drawing.Point(6, 6);
- this.label2.Name = "label2";
- this.label2.Size = new System.Drawing.Size(77, 12);
- this.label2.TabIndex = 0;
- this.label2.Text = "RFID 매핑 목록";
- //
- // tabPageProperties
- //
- this.tabPageProperties.Controls.Add(this.labelSelectedNode);
- this.tabPageProperties.Controls.Add(this.label3);
- this.tabPageProperties.Location = new System.Drawing.Point(4, 22);
- this.tabPageProperties.Name = "tabPageProperties";
- this.tabPageProperties.Size = new System.Drawing.Size(292, 701);
- this.tabPageProperties.TabIndex = 2;
- this.tabPageProperties.Text = "속성";
- this.tabPageProperties.UseVisualStyleBackColor = true;
- //
- // labelSelectedNode
- //
- this.labelSelectedNode.AutoSize = true;
- this.labelSelectedNode.Location = new System.Drawing.Point(8, 35);
- this.labelSelectedNode.Name = "labelSelectedNode";
- this.labelSelectedNode.Size = new System.Drawing.Size(93, 12);
- this.labelSelectedNode.TabIndex = 1;
- this.labelSelectedNode.Text = "선택된 노드: 없음";
- //
- // label3
- //
- this.label3.AutoSize = true;
- this.label3.Location = new System.Drawing.Point(8, 12);
- this.label3.Name = "label3";
- this.label3.Size = new System.Drawing.Size(53, 12);
- this.label3.TabIndex = 0;
- this.label3.Text = "노드 속성";
- //
// MainForm
//
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 12F);
@@ -378,11 +239,6 @@ namespace AGVMapEditor.Forms
this.tabControl1.ResumeLayout(false);
this.tabPageNodes.ResumeLayout(false);
this.tabPageNodes.PerformLayout();
- this.tabPageRfid.ResumeLayout(false);
- this.tabPageRfid.ResumeLayout(false);
- this.tabPageRfid.PerformLayout();
- this.tabPageProperties.ResumeLayout(false);
- this.tabPageProperties.PerformLayout();
this.ResumeLayout(false);
this.PerformLayout();
@@ -397,6 +253,7 @@ namespace AGVMapEditor.Forms
private System.Windows.Forms.ToolStripSeparator toolStripSeparator1;
private System.Windows.Forms.ToolStripMenuItem saveToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem saveAsToolStripMenuItem;
+ private System.Windows.Forms.ToolStripMenuItem closeToolStripMenuItem;
private System.Windows.Forms.ToolStripSeparator toolStripSeparator2;
private System.Windows.Forms.ToolStripMenuItem exitToolStripMenuItem;
private System.Windows.Forms.StatusStrip statusStrip1;
@@ -404,19 +261,8 @@ namespace AGVMapEditor.Forms
private System.Windows.Forms.SplitContainer splitContainer1;
private System.Windows.Forms.TabControl tabControl1;
private System.Windows.Forms.TabPage tabPageNodes;
- private System.Windows.Forms.TabPage tabPageRfid;
- private System.Windows.Forms.Button btnDeleteNode;
- private System.Windows.Forms.Button btnAddNode;
private System.Windows.Forms.ListBox listBoxNodes;
private System.Windows.Forms.Label label1;
- private System.Windows.Forms.Button btnDeleteRfidMapping;
- private System.Windows.Forms.Button btnAddRfidMapping;
- private System.Windows.Forms.ListBox listBoxRfidMappings;
- private System.Windows.Forms.Label label2;
- private System.Windows.Forms.TabPage tabPageProperties;
- private System.Windows.Forms.Label labelSelectedNode;
- private System.Windows.Forms.Label label3;
- private System.Windows.Forms.Button btnRemoveConnection;
- private System.Windows.Forms.Button btnAddConnection;
+ private System.Windows.Forms.PropertyGrid _propertyGrid;
}
}
\ No newline at end of file
diff --git a/Cs_HMI/AGVMapEditor/Forms/MainForm.cs b/Cs_HMI/AGVMapEditor/Forms/MainForm.cs
index 5c0ab43..9dde1b9 100644
--- a/Cs_HMI/AGVMapEditor/Forms/MainForm.cs
+++ b/Cs_HMI/AGVMapEditor/Forms/MainForm.cs
@@ -5,7 +5,8 @@ using System.IO;
using System.Linq;
using System.Windows.Forms;
using AGVMapEditor.Models;
-using AGVMapEditor.Controls;
+using AGVNavigationCore.Controls;
+using AGVNavigationCore.Models;
using Newtonsoft.Json;
namespace AGVMapEditor.Forms
@@ -17,28 +18,46 @@ namespace AGVMapEditor.Forms
{
#region Fields
- private NodeResolver _nodeResolver;
private List _mapNodes;
- private List _rfidMappings;
- private MapCanvas _mapCanvas;
-
+ private UnifiedAGVCanvas _mapCanvas;
+
// 현재 선택된 노드
private MapNode _selectedNode;
-
+
// 파일 경로
private string _currentMapFile = string.Empty;
private bool _hasChanges = false;
+
#endregion
#region Constructor
- public MainForm()
+ public MainForm() : this(null)
+ {
+ }
+
+ public MainForm(string[] args)
{
InitializeComponent();
InitializeData();
InitializeMapCanvas();
UpdateTitle();
+
+ // 명령줄 인수로 파일이 전달되었으면 자동으로 열기
+ if (args != null && args.Length > 0)
+ {
+ string filePath = args[0];
+ if (System.IO.File.Exists(filePath))
+ {
+ LoadMapFromFile(filePath);
+ }
+ else
+ {
+ MessageBox.Show($"지정된 파일을 찾을 수 없습니다: {filePath}", "파일 오류",
+ MessageBoxButtons.OK, MessageBoxIcon.Warning);
+ }
+ }
}
#endregion
@@ -48,20 +67,117 @@ namespace AGVMapEditor.Forms
private void InitializeData()
{
_mapNodes = new List();
- _rfidMappings = new List();
- _nodeResolver = new NodeResolver(_rfidMappings, _mapNodes);
}
private void InitializeMapCanvas()
{
- _mapCanvas = new MapCanvas(_mapNodes);
+ _mapCanvas = new UnifiedAGVCanvas();
_mapCanvas.Dock = DockStyle.Fill;
+ _mapCanvas.Mode = UnifiedAGVCanvas.CanvasMode.Edit;
+ _mapCanvas.Nodes = _mapNodes;
+ // RfidMappings 제거 - MapNode에 통합됨
+
+ // 이벤트 연결
+ _mapCanvas.NodeAdded += OnNodeAdded;
_mapCanvas.NodeSelected += OnNodeSelected;
_mapCanvas.NodeMoved += OnNodeMoved;
- _mapCanvas.BackgroundClicked += OnBackgroundClicked;
-
+ _mapCanvas.NodeDeleted += OnNodeDeleted;
+ _mapCanvas.MapChanged += OnMapChanged;
+
// 스플리터 패널에 맵 캔버스 추가
splitContainer1.Panel2.Controls.Add(_mapCanvas);
+
+ // 편집 모드 툴바 초기화
+ InitializeEditModeToolbar();
+ }
+
+ private void InitializeEditModeToolbar()
+ {
+ // 툴바 패널 생성
+ var toolbarPanel = new Panel();
+ toolbarPanel.Height = 35;
+ toolbarPanel.Dock = DockStyle.Top;
+ toolbarPanel.BackColor = SystemColors.Control;
+
+ // 선택 모드 버튼
+ var btnSelect = new Button();
+ btnSelect.Text = "선택 (S)";
+ btnSelect.Size = new Size(70, 28);
+ btnSelect.Location = new Point(5, 3);
+ btnSelect.Click += (s, e) => _mapCanvas.CurrentEditMode = UnifiedAGVCanvas.EditMode.Select;
+
+ // 이동 모드 버튼
+ var btnMove = new Button();
+ btnMove.Text = "이동 (M)";
+ btnMove.Size = new Size(70, 28);
+ btnMove.Location = new Point(80, 3);
+ btnMove.Click += (s, e) => _mapCanvas.CurrentEditMode = UnifiedAGVCanvas.EditMode.Move;
+
+ // 노드 추가 버튼
+ var btnAddNode = new Button();
+ btnAddNode.Text = "노드 추가 (A)";
+ btnAddNode.Size = new Size(80, 28);
+ btnAddNode.Location = new Point(155, 3);
+ btnAddNode.Click += (s, e) => _mapCanvas.CurrentEditMode = UnifiedAGVCanvas.EditMode.AddNode;
+
+ // 라벨 추가 버튼
+ var btnAddLabel = new Button();
+ btnAddLabel.Text = "라벨 추가 (L)";
+ btnAddLabel.Size = new Size(80, 28);
+ btnAddLabel.Location = new Point(240, 3);
+ btnAddLabel.Click += (s, e) => _mapCanvas.CurrentEditMode = UnifiedAGVCanvas.EditMode.AddLabel;
+
+ // 이미지 추가 버튼
+ var btnAddImage = new Button();
+ btnAddImage.Text = "이미지 추가 (I)";
+ btnAddImage.Size = new Size(90, 28);
+ btnAddImage.Location = new Point(325, 3);
+ btnAddImage.Click += (s, e) => _mapCanvas.CurrentEditMode = UnifiedAGVCanvas.EditMode.AddImage;
+
+ // 연결 모드 버튼
+ var btnConnect = new Button();
+ btnConnect.Text = "연결 (C)";
+ btnConnect.Size = new Size(70, 28);
+ btnConnect.Location = new Point(420, 3);
+ btnConnect.Click += (s, e) => _mapCanvas.CurrentEditMode = UnifiedAGVCanvas.EditMode.Connect;
+
+ // 삭제 모드 버튼
+ var btnDelete = new Button();
+ btnDelete.Text = "삭제 (D)";
+ btnDelete.Size = new Size(70, 28);
+ btnDelete.Location = new Point(495, 3);
+ btnDelete.Click += (s, e) => _mapCanvas.CurrentEditMode = UnifiedAGVCanvas.EditMode.Delete;
+
+ // 구분선
+ var separator1 = new Label();
+ separator1.Text = "|";
+ separator1.Size = new Size(10, 28);
+ separator1.Location = new Point(570, 3);
+ separator1.TextAlign = ContentAlignment.MiddleCenter;
+
+ // 그리드 토글 버튼
+ var btnToggleGrid = new Button();
+ btnToggleGrid.Text = "그리드";
+ btnToggleGrid.Size = new Size(60, 28);
+ btnToggleGrid.Location = new Point(585, 3);
+ btnToggleGrid.Click += (s, e) => _mapCanvas.ShowGrid = !_mapCanvas.ShowGrid;
+
+ // 맵 맞춤 버튼
+ var btnFitMap = new Button();
+ btnFitMap.Text = "맵 맞춤";
+ btnFitMap.Size = new Size(70, 28);
+ btnFitMap.Location = new Point(650, 3);
+ btnFitMap.Click += (s, e) => _mapCanvas.FitToNodes();
+
+ // 툴바에 버튼들 추가
+ toolbarPanel.Controls.AddRange(new Control[]
+ {
+ btnSelect, btnMove, btnAddNode, btnAddLabel, btnAddImage, btnConnect, btnDelete, separator1, btnToggleGrid, btnFitMap
+ });
+
+ // 스플리터 패널에 툴바 추가 (맨 위에)
+ splitContainer1.Panel2.Controls.Add(toolbarPanel);
+ toolbarPanel.BringToFront();
}
#endregion
@@ -71,7 +187,16 @@ namespace AGVMapEditor.Forms
private void MainForm_Load(object sender, EventArgs e)
{
RefreshNodeList();
- RefreshRfidMappingList();
+ // 속성 변경 시 이벤트 연결
+ _propertyGrid.PropertyValueChanged += PropertyGrid_PropertyValueChanged;
+ }
+
+ private void OnNodeAdded(object sender, MapNode node)
+ {
+ _hasChanges = true;
+ UpdateTitle();
+ RefreshNodeList();
+ // RFID 자동 할당
}
private void OnNodeSelected(object sender, MapNode node)
@@ -87,6 +212,28 @@ namespace AGVMapEditor.Forms
RefreshNodeList();
}
+ private void OnNodeDeleted(object sender, MapNode node)
+ {
+ _hasChanges = true;
+ UpdateTitle();
+ RefreshNodeList();
+ ClearNodeProperties();
+ // RFID 자동 할당
+ }
+
+ private void OnConnectionCreated(object sender, (MapNode From, MapNode To) connection)
+ {
+ _hasChanges = true;
+ UpdateTitle();
+ UpdateNodeProperties(); // 연결 정보 업데이트
+ }
+
+ private void OnMapChanged(object sender, EventArgs e)
+ {
+ _hasChanges = true;
+ UpdateTitle();
+ }
+
private void OnBackgroundClicked(object sender, Point location)
{
_selectedNode = null;
@@ -123,6 +270,11 @@ namespace AGVMapEditor.Forms
SaveAsMap();
}
+ private void closeToolStripMenuItem_Click(object sender, EventArgs e)
+ {
+ CloseMap();
+ }
+
private void exitToolStripMenuItem_Click(object sender, EventArgs e)
{
this.Close();
@@ -152,16 +304,6 @@ namespace AGVMapEditor.Forms
RemoveConnectionFromSelectedNode();
}
- private void btnAddRfidMapping_Click(object sender, EventArgs e)
- {
- AddNewRfidMapping();
- }
-
- private void btnDeleteRfidMapping_Click(object sender, EventArgs e)
- {
- DeleteSelectedRfidMapping();
- }
-
#endregion
#region Node Management
@@ -171,12 +313,12 @@ namespace AGVMapEditor.Forms
var nodeId = GenerateNodeId();
var nodeName = $"노드{_mapNodes.Count + 1}";
var position = new Point(100 + _mapNodes.Count * 50, 100 + _mapNodes.Count * 50);
-
+
var node = new MapNode(nodeId, nodeName, position, NodeType.Normal);
-
+
_mapNodes.Add(node);
_hasChanges = true;
-
+
RefreshNodeList();
RefreshMapCanvas();
UpdateTitle();
@@ -190,17 +332,17 @@ namespace AGVMapEditor.Forms
return;
}
- var result = MessageBox.Show($"노드 '{_selectedNode.Name}'를 삭제하시겠습니까?\n연결된 RFID 매핑도 함께 삭제됩니다.",
+ var result = MessageBox.Show($"노드 '{_selectedNode.Name}'를 삭제하시겠습니까?\n연결된 RFID 매핑도 함께 삭제됩니다.",
"삭제 확인", MessageBoxButtons.YesNo, MessageBoxIcon.Question);
-
+
if (result == DialogResult.Yes)
{
- _nodeResolver.RemoveMapNode(_selectedNode.NodeId);
+ // 노드 제거
+ _mapNodes.Remove(_selectedNode);
_selectedNode = null;
_hasChanges = true;
-
+
RefreshNodeList();
- RefreshRfidMappingList();
RefreshMapCanvas();
ClearNodeProperties();
UpdateTitle();
@@ -216,9 +358,9 @@ namespace AGVMapEditor.Forms
}
// 다른 노드들 중에서 선택
- var availableNodes = _mapNodes.Where(n => n.NodeId != _selectedNode.NodeId &&
+ var availableNodes = _mapNodes.Where(n => n.NodeId != _selectedNode.NodeId &&
!_selectedNode.ConnectedNodes.Contains(n.NodeId)).ToList();
-
+
if (availableNodes.Count == 0)
{
MessageBox.Show("연결 가능한 노드가 없습니다.", "알림", MessageBoxButtons.OK, MessageBoxIcon.Information);
@@ -228,7 +370,7 @@ namespace AGVMapEditor.Forms
// 간단한 선택 다이얼로그 (실제로는 별도 폼을 만들어야 함)
var nodeNames = availableNodes.Select(n => $"{n.NodeId}: {n.Name}").ToArray();
var input = Microsoft.VisualBasic.Interaction.InputBox("연결할 노드를 선택하세요:", "노드 연결", nodeNames[0]);
-
+
var targetNode = availableNodes.FirstOrDefault(n => input.StartsWith(n.NodeId));
if (targetNode != null)
{
@@ -249,14 +391,14 @@ namespace AGVMapEditor.Forms
}
// 연결된 노드들 중에서 선택
- var connectedNodeNames = _selectedNode.ConnectedNodes.Select(connectedNodeId =>
+ var connectedNodeNames = _selectedNode.ConnectedNodes.Select(connectedNodeId =>
{
var node = _mapNodes.FirstOrDefault(n => n.NodeId == connectedNodeId);
return node != null ? $"{node.NodeId}: {node.Name}" : connectedNodeId;
}).ToArray();
var input = Microsoft.VisualBasic.Interaction.InputBox("제거할 연결을 선택하세요:", "연결 제거", connectedNodeNames[0]);
-
+
var targetNodeId = input.Split(':')[0];
if (_selectedNode.ConnectedNodes.Contains(targetNodeId))
{
@@ -272,110 +414,50 @@ namespace AGVMapEditor.Forms
{
int counter = 1;
string nodeId;
-
+
do
{
nodeId = $"N{counter:D3}";
counter++;
} while (_mapNodes.Any(n => n.NodeId == nodeId));
-
+
return nodeId;
}
#endregion
- #region RFID Mapping Management
-
- private void AddNewRfidMapping()
- {
- if (_mapNodes.Count == 0)
- {
- MessageBox.Show("매핑할 노드가 없습니다. 먼저 노드를 추가하세요.", "알림", MessageBoxButtons.OK, MessageBoxIcon.Information);
- return;
- }
-
- var unmappedNodes = _nodeResolver.GetUnmappedNodes();
- if (unmappedNodes.Count == 0)
- {
- MessageBox.Show("모든 노드가 이미 RFID에 매핑되어 있습니다.", "알림", MessageBoxButtons.OK, MessageBoxIcon.Information);
- return;
- }
-
- // RFID 값 입력
- var rfidValue = Microsoft.VisualBasic.Interaction.InputBox("RFID 값을 입력하세요:", "RFID 매핑 추가");
- if (string.IsNullOrEmpty(rfidValue))
- return;
-
- // 노드 선택
- var nodeNames = unmappedNodes.Select(n => $"{n.NodeId}: {n.Name}").ToArray();
- var selectedNode = Microsoft.VisualBasic.Interaction.InputBox("매핑할 노드를 선택하세요:", "노드 선택", nodeNames[0]);
-
- var nodeId = selectedNode.Split(':')[0];
- var description = Microsoft.VisualBasic.Interaction.InputBox("설명을 입력하세요 (선택사항):", "설명");
-
- if (_nodeResolver.AddRfidMapping(rfidValue, nodeId, description))
- {
- _hasChanges = true;
- RefreshRfidMappingList();
- UpdateTitle();
- MessageBox.Show("RFID 매핑이 추가되었습니다.", "성공", MessageBoxButtons.OK, MessageBoxIcon.Information);
- }
- else
- {
- MessageBox.Show("RFID 매핑 추가에 실패했습니다. 중복된 RFID이거나 노드가 존재하지 않습니다.", "오류", MessageBoxButtons.OK, MessageBoxIcon.Error);
- }
- }
-
- private void DeleteSelectedRfidMapping()
- {
- if (listBoxRfidMappings.SelectedItem == null)
- {
- MessageBox.Show("삭제할 RFID 매핑을 선택하세요.", "알림", MessageBoxButtons.OK, MessageBoxIcon.Information);
- return;
- }
-
- var mapping = listBoxRfidMappings.SelectedItem as RfidMapping;
- var result = MessageBox.Show($"RFID 매핑 '{mapping.RfidId} → {mapping.LogicalNodeId}'를 삭제하시겠습니까?",
- "삭제 확인", MessageBoxButtons.YesNo, MessageBoxIcon.Question);
-
- if (result == DialogResult.Yes)
- {
- if (_nodeResolver.RemoveRfidMapping(mapping.RfidId))
- {
- _hasChanges = true;
- RefreshRfidMappingList();
- UpdateTitle();
- MessageBox.Show("RFID 매핑이 삭제되었습니다.", "성공", MessageBoxButtons.OK, MessageBoxIcon.Information);
- }
- else
- {
- MessageBox.Show("RFID 매핑 삭제에 실패했습니다.", "오류", MessageBoxButtons.OK, MessageBoxIcon.Error);
- }
- }
- }
-
- #endregion
-
#region File Operations
private void NewMap()
{
_mapNodes.Clear();
- _rfidMappings.Clear();
- _nodeResolver = new NodeResolver(_rfidMappings, _mapNodes);
_selectedNode = null;
_currentMapFile = string.Empty;
_hasChanges = false;
-
+
RefreshAll();
UpdateTitle();
}
+ private void CloseMap()
+ {
+ if (CheckSaveChanges())
+ {
+ _mapNodes.Clear();
+ _selectedNode = null;
+ _currentMapFile = string.Empty;
+ _hasChanges = false;
+
+ RefreshAll();
+ UpdateTitle();
+ }
+ }
+
private void OpenMap()
{
var openFileDialog = new OpenFileDialog
{
- Filter = "AGV Map Files (*.agvmap)|*.agvmap|JSON Files (*.json)|*.json|All Files (*.*)|*.*",
+ Filter = "AGV Map Files (*.agvmap)|*.agvmap|All Files (*.*)|*.*",
DefaultExt = "agvmap"
};
@@ -388,7 +470,6 @@ namespace AGVMapEditor.Forms
_hasChanges = false;
RefreshAll();
UpdateTitle();
- MessageBox.Show("맵이 성공적으로 로드되었습니다.", "성공", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
catch (Exception ex)
{
@@ -423,7 +504,7 @@ namespace AGVMapEditor.Forms
{
var saveFileDialog = new SaveFileDialog
{
- Filter = "AGV Map Files (*.agvmap)|*.agvmap|JSON Files (*.json)|*.json",
+ Filter = "AGV Map Files (*.agvmap)|*.agvmap",
DefaultExt = "agvmap",
FileName = "NewMap.agvmap"
};
@@ -447,35 +528,48 @@ namespace AGVMapEditor.Forms
private void LoadMapFromFile(string filePath)
{
- var json = File.ReadAllText(filePath);
- var mapData = JsonConvert.DeserializeObject(json);
+ var result = MapLoader.LoadMapFromFile(filePath);
- _mapNodes = mapData.Nodes ?? new List();
- _rfidMappings = mapData.RfidMappings ?? new List();
- _nodeResolver = new NodeResolver(_rfidMappings, _mapNodes);
+ if (result.Success)
+ {
+ _mapNodes = result.Nodes;
+
+ // 맵 캔버스에 데이터 설정
+ _mapCanvas.Nodes = _mapNodes;
+ // RfidMappings 제거됨 - MapNode에 통합
+ }
+ else
+ {
+ MessageBox.Show($"맵 파일 로딩 실패: {result.ErrorMessage}", "오류",
+ MessageBoxButtons.OK, MessageBoxIcon.Error);
+ }
}
private void SaveMapToFile(string filePath)
{
- var mapData = new MapData
+ if (!MapLoader.SaveMapToFile(filePath, _mapNodes))
{
- Nodes = _mapNodes,
- RfidMappings = _rfidMappings,
- CreatedDate = DateTime.Now,
- Version = "1.0"
- };
-
- var json = JsonConvert.SerializeObject(mapData, Formatting.Indented);
- File.WriteAllText(filePath, json);
+ MessageBox.Show("맵 파일 저장 실패", "오류",
+ MessageBoxButtons.OK, MessageBoxIcon.Error);
+ }
+ }
+
+ ///
+ /// RFID 매핑 업데이트 (공용 MapLoader 사용)
+ ///
+ private void UpdateRfidMappings()
+ {
+ // 네비게이션 노드들에 RFID 자동 할당
+ MapLoader.AssignAutoRfidIds(_mapNodes);
}
private bool CheckSaveChanges()
{
if (_hasChanges)
{
- var result = MessageBox.Show("변경사항이 있습니다. 저장하시겠습니까?", "변경사항 저장",
+ var result = MessageBox.Show("변경사항이 있습니다. 저장하시겠습니까?", "변경사항 저장",
MessageBoxButtons.YesNoCancel, MessageBoxIcon.Question);
-
+
if (result == DialogResult.Yes)
{
SaveMap();
@@ -486,7 +580,7 @@ namespace AGVMapEditor.Forms
return false;
}
}
-
+
return true;
}
@@ -497,7 +591,6 @@ namespace AGVMapEditor.Forms
private void RefreshAll()
{
RefreshNodeList();
- RefreshRfidMappingList();
RefreshMapCanvas();
ClearNodeProperties();
}
@@ -506,15 +599,99 @@ namespace AGVMapEditor.Forms
{
listBoxNodes.DataSource = null;
listBoxNodes.DataSource = _mapNodes;
- listBoxNodes.DisplayMember = "Name";
+ listBoxNodes.DisplayMember = "DisplayText";
listBoxNodes.ValueMember = "NodeId";
+
+ // 노드 목록 클릭 이벤트 연결
+ listBoxNodes.SelectedIndexChanged -= ListBoxNodes_SelectedIndexChanged;
+ listBoxNodes.SelectedIndexChanged += ListBoxNodes_SelectedIndexChanged;
+
+ // 노드 타입별 색상 적용
+ listBoxNodes.DrawMode = DrawMode.OwnerDrawFixed;
+ listBoxNodes.DrawItem -= ListBoxNodes_DrawItem;
+ listBoxNodes.DrawItem += ListBoxNodes_DrawItem;
}
- private void RefreshRfidMappingList()
+ private void ListBoxNodes_SelectedIndexChanged(object sender, EventArgs e)
{
- listBoxRfidMappings.DataSource = null;
- listBoxRfidMappings.DataSource = _rfidMappings;
- listBoxRfidMappings.DisplayMember = "ToString";
+ if (listBoxNodes.SelectedItem is MapNode selectedNode)
+ {
+ _selectedNode = selectedNode;
+ UpdateNodeProperties();
+ // 맵 캔버스에서도 선택된 노드 표시
+ if (_mapCanvas != null)
+ {
+ _mapCanvas.Invalidate();
+ }
+ }
+ }
+
+ private void ListBoxNodes_DrawItem(object sender, DrawItemEventArgs e)
+ {
+ e.DrawBackground();
+
+ if (e.Index >= 0 && e.Index < _mapNodes.Count)
+ {
+ var node = _mapNodes[e.Index];
+
+ // 노드 타입에 따른 색상 설정
+ Color foreColor = Color.Black;
+ Color backColor = e.BackColor;
+
+ if ((e.State & DrawItemState.Selected) == DrawItemState.Selected)
+ {
+ backColor = SystemColors.Highlight;
+ foreColor = SystemColors.HighlightText;
+ }
+ else
+ {
+ switch (node.Type)
+ {
+ case NodeType.Normal:
+ foreColor = Color.Black;
+ backColor = Color.White;
+ break;
+ case NodeType.Rotation:
+ foreColor = Color.DarkOrange;
+ backColor = Color.LightYellow;
+ break;
+ case NodeType.Docking:
+ foreColor = Color.DarkGreen;
+ backColor = Color.LightGreen;
+ break;
+ case NodeType.Charging:
+ foreColor = Color.DarkRed;
+ backColor = Color.LightPink;
+ break;
+ }
+ }
+
+ // 배경 그리기
+ using (var brush = new SolidBrush(backColor))
+ {
+ e.Graphics.FillRectangle(brush, e.Bounds);
+ }
+
+ // 텍스트 그리기 (노드ID - 설명 - RFID 순서)
+ var displayText = node.NodeId;
+
+ if (!string.IsNullOrEmpty(node.Description))
+ {
+ displayText += $" - {node.Description}";
+ }
+
+ if (!string.IsNullOrEmpty(node.RfidId))
+ {
+ displayText += $" - [{node.RfidId}]";
+ }
+
+ using (var brush = new SolidBrush(foreColor))
+ {
+ e.Graphics.DrawString(displayText, e.Font, brush, e.Bounds.X + 2, e.Bounds.Y + 2);
+ }
+ }
+
+ e.DrawFocusRectangle();
}
private void RefreshMapCanvas()
@@ -530,30 +707,31 @@ namespace AGVMapEditor.Forms
return;
}
- // 선택된 노드의 속성을 프로퍼티 패널에 표시
- // (실제로는 PropertyGrid나 별도 컨트롤 사용)
- labelSelectedNode.Text = $"선택된 노드: {_selectedNode.Name} ({_selectedNode.NodeId})";
+ // 노드 래퍼 객체 생성 (타입에 따라 다른 래퍼 사용)
+ var nodeWrapper = NodePropertyWrapperFactory.CreateWrapper(_selectedNode, _mapNodes);
+ _propertyGrid.SelectedObject = nodeWrapper;
+ _propertyGrid.Focus();
}
private void ClearNodeProperties()
{
- labelSelectedNode.Text = "선택된 노드: 없음";
+ _propertyGrid.SelectedObject = null;
}
private void UpdateTitle()
{
var title = "AGV Map Editor";
-
+
if (!string.IsNullOrEmpty(_currentMapFile))
{
title += $" - {Path.GetFileName(_currentMapFile)}";
}
-
+
if (_hasChanges)
{
title += " *";
}
-
+
this.Text = title;
}
@@ -571,16 +749,38 @@ namespace AGVMapEditor.Forms
#endregion
- #region Data Model for Serialization
+ #region PropertyGrid
- private class MapData
+
+ private void PropertyGrid_PropertyValueChanged(object s, PropertyValueChangedEventArgs e)
{
- public List Nodes { get; set; } = new List();
- public List RfidMappings { get; set; } = new List();
- public DateTime CreatedDate { get; set; }
- public string Version { get; set; } = "1.0";
+ // 속성이 변경되었을 때 자동으로 변경사항 표시
+ _hasChanges = true;
+ UpdateTitle();
+
+ // 현재 선택된 노드를 기억
+ var currentSelectedNode = _selectedNode;
+
+ RefreshNodeList();
+ RefreshMapCanvas();
+
+ // 선택된 노드를 다시 선택
+ if (currentSelectedNode != null)
+ {
+ var nodeIndex = _mapNodes.IndexOf(currentSelectedNode);
+ if (nodeIndex >= 0)
+ {
+ listBoxNodes.SelectedIndex = nodeIndex;
+ }
+ }
}
#endregion
+
+ #region Data Model for Serialization
+
+
+ #endregion
+
}
}
\ No newline at end of file
diff --git a/Cs_HMI/AGVMapEditor/Forms/MainForm.resx b/Cs_HMI/AGVMapEditor/Forms/MainForm.resx
index 9564200..69e20d7 100644
--- a/Cs_HMI/AGVMapEditor/Forms/MainForm.resx
+++ b/Cs_HMI/AGVMapEditor/Forms/MainForm.resx
@@ -1,5 +1,64 @@
+
@@ -28,9 +87,9 @@
-
-
-
+
+
+
@@ -39,7 +98,7 @@
-
+
diff --git a/Cs_HMI/AGVMapEditor/Models/Enums.cs b/Cs_HMI/AGVMapEditor/Models/Enums.cs
index 741f2f8..36baf9f 100644
--- a/Cs_HMI/AGVMapEditor/Models/Enums.cs
+++ b/Cs_HMI/AGVMapEditor/Models/Enums.cs
@@ -14,7 +14,11 @@ namespace AGVMapEditor.Models
/// 도킹 스테이션
Docking,
/// 충전 스테이션
- Charging
+ Charging,
+ /// 라벨 (UI 요소)
+ Label,
+ /// 이미지 (UI 요소)
+ Image
}
///
diff --git a/Cs_HMI/AGVMapEditor/Models/MapData.cs b/Cs_HMI/AGVMapEditor/Models/MapData.cs
new file mode 100644
index 0000000..9dde918
--- /dev/null
+++ b/Cs_HMI/AGVMapEditor/Models/MapData.cs
@@ -0,0 +1,15 @@
+using System;
+using System.Collections.Generic;
+using AGVNavigationCore.Models;
+
+namespace AGVMapEditor.Models
+{
+
+ public class MapData
+ {
+ public List Nodes { get; set; } = new List();
+ public DateTime CreatedDate { get; set; }
+ public string Version { get; set; } = "1.0";
+ }
+
+}
\ No newline at end of file
diff --git a/Cs_HMI/AGVMapEditor/Models/MapImage.cs b/Cs_HMI/AGVMapEditor/Models/MapImage.cs
new file mode 100644
index 0000000..1df1e12
--- /dev/null
+++ b/Cs_HMI/AGVMapEditor/Models/MapImage.cs
@@ -0,0 +1,210 @@
+using System;
+using System.Drawing;
+
+namespace AGVMapEditor.Models
+{
+ ///
+ /// 맵 이미지 정보를 관리하는 클래스
+ /// 디자인 요소용 이미지/비트맵 요소
+ ///
+ public class MapImage
+ {
+ ///
+ /// 이미지 고유 ID
+ ///
+ public string ImageId { get; set; } = string.Empty;
+
+ ///
+ /// 이미지 파일 경로
+ ///
+ public string ImagePath { get; set; } = string.Empty;
+
+ ///
+ /// 맵 상의 위치 좌표 (좌상단 기준)
+ ///
+ public Point Position { get; set; } = Point.Empty;
+
+ ///
+ /// 이미지 크기 (원본 크기 기준 배율)
+ ///
+ public SizeF Scale { get; set; } = new SizeF(1.0f, 1.0f);
+
+ ///
+ /// 이미지 투명도 (0.0 ~ 1.0)
+ ///
+ public float Opacity { get; set; } = 1.0f;
+
+ ///
+ /// 이미지 회전 각도 (도 단위)
+ ///
+ public float Rotation { get; set; } = 0.0f;
+
+ ///
+ /// 이미지 설명
+ ///
+ public string Description { get; set; } = string.Empty;
+
+ ///
+ /// 이미지 생성 일자
+ ///
+ public DateTime CreatedDate { get; set; } = DateTime.Now;
+
+ ///
+ /// 이미지 수정 일자
+ ///
+ public DateTime ModifiedDate { get; set; } = DateTime.Now;
+
+ ///
+ /// 이미지 활성화 여부
+ ///
+ public bool IsActive { get; set; } = true;
+
+ ///
+ /// 로딩된 이미지 (런타임에서만 사용, JSON 직렬화 제외)
+ ///
+ [Newtonsoft.Json.JsonIgnore]
+ public Image LoadedImage { get; set; }
+
+ ///
+ /// 기본 생성자
+ ///
+ public MapImage()
+ {
+ }
+
+ ///
+ /// 매개변수 생성자
+ ///
+ /// 이미지 ID
+ /// 이미지 파일 경로
+ /// 위치
+ public MapImage(string imageId, string imagePath, Point position)
+ {
+ ImageId = imageId;
+ ImagePath = imagePath;
+ Position = position;
+ CreatedDate = DateTime.Now;
+ ModifiedDate = DateTime.Now;
+ }
+
+ ///
+ /// 이미지 로드 (256x256 이상일 경우 자동 리사이즈)
+ ///
+ /// 로드 성공 여부
+ public bool LoadImage()
+ {
+ try
+ {
+ if (!string.IsNullOrEmpty(ImagePath) && System.IO.File.Exists(ImagePath))
+ {
+ LoadedImage?.Dispose();
+ var originalImage = Image.FromFile(ImagePath);
+
+ // 이미지 크기 체크 및 리사이즈
+ if (originalImage.Width > 256 || originalImage.Height > 256)
+ {
+ LoadedImage = ResizeImage(originalImage, 256, 256);
+ originalImage.Dispose();
+ }
+ else
+ {
+ LoadedImage = originalImage;
+ }
+
+ return true;
+ }
+ }
+ catch (Exception)
+ {
+ // 이미지 로드 실패
+ }
+ return false;
+ }
+
+ ///
+ /// 이미지 리사이즈 (비율 유지)
+ ///
+ /// 원본 이미지
+ /// 최대 너비
+ /// 최대 높이
+ /// 리사이즈된 이미지
+ private Image ResizeImage(Image image, int maxWidth, int maxHeight)
+ {
+ // 비율 계산
+ double ratioX = (double)maxWidth / image.Width;
+ double ratioY = (double)maxHeight / image.Height;
+ double ratio = Math.Min(ratioX, ratioY);
+
+ // 새로운 크기 계산
+ int newWidth = (int)(image.Width * ratio);
+ int newHeight = (int)(image.Height * ratio);
+
+ // 리사이즈된 이미지 생성
+ var resizedImage = new Bitmap(newWidth, newHeight);
+ using (var graphics = Graphics.FromImage(resizedImage))
+ {
+ graphics.CompositingQuality = System.Drawing.Drawing2D.CompositingQuality.HighQuality;
+ graphics.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
+ graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;
+ graphics.DrawImage(image, 0, 0, newWidth, newHeight);
+ }
+
+ return resizedImage;
+ }
+
+ ///
+ /// 실제 표시될 크기 계산
+ ///
+ /// 실제 크기
+ public Size GetDisplaySize()
+ {
+ if (LoadedImage == null) return Size.Empty;
+
+ return new Size(
+ (int)(LoadedImage.Width * Scale.Width),
+ (int)(LoadedImage.Height * Scale.Height)
+ );
+ }
+
+ ///
+ /// 문자열 표현
+ ///
+ public override string ToString()
+ {
+ return $"{ImageId}: {System.IO.Path.GetFileName(ImagePath)} at ({Position.X}, {Position.Y})";
+ }
+
+ ///
+ /// 이미지 복사
+ ///
+ /// 복사된 이미지
+ public MapImage Clone()
+ {
+ var clone = new MapImage
+ {
+ ImageId = ImageId,
+ ImagePath = ImagePath,
+ Position = Position,
+ Scale = Scale,
+ Opacity = Opacity,
+ Rotation = Rotation,
+ Description = Description,
+ CreatedDate = CreatedDate,
+ ModifiedDate = ModifiedDate,
+ IsActive = IsActive
+ };
+
+ // 이미지는 복사하지 않음 (필요시 LoadImage() 호출)
+ return clone;
+ }
+
+ ///
+ /// 리소스 정리
+ ///
+ public void Dispose()
+ {
+ LoadedImage?.Dispose();
+ LoadedImage = null;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Cs_HMI/AGVMapEditor/Models/MapLabel.cs b/Cs_HMI/AGVMapEditor/Models/MapLabel.cs
new file mode 100644
index 0000000..a324003
--- /dev/null
+++ b/Cs_HMI/AGVMapEditor/Models/MapLabel.cs
@@ -0,0 +1,125 @@
+using System;
+using System.Drawing;
+
+namespace AGVMapEditor.Models
+{
+ ///
+ /// 맵 라벨 정보를 관리하는 클래스
+ /// 디자인 요소용 텍스트 라벨
+ ///
+ public class MapLabel
+ {
+ ///
+ /// 라벨 고유 ID
+ ///
+ public string LabelId { get; set; } = string.Empty;
+
+ ///
+ /// 라벨 텍스트
+ ///
+ public string Text { get; set; } = string.Empty;
+
+ ///
+ /// 맵 상의 위치 좌표
+ ///
+ public Point Position { get; set; } = Point.Empty;
+
+ ///
+ /// 폰트 정보
+ ///
+ public string FontFamily { get; set; } = "Arial";
+
+ ///
+ /// 폰트 크기
+ ///
+ public float FontSize { get; set; } = 12;
+
+ ///
+ /// 폰트 스타일 (Bold, Italic 등)
+ ///
+ public FontStyle FontStyle { get; set; } = FontStyle.Regular;
+
+ ///
+ /// 글자 색상
+ ///
+ public Color ForeColor { get; set; } = Color.Black;
+
+ ///
+ /// 배경 색상
+ ///
+ public Color BackColor { get; set; } = Color.Transparent;
+
+ ///
+ /// 배경 표시 여부
+ ///
+ public bool ShowBackground { get; set; } = false;
+
+ ///
+ /// 라벨 생성 일자
+ ///
+ public DateTime CreatedDate { get; set; } = DateTime.Now;
+
+ ///
+ /// 라벨 수정 일자
+ ///
+ public DateTime ModifiedDate { get; set; } = DateTime.Now;
+
+ ///
+ /// 라벨 활성화 여부
+ ///
+ public bool IsActive { get; set; } = true;
+
+ ///
+ /// 기본 생성자
+ ///
+ public MapLabel()
+ {
+ }
+
+ ///
+ /// 매개변수 생성자
+ ///
+ /// 라벨 ID
+ /// 라벨 텍스트
+ /// 위치
+ public MapLabel(string labelId, string text, Point position)
+ {
+ LabelId = labelId;
+ Text = text;
+ Position = position;
+ CreatedDate = DateTime.Now;
+ ModifiedDate = DateTime.Now;
+ }
+
+ ///
+ /// 문자열 표현
+ ///
+ public override string ToString()
+ {
+ return $"{LabelId}: {Text} at ({Position.X}, {Position.Y})";
+ }
+
+ ///
+ /// 라벨 복사
+ ///
+ /// 복사된 라벨
+ public MapLabel Clone()
+ {
+ return new MapLabel
+ {
+ LabelId = LabelId,
+ Text = Text,
+ Position = Position,
+ FontFamily = FontFamily,
+ FontSize = FontSize,
+ FontStyle = FontStyle,
+ ForeColor = ForeColor,
+ BackColor = BackColor,
+ ShowBackground = ShowBackground,
+ CreatedDate = CreatedDate,
+ ModifiedDate = ModifiedDate,
+ IsActive = IsActive
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/Cs_HMI/AGVMapEditor/Models/MapNode.cs b/Cs_HMI/AGVMapEditor/Models/MapNode.cs
index 267509b..570a256 100644
--- a/Cs_HMI/AGVMapEditor/Models/MapNode.cs
+++ b/Cs_HMI/AGVMapEditor/Models/MapNode.cs
@@ -83,6 +83,72 @@ namespace AGVMapEditor.Models
///
public Color DisplayColor { get; set; } = Color.Blue;
+ ///
+ /// RFID 태그 ID (이 노드에 매핑된 RFID)
+ ///
+ public string RfidId { get; set; } = string.Empty;
+
+ ///
+ /// 라벨 텍스트 (NodeType.Label인 경우 사용)
+ ///
+ public string LabelText { get; set; } = string.Empty;
+
+ ///
+ /// 라벨 폰트 패밀리 (NodeType.Label인 경우 사용)
+ ///
+ public string FontFamily { get; set; } = "Arial";
+
+ ///
+ /// 라벨 폰트 크기 (NodeType.Label인 경우 사용)
+ ///
+ public float FontSize { get; set; } = 12.0f;
+
+ ///
+ /// 라벨 폰트 스타일 (NodeType.Label인 경우 사용)
+ ///
+ public FontStyle FontStyle { get; set; } = FontStyle.Regular;
+
+ ///
+ /// 라벨 전경색 (NodeType.Label인 경우 사용)
+ ///
+ public Color ForeColor { get; set; } = Color.Black;
+
+ ///
+ /// 라벨 배경색 (NodeType.Label인 경우 사용)
+ ///
+ public Color BackColor { get; set; } = Color.Transparent;
+
+ ///
+ /// 라벨 배경 표시 여부 (NodeType.Label인 경우 사용)
+ ///
+ public bool ShowBackground { get; set; } = false;
+
+ ///
+ /// 이미지 파일 경로 (NodeType.Image인 경우 사용)
+ ///
+ public string ImagePath { get; set; } = string.Empty;
+
+ ///
+ /// 이미지 크기 배율 (NodeType.Image인 경우 사용)
+ ///
+ public SizeF Scale { get; set; } = new SizeF(1.0f, 1.0f);
+
+ ///
+ /// 이미지 투명도 (NodeType.Image인 경우 사용, 0.0~1.0)
+ ///
+ public float Opacity { get; set; } = 1.0f;
+
+ ///
+ /// 이미지 회전 각도 (NodeType.Image인 경우 사용, 도 단위)
+ ///
+ public float Rotation { get; set; } = 0.0f;
+
+ ///
+ /// 로딩된 이미지 (런타임에서만 사용, JSON 직렬화 제외)
+ ///
+ [Newtonsoft.Json.JsonIgnore]
+ public Image LoadedImage { get; set; }
+
///
/// 기본 생성자
///
@@ -130,6 +196,12 @@ namespace AGVMapEditor.Models
case NodeType.Charging:
DisplayColor = Color.Red;
break;
+ case NodeType.Label:
+ DisplayColor = Color.Purple;
+ break;
+ case NodeType.Image:
+ DisplayColor = Color.Brown;
+ break;
}
}
@@ -196,6 +268,29 @@ namespace AGVMapEditor.Models
return $"{NodeId}: {Name} ({Type}) at ({Position.X}, {Position.Y})";
}
+ ///
+ /// 리스트박스 표시용 텍스트 (노드ID - 설명 - RFID 순서)
+ ///
+ public string DisplayText
+ {
+ get
+ {
+ var displayText = NodeId;
+
+ if (!string.IsNullOrEmpty(Description))
+ {
+ displayText += $" - {Description}";
+ }
+
+ if (!string.IsNullOrEmpty(RfidId))
+ {
+ displayText += $" - [{RfidId}]";
+ }
+
+ return displayText;
+ }
+ }
+
///
/// 노드 복사
///
@@ -217,9 +312,111 @@ namespace AGVMapEditor.Models
ModifiedDate = ModifiedDate,
Description = Description,
IsActive = IsActive,
- DisplayColor = DisplayColor
+ DisplayColor = DisplayColor,
+ RfidId = RfidId,
+ LabelText = LabelText,
+ FontFamily = FontFamily,
+ FontSize = FontSize,
+ FontStyle = FontStyle,
+ ForeColor = ForeColor,
+ BackColor = BackColor,
+ ShowBackground = ShowBackground,
+ ImagePath = ImagePath,
+ Scale = Scale,
+ Opacity = Opacity,
+ Rotation = Rotation
};
return clone;
}
+
+ ///
+ /// 이미지 로드 (256x256 이상일 경우 자동 리사이즈)
+ ///
+ /// 로드 성공 여부
+ public bool LoadImage()
+ {
+ if (Type != NodeType.Image) return false;
+
+ try
+ {
+ if (!string.IsNullOrEmpty(ImagePath) && System.IO.File.Exists(ImagePath))
+ {
+ LoadedImage?.Dispose();
+ var originalImage = Image.FromFile(ImagePath);
+
+ // 이미지 크기 체크 및 리사이즈
+ if (originalImage.Width > 256 || originalImage.Height > 256)
+ {
+ LoadedImage = ResizeImage(originalImage, 256, 256);
+ originalImage.Dispose();
+ }
+ else
+ {
+ LoadedImage = originalImage;
+ }
+
+ return true;
+ }
+ }
+ catch (Exception)
+ {
+ // 이미지 로드 실패
+ }
+ return false;
+ }
+
+ ///
+ /// 이미지 리사이즈 (비율 유지)
+ ///
+ /// 원본 이미지
+ /// 최대 너비
+ /// 최대 높이
+ /// 리사이즈된 이미지
+ private Image ResizeImage(Image image, int maxWidth, int maxHeight)
+ {
+ // 비율 계산
+ double ratioX = (double)maxWidth / image.Width;
+ double ratioY = (double)maxHeight / image.Height;
+ double ratio = Math.Min(ratioX, ratioY);
+
+ // 새로운 크기 계산
+ int newWidth = (int)(image.Width * ratio);
+ int newHeight = (int)(image.Height * ratio);
+
+ // 리사이즈된 이미지 생성
+ var resizedImage = new Bitmap(newWidth, newHeight);
+ using (var graphics = Graphics.FromImage(resizedImage))
+ {
+ graphics.CompositingQuality = System.Drawing.Drawing2D.CompositingQuality.HighQuality;
+ graphics.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
+ graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;
+ graphics.DrawImage(image, 0, 0, newWidth, newHeight);
+ }
+
+ return resizedImage;
+ }
+
+ ///
+ /// 실제 표시될 크기 계산 (이미지 노드인 경우)
+ ///
+ /// 실제 크기
+ public Size GetDisplaySize()
+ {
+ if (Type != NodeType.Image || LoadedImage == null) return Size.Empty;
+
+ return new Size(
+ (int)(LoadedImage.Width * Scale.Width),
+ (int)(LoadedImage.Height * Scale.Height)
+ );
+ }
+
+ ///
+ /// 리소스 정리
+ ///
+ public void Dispose()
+ {
+ LoadedImage?.Dispose();
+ LoadedImage = null;
+ }
}
}
\ No newline at end of file
diff --git a/Cs_HMI/AGVMapEditor/Models/NodePropertyWrapper.cs b/Cs_HMI/AGVMapEditor/Models/NodePropertyWrapper.cs
new file mode 100644
index 0000000..4eda91f
--- /dev/null
+++ b/Cs_HMI/AGVMapEditor/Models/NodePropertyWrapper.cs
@@ -0,0 +1,499 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Drawing;
+using AGVNavigationCore.Models;
+
+namespace AGVMapEditor.Models
+{
+ ///
+ /// 노드 타입에 따른 PropertyWrapper 팩토리
+ ///
+ public static class NodePropertyWrapperFactory
+ {
+ public static object CreateWrapper(MapNode node, List mapNodes)
+ {
+ switch (node.Type)
+ {
+ case NodeType.Label:
+ return new LabelNodePropertyWrapper(node, mapNodes);
+ case NodeType.Image:
+ return new ImageNodePropertyWrapper(node, mapNodes);
+ default:
+ return new NodePropertyWrapper(node, mapNodes);
+ }
+ }
+ }
+
+ ///
+ /// 라벨 노드 전용 PropertyWrapper
+ ///
+ public class LabelNodePropertyWrapper
+ {
+ private MapNode _node;
+ private List _mapNodes;
+
+ public LabelNodePropertyWrapper(MapNode node, List mapNodes)
+ {
+ _node = node;
+ _mapNodes = mapNodes;
+ }
+
+ [Category("기본 정보")]
+ [DisplayName("노드 ID")]
+ [Description("노드의 고유 식별자")]
+ [ReadOnly(true)]
+ public string NodeId
+ {
+ get => _node.NodeId;
+ }
+
+ [Category("기본 정보")]
+ [DisplayName("노드 타입")]
+ [Description("노드의 타입")]
+ [ReadOnly(true)]
+ public NodeType Type
+ {
+ get => _node.Type;
+ }
+
+ [Category("라벨")]
+ [DisplayName("텍스트")]
+ [Description("표시할 텍스트")]
+ public string LabelText
+ {
+ get => _node.LabelText;
+ set
+ {
+ _node.LabelText = value ?? "";
+ _node.ModifiedDate = DateTime.Now;
+ }
+ }
+
+ [Category("라벨")]
+ [DisplayName("폰트 패밀리")]
+ [Description("폰트 패밀리")]
+ public string FontFamily
+ {
+ get => _node.FontFamily;
+ set
+ {
+ _node.FontFamily = value ?? "Arial";
+ _node.ModifiedDate = DateTime.Now;
+ }
+ }
+
+ [Category("라벨")]
+ [DisplayName("폰트 크기")]
+ [Description("폰트 크기")]
+ public float FontSize
+ {
+ get => _node.FontSize;
+ set
+ {
+ _node.FontSize = Math.Max(6, Math.Min(72, value));
+ _node.ModifiedDate = DateTime.Now;
+ }
+ }
+
+ [Category("라벨")]
+ [DisplayName("폰트 스타일")]
+ [Description("폰트 스타일")]
+ public FontStyle FontStyle
+ {
+ get => _node.FontStyle;
+ set
+ {
+ _node.FontStyle = value;
+ _node.ModifiedDate = DateTime.Now;
+ }
+ }
+
+ [Category("라벨")]
+ [DisplayName("전경색")]
+ [Description("텍스트 색상")]
+ public Color ForeColor
+ {
+ get => _node.ForeColor;
+ set
+ {
+ _node.ForeColor = value;
+ _node.ModifiedDate = DateTime.Now;
+ }
+ }
+
+ [Category("라벨")]
+ [DisplayName("배경색")]
+ [Description("배경 색상")]
+ public Color BackColor
+ {
+ get => _node.BackColor;
+ set
+ {
+ _node.BackColor = value;
+ _node.ModifiedDate = DateTime.Now;
+ }
+ }
+
+ [Category("라벨")]
+ [DisplayName("배경 표시")]
+ [Description("배경을 표시할지 여부")]
+ public bool ShowBackground
+ {
+ get => _node.ShowBackground;
+ set
+ {
+ _node.ShowBackground = value;
+ _node.ModifiedDate = DateTime.Now;
+ }
+ }
+
+ [Category("위치")]
+ [DisplayName("X 좌표")]
+ [Description("맵에서의 X 좌표")]
+ public int PositionX
+ {
+ get => _node.Position.X;
+ set
+ {
+ _node.Position = new Point(value, _node.Position.Y);
+ _node.ModifiedDate = DateTime.Now;
+ }
+ }
+
+ [Category("위치")]
+ [DisplayName("Y 좌표")]
+ [Description("맵에서의 Y 좌표")]
+ public int PositionY
+ {
+ get => _node.Position.Y;
+ set
+ {
+ _node.Position = new Point(_node.Position.X, value);
+ _node.ModifiedDate = DateTime.Now;
+ }
+ }
+
+ [Category("정보")]
+ [DisplayName("생성 일시")]
+ [Description("노드가 생성된 일시")]
+ [ReadOnly(true)]
+ public DateTime CreatedDate
+ {
+ get => _node.CreatedDate;
+ }
+
+ [Category("정보")]
+ [DisplayName("수정 일시")]
+ [Description("노드가 마지막으로 수정된 일시")]
+ [ReadOnly(true)]
+ public DateTime ModifiedDate
+ {
+ get => _node.ModifiedDate;
+ }
+ }
+
+ ///
+ /// 이미지 노드 전용 PropertyWrapper
+ ///
+ public class ImageNodePropertyWrapper
+ {
+ private MapNode _node;
+ private List _mapNodes;
+
+ public ImageNodePropertyWrapper(MapNode node, List mapNodes)
+ {
+ _node = node;
+ _mapNodes = mapNodes;
+ }
+
+ [Category("기본 정보")]
+ [DisplayName("노드 ID")]
+ [Description("노드의 고유 식별자")]
+ [ReadOnly(true)]
+ public string NodeId
+ {
+ get => _node.NodeId;
+ }
+
+ [Category("기본 정보")]
+ [DisplayName("노드 타입")]
+ [Description("노드의 타입")]
+ [ReadOnly(true)]
+ public NodeType Type
+ {
+ get => _node.Type;
+ }
+
+ [Category("이미지")]
+ [DisplayName("이미지 경로")]
+ [Description("이미지 파일 경로")]
+ // 파일 선택 에디터는 나중에 구현
+ public string ImagePath
+ {
+ get => _node.ImagePath;
+ set
+ {
+ _node.ImagePath = value ?? "";
+ _node.LoadImage(); // 이미지 다시 로드
+ _node.ModifiedDate = DateTime.Now;
+ }
+ }
+
+ [Category("이미지")]
+ [DisplayName("가로 배율")]
+ [Description("가로 배율 (1.0 = 원본 크기)")]
+ public float ScaleWidth
+ {
+ get => _node.Scale.Width;
+ set
+ {
+ _node.Scale = new SizeF(Math.Max(0.1f, Math.Min(5.0f, value)), _node.Scale.Height);
+ _node.ModifiedDate = DateTime.Now;
+ }
+ }
+
+ [Category("이미지")]
+ [DisplayName("세로 배율")]
+ [Description("세로 배율 (1.0 = 원본 크기)")]
+ public float ScaleHeight
+ {
+ get => _node.Scale.Height;
+ set
+ {
+ _node.Scale = new SizeF(_node.Scale.Width, Math.Max(0.1f, Math.Min(5.0f, value)));
+ _node.ModifiedDate = DateTime.Now;
+ }
+ }
+
+ [Category("이미지")]
+ [DisplayName("투명도")]
+ [Description("투명도 (0.0 = 투명, 1.0 = 불투명)")]
+ public float Opacity
+ {
+ get => _node.Opacity;
+ set
+ {
+ _node.Opacity = Math.Max(0.0f, Math.Min(1.0f, value));
+ _node.ModifiedDate = DateTime.Now;
+ }
+ }
+
+ [Category("이미지")]
+ [DisplayName("회전각도")]
+ [Description("회전 각도 (도 단위)")]
+ public float Rotation
+ {
+ get => _node.Rotation;
+ set
+ {
+ _node.Rotation = value % 360;
+ _node.ModifiedDate = DateTime.Now;
+ }
+ }
+
+ [Category("위치")]
+ [DisplayName("X 좌표")]
+ [Description("맵에서의 X 좌표")]
+ public int PositionX
+ {
+ get => _node.Position.X;
+ set
+ {
+ _node.Position = new Point(value, _node.Position.Y);
+ _node.ModifiedDate = DateTime.Now;
+ }
+ }
+
+ [Category("위치")]
+ [DisplayName("Y 좌표")]
+ [Description("맵에서의 Y 좌표")]
+ public int PositionY
+ {
+ get => _node.Position.Y;
+ set
+ {
+ _node.Position = new Point(_node.Position.X, value);
+ _node.ModifiedDate = DateTime.Now;
+ }
+ }
+
+ [Category("정보")]
+ [DisplayName("생성 일시")]
+ [Description("노드가 생성된 일시")]
+ [ReadOnly(true)]
+ public DateTime CreatedDate
+ {
+ get => _node.CreatedDate;
+ }
+
+ [Category("정보")]
+ [DisplayName("수정 일시")]
+ [Description("노드가 마지막으로 수정된 일시")]
+ [ReadOnly(true)]
+ public DateTime ModifiedDate
+ {
+ get => _node.ModifiedDate;
+ }
+ }
+ ///
+ /// PropertyGrid에서 사용할 노드 속성 래퍼 클래스
+ ///
+ public class NodePropertyWrapper
+ {
+ private MapNode _node;
+ private List _mapNodes;
+
+ public NodePropertyWrapper(MapNode node, List mapNodes)
+ {
+ _node = node;
+ _mapNodes = mapNodes;
+ }
+
+ [Category("기본 정보")]
+ [DisplayName("노드 ID")]
+ [Description("노드의 고유 식별자")]
+ [ReadOnly(true)]
+ public string NodeId
+ {
+ get => _node.NodeId;
+ set => _node.NodeId = value;
+ }
+
+ [Category("기본 정보")]
+ [DisplayName("노드 이름")]
+ [Description("노드의 표시 이름")]
+ public string Name
+ {
+ get => _node.Name;
+ set
+ {
+ _node.Name = value;
+ _node.ModifiedDate = DateTime.Now;
+ }
+ }
+
+
+
+ [Category("기본 정보")]
+ [DisplayName("노드 타입")]
+ [Description("노드의 타입 (Normal, Rotation, Docking, Charging)")]
+ public NodeType Type
+ {
+ get => _node.Type;
+ set
+ {
+ _node.Type = value;
+ _node.CanRotate = value == NodeType.Rotation;
+ _node.SetDefaultColorByType(value);
+ _node.ModifiedDate = DateTime.Now;
+ }
+ }
+
+
+
+
+
+ [Category("위치")]
+ [DisplayName("X 좌표")]
+ [Description("맵에서의 X 좌표")]
+ public int PositionX
+ {
+ get => _node.Position.X;
+ set
+ {
+ _node.Position = new Point(value, _node.Position.Y);
+ _node.ModifiedDate = DateTime.Now;
+ }
+ }
+
+ [Category("위치")]
+ [DisplayName("Y 좌표")]
+ [Description("맵에서의 Y 좌표")]
+ public int PositionY
+ {
+ get => _node.Position.Y;
+ set
+ {
+ _node.Position = new Point(_node.Position.X, value);
+ _node.ModifiedDate = DateTime.Now;
+ }
+ }
+
+
+
+ [Category("고급")]
+ [DisplayName("회전 가능")]
+ [Description("이 노드에서 AGV가 회전할 수 있는지 여부")]
+ public bool CanRotate
+ {
+ get => _node.CanRotate;
+ set
+ {
+ _node.CanRotate = value;
+ _node.ModifiedDate = DateTime.Now;
+ }
+ }
+
+ [Category("고급")]
+ [DisplayName("RFID")]
+ [Description("RFID ID")]
+ public string RFID
+ {
+ get => _node.RfidId;
+ set
+ {
+ _node.RfidId = value;
+ _node.ModifiedDate = DateTime.Now;
+ }
+ }
+
+
+
+ [Category("고급")]
+ [DisplayName("설명")]
+ [Description("노드에 대한 추가 설명")]
+ public string Description
+ {
+ get => _node.Description;
+ set
+ {
+ _node.Description = value ?? "";
+ _node.ModifiedDate = DateTime.Now;
+ }
+ }
+
+ [Category("고급")]
+ [DisplayName("활성화")]
+ [Description("노드 활성화 여부")]
+ public bool IsActive
+ {
+ get => _node.IsActive;
+ set
+ {
+ _node.IsActive = value;
+ _node.ModifiedDate = DateTime.Now;
+ }
+ }
+
+ [Category("정보")]
+ [DisplayName("생성 일시")]
+ [Description("노드가 생성된 일시")]
+ [ReadOnly(true)]
+ public DateTime CreatedDate
+ {
+ get => _node.CreatedDate;
+ }
+
+ [Category("정보")]
+ [DisplayName("수정 일시")]
+ [Description("노드가 마지막으로 수정된 일시")]
+ [ReadOnly(true)]
+ public DateTime ModifiedDate
+ {
+ get => _node.ModifiedDate;
+ }
+ }
+
+
+}
\ No newline at end of file
diff --git a/Cs_HMI/AGVMapEditor/Models/NodeResolver.cs b/Cs_HMI/AGVMapEditor/Models/NodeResolver.cs
index 8b350d7..47523ec 100644
--- a/Cs_HMI/AGVMapEditor/Models/NodeResolver.cs
+++ b/Cs_HMI/AGVMapEditor/Models/NodeResolver.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using AGVNavigationCore.Models;
namespace AGVMapEditor.Models
{
diff --git a/Cs_HMI/AGVMapEditor/Models/PathCalculator.cs b/Cs_HMI/AGVMapEditor/Models/PathCalculator.cs
index e702140..27aa135 100644
--- a/Cs_HMI/AGVMapEditor/Models/PathCalculator.cs
+++ b/Cs_HMI/AGVMapEditor/Models/PathCalculator.cs
@@ -3,467 +3,266 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Linq;
+using AGVNavigationCore.Models;
+using AGVNavigationCore.PathFinding;
namespace AGVMapEditor.Models
{
///
- /// AGV 전용 경로 계산기 (A* 알고리즘 기반)
- /// AGV의 방향성, 도킹 제약, 회전 제약을 고려한 경로 계산
+ /// AGV 전용 경로 계산기 (AGVNavigationCore 래퍼)
+ /// AGVMapEditor와 AGVNavigationCore 간의 호환성 제공
+ /// RFID 기반 경로 계산을 우선 사용
///
public class PathCalculator
{
- #region Constants
-
- private const float BASE_MOVE_COST = 1.0f; // 기본 이동 비용
- private const float ROTATION_COST = 0.5f; // 회전 비용
- private const float DOCKING_APPROACH_COST = 0.2f; // 도킹 접근 추가 비용
- private const float HEURISTIC_WEIGHT = 1.0f; // 휴리스틱 가중치
-
- #endregion
-
- #region Fields
-
- private List _mapNodes;
- private NodeResolver _nodeResolver;
-
- #endregion
-
- #region Constructor
+ private AGVPathfinder _agvPathfinder;
+ private AStarPathfinder _astarPathfinder;
+ private RfidBasedPathfinder _rfidPathfinder;
///
/// 생성자
///
- /// 맵 노드 목록
- /// 노드 해결기
- public PathCalculator(List mapNodes, NodeResolver nodeResolver)
+ public PathCalculator()
{
- _mapNodes = mapNodes ?? throw new ArgumentNullException(nameof(mapNodes));
- _nodeResolver = nodeResolver ?? throw new ArgumentNullException(nameof(nodeResolver));
+ _agvPathfinder = new AGVPathfinder();
+ _astarPathfinder = new AStarPathfinder();
+ _rfidPathfinder = new RfidBasedPathfinder();
}
- #endregion
-
- #region Public Methods
+ ///
+ /// 맵 노드 설정
+ ///
+ /// 맵 노드 목록
+ public void SetMapNodes(List mapNodes)
+ {
+ _agvPathfinder.SetMapNodes(mapNodes);
+ _astarPathfinder.SetMapNodes(mapNodes);
+ }
///
- /// 경로 계산 (메인 메서드)
+ /// 맵 데이터 설정
+ ///
+ /// 맵 노드 목록
+ public void SetMapData(List mapNodes)
+ {
+ _agvPathfinder.SetMapNodes(mapNodes);
+ _astarPathfinder.SetMapNodes(mapNodes);
+ // RfidPathfinder는 MapNode의 RFID 정보를 직접 사용
+ _rfidPathfinder.SetMapNodes(mapNodes);
+ }
+
+ ///
+ /// AGV 경로 계산
///
/// 시작 노드 ID
- /// 목표 노드 ID
- /// 현재 AGV 방향
+ /// 목적지 노드 ID
+ /// 목적지 도착 방향
+ /// AGV 경로 계산 결과
+ public AGVPathResult FindAGVPath(string startNodeId, string endNodeId, AgvDirection? targetDirection = null)
+ {
+ return _agvPathfinder.FindAGVPath(startNodeId, endNodeId, targetDirection);
+ }
+
+
+ ///
+ /// 충전 스테이션으로의 경로 찾기
+ ///
+ /// 시작 노드 ID
+ /// AGV 경로 계산 결과
+ public AGVPathResult FindPathToChargingStation(string startNodeId)
+ {
+ return _agvPathfinder.FindPathToChargingStation(startNodeId);
+ }
+
+ ///
+ /// 도킹 스테이션으로의 경로 찾기
+ ///
+ /// 시작 노드 ID
+ /// 장비 타입
+ /// AGV 경로 계산 결과
+ public AGVPathResult FindPathToDockingStation(string startNodeId, StationType stationType)
+ {
+ return _agvPathfinder.FindPathToDockingStation(startNodeId, stationType);
+ }
+
+ ///
+ /// 여러 목적지 중 가장 가까운 노드로의 경로 찾기
+ ///
+ /// 시작 노드 ID
+ /// 목적지 후보 노드 ID 목록
/// 경로 계산 결과
- public PathResult CalculatePath(string startNodeId, string targetNodeId, AgvDirection currentDirection)
+ public PathResult FindNearestPath(string startNodeId, List targetNodeIds)
{
- var stopwatch = Stopwatch.StartNew();
-
- try
- {
- // 입력 검증
- var validationResult = ValidateInput(startNodeId, targetNodeId, currentDirection);
- if (!validationResult.Success)
- {
- stopwatch.Stop();
- validationResult.CalculationTime = stopwatch.ElapsedMilliseconds;
- return validationResult;
- }
-
- // A* 알고리즘 실행
- var result = ExecuteAStar(startNodeId, targetNodeId, currentDirection);
-
- stopwatch.Stop();
- result.CalculationTime = stopwatch.ElapsedMilliseconds;
-
- return result;
- }
- catch (Exception ex)
- {
- stopwatch.Stop();
- return new PathResult($"경로 계산 중 오류 발생: {ex.Message}")
- {
- CalculationTime = stopwatch.ElapsedMilliseconds
- };
- }
+ return _astarPathfinder.FindNearestPath(startNodeId, targetNodeIds);
}
///
- /// 경로 유효성 검증 (RFID 이탈 감지시 사용)
+ /// 두 노드가 연결되어 있는지 확인
///
- /// 현재 경로
- /// 현재 감지된 RFID
- /// 경로 유효성 여부
- public bool ValidateCurrentPath(PathResult currentPath, string currentRfidId)
+ /// 노드 1 ID
+ /// 노드 2 ID
+ /// 연결 여부
+ public bool AreNodesConnected(string nodeId1, string nodeId2)
{
- if (currentPath == null || !currentPath.Success)
- return false;
-
- var currentNode = _nodeResolver.GetNodeByRfid(currentRfidId);
- if (currentNode == null)
- return false;
-
- // 현재 노드가 계획된 경로에 포함되어 있는지 확인
- return currentPath.NodeSequence.Contains(currentNode.NodeId);
+ return _astarPathfinder.AreNodesConnected(nodeId1, nodeId2);
}
///
- /// 동적 경로 재계산 (경로 이탈시 사용)
+ /// 경로 유효성 검증
///
- /// 현재 RFID 위치
- /// 목표 노드 ID
- /// 현재 방향
- /// 원래 경로 (참고용)
- /// 새로운 경로
- public PathResult RecalculatePath(string currentRfidId, string targetNodeId,
- AgvDirection currentDirection, PathResult originalPath = null)
+ /// 검증할 경로
+ /// 유효성 검증 결과
+ public bool ValidatePath(List path)
{
- var currentNode = _nodeResolver.GetNodeByRfid(currentRfidId);
- if (currentNode == null)
- {
- return new PathResult("현재 위치를 확인할 수 없습니다.");
- }
-
- // 새로운 경로 계산
- var result = CalculatePath(currentNode.NodeId, targetNodeId, currentDirection);
-
- // 원래 경로와 비교 (로그용)
- if (originalPath != null && result.Success)
- {
- // TODO: 경로 변경 로그 기록
- }
-
- return result;
- }
-
- #endregion
-
- #region Private Methods - Input Validation
-
- ///
- /// 입력 값 검증
- ///
- private PathResult ValidateInput(string startNodeId, string targetNodeId, AgvDirection currentDirection)
- {
- if (string.IsNullOrEmpty(startNodeId))
- return new PathResult("시작 노드가 지정되지 않았습니다.");
-
- if (string.IsNullOrEmpty(targetNodeId))
- return new PathResult("목표 노드가 지정되지 않았습니다.");
-
- var startNode = _mapNodes.FirstOrDefault(n => n.NodeId == startNodeId);
- if (startNode == null)
- return new PathResult($"시작 노드를 찾을 수 없습니다: {startNodeId}");
-
- var targetNode = _mapNodes.FirstOrDefault(n => n.NodeId == targetNodeId);
- if (targetNode == null)
- return new PathResult($"목표 노드를 찾을 수 없습니다: {targetNodeId}");
-
- if (startNodeId == targetNodeId)
- return new PathResult("시작점과 목표점이 동일합니다.");
-
- return new PathResult { Success = true };
- }
-
- #endregion
-
- #region Private Methods - A* Algorithm
-
- ///
- /// A* 알고리즘 실행
- ///
- private PathResult ExecuteAStar(string startNodeId, string targetNodeId, AgvDirection currentDirection)
- {
- var openSet = new SortedSet();
- var closedSet = new HashSet();
- var gScore = new Dictionary();
-
- // 시작 노드 설정
- var startPathNode = new PathNode(startNodeId, currentDirection)
- {
- GCost = 0,
- HCost = CalculateHeuristic(startNodeId, targetNodeId)
- };
-
- openSet.Add(startPathNode);
- gScore[startPathNode.GetKey()] = 0;
-
- while (openSet.Count > 0)
- {
- // 가장 낮은 F 비용을 가진 노드 선택
- var current = openSet.Min;
- openSet.Remove(current);
-
- // 목표 도달 확인
- if (current.NodeId == targetNodeId)
- {
- // 도킹 방향 검증
- if (IsValidDockingApproach(targetNodeId, current.Direction))
- {
- return ReconstructPath(current, startNodeId, targetNodeId, currentDirection);
- }
- // 도킹 방향이 맞지 않으면 계속 탐색
- }
-
- closedSet.Add(current.GetKey());
-
- // 인접 노드들 처리
- ProcessNeighbors(current, targetNodeId, openSet, closedSet, gScore);
- }
-
- return new PathResult("경로를 찾을 수 없습니다.");
+ return _agvPathfinder.ValidatePath(path);
}
///
- /// 인접 노드들 처리
+ /// 네비게이션 가능한 노드 목록 반환
///
- private void ProcessNeighbors(PathNode current, string targetNodeId,
- SortedSet openSet, HashSet closedSet,
- Dictionary gScore)
+ /// 노드 ID 목록
+ public List GetNavigationNodes()
{
- var currentMapNode = _mapNodes.FirstOrDefault(n => n.NodeId == current.NodeId);
- if (currentMapNode == null) return;
-
- foreach (var neighborId in currentMapNode.ConnectedNodes)
- {
- var neighborMapNode = _mapNodes.FirstOrDefault(n => n.NodeId == neighborId);
- if (neighborMapNode == null) continue;
-
- // 가능한 모든 방향으로 이웃 노드 방문
- foreach (var direction in GetPossibleDirections(current, neighborMapNode))
- {
- var neighborPathNode = new PathNode(neighborId, direction);
- var neighborKey = neighborPathNode.GetKey();
-
- if (closedSet.Contains(neighborKey))
- continue;
-
- // 이동 비용 계산
- var moveCost = CalculateMoveCost(current, neighborPathNode, neighborMapNode);
- var tentativeGScore = current.GCost + moveCost;
-
- // 더 좋은 경로인지 확인
- if (!gScore.ContainsKey(neighborKey) || tentativeGScore < gScore[neighborKey])
- {
- // 경로 정보 업데이트
- neighborPathNode.Parent = current;
- neighborPathNode.GCost = tentativeGScore;
- neighborPathNode.HCost = CalculateHeuristic(neighborId, targetNodeId);
- neighborPathNode.RotationCount = current.RotationCount +
- (current.Direction != direction ? 1 : 0);
-
- // 이동 명령 시퀀스 구성
- neighborPathNode.MovementSequence = GenerateMovementSequence(current, neighborPathNode);
-
- gScore[neighborKey] = tentativeGScore;
-
- // openSet에 추가 (중복 제거)
- openSet.RemoveWhere(n => n.GetKey() == neighborKey);
- openSet.Add(neighborPathNode);
- }
- }
- }
+ return _astarPathfinder.GetNavigationNodes();
}
///
- /// 가능한 방향들 계산
+ /// AGV 현재 방향 설정
///
- private List GetPossibleDirections(PathNode current, MapNode neighborNode)
+ /// 현재 방향
+ public void SetCurrentDirection(AgvDirection direction)
{
- var directions = new List();
-
- // 기본적으로 전진/후진 가능
- directions.Add(AgvDirection.Forward);
- directions.Add(AgvDirection.Backward);
-
- // 회전 가능한 노드에서만 방향 전환 가능
- if (CanRotateAt(current.NodeId))
- {
- // 현재 방향과 다른 방향도 고려
- if (current.Direction == AgvDirection.Forward)
- directions.Add(AgvDirection.Backward);
- else if (current.Direction == AgvDirection.Backward)
- directions.Add(AgvDirection.Forward);
- }
-
- return directions;
+ _agvPathfinder.CurrentDirection = direction;
}
///
- /// 이동 비용 계산
+ /// 회전 비용 가중치 설정
///
- private float CalculateMoveCost(PathNode from, PathNode to, MapNode toMapNode)
+ /// 회전 비용 가중치
+ public void SetRotationCostWeight(float weight)
{
- float cost = BASE_MOVE_COST;
-
- // 방향 전환 비용
- if (from.Direction != to.Direction)
- {
- cost += ROTATION_COST;
- }
-
- // 도킹 스테이션 접근 비용
- if (toMapNode.Type == NodeType.Docking || toMapNode.Type == NodeType.Charging)
- {
- cost += DOCKING_APPROACH_COST;
- }
-
- // 실제 거리 기반 비용 (좌표가 있는 경우)
- var fromMapNode = _mapNodes.FirstOrDefault(n => n.NodeId == from.NodeId);
- if (fromMapNode != null && toMapNode != null)
- {
- var distance = CalculateDistance(fromMapNode.Position, toMapNode.Position);
- cost *= (distance / 100.0f); // 좌표 단위를 거리 단위로 조정
- }
-
- return cost;
+ _agvPathfinder.RotationCostWeight = weight;
}
///
- /// 휴리스틱 함수 (목표까지의 추정 거리)
+ /// 휴리스틱 가중치 설정
///
- private float CalculateHeuristic(string fromNodeId, string toNodeId)
+ /// 휴리스틱 가중치
+ public void SetHeuristicWeight(float weight)
{
- var fromNode = _mapNodes.FirstOrDefault(n => n.NodeId == fromNodeId);
- var toNode = _mapNodes.FirstOrDefault(n => n.NodeId == toNodeId);
-
- if (fromNode == null || toNode == null)
- return 0;
-
- // 유클리드 거리 계산
- var distance = CalculateDistance(fromNode.Position, toNode.Position);
- return distance * HEURISTIC_WEIGHT / 100.0f; // 좌표 단위 조정
+ _astarPathfinder.HeuristicWeight = weight;
}
///
- /// 두 점 사이의 거리 계산
+ /// 최대 탐색 노드 수 설정
///
- private float CalculateDistance(Point from, Point to)
+ /// 최대 탐색 노드 수
+ public void SetMaxSearchNodes(int maxNodes)
{
- var dx = to.X - from.X;
- var dy = to.Y - from.Y;
- return (float)Math.Sqrt(dx * dx + dy * dy);
+ _astarPathfinder.MaxSearchNodes = maxNodes;
+ }
+
+ // ==================== RFID 기반 경로 계산 메서드들 ====================
+
+ ///
+ /// RFID 기반 AGV 경로 계산
+ ///
+ /// 시작 RFID
+ /// 목적지 RFID
+ /// 목적지 도착 방향
+ /// RFID 기반 AGV 경로 계산 결과
+ public RfidPathResult FindAGVPathByRfid(string startRfidId, string endRfidId, AgvDirection? targetDirection = null)
+ {
+ return _rfidPathfinder.FindAGVPath(startRfidId, endRfidId, targetDirection);
}
///
- /// 회전 가능한 위치인지 확인
+ /// RFID 기반 충전소 경로 찾기
///
- private bool CanRotateAt(string nodeId)
+ /// 시작 RFID
+ /// RFID 기반 경로 계산 결과
+ public RfidPathResult FindPathToChargingStationByRfid(string startRfidId)
{
- var node = _mapNodes.FirstOrDefault(n => n.NodeId == nodeId);
- return node != null && (node.CanRotate || node.Type == NodeType.Rotation);
+ return _rfidPathfinder.FindPathToChargingStation(startRfidId);
}
///
- /// 도킹 접근 방향이 유효한지 확인
+ /// RFID 기반 도킹 스테이션 경로 찾기
///
- private bool IsValidDockingApproach(string nodeId, AgvDirection approachDirection)
+ /// 시작 RFID
+ /// 장비 타입
+ /// RFID 기반 경로 계산 결과
+ public RfidPathResult FindPathToDockingStationByRfid(string startRfidId, StationType stationType)
{
- var node = _mapNodes.FirstOrDefault(n => n.NodeId == nodeId);
- if (node == null) return true;
-
- // 도킹/충전 스테이션이 아니면 방향 제약 없음
- if (node.Type != NodeType.Docking && node.Type != NodeType.Charging)
- return true;
-
- // 도킹 방향 확인
- if (node.DockDirection == null)
- return true;
-
- // 충전기는 전진으로만, 다른 장비는 후진으로만
- if (node.Type == NodeType.Charging)
- return approachDirection == AgvDirection.Forward;
- else
- return approachDirection == AgvDirection.Backward;
+ return _rfidPathfinder.FindPathToDockingStation(startRfidId, stationType);
}
///
- /// 이동 명령 시퀀스 생성
+ /// 여러 RFID 목적지 중 가장 가까운 곳으로의 경로 찾기
///
- private List GenerateMovementSequence(PathNode from, PathNode to)
+ /// 시작 RFID
+ /// 목적지 후보 RFID 목록
+ /// RFID 기반 경로 계산 결과
+ public RfidPathResult FindNearestPathByRfid(string startRfidId, List targetRfidIds)
{
- var sequence = new List();
-
- // 방향 전환이 필요한 경우
- if (from.Direction != to.Direction)
- {
- if (from.Direction == AgvDirection.Forward && to.Direction == AgvDirection.Backward)
- {
- sequence.Add(AgvDirection.Right); // 180도 회전 (마크센서까지)
- }
- else if (from.Direction == AgvDirection.Backward && to.Direction == AgvDirection.Forward)
- {
- sequence.Add(AgvDirection.Right); // 180도 회전 (마크센서까지)
- }
- }
-
- // 이동 명령
- sequence.Add(to.Direction);
-
- return sequence;
+ return _rfidPathfinder.FindNearestPath(startRfidId, targetRfidIds);
}
///
- /// 경로 재구성
+ /// RFID 매핑 정보 조회 (MapNode 반환)
///
- private PathResult ReconstructPath(PathNode goalNode, string startNodeId, string targetNodeId, AgvDirection startDirection)
+ /// RFID
+ /// MapNode 또는 null
+ public MapNode GetRfidMapping(string rfidId)
{
- var path = new List();
- var current = goalNode;
-
- // 역순으로 경로 구성
- while (current != null)
- {
- path.Insert(0, current);
- current = current.Parent;
- }
-
- return new PathResult(path, startNodeId, targetNodeId, startDirection);
- }
-
- #endregion
-
- #region Public Utility Methods
-
- ///
- /// 노드 간 직선 거리 계산
- ///
- public float GetDistance(string fromNodeId, string toNodeId)
- {
- var fromNode = _mapNodes.FirstOrDefault(n => n.NodeId == fromNodeId);
- var toNode = _mapNodes.FirstOrDefault(n => n.NodeId == toNodeId);
-
- if (fromNode == null || toNode == null)
- return float.MaxValue;
-
- return CalculateDistance(fromNode.Position, toNode.Position);
+ return _rfidPathfinder.GetRfidMapping(rfidId);
}
///
- /// 특정 노드에서 가능한 다음 노드들 조회
+ /// RFID로 NodeId 조회
///
- public List GetPossibleNextNodes(string currentNodeId, AgvDirection currentDirection)
+ /// RFID
+ /// NodeId 또는 null
+ public string GetNodeIdByRfid(string rfidId)
{
- var currentNode = _mapNodes.FirstOrDefault(n => n.NodeId == currentNodeId);
- if (currentNode == null)
- return new List();
-
- return currentNode.ConnectedNodes.ToList();
+ return _rfidPathfinder.GetNodeIdByRfid(rfidId);
}
///
- /// 경로 최적화 (선택적 기능)
+ /// NodeId로 RFID 조회
///
- public PathResult OptimizePath(PathResult originalPath)
+ /// NodeId
+ /// RFID 또는 null
+ public string GetRfidByNodeId(string nodeId)
{
- if (originalPath == null || !originalPath.Success)
- return originalPath;
-
- // TODO: 경로 최적화 로직 구현
- // - 불필요한 중간 정지점 제거
- // - 회전 최소화
- // - 경로 단순화
-
- return originalPath;
+ return _rfidPathfinder.GetRfidByNodeId(nodeId);
}
- #endregion
+ ///
+ /// 활성화된 RFID 목록 반환
+ ///
+ /// 활성화된 RFID 목록
+ public List GetActiveRfidList()
+ {
+ return _rfidPathfinder.GetActiveRfidList();
+ }
+
+ ///
+ /// RFID pathfinder의 AGV 현재 방향 설정
+ ///
+ /// 현재 방향
+ public void SetRfidPathfinderCurrentDirection(AgvDirection direction)
+ {
+ _rfidPathfinder.CurrentDirection = direction;
+ }
+
+ ///
+ /// RFID pathfinder의 회전 비용 가중치 설정
+ ///
+ /// 회전 비용 가중치
+ public void SetRfidPathfinderRotationCostWeight(float weight)
+ {
+ _rfidPathfinder.RotationCostWeight = weight;
+ }
}
}
\ No newline at end of file
diff --git a/Cs_HMI/AGVMapEditor/Models/RfidMapping.cs b/Cs_HMI/AGVMapEditor/Models/RfidMapping.cs
index 9f09b5f..8cb949c 100644
--- a/Cs_HMI/AGVMapEditor/Models/RfidMapping.cs
+++ b/Cs_HMI/AGVMapEditor/Models/RfidMapping.cs
@@ -1,4 +1,5 @@
using System;
+using AGVNavigationCore.Models;
namespace AGVMapEditor.Models
{
diff --git a/Cs_HMI/AGVMapEditor/Program.cs b/Cs_HMI/AGVMapEditor/Program.cs
index 4d11b9e..868156a 100644
--- a/Cs_HMI/AGVMapEditor/Program.cs
+++ b/Cs_HMI/AGVMapEditor/Program.cs
@@ -13,14 +13,14 @@ namespace AGVMapEditor
/// 애플리케이션의 기본 진입점입니다.
///
[STAThread]
- static void Main()
+ static void Main(string[] args)
{
// Windows Forms 애플리케이션 초기화
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
- // 메인 폼 실행
- Application.Run(new MainForm());
+ // 메인 폼 실행 (명령줄 인수 전달)
+ Application.Run(new MainForm(args));
}
}
}
\ No newline at end of file
diff --git a/Cs_HMI/AGVMapEditor/build.bat b/Cs_HMI/AGVMapEditor/build.bat
new file mode 100644
index 0000000..d18c6e5
--- /dev/null
+++ b/Cs_HMI/AGVMapEditor/build.bat
@@ -0,0 +1,29 @@
+@echo off
+echo Building V2GDecoder VC++ Project...
+
+REM Check if Visual Studio 2022 is installed (Professional or Community)
+set MSBUILD_PRO="C:\Program Files\Microsoft Visual Studio\2022\Professional\MSBuild\Current\Bin\MSBuild.exe"
+set MSBUILD_COM="C:\Program Files\Microsoft Visual Studio\2022\Community\MSBuild\Current\Bin\MSBuild.exe"
+set MSBUILD_BT="F:\(VHD) Program Files\Microsoft Visual Studio\2022\MSBuild\Current\Bin\MSBuild.exe"
+
+if exist %MSBUILD_PRO% (
+ echo "Found Visual Studio 2022 Professional"
+ set MSBUILD=%MSBUILD_PRO%
+) else if exist %MSBUILD_COM% (
+ echo "Found Visual Studio 2022 Community"
+ set MSBUILD=%MSBUILD_COM%
+) else if exist %MSBUILD_BT% (
+ echo "Found Visual Studio 2022 BuildTools"
+ set MSBUILD=%MSBUILD_BT%
+) else (
+ echo "Visual Studio 2022 (Professional or Community) not found!"
+ echo "Please install Visual Studio 2022 or update the MSBuild path."
+ pause
+ exit /b 1
+)
+
+REM Build Debug x64 configuration
+echo Building Debug x64 configuration...
+%MSBUILD% agvmapeditor.csproj
+
+pause
\ No newline at end of file
diff --git a/Cs_HMI/AGVNavigationCore/AGVNavigationCore.csproj b/Cs_HMI/AGVNavigationCore/AGVNavigationCore.csproj
new file mode 100644
index 0000000..6def2d0
--- /dev/null
+++ b/Cs_HMI/AGVNavigationCore/AGVNavigationCore.csproj
@@ -0,0 +1,101 @@
+
+
+
+
+ Debug
+ AnyCPU
+ {C5F7A8B2-8D3E-4A1B-9C6E-7F4D5E2A9B1C}
+ Library
+ Properties
+ AGVNavigationCore
+ AGVNavigationCore
+ v4.8
+ 512
+ true
+
+
+ true
+ full
+ false
+ bin\Debug\
+ DEBUG;TRACE
+ prompt
+ 4
+
+
+ pdbonly
+ true
+ bin\Release\
+ TRACE
+ prompt
+ 4
+
+
+ true
+ bin\x86\Debug\
+ DEBUG;TRACE
+ full
+ x86
+ prompt
+
+
+ bin\x86\Release\
+ TRACE
+ true
+ pdbonly
+ x86
+ prompt
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ..\packages\Newtonsoft.Json.13.0.3\lib\net45\Newtonsoft.Json.dll
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ UserControl
+
+
+ UnifiedAGVCanvas.cs
+
+
+ UnifiedAGVCanvas.cs
+ UserControl
+
+
+ UnifiedAGVCanvas.cs
+ UserControl
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Cs_HMI/AGVNavigationCore/Controls/UnifiedAGVCanvas.Designer.cs b/Cs_HMI/AGVNavigationCore/Controls/UnifiedAGVCanvas.Designer.cs
new file mode 100644
index 0000000..a035962
--- /dev/null
+++ b/Cs_HMI/AGVNavigationCore/Controls/UnifiedAGVCanvas.Designer.cs
@@ -0,0 +1,41 @@
+namespace AGVNavigationCore.Controls
+{
+ partial class UnifiedAGVCanvas
+ {
+ ///
+ /// 필수 디자이너 변수입니다.
+ ///
+ private System.ComponentModel.IContainer components = null;
+
+
+ #region 구성 요소 디자이너에서 생성한 코드
+
+ ///
+ /// 디자이너 지원에 필요한 메서드입니다.
+ /// 이 메서드의 내용을 코드 편집기로 수정하지 마세요.
+ ///
+ private void InitializeComponent()
+ {
+ this.SuspendLayout();
+ //
+ // UnifiedAGVCanvas
+ //
+ this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 12F);
+ this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
+ this.BackColor = System.Drawing.Color.White;
+ this.Name = "UnifiedAGVCanvas";
+ this.Size = new System.Drawing.Size(800, 600);
+ this.Paint += new System.Windows.Forms.PaintEventHandler(this.UnifiedAGVCanvas_Paint);
+ this.MouseClick += new System.Windows.Forms.MouseEventHandler(this.UnifiedAGVCanvas_MouseClick);
+ this.MouseDoubleClick += new System.Windows.Forms.MouseEventHandler(this.UnifiedAGVCanvas_MouseDoubleClick);
+ this.MouseDown += new System.Windows.Forms.MouseEventHandler(this.UnifiedAGVCanvas_MouseDown);
+ this.MouseMove += new System.Windows.Forms.MouseEventHandler(this.UnifiedAGVCanvas_MouseMove);
+ this.MouseUp += new System.Windows.Forms.MouseEventHandler(this.UnifiedAGVCanvas_MouseUp);
+ this.MouseWheel += new System.Windows.Forms.MouseEventHandler(this.UnifiedAGVCanvas_MouseWheel);
+ this.ResumeLayout(false);
+
+ }
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/Cs_HMI/AGVNavigationCore/Controls/UnifiedAGVCanvas.Events.cs b/Cs_HMI/AGVNavigationCore/Controls/UnifiedAGVCanvas.Events.cs
new file mode 100644
index 0000000..d042028
--- /dev/null
+++ b/Cs_HMI/AGVNavigationCore/Controls/UnifiedAGVCanvas.Events.cs
@@ -0,0 +1,801 @@
+using System;
+using System.Drawing;
+using System.Drawing.Drawing2D;
+using System.Linq;
+using System.Windows.Forms;
+using AGVNavigationCore.Models;
+using AGVNavigationCore.PathFinding;
+
+namespace AGVNavigationCore.Controls
+{
+ public partial class UnifiedAGVCanvas
+ {
+ #region Paint Events
+
+ private void UnifiedAGVCanvas_Paint(object sender, PaintEventArgs e)
+ {
+ var g = e.Graphics;
+ g.SmoothingMode = SmoothingMode.AntiAlias;
+ g.InterpolationMode = InterpolationMode.High;
+
+ // 변환 행렬 설정 (줌 및 팬)
+ var transform = new Matrix();
+ transform.Scale(_zoomFactor, _zoomFactor);
+ transform.Translate(_panOffset.X, _panOffset.Y);
+ g.Transform = transform;
+
+ try
+ {
+ // 그리드 그리기
+ if (_showGrid)
+ {
+ DrawGrid(g);
+ }
+
+ // 노드 연결선 그리기
+ DrawConnections(g);
+
+ // 경로 그리기
+ DrawPaths(g);
+
+ // 노드 그리기
+ DrawNodes(g);
+
+ // AGV 그리기
+ DrawAGVs(g);
+
+ // 임시 연결선 그리기 (편집 모드)
+ if (_canvasMode == CanvasMode.Edit && _isConnectionMode)
+ {
+ DrawTemporaryConnection(g);
+ }
+ }
+ finally
+ {
+ g.Transform = new Matrix(); // 변환 행렬 리셋
+ }
+
+ // UI 정보 그리기 (변환 없이)
+ DrawUIInfo(g);
+ }
+
+ private void DrawGrid(Graphics g)
+ {
+ if (!_showGrid) return;
+
+ var bounds = GetVisibleBounds();
+ var gridSize = (int)(GRID_SIZE * _zoomFactor);
+
+ if (gridSize < 5) return; // 너무 작으면 그리지 않음
+
+ for (int x = bounds.Left; x < bounds.Right; x += GRID_SIZE)
+ {
+ if (x % (GRID_SIZE * 5) == 0)
+ g.DrawLine(new Pen(Color.Gray, 1), x, bounds.Top, x, bounds.Bottom);
+ else
+ g.DrawLine(_gridPen, x, bounds.Top, x, bounds.Bottom);
+ }
+
+ for (int y = bounds.Top; y < bounds.Bottom; y += GRID_SIZE)
+ {
+ if (y % (GRID_SIZE * 5) == 0)
+ g.DrawLine(new Pen(Color.Gray, 1), bounds.Left, y, bounds.Right, y);
+ else
+ g.DrawLine(_gridPen, bounds.Left, y, bounds.Right, y);
+ }
+ }
+
+ private void DrawConnections(Graphics g)
+ {
+ if (_nodes == null) return;
+
+ foreach (var node in _nodes)
+ {
+ if (node.ConnectedNodes == null) continue;
+
+ foreach (var connectedNodeId in node.ConnectedNodes)
+ {
+ var targetNode = _nodes.FirstOrDefault(n => n.NodeId == connectedNodeId);
+ if (targetNode == null) continue;
+
+ DrawConnection(g, node, targetNode);
+ }
+ }
+ }
+
+ private void DrawConnection(Graphics g, MapNode fromNode, MapNode toNode)
+ {
+ var startPoint = fromNode.Position;
+ var endPoint = toNode.Position;
+
+ // 연결선만 그리기 (단순한 도로 연결, 방향성 없음)
+ g.DrawLine(_connectionPen, startPoint, endPoint);
+ }
+
+ private void DrawDirectionArrow(Graphics g, Point point, double angle, AgvDirection direction)
+ {
+ var arrowSize = CONNECTION_ARROW_SIZE;
+ var arrowAngle = Math.PI / 6; // 30도
+
+ var cos = Math.Cos(angle);
+ var sin = Math.Sin(angle);
+
+ var arrowPoint1 = new Point(
+ (int)(point.X - arrowSize * Math.Cos(angle - arrowAngle)),
+ (int)(point.Y - arrowSize * Math.Sin(angle - arrowAngle))
+ );
+
+ var arrowPoint2 = new Point(
+ (int)(point.X - arrowSize * Math.Cos(angle + arrowAngle)),
+ (int)(point.Y - arrowSize * Math.Sin(angle + arrowAngle))
+ );
+
+ var arrowColor = direction == AgvDirection.Forward ? Color.Blue : Color.Red;
+ var arrowPen = new Pen(arrowColor, 2);
+
+ g.DrawLine(arrowPen, point, arrowPoint1);
+ g.DrawLine(arrowPen, point, arrowPoint2);
+
+ arrowPen.Dispose();
+ }
+
+ private void DrawPaths(Graphics g)
+ {
+ // 모든 경로 그리기
+ if (_allPaths != null)
+ {
+ foreach (var path in _allPaths)
+ {
+ DrawPath(g, path, Color.LightBlue);
+ }
+ }
+
+ // 현재 선택된 경로 그리기
+ if (_currentPath != null)
+ {
+ DrawPath(g, _currentPath, Color.Purple);
+ }
+ }
+
+ private void DrawPath(Graphics g, PathResult path, Color color)
+ {
+ if (path?.Path == null || path.Path.Count < 2) return;
+
+ var pathPen = new Pen(color, 4) { DashStyle = DashStyle.Dash };
+
+ for (int i = 0; i < path.Path.Count - 1; i++)
+ {
+ var currentNodeId = path.Path[i];
+ var nextNodeId = path.Path[i + 1];
+
+ var currentNode = _nodes?.FirstOrDefault(n => n.NodeId == currentNodeId);
+ var nextNode = _nodes?.FirstOrDefault(n => n.NodeId == nextNodeId);
+
+ if (currentNode != null && nextNode != null)
+ {
+ // 경로 선 그리기
+ g.DrawLine(pathPen, currentNode.Position, nextNode.Position);
+
+ // 경로 방향 표시 (계산된 경로의 경우에만 방향 화살표 표시)
+ var midPoint = new Point(
+ (currentNode.Position.X + nextNode.Position.X) / 2,
+ (currentNode.Position.Y + nextNode.Position.Y) / 2
+ );
+
+ var angle = Math.Atan2(nextNode.Position.Y - currentNode.Position.Y,
+ nextNode.Position.X - currentNode.Position.X);
+ DrawDirectionArrow(g, midPoint, angle, AgvDirection.Forward);
+ }
+ }
+
+ pathPen.Dispose();
+ }
+
+ private void DrawNodes(Graphics g)
+ {
+ if (_nodes == null) return;
+
+ foreach (var node in _nodes)
+ {
+ DrawNode(g, node);
+ }
+ }
+
+ private void DrawNode(Graphics g, MapNode node)
+ {
+ switch (node.Type)
+ {
+ case NodeType.Label:
+ DrawLabelNode(g, node);
+ break;
+ case NodeType.Image:
+ DrawImageNode(g, node);
+ break;
+ default:
+ DrawCircularNode(g, node);
+ break;
+ }
+ }
+
+ private void DrawCircularNode(Graphics g, MapNode node)
+ {
+ var brush = GetNodeBrush(node);
+
+ switch (node.Type)
+ {
+ case NodeType.Docking:
+ DrawPentagonNode(g, node, brush);
+ break;
+ case NodeType.Charging:
+ DrawTriangleNode(g, node, brush);
+ break;
+ default:
+ DrawCircleNode(g, node, brush);
+ break;
+ }
+ }
+
+ private void DrawCircleNode(Graphics g, MapNode node, Brush brush)
+ {
+ var rect = new Rectangle(
+ node.Position.X - NODE_RADIUS,
+ node.Position.Y - NODE_RADIUS,
+ NODE_SIZE,
+ NODE_SIZE
+ );
+
+ // 노드 그리기
+ g.FillEllipse(brush, rect);
+ g.DrawEllipse(Pens.Black, rect);
+
+ // 선택된 노드 강조
+ if (node == _selectedNode)
+ {
+ g.DrawEllipse(_selectedNodePen, rect);
+ }
+
+ // 호버된 노드 강조
+ if (node == _hoveredNode)
+ {
+ var hoverRect = new Rectangle(rect.X - 2, rect.Y - 2, rect.Width + 4, rect.Height + 4);
+ g.DrawEllipse(new Pen(Color.Orange, 2), hoverRect);
+ }
+
+ DrawNodeLabel(g, node);
+ }
+
+ private void DrawPentagonNode(Graphics g, MapNode node, Brush brush)
+ {
+ var radius = NODE_RADIUS;
+ var center = node.Position;
+
+ // 5각형 꼭짓점 계산 (위쪽부터 시계방향)
+ var points = new Point[5];
+ for (int i = 0; i < 5; i++)
+ {
+ var angle = (Math.PI * 2 * i / 5) - Math.PI / 2; // -90도부터 시작 (위쪽)
+ points[i] = new Point(
+ (int)(center.X + radius * Math.Cos(angle)),
+ (int)(center.Y + radius * Math.Sin(angle))
+ );
+ }
+
+ // 5각형 그리기
+ g.FillPolygon(brush, points);
+ g.DrawPolygon(Pens.Black, points);
+
+ // 선택된 노드 강조
+ if (node == _selectedNode)
+ {
+ g.DrawPolygon(_selectedNodePen, points);
+ }
+
+ // 호버된 노드 강조
+ if (node == _hoveredNode)
+ {
+ // 확장된 5각형 계산
+ var hoverPoints = new Point[5];
+ for (int i = 0; i < 5; i++)
+ {
+ var angle = (Math.PI * 2 * i / 5) - Math.PI / 2;
+ hoverPoints[i] = new Point(
+ (int)(center.X + (radius + 3) * Math.Cos(angle)),
+ (int)(center.Y + (radius + 3) * Math.Sin(angle))
+ );
+ }
+ g.DrawPolygon(new Pen(Color.Orange, 2), hoverPoints);
+ }
+
+ DrawNodeLabel(g, node);
+ }
+
+ private void DrawTriangleNode(Graphics g, MapNode node, Brush brush)
+ {
+ var radius = NODE_RADIUS;
+ var center = node.Position;
+
+ // 삼각형 꼭짓점 계산 (위쪽 꼭짓점부터 시계방향)
+ var points = new Point[3];
+ for (int i = 0; i < 3; i++)
+ {
+ var angle = (Math.PI * 2 * i / 3) - Math.PI / 2; // -90도부터 시작 (위쪽)
+ points[i] = new Point(
+ (int)(center.X + radius * Math.Cos(angle)),
+ (int)(center.Y + radius * Math.Sin(angle))
+ );
+ }
+
+ // 삼각형 그리기
+ g.FillPolygon(brush, points);
+ g.DrawPolygon(Pens.Black, points);
+
+ // 선택된 노드 강조
+ if (node == _selectedNode)
+ {
+ g.DrawPolygon(_selectedNodePen, points);
+ }
+
+ // 호버된 노드 강조
+ if (node == _hoveredNode)
+ {
+ // 확장된 삼각형 계산
+ var hoverPoints = new Point[3];
+ for (int i = 0; i < 3; i++)
+ {
+ var angle = (Math.PI * 2 * i / 3) - Math.PI / 2;
+ hoverPoints[i] = new Point(
+ (int)(center.X + (radius + 3) * Math.Cos(angle)),
+ (int)(center.Y + (radius + 3) * Math.Sin(angle))
+ );
+ }
+ g.DrawPolygon(new Pen(Color.Orange, 2), hoverPoints);
+ }
+
+ DrawNodeLabel(g, node);
+ }
+
+ private void DrawNodeLabel(Graphics g, MapNode node)
+ {
+ string displayText;
+ Color textColor;
+ string descriptionText;
+
+ // 위쪽에 표시할 설명 (노드의 Description 속성)
+ descriptionText = string.IsNullOrEmpty(node.Description) ? "" : node.Description;
+
+ // 아래쪽에 표시할 값 (RFID 우선, 없으면 노드ID)
+ if (node.HasRfid())
+ {
+ // RFID가 있는 경우: 순수 RFID 값만 표시 (진한 색상)
+ displayText = node.RfidId;
+ textColor = Color.Black;
+ }
+ else
+ {
+ // RFID가 없는 경우: 노드 ID 표시 (연한 색상)
+ displayText = node.NodeId;
+ textColor = Color.Gray;
+ }
+
+ var font = new Font("Arial", 8, FontStyle.Bold);
+ var descFont = new Font("Arial", 6, FontStyle.Regular);
+
+ // 메인 텍스트 크기 측정
+ var textSize = g.MeasureString(displayText, font);
+ var descSize = g.MeasureString(descriptionText, descFont);
+
+ // 설명 텍스트 위치 (노드 위쪽)
+ var descPoint = new Point(
+ (int)(node.Position.X - descSize.Width / 2),
+ (int)(node.Position.Y - NODE_RADIUS - descSize.Height - 2)
+ );
+
+ // 메인 텍스트 위치 (노드 아래쪽)
+ var textPoint = new Point(
+ (int)(node.Position.X - textSize.Width / 2),
+ (int)(node.Position.Y + NODE_RADIUS + 2)
+ );
+
+ // 설명 텍스트 그리기 (설명이 있는 경우에만)
+ if (!string.IsNullOrEmpty(descriptionText))
+ {
+ using (var descBrush = new SolidBrush(Color.FromArgb(120, Color.Black)))
+ {
+ g.DrawString(descriptionText, descFont, descBrush, descPoint);
+ }
+ }
+
+ // 메인 텍스트 그리기
+ using (var textBrush = new SolidBrush(textColor))
+ {
+ g.DrawString(displayText, font, textBrush, textPoint);
+ }
+
+ font.Dispose();
+ descFont.Dispose();
+ }
+
+ private void DrawLabelNode(Graphics g, MapNode node)
+ {
+ var text = string.IsNullOrEmpty(node.LabelText) ? node.NodeId : node.LabelText;
+
+ // 폰트 설정
+ var font = new Font(node.FontFamily, node.FontSize, node.FontStyle);
+ var textBrush = new SolidBrush(node.ForeColor);
+
+ // 텍스트 크기 측정
+ var textSize = g.MeasureString(text, font);
+ var textPoint = new Point(
+ (int)(node.Position.X - textSize.Width / 2),
+ (int)(node.Position.Y - textSize.Height / 2)
+ );
+
+ // 배경 그리기 (설정된 경우)
+ if (node.ShowBackground)
+ {
+ var backgroundBrush = new SolidBrush(node.BackColor);
+ var backgroundRect = new Rectangle(
+ textPoint.X - 2,
+ textPoint.Y - 2,
+ (int)textSize.Width + 4,
+ (int)textSize.Height + 4
+ );
+ g.FillRectangle(backgroundBrush, backgroundRect);
+ g.DrawRectangle(Pens.Black, backgroundRect);
+ backgroundBrush.Dispose();
+ }
+
+ // 텍스트 그리기
+ g.DrawString(text, font, textBrush, textPoint);
+
+ // 선택된 노드 강조
+ if (node == _selectedNode)
+ {
+ var selectionRect = new Rectangle(
+ textPoint.X - 4,
+ textPoint.Y - 4,
+ (int)textSize.Width + 8,
+ (int)textSize.Height + 8
+ );
+ g.DrawRectangle(_selectedNodePen, selectionRect);
+ }
+
+ // 호버된 노드 강조
+ if (node == _hoveredNode)
+ {
+ var hoverRect = new Rectangle(
+ textPoint.X - 6,
+ textPoint.Y - 6,
+ (int)textSize.Width + 12,
+ (int)textSize.Height + 12
+ );
+ g.DrawRectangle(new Pen(Color.Orange, 2), hoverRect);
+ }
+
+ font.Dispose();
+ textBrush.Dispose();
+ }
+
+ private void DrawImageNode(Graphics g, MapNode node)
+ {
+ // 이미지 로드 (필요시)
+ if (node.LoadedImage == null && !string.IsNullOrEmpty(node.ImagePath))
+ {
+ node.LoadImage();
+ }
+
+ if (node.LoadedImage != null)
+ {
+ // 실제 표시 크기 계산
+ var displaySize = node.GetDisplaySize();
+ if (displaySize.IsEmpty)
+ displaySize = new Size(50, 50); // 기본 크기
+
+ var imageRect = new Rectangle(
+ node.Position.X - displaySize.Width / 2,
+ node.Position.Y - displaySize.Height / 2,
+ displaySize.Width,
+ displaySize.Height
+ );
+
+ // 회전이 있는 경우
+ if (node.Rotation != 0)
+ {
+ var oldTransform = g.Transform;
+ g.TranslateTransform(node.Position.X, node.Position.Y);
+ g.RotateTransform(node.Rotation);
+ g.TranslateTransform(-node.Position.X, -node.Position.Y);
+
+ // 투명도 적용하여 이미지 그리기
+ if (node.Opacity < 1.0f)
+ {
+ var imageAttributes = new System.Drawing.Imaging.ImageAttributes();
+ var colorMatrix = new System.Drawing.Imaging.ColorMatrix();
+ colorMatrix.Matrix33 = node.Opacity;
+ imageAttributes.SetColorMatrix(colorMatrix, System.Drawing.Imaging.ColorMatrixFlag.Default,
+ System.Drawing.Imaging.ColorAdjustType.Bitmap);
+ g.DrawImage(node.LoadedImage, imageRect, 0, 0, node.LoadedImage.Width, node.LoadedImage.Height,
+ GraphicsUnit.Pixel, imageAttributes);
+ imageAttributes.Dispose();
+ }
+ else
+ {
+ g.DrawImage(node.LoadedImage, imageRect);
+ }
+
+ g.Transform = oldTransform;
+ }
+ else
+ {
+ // 투명도 적용하여 이미지 그리기
+ if (node.Opacity < 1.0f)
+ {
+ var imageAttributes = new System.Drawing.Imaging.ImageAttributes();
+ var colorMatrix = new System.Drawing.Imaging.ColorMatrix();
+ colorMatrix.Matrix33 = node.Opacity;
+ imageAttributes.SetColorMatrix(colorMatrix, System.Drawing.Imaging.ColorMatrixFlag.Default,
+ System.Drawing.Imaging.ColorAdjustType.Bitmap);
+ g.DrawImage(node.LoadedImage, imageRect, 0, 0, node.LoadedImage.Width, node.LoadedImage.Height,
+ GraphicsUnit.Pixel, imageAttributes);
+ imageAttributes.Dispose();
+ }
+ else
+ {
+ g.DrawImage(node.LoadedImage, imageRect);
+ }
+ }
+
+ // 선택된 노드 강조
+ if (node == _selectedNode)
+ {
+ g.DrawRectangle(_selectedNodePen, imageRect);
+ }
+
+ // 호버된 노드 강조
+ if (node == _hoveredNode)
+ {
+ var hoverRect = new Rectangle(imageRect.X - 2, imageRect.Y - 2, imageRect.Width + 4, imageRect.Height + 4);
+ g.DrawRectangle(new Pen(Color.Orange, 2), hoverRect);
+ }
+
+ }
+ else
+ {
+ // 이미지가 없는 경우 기본 사각형으로 표시
+ var rect = new Rectangle(
+ node.Position.X - 25,
+ node.Position.Y - 25,
+ 50,
+ 50
+ );
+
+ g.FillRectangle(Brushes.LightGray, rect);
+ g.DrawRectangle(Pens.Black, rect);
+
+ // "이미지 없음" 텍스트
+ var font = new Font("Arial", 8);
+ var text = "No Image";
+ var textSize = g.MeasureString(text, font);
+ var textPoint = new Point(
+ (int)(node.Position.X - textSize.Width / 2),
+ (int)(node.Position.Y - textSize.Height / 2)
+ );
+ g.DrawString(text, font, Brushes.Black, textPoint);
+ font.Dispose();
+
+ // 선택된 노드 강조
+ if (node == _selectedNode)
+ {
+ g.DrawRectangle(_selectedNodePen, rect);
+ }
+
+ // 호버된 노드 강조
+ if (node == _hoveredNode)
+ {
+ var hoverRect = new Rectangle(rect.X - 2, rect.Y - 2, rect.Width + 4, rect.Height + 4);
+ g.DrawRectangle(new Pen(Color.Orange, 2), hoverRect);
+ }
+ }
+ }
+
+ private Brush GetNodeBrush(MapNode node)
+ {
+ switch (node.Type)
+ {
+ case NodeType.Normal:
+ return _normalNodeBrush;
+ case NodeType.Rotation:
+ return _rotationNodeBrush;
+ case NodeType.Docking:
+ return _dockingNodeBrush;
+ case NodeType.Charging:
+ return _chargingNodeBrush;
+ case NodeType.Label:
+ return new SolidBrush(Color.Purple);
+ case NodeType.Image:
+ return new SolidBrush(Color.Brown);
+ default:
+ return _normalNodeBrush;
+ }
+ }
+
+ private void DrawAGVs(Graphics g)
+ {
+ if (_agvList == null) return;
+
+ foreach (var agv in _agvList)
+ {
+ if (_agvPositions.ContainsKey(agv.AgvId))
+ {
+ DrawAGV(g, agv);
+ }
+ }
+ }
+
+ private void DrawAGV(Graphics g, IAGV agv)
+ {
+ if (!_agvPositions.ContainsKey(agv.AgvId)) return;
+
+ var position = _agvPositions[agv.AgvId];
+ var direction = _agvDirections.ContainsKey(agv.AgvId) ? _agvDirections[agv.AgvId] : AgvDirection.Forward;
+ var state = _agvStates.ContainsKey(agv.AgvId) ? _agvStates[agv.AgvId] : AGVState.Idle;
+
+ // AGV 색상 결정
+ var brush = GetAGVBrush(state);
+
+ // AGV 사각형 그리기
+ var rect = new Rectangle(
+ position.X - AGV_SIZE / 2,
+ position.Y - AGV_SIZE / 2,
+ AGV_SIZE,
+ AGV_SIZE
+ );
+
+ g.FillRectangle(brush, rect);
+ g.DrawRectangle(_agvPen, rect);
+
+ // 방향 표시 (화살표)
+ DrawAGVDirection(g, position, direction);
+
+ // AGV ID 표시
+ var font = new Font("Arial", 10, FontStyle.Bold);
+ var textSize = g.MeasureString(agv.AgvId, font);
+ var textPoint = new Point(
+ (int)(position.X - textSize.Width / 2),
+ (int)(position.Y - AGV_SIZE / 2 - textSize.Height - 2)
+ );
+
+ g.DrawString(agv.AgvId, font, Brushes.Black, textPoint);
+
+ // 배터리 레벨 표시
+ var batteryText = $"{agv.BatteryLevel:F0}%";
+ var batterySize = g.MeasureString(batteryText, font);
+ var batteryPoint = new Point(
+ (int)(position.X - batterySize.Width / 2),
+ (int)(position.Y + AGV_SIZE / 2 + 2)
+ );
+
+ g.DrawString(batteryText, font, Brushes.Black, batteryPoint);
+ font.Dispose();
+ }
+
+ private Brush GetAGVBrush(AGVState state)
+ {
+ switch (state)
+ {
+ case AGVState.Idle:
+ return Brushes.LightGray;
+ case AGVState.Moving:
+ return Brushes.LightGreen;
+ case AGVState.Rotating:
+ return Brushes.Yellow;
+ case AGVState.Docking:
+ return Brushes.Orange;
+ case AGVState.Charging:
+ return Brushes.Blue;
+ case AGVState.Error:
+ return Brushes.Red;
+ default:
+ return Brushes.LightGray;
+ }
+ }
+
+ private void DrawAGVDirection(Graphics g, Point position, AgvDirection direction)
+ {
+ var arrowSize = 10;
+ Point[] arrowPoints = null;
+
+ switch (direction)
+ {
+ case AgvDirection.Forward:
+ arrowPoints = new Point[]
+ {
+ new Point(position.X + arrowSize, position.Y),
+ new Point(position.X - arrowSize/2, position.Y - arrowSize/2),
+ new Point(position.X - arrowSize/2, position.Y + arrowSize/2)
+ };
+ break;
+ case AgvDirection.Backward:
+ arrowPoints = new Point[]
+ {
+ new Point(position.X - arrowSize, position.Y),
+ new Point(position.X + arrowSize/2, position.Y - arrowSize/2),
+ new Point(position.X + arrowSize/2, position.Y + arrowSize/2)
+ };
+ break;
+ }
+
+ if (arrowPoints != null)
+ {
+ g.FillPolygon(Brushes.White, arrowPoints);
+ g.DrawPolygon(Pens.Black, arrowPoints);
+ }
+ }
+
+ private void DrawTemporaryConnection(Graphics g)
+ {
+ if (_connectionStartNode != null && _connectionEndPoint != Point.Empty)
+ {
+ g.DrawLine(_tempConnectionPen, _connectionStartNode.Position, _connectionEndPoint);
+ }
+ }
+
+ private void DrawUIInfo(Graphics g)
+ {
+ // 회사 로고
+ if (_companyLogo != null)
+ {
+ var logoRect = new Rectangle(10, 10, 100, 50);
+ g.DrawImage(_companyLogo, logoRect);
+ }
+
+ // 측정 정보
+ if (!string.IsNullOrEmpty(_measurementInfo))
+ {
+ var font = new Font("Arial", 9);
+ var textBrush = new SolidBrush(Color.Black);
+ var backgroundBrush = new SolidBrush(Color.FromArgb(200, Color.White));
+
+ var textSize = g.MeasureString(_measurementInfo, font);
+ var textRect = new Rectangle(
+ Width - (int)textSize.Width - 20,
+ Height - (int)textSize.Height - 20,
+ (int)textSize.Width + 10,
+ (int)textSize.Height + 10
+ );
+
+ g.FillRectangle(backgroundBrush, textRect);
+ g.DrawRectangle(Pens.Gray, textRect);
+ g.DrawString(_measurementInfo, font, textBrush, textRect.X + 5, textRect.Y + 5);
+
+ font.Dispose();
+ textBrush.Dispose();
+ backgroundBrush.Dispose();
+ }
+
+ // 줌 정보
+ var zoomText = $"Zoom: {_zoomFactor:P0}";
+ var zoomFont = new Font("Arial", 10, FontStyle.Bold);
+ var zoomSize = g.MeasureString(zoomText, zoomFont);
+ var zoomPoint = new Point(10, Height - (int)zoomSize.Height - 10);
+
+ g.FillRectangle(new SolidBrush(Color.FromArgb(200, Color.White)),
+ zoomPoint.X - 5, zoomPoint.Y - 5,
+ zoomSize.Width + 10, zoomSize.Height + 10);
+ g.DrawString(zoomText, zoomFont, Brushes.Black, zoomPoint);
+ zoomFont.Dispose();
+ }
+
+ private Rectangle GetVisibleBounds()
+ {
+ var left = (int)(-_panOffset.X / _zoomFactor);
+ var top = (int)(-_panOffset.Y / _zoomFactor);
+ var right = (int)((Width - _panOffset.X) / _zoomFactor);
+ var bottom = (int)((Height - _panOffset.Y) / _zoomFactor);
+
+ return new Rectangle(left, top, right - left, bottom - top);
+ }
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/Cs_HMI/AGVNavigationCore/Controls/UnifiedAGVCanvas.Mouse.cs b/Cs_HMI/AGVNavigationCore/Controls/UnifiedAGVCanvas.Mouse.cs
new file mode 100644
index 0000000..2b32c3c
--- /dev/null
+++ b/Cs_HMI/AGVNavigationCore/Controls/UnifiedAGVCanvas.Mouse.cs
@@ -0,0 +1,605 @@
+using System;
+using System.Drawing;
+using System.Linq;
+using System.Windows.Forms;
+using AGVNavigationCore.Models;
+
+namespace AGVNavigationCore.Controls
+{
+ public partial class UnifiedAGVCanvas
+ {
+ #region Mouse Events
+
+ private void UnifiedAGVCanvas_MouseClick(object sender, MouseEventArgs e)
+ {
+ Focus(); // 포커스 설정
+
+ if (_canvasMode == CanvasMode.ViewOnly) return;
+
+ var worldPoint = ScreenToWorld(e.Location);
+ var hitNode = GetNodeAt(worldPoint);
+
+ switch (_editMode)
+ {
+ case EditMode.Select:
+ HandleSelectClick(hitNode);
+ break;
+
+ case EditMode.AddNode:
+ HandleAddNodeClick(worldPoint);
+ break;
+
+ case EditMode.Connect:
+ HandleConnectClick(hitNode);
+ break;
+
+ case EditMode.Delete:
+ HandleDeleteClick(hitNode);
+ break;
+ }
+ }
+
+ private void UnifiedAGVCanvas_MouseDoubleClick(object sender, MouseEventArgs e)
+ {
+ if (_canvasMode == CanvasMode.ViewOnly) return;
+
+ var worldPoint = ScreenToWorld(e.Location);
+ var hitNode = GetNodeAt(worldPoint);
+
+ if (hitNode != null)
+ {
+ // 노드 속성 편집 (이벤트 발생)
+ NodeSelected?.Invoke(this, hitNode);
+ }
+ }
+
+ private void UnifiedAGVCanvas_MouseDown(object sender, MouseEventArgs e)
+ {
+ var worldPoint = ScreenToWorld(e.Location);
+
+ if (e.Button == MouseButtons.Left)
+ {
+ if (_canvasMode == CanvasMode.Edit && _editMode == EditMode.Move)
+ {
+ var hitNode = GetNodeAt(worldPoint);
+ if (hitNode != null)
+ {
+ _isDragging = true;
+ _selectedNode = hitNode;
+ _dragOffset = new Point(
+ worldPoint.X - hitNode.Position.X,
+ worldPoint.Y - hitNode.Position.Y
+ );
+ Cursor = Cursors.SizeAll;
+ Invalidate();
+ return;
+ }
+ }
+
+ // 팬 시작 (우클릭 또는 중간버튼)
+ _isPanning = true;
+ _lastMousePosition = e.Location;
+ Cursor = Cursors.SizeAll;
+ }
+ else if (e.Button == MouseButtons.Right)
+ {
+ // 컨텍스트 메뉴 (편집 모드에서만)
+ if (_canvasMode == CanvasMode.Edit)
+ {
+ var hitNode = GetNodeAt(worldPoint);
+ ShowContextMenu(e.Location, hitNode);
+ }
+ }
+ }
+
+ private void UnifiedAGVCanvas_MouseMove(object sender, MouseEventArgs e)
+ {
+ var worldPoint = ScreenToWorld(e.Location);
+
+ // 호버 노드 업데이트
+ var newHoveredNode = GetNodeAt(worldPoint);
+ if (newHoveredNode != _hoveredNode)
+ {
+ _hoveredNode = newHoveredNode;
+ Invalidate();
+ }
+
+ if (_isPanning)
+ {
+ // 팬 처리
+ var deltaX = e.X - _lastMousePosition.X;
+ var deltaY = e.Y - _lastMousePosition.Y;
+
+ _panOffset.X += deltaX;
+ _panOffset.Y += deltaY;
+
+ _lastMousePosition = e.Location;
+ Invalidate();
+ }
+ else if (_isDragging && _canvasMode == CanvasMode.Edit)
+ {
+ // 노드 드래그
+ if (_selectedNode != null)
+ {
+ var newPosition = new Point(
+ worldPoint.X - _dragOffset.X,
+ worldPoint.Y - _dragOffset.Y
+ );
+
+ // 그리드 스냅
+ if (ModifierKeys.HasFlag(Keys.Control))
+ {
+ newPosition.X = (newPosition.X / GRID_SIZE) * GRID_SIZE;
+ newPosition.Y = (newPosition.Y / GRID_SIZE) * GRID_SIZE;
+ }
+
+ _selectedNode.Position = newPosition;
+ NodeMoved?.Invoke(this, _selectedNode);
+ MapChanged?.Invoke(this, EventArgs.Empty);
+ Invalidate();
+ }
+ }
+ else if (_isConnectionMode && _canvasMode == CanvasMode.Edit)
+ {
+ // 임시 연결선 업데이트
+ _connectionEndPoint = worldPoint;
+ Invalidate();
+ }
+
+ // 툴팁 표시 (호버된 노드/AGV 정보)
+ UpdateTooltip(worldPoint);
+ }
+
+ private void UnifiedAGVCanvas_MouseUp(object sender, MouseEventArgs e)
+ {
+ if (e.Button == MouseButtons.Left)
+ {
+ if (_isDragging && _canvasMode == CanvasMode.Edit)
+ {
+ _isDragging = false;
+ Cursor = GetCursorForMode(_editMode);
+ }
+
+ if (_isPanning)
+ {
+ _isPanning = false;
+ Cursor = Cursors.Default;
+ }
+ }
+ }
+
+ private void UnifiedAGVCanvas_MouseWheel(object sender, MouseEventArgs e)
+ {
+ // 줌 처리
+ var mouseWorldPoint = ScreenToWorld(e.Location);
+ var oldZoom = _zoomFactor;
+
+ if (e.Delta > 0)
+ _zoomFactor = Math.Min(_zoomFactor * 1.2f, 5.0f);
+ else
+ _zoomFactor = Math.Max(_zoomFactor / 1.2f, 0.1f);
+
+ // 마우스 위치를 중심으로 줌
+ var zoomRatio = _zoomFactor / oldZoom;
+ _panOffset.X = (int)(e.X - (e.X - _panOffset.X) * zoomRatio);
+ _panOffset.Y = (int)(e.Y - (e.Y - _panOffset.Y) * zoomRatio);
+
+ Invalidate();
+ }
+
+ #endregion
+
+ #region Mouse Helper Methods
+
+ private Point ScreenToWorld(Point screenPoint)
+ {
+ return new Point(
+ (int)((screenPoint.X - _panOffset.X) / _zoomFactor),
+ (int)((screenPoint.Y - _panOffset.Y) / _zoomFactor)
+ );
+ }
+
+ private Point WorldToScreen(Point worldPoint)
+ {
+ return new Point(
+ (int)(worldPoint.X * _zoomFactor + _panOffset.X),
+ (int)(worldPoint.Y * _zoomFactor + _panOffset.Y)
+ );
+ }
+
+ private MapNode GetNodeAt(Point worldPoint)
+ {
+ if (_nodes == null) return null;
+
+ // 역순으로 검사하여 위에 그려진 노드부터 확인
+ for (int i = _nodes.Count - 1; i >= 0; i--)
+ {
+ var node = _nodes[i];
+ if (IsPointInNode(worldPoint, node))
+ return node;
+ }
+
+ return null;
+ }
+
+ private bool IsPointInNode(Point point, MapNode node)
+ {
+ switch (node.Type)
+ {
+ case NodeType.Label:
+ return IsPointInLabelNode(point, node);
+ case NodeType.Image:
+ return IsPointInImageNode(point, node);
+ default:
+ return IsPointInCircularNode(point, node);
+ }
+ }
+
+ private bool IsPointInCircularNode(Point point, MapNode node)
+ {
+ switch (node.Type)
+ {
+ case NodeType.Docking:
+ return IsPointInPentagon(point, node);
+ case NodeType.Charging:
+ return IsPointInTriangle(point, node);
+ default:
+ return IsPointInCircle(point, node);
+ }
+ }
+
+ private bool IsPointInCircle(Point point, MapNode node)
+ {
+ var hitRadius = Math.Max(NODE_RADIUS, 10 / _zoomFactor);
+ var distance = Math.Sqrt(
+ Math.Pow(node.Position.X - point.X, 2) +
+ Math.Pow(node.Position.Y - point.Y, 2)
+ );
+ return distance <= hitRadius;
+ }
+
+ private bool IsPointInPentagon(Point point, MapNode node)
+ {
+ var radius = NODE_RADIUS;
+ var center = node.Position;
+
+ // 5각형 꼭짓점 계산
+ var points = new Point[5];
+ for (int i = 0; i < 5; i++)
+ {
+ var angle = (Math.PI * 2 * i / 5) - Math.PI / 2;
+ points[i] = new Point(
+ (int)(center.X + radius * Math.Cos(angle)),
+ (int)(center.Y + radius * Math.Sin(angle))
+ );
+ }
+
+ return IsPointInPolygon(point, points);
+ }
+
+ private bool IsPointInTriangle(Point point, MapNode node)
+ {
+ var radius = NODE_RADIUS;
+ var center = node.Position;
+
+ // 삼각형 꼭짓점 계산
+ var points = new Point[3];
+ for (int i = 0; i < 3; i++)
+ {
+ var angle = (Math.PI * 2 * i / 3) - Math.PI / 2;
+ points[i] = new Point(
+ (int)(center.X + radius * Math.Cos(angle)),
+ (int)(center.Y + radius * Math.Sin(angle))
+ );
+ }
+
+ return IsPointInPolygon(point, points);
+ }
+
+ private bool IsPointInPolygon(Point point, Point[] polygon)
+ {
+ // Ray casting 알고리즘 사용
+ bool inside = false;
+ int j = polygon.Length - 1;
+
+ for (int i = 0; i < polygon.Length; i++)
+ {
+ var xi = polygon[i].X;
+ var yi = polygon[i].Y;
+ var xj = polygon[j].X;
+ var yj = polygon[j].Y;
+
+ if (((yi > point.Y) != (yj > point.Y)) &&
+ (point.X < (xj - xi) * (point.Y - yi) / (yj - yi) + xi))
+ {
+ inside = !inside;
+ }
+ j = i;
+ }
+
+ return inside;
+ }
+
+ private bool IsPointInLabelNode(Point point, MapNode node)
+ {
+ var text = string.IsNullOrEmpty(node.LabelText) ? node.NodeId : node.LabelText;
+
+ // 임시 Graphics로 텍스트 크기 측정
+ using (var tempBitmap = new Bitmap(1, 1))
+ using (var tempGraphics = Graphics.FromImage(tempBitmap))
+ {
+ var font = new Font(node.FontFamily, node.FontSize, node.FontStyle);
+ var textSize = tempGraphics.MeasureString(text, font);
+
+ var textRect = new Rectangle(
+ (int)(node.Position.X - textSize.Width / 2),
+ (int)(node.Position.Y - textSize.Height / 2),
+ (int)textSize.Width,
+ (int)textSize.Height
+ );
+
+ font.Dispose();
+ return textRect.Contains(point);
+ }
+ }
+
+ private bool IsPointInImageNode(Point point, MapNode node)
+ {
+ var displaySize = node.GetDisplaySize();
+ if (displaySize.IsEmpty)
+ displaySize = new Size(50, 50); // 기본 크기
+
+ var imageRect = new Rectangle(
+ node.Position.X - displaySize.Width / 2,
+ node.Position.Y - displaySize.Height / 2,
+ displaySize.Width,
+ displaySize.Height
+ );
+
+ return imageRect.Contains(point);
+ }
+
+ private IAGV GetAGVAt(Point worldPoint)
+ {
+ if (_agvList == null) return null;
+
+ var hitRadius = Math.Max(AGV_SIZE / 2, 15 / _zoomFactor);
+
+ return _agvList.FirstOrDefault(agv =>
+ {
+ if (!_agvPositions.ContainsKey(agv.AgvId)) return false;
+
+ var agvPos = _agvPositions[agv.AgvId];
+ var distance = Math.Sqrt(
+ Math.Pow(agvPos.X - worldPoint.X, 2) +
+ Math.Pow(agvPos.Y - worldPoint.Y, 2)
+ );
+ return distance <= hitRadius;
+ });
+ }
+
+ private void HandleSelectClick(MapNode hitNode)
+ {
+ if (hitNode != _selectedNode)
+ {
+ _selectedNode = hitNode;
+ NodeSelected?.Invoke(this, hitNode);
+ Invalidate();
+ }
+ }
+
+ private void HandleAddNodeClick(Point worldPoint)
+ {
+ // 그리드 스냅
+ if (ModifierKeys.HasFlag(Keys.Control))
+ {
+ worldPoint.X = (worldPoint.X / GRID_SIZE) * GRID_SIZE;
+ worldPoint.Y = (worldPoint.Y / GRID_SIZE) * GRID_SIZE;
+ }
+
+ var newNode = new MapNode
+ {
+ NodeId = $"N{_nodeCounter:D3}",
+ Position = worldPoint,
+ Type = NodeType.Normal
+ };
+
+ _nodeCounter++;
+ _nodes.Add(newNode);
+
+ NodeAdded?.Invoke(this, newNode);
+ MapChanged?.Invoke(this, EventArgs.Empty);
+ Invalidate();
+ }
+
+ private void HandleConnectClick(MapNode hitNode)
+ {
+ if (hitNode == null) return;
+
+ if (!_isConnectionMode)
+ {
+ // 연결 시작
+ _isConnectionMode = true;
+ _connectionStartNode = hitNode;
+ _selectedNode = hitNode;
+ }
+ else
+ {
+ // 연결 완료
+ if (_connectionStartNode != null && _connectionStartNode != hitNode)
+ {
+ CreateConnection(_connectionStartNode, hitNode);
+ }
+ CancelConnection();
+ }
+
+ Invalidate();
+ }
+
+ private void HandleDeleteClick(MapNode hitNode)
+ {
+ if (hitNode == null) return;
+
+ // 연결된 모든 연결선도 제거
+ foreach (var node in _nodes)
+ {
+ node.RemoveConnection(hitNode.NodeId);
+ }
+
+ _nodes.Remove(hitNode);
+
+ if (_selectedNode == hitNode)
+ _selectedNode = null;
+
+ NodeDeleted?.Invoke(this, hitNode);
+ MapChanged?.Invoke(this, EventArgs.Empty);
+ Invalidate();
+ }
+
+ private void CreateConnection(MapNode fromNode, MapNode toNode)
+ {
+ // 중복 연결 체크
+ if (fromNode.ConnectedNodes.Contains(toNode.NodeId))
+ return;
+
+ fromNode.AddConnection(toNode.NodeId);
+ MapChanged?.Invoke(this, EventArgs.Empty);
+ }
+
+ 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 ShowContextMenu(Point location, MapNode hitNode)
+ {
+ _contextMenu.Items.Clear();
+
+ if (hitNode != null)
+ {
+ _contextMenu.Items.Add("노드 속성...", null, (s, e) => NodeSelected?.Invoke(this, hitNode));
+ _contextMenu.Items.Add("노드 삭제", null, (s, e) => HandleDeleteClick(hitNode));
+ _contextMenu.Items.Add("-");
+ }
+
+ _contextMenu.Items.Add("노드 추가", null, (s, e) =>
+ {
+ var worldPoint = ScreenToWorld(location);
+ HandleAddNodeClick(worldPoint);
+ });
+
+ _contextMenu.Items.Add("전체 맞춤", null, (s, e) => FitToNodes());
+ _contextMenu.Items.Add("줌 리셋", null, (s, e) => ResetZoom());
+
+ _contextMenu.Show(this, location);
+ }
+
+ private void UpdateTooltip(Point worldPoint)
+ {
+ string tooltipText = "";
+
+ // 노드 툴팁
+ var hitNode = GetNodeAt(worldPoint);
+ if (hitNode != null)
+ {
+ tooltipText = $"노드: {hitNode.NodeId}\n타입: {hitNode.Type}\n위치: ({hitNode.Position.X}, {hitNode.Position.Y})";
+ }
+ else
+ {
+ // AGV 툴팁
+ var hitAGV = GetAGVAt(worldPoint);
+ if (hitAGV != null)
+ {
+ var state = _agvStates.ContainsKey(hitAGV.AgvId) ? _agvStates[hitAGV.AgvId] : AGVState.Idle;
+ tooltipText = $"AGV: {hitAGV.AgvId}\n상태: {state}\n배터리: {hitAGV.BatteryLevel:F1}%\n위치: ({hitAGV.CurrentPosition.X}, {hitAGV.CurrentPosition.Y})";
+ }
+ }
+
+ // 툴팁 업데이트 (기존 ToolTip 컨트롤 사용)
+ if (!string.IsNullOrEmpty(tooltipText))
+ {
+ // ToolTip 설정 (필요시 추가 구현)
+ }
+ }
+
+ #endregion
+
+ #region View Control Methods
+
+ ///
+ /// 모든 노드가 보이도록 뷰 조정
+ ///
+ public void FitToNodes()
+ {
+ if (_nodes == null || _nodes.Count == 0) return;
+
+ var minX = _nodes.Min(n => n.Position.X);
+ var maxX = _nodes.Max(n => n.Position.X);
+ var minY = _nodes.Min(n => n.Position.Y);
+ var maxY = _nodes.Max(n => n.Position.Y);
+
+ var margin = 50;
+ var contentWidth = maxX - minX + margin * 2;
+ var contentHeight = maxY - minY + margin * 2;
+
+ var zoomX = (float)Width / contentWidth;
+ var zoomY = (float)Height / contentHeight;
+ _zoomFactor = Math.Min(zoomX, zoomY) * 0.9f;
+
+ var centerX = (minX + maxX) / 2;
+ var centerY = (minY + maxY) / 2;
+
+ _panOffset.X = (int)(Width / 2 - centerX * _zoomFactor);
+ _panOffset.Y = (int)(Height / 2 - centerY * _zoomFactor);
+
+ Invalidate();
+ }
+
+ ///
+ /// 줌 리셋
+ ///
+ public void ResetZoom()
+ {
+ _zoomFactor = 1.0f;
+ _panOffset = Point.Empty;
+ Invalidate();
+ }
+
+ ///
+ /// 특정 위치로 이동
+ ///
+ public void PanTo(Point worldPoint)
+ {
+ _panOffset.X = (int)(Width / 2 - worldPoint.X * _zoomFactor);
+ _panOffset.Y = (int)(Height / 2 - worldPoint.Y * _zoomFactor);
+ Invalidate();
+ }
+
+ ///
+ /// 특정 노드로 이동
+ ///
+ public void PanToNode(string nodeId)
+ {
+ var node = _nodes?.FirstOrDefault(n => n.NodeId == nodeId);
+ if (node != null)
+ {
+ PanTo(node.Position);
+ }
+ }
+
+ ///
+ /// 특정 AGV로 이동
+ ///
+ public void PanToAGV(string agvId)
+ {
+ if (_agvPositions.ContainsKey(agvId))
+ {
+ PanTo(_agvPositions[agvId]);
+ }
+ }
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/Cs_HMI/AGVNavigationCore/Controls/UnifiedAGVCanvas.cs b/Cs_HMI/AGVNavigationCore/Controls/UnifiedAGVCanvas.cs
new file mode 100644
index 0000000..91e0788
--- /dev/null
+++ b/Cs_HMI/AGVNavigationCore/Controls/UnifiedAGVCanvas.cs
@@ -0,0 +1,536 @@
+using System;
+using System.Collections.Generic;
+using System.Drawing;
+using System.Drawing.Drawing2D;
+using System.Linq;
+using System.Windows.Forms;
+using AGVNavigationCore.Models;
+using AGVNavigationCore.PathFinding;
+
+namespace AGVNavigationCore.Controls
+{
+ ///
+ /// 통합 AGV 캔버스 컨트롤
+ /// 맵 편집, AGV 시뮬레이션, 실시간 모니터링을 모두 지원
+ ///
+ public partial class UnifiedAGVCanvas : UserControl
+ {
+ #region Constants
+
+ private const int NODE_SIZE = 24;
+ private const int NODE_RADIUS = NODE_SIZE / 2;
+ private const int GRID_SIZE = 20;
+ private const float CONNECTION_WIDTH = 2.0f;
+ private const int SNAP_DISTANCE = 10;
+ private const int AGV_SIZE = 30;
+ private const int CONNECTION_ARROW_SIZE = 8;
+
+ #endregion
+
+ #region Enums
+
+ ///
+ /// 캔버스 모드
+ ///
+ public enum CanvasMode
+ {
+ ViewOnly, // 읽기 전용 (시뮬레이터, 모니터링)
+ Edit // 편집 가능 (맵 에디터)
+ }
+
+ ///
+ /// 편집 모드 (CanvasMode.Edit일 때만 적용)
+ ///
+ public enum EditMode
+ {
+ Select, // 선택 모드
+ Move, // 이동 모드
+ AddNode, // 노드 추가 모드
+ Connect, // 연결 모드
+ Delete, // 삭제 모드
+ AddLabel, // 라벨 추가 모드
+ AddImage // 이미지 추가 모드
+ }
+
+ #endregion
+
+ #region Fields
+
+ // 캔버스 모드
+ private CanvasMode _canvasMode = CanvasMode.ViewOnly;
+ private EditMode _editMode = EditMode.Select;
+
+ // 맵 데이터
+ private List _nodes;
+ private MapNode _selectedNode;
+ private MapNode _hoveredNode;
+
+ // AGV 관련
+ private List _agvList;
+ private Dictionary _agvPositions;
+ private Dictionary _agvDirections;
+ private Dictionary _agvStates;
+
+ // 경로 관련
+ private PathResult _currentPath;
+ private List _allPaths;
+
+ // UI 요소들
+ private Image _companyLogo;
+ private string _companyLogoPath = string.Empty;
+ private string _measurementInfo = "스케일: 1:100\n면적: 1000㎡\n최종 수정: " + DateTime.Now.ToString("yyyy-MM-dd");
+
+ // 편집 관련 (EditMode에서만 사용)
+ private bool _isDragging;
+ private Point _dragOffset;
+ private Point _lastMousePosition;
+ private bool _isConnectionMode;
+ private MapNode _connectionStartNode;
+ private Point _connectionEndPoint;
+
+ // 그리드 및 줌 관련
+ private bool _showGrid = true;
+ private float _zoomFactor = 1.0f;
+ private Point _panOffset = Point.Empty;
+ private bool _isPanning;
+
+ // 자동 증가 카운터
+ private int _nodeCounter = 1;
+
+
+ // 브러쉬 및 펜
+ private Brush _normalNodeBrush;
+ private Brush _rotationNodeBrush;
+ private Brush _dockingNodeBrush;
+ private Brush _chargingNodeBrush;
+ private Brush _selectedNodeBrush;
+ private Brush _hoveredNodeBrush;
+ private Brush _gridBrush;
+ private Brush _agvBrush;
+ private Brush _pathBrush;
+
+ private Pen _connectionPen;
+ private Pen _gridPen;
+ private Pen _tempConnectionPen;
+ private Pen _selectedNodePen;
+ private Pen _pathPen;
+ private Pen _agvPen;
+
+ // 컨텍스트 메뉴
+ private ContextMenuStrip _contextMenu;
+
+ #endregion
+
+ #region Events
+
+ // 맵 편집 이벤트
+ public event EventHandler NodeAdded;
+ public event EventHandler NodeSelected;
+ public event EventHandler NodeDeleted;
+ public event EventHandler NodeMoved;
+ public event EventHandler MapChanged;
+
+ // AGV 이벤트
+ public event EventHandler AGVSelected;
+ public event EventHandler AGVStateChanged;
+
+ #endregion
+
+ #region Properties
+
+ ///
+ /// 캔버스 모드
+ ///
+ public CanvasMode Mode
+ {
+ get => _canvasMode;
+ set
+ {
+ _canvasMode = value;
+ UpdateModeUI();
+ Invalidate();
+ }
+ }
+
+ ///
+ /// 편집 모드 (CanvasMode.Edit일 때만 적용)
+ ///
+ public EditMode CurrentEditMode
+ {
+ get => _editMode;
+ set
+ {
+ if (_canvasMode != CanvasMode.Edit) return;
+
+ _editMode = value;
+ if (_editMode != EditMode.Connect)
+ {
+ CancelConnection();
+ }
+ Cursor = GetCursorForMode(_editMode);
+ Invalidate();
+ }
+ }
+
+ ///
+ /// 그리드 표시 여부
+ ///
+ public bool ShowGrid
+ {
+ get => _showGrid;
+ set
+ {
+ _showGrid = value;
+ Invalidate();
+ }
+ }
+
+ ///
+ /// 줌 팩터
+ ///
+ public float ZoomFactor
+ {
+ get => _zoomFactor;
+ set
+ {
+ _zoomFactor = Math.Max(0.1f, Math.Min(5.0f, value));
+ Invalidate();
+ }
+ }
+
+ ///
+ /// 선택된 노드
+ ///
+ public MapNode SelectedNode => _selectedNode;
+
+ ///
+ /// 노드 목록
+ ///
+ public List Nodes
+ {
+ get => _nodes ?? new List();
+ set
+ {
+ _nodes = value ?? new List();
+ Invalidate();
+ }
+ }
+
+ ///
+ /// AGV 목록
+ ///
+ public List AGVList
+ {
+ get => _agvList ?? new List();
+ set
+ {
+ _agvList = value ?? new List();
+ UpdateAGVData();
+ Invalidate();
+ }
+ }
+
+ ///
+ /// 현재 표시할 경로
+ ///
+ public PathResult CurrentPath
+ {
+ get => _currentPath;
+ set
+ {
+ _currentPath = value;
+ Invalidate();
+ }
+ }
+
+ ///
+ /// 모든 경로 목록 (다중 AGV 경로 표시용)
+ ///
+ public List AllPaths
+ {
+ get => _allPaths ?? new List();
+ set
+ {
+ _allPaths = value ?? new List();
+ Invalidate();
+ }
+ }
+
+ ///
+ /// 회사 로고 이미지
+ ///
+ public Image CompanyLogo
+ {
+ get => _companyLogo;
+ set
+ {
+ _companyLogo = value;
+ Invalidate();
+ }
+ }
+
+ ///
+ /// 측정 정보 텍스트
+ ///
+ public string MeasurementInfo
+ {
+ get => _measurementInfo;
+ set
+ {
+ _measurementInfo = value;
+ Invalidate();
+ }
+ }
+
+
+ #endregion
+
+ #region Constructor
+
+ public UnifiedAGVCanvas()
+ {
+ InitializeComponent();
+ InitializeCanvas();
+ }
+
+ #endregion
+
+ #region Initialization
+
+ private void InitializeCanvas()
+ {
+ SetStyle(ControlStyles.AllPaintingInWmPaint |
+ ControlStyles.UserPaint |
+ ControlStyles.DoubleBuffer |
+ ControlStyles.ResizeRedraw, true);
+
+ _nodes = new List();
+ _agvList = new List();
+ _agvPositions = new Dictionary();
+ _agvDirections = new Dictionary();
+ _agvStates = new Dictionary();
+ _allPaths = new List();
+
+ InitializeBrushesAndPens();
+ CreateContextMenu();
+ }
+
+ private void InitializeBrushesAndPens()
+ {
+ // 노드 브러쉬
+ _normalNodeBrush = new SolidBrush(Color.LightBlue);
+ _rotationNodeBrush = new SolidBrush(Color.Yellow);
+ _dockingNodeBrush = new SolidBrush(Color.Orange);
+ _chargingNodeBrush = new SolidBrush(Color.Green);
+ _selectedNodeBrush = new SolidBrush(Color.Red);
+ _hoveredNodeBrush = new SolidBrush(Color.LightCyan);
+
+ // AGV 및 경로 브러쉬
+ _agvBrush = new SolidBrush(Color.Red);
+ _pathBrush = new SolidBrush(Color.Purple);
+
+ // 그리드 브러쉬
+ _gridBrush = new SolidBrush(Color.LightGray);
+
+ // 펜
+ _connectionPen = new Pen(Color.DarkBlue, CONNECTION_WIDTH);
+ _connectionPen.EndCap = LineCap.ArrowAnchor;
+
+ _gridPen = new Pen(Color.LightGray, 1);
+ _tempConnectionPen = new Pen(Color.Orange, 2) { DashStyle = DashStyle.Dash };
+ _selectedNodePen = new Pen(Color.Red, 3);
+ _pathPen = new Pen(Color.Purple, 3);
+ _agvPen = new Pen(Color.Red, 3);
+ }
+
+ private void CreateContextMenu()
+ {
+ _contextMenu = new ContextMenuStrip();
+ // 컨텍스트 메뉴는 EditMode에서만 사용
+ }
+
+ private void UpdateModeUI()
+ {
+ // 모드에 따른 UI 업데이트
+ if (_canvasMode == CanvasMode.ViewOnly)
+ {
+ Cursor = Cursors.Default;
+ _contextMenu.Enabled = false;
+ }
+ else
+ {
+ _contextMenu.Enabled = true;
+ Cursor = GetCursorForMode(_editMode);
+ }
+ }
+
+ #endregion
+
+ #region AGV Management
+
+ ///
+ /// AGV 위치 업데이트
+ ///
+ public void UpdateAGVPosition(string agvId, Point position)
+ {
+ if (_agvPositions.ContainsKey(agvId))
+ _agvPositions[agvId] = position;
+ else
+ _agvPositions.Add(agvId, position);
+
+ Invalidate();
+ }
+
+ ///
+ /// AGV 방향 업데이트
+ ///
+ public void UpdateAGVDirection(string agvId, AgvDirection direction)
+ {
+ if (_agvDirections.ContainsKey(agvId))
+ _agvDirections[agvId] = direction;
+ else
+ _agvDirections.Add(agvId, direction);
+
+ Invalidate();
+ }
+
+ ///
+ /// AGV 상태 업데이트
+ ///
+ public void UpdateAGVState(string agvId, AGVState state)
+ {
+ if (_agvStates.ContainsKey(agvId))
+ _agvStates[agvId] = state;
+ else
+ _agvStates.Add(agvId, state);
+
+ Invalidate();
+ }
+
+ ///
+ /// AGV 위치 설정 (시뮬레이터용)
+ ///
+ /// AGV ID
+ /// 새로운 위치
+ public void SetAGVPosition(string agvId, Point position)
+ {
+ UpdateAGVPosition(agvId, position);
+ }
+
+ ///
+ /// AGV 데이터 동기화
+ ///
+ private void UpdateAGVData()
+ {
+ if (_agvList == null) return;
+
+ foreach (var agv in _agvList)
+ {
+ UpdateAGVPosition(agv.AgvId, agv.CurrentPosition);
+ UpdateAGVDirection(agv.AgvId, agv.CurrentDirection);
+ UpdateAGVState(agv.AgvId, agv.CurrentState);
+ }
+ }
+
+ #endregion
+
+ #region Helper Methods
+
+ private Cursor GetCursorForMode(EditMode mode)
+ {
+ if (_canvasMode != CanvasMode.Edit)
+ return Cursors.Default;
+
+ switch (mode)
+ {
+ case EditMode.AddNode:
+ return Cursors.Cross;
+ case EditMode.Move:
+ return Cursors.SizeAll;
+ case EditMode.Connect:
+ return Cursors.Hand;
+ case EditMode.Delete:
+ return Cursors.No;
+ default:
+ return Cursors.Default;
+ }
+ }
+
+ private void CancelConnection()
+ {
+ _isConnectionMode = false;
+ _connectionStartNode = null;
+ _connectionEndPoint = Point.Empty;
+ Invalidate();
+ }
+
+ #endregion
+
+ #region Cleanup
+
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+
+ // 브러쉬 정리
+ _normalNodeBrush?.Dispose();
+ _rotationNodeBrush?.Dispose();
+ _dockingNodeBrush?.Dispose();
+ _chargingNodeBrush?.Dispose();
+ _selectedNodeBrush?.Dispose();
+ _hoveredNodeBrush?.Dispose();
+ _gridBrush?.Dispose();
+ _agvBrush?.Dispose();
+ _pathBrush?.Dispose();
+
+ // 펜 정리
+ _connectionPen?.Dispose();
+ _gridPen?.Dispose();
+ _tempConnectionPen?.Dispose();
+ _selectedNodePen?.Dispose();
+ _pathPen?.Dispose();
+ _agvPen?.Dispose();
+
+ // 컨텍스트 메뉴 정리
+ _contextMenu?.Dispose();
+
+ // 이미지 정리
+ _companyLogo?.Dispose();
+ }
+
+ base.Dispose(disposing);
+ }
+
+ #endregion
+ }
+
+ #region Interfaces
+
+ ///
+ /// AGV 인터페이스 (가상/실제 AGV 통합)
+ ///
+ public interface IAGV
+ {
+ string AgvId { get; }
+ Point CurrentPosition { get; }
+ AgvDirection CurrentDirection { get; }
+ AGVState CurrentState { get; }
+ float BatteryLevel { get; }
+ }
+
+ ///
+ /// AGV 상태 열거형
+ ///
+ public enum AGVState
+ {
+ Idle, // 대기
+ Moving, // 이동 중
+ Rotating, // 회전 중
+ Docking, // 도킹 중
+ Charging, // 충전 중
+ Error // 오류
+ }
+
+ #endregion
+}
\ No newline at end of file
diff --git a/Cs_HMI/AGVNavigationCore/Models/Enums.cs b/Cs_HMI/AGVNavigationCore/Models/Enums.cs
new file mode 100644
index 0000000..7afe428
--- /dev/null
+++ b/Cs_HMI/AGVNavigationCore/Models/Enums.cs
@@ -0,0 +1,68 @@
+using System;
+
+namespace AGVNavigationCore.Models
+{
+ ///
+ /// 노드 타입 열거형
+ ///
+ public enum NodeType
+ {
+ /// 일반 경로 노드
+ Normal,
+ /// 회전 가능 지점
+ Rotation,
+ /// 도킹 스테이션
+ Docking,
+ /// 충전 스테이션
+ Charging,
+ /// 라벨 (UI 요소)
+ Label,
+ /// 이미지 (UI 요소)
+ Image
+ }
+
+ ///
+ /// 도킹 방향 열거형
+ ///
+ public enum DockingDirection
+ {
+ /// 전진 도킹 (충전기)
+ Forward,
+ /// 후진 도킹 (로더, 클리너, 오프로더, 버퍼)
+ Backward
+ }
+
+ ///
+ /// AGV 이동 방향 열거형
+ ///
+ public enum AgvDirection
+ {
+ /// 전진 (모니터 방향)
+ Forward,
+ /// 후진 (리프트 방향)
+ Backward,
+ /// 좌회전
+ Left,
+ /// 우회전
+ Right,
+ /// 정지
+ Stop
+ }
+
+ ///
+ /// 장비 타입 열거형
+ ///
+ public enum StationType
+ {
+ /// 로더
+ Loader,
+ /// 클리너
+ Cleaner,
+ /// 오프로더
+ Offloader,
+ /// 버퍼
+ Buffer,
+ /// 충전기
+ Charger
+ }
+}
\ No newline at end of file
diff --git a/Cs_HMI/AGVNavigationCore/Models/MapLoader.cs b/Cs_HMI/AGVNavigationCore/Models/MapLoader.cs
new file mode 100644
index 0000000..10f043a
--- /dev/null
+++ b/Cs_HMI/AGVNavigationCore/Models/MapLoader.cs
@@ -0,0 +1,144 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using Newtonsoft.Json;
+
+namespace AGVNavigationCore.Models
+{
+ ///
+ /// AGV 맵 파일 로딩/저장을 위한 공용 유틸리티 클래스
+ /// AGVMapEditor와 AGVSimulator에서 공통으로 사용
+ ///
+ public static class MapLoader
+ {
+ ///
+ /// 맵 파일 로딩 결과
+ ///
+ public class MapLoadResult
+ {
+ public bool Success { get; set; }
+ public List Nodes { get; set; } = new List();
+ public string ErrorMessage { get; set; } = string.Empty;
+ public string Version { get; set; } = string.Empty;
+ public DateTime CreatedDate { get; set; }
+ }
+
+ ///
+ /// 맵 파일 저장용 데이터 구조
+ ///
+ public class MapFileData
+ {
+ public List Nodes { get; set; } = new List();
+ public DateTime CreatedDate { get; set; }
+ public string Version { get; set; } = "1.0";
+ }
+
+ ///
+ /// 맵 파일을 로드하여 노드를 반환
+ ///
+ /// 맵 파일 경로
+ /// 로딩 결과
+ public static MapLoadResult LoadMapFromFile(string filePath)
+ {
+ var result = new MapLoadResult();
+
+ try
+ {
+ if (!File.Exists(filePath))
+ {
+ result.ErrorMessage = $"파일을 찾을 수 없습니다: {filePath}";
+ return result;
+ }
+
+ var json = File.ReadAllText(filePath);
+ var mapData = JsonConvert.DeserializeObject(json);
+
+ if (mapData != null)
+ {
+ result.Nodes = mapData.Nodes ?? new List();
+ result.Version = mapData.Version ?? "1.0";
+ result.CreatedDate = mapData.CreatedDate;
+
+ // 이미지 노드들의 이미지 로드
+ LoadImageNodes(result.Nodes);
+
+ result.Success = true;
+ }
+ else
+ {
+ result.ErrorMessage = "맵 데이터 파싱에 실패했습니다.";
+ }
+ }
+ catch (Exception ex)
+ {
+ result.ErrorMessage = $"맵 파일 로딩 중 오류 발생: {ex.Message}";
+ }
+
+ return result;
+ }
+
+ ///
+ /// 맵 데이터를 파일로 저장
+ ///
+ /// 저장할 파일 경로
+ /// 맵 노드 목록
+ /// 저장 성공 여부
+ public static bool SaveMapToFile(string filePath, List nodes)
+ {
+ try
+ {
+ var mapData = new MapFileData
+ {
+ Nodes = nodes,
+ CreatedDate = DateTime.Now,
+ Version = "1.0"
+ };
+
+ var json = JsonConvert.SerializeObject(mapData, Formatting.Indented);
+ File.WriteAllText(filePath, json);
+
+ return true;
+ }
+ catch (Exception)
+ {
+ return false;
+ }
+ }
+
+ ///
+ /// 이미지 노드들의 이미지 로드
+ ///
+ /// 노드 목록
+ private static void LoadImageNodes(List nodes)
+ {
+ foreach (var node in nodes)
+ {
+ if (node.Type == NodeType.Image)
+ {
+ node.LoadImage();
+ }
+ }
+ }
+
+ ///
+ /// MapNode 목록에서 RFID가 없는 노드들에 자동으로 RFID ID를 할당합니다.
+ ///
+ /// 맵 노드 목록
+ public static void AssignAutoRfidIds(List mapNodes)
+ {
+ foreach (var node in mapNodes)
+ {
+ // 네비게이션 가능한 노드이면서 RFID가 없는 경우에만 자동 할당
+ if (node.IsNavigationNode() && !node.HasRfid())
+ {
+ // 기본 RFID ID 생성 (N001 -> 001)
+ var rfidId = node.NodeId.Replace("N", "").PadLeft(3, '0');
+
+ node.SetRfidInfo(rfidId, "", "정상");
+ }
+ }
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/Cs_HMI/AGVNavigationCore/Models/MapNode.cs b/Cs_HMI/AGVNavigationCore/Models/MapNode.cs
new file mode 100644
index 0000000..4fe5897
--- /dev/null
+++ b/Cs_HMI/AGVNavigationCore/Models/MapNode.cs
@@ -0,0 +1,486 @@
+using System;
+using System.Collections.Generic;
+using System.Drawing;
+using System.Drawing.Drawing2D;
+
+namespace AGVNavigationCore.Models
+{
+ ///
+ /// 맵 노드 정보를 관리하는 클래스
+ /// 논리적 노드로서 실제 맵의 위치와 속성을 정의
+ ///
+ public class MapNode
+ {
+ ///
+ /// 논리적 노드 ID (맵 에디터에서 관리하는 고유 ID)
+ /// 예: "N001", "N002", "LOADER1", "CHARGER1"
+ ///
+ public string NodeId { get; set; } = string.Empty;
+
+ ///
+ /// 노드 표시 이름 (사용자 친화적)
+ /// 예: "로더1", "충전기1", "교차점A", "회전지점1"
+ ///
+ public string Name { get; set; } = string.Empty;
+
+ ///
+ /// 맵 상의 위치 좌표 (픽셀 단위)
+ ///
+ public Point Position { get; set; } = Point.Empty;
+
+ ///
+ /// 노드 타입
+ ///
+ public NodeType Type { get; set; } = NodeType.Normal;
+
+ ///
+ /// 도킹 방향 (도킹/충전 노드인 경우만 사용)
+ ///
+ public DockingDirection? DockDirection { get; set; } = null;
+
+ ///
+ /// 연결된 노드 ID 목록 (경로 정보)
+ ///
+ public List ConnectedNodes { get; set; } = new List();
+
+ ///
+ /// 회전 가능 여부 (180도 회전 가능한 지점)
+ ///
+ public bool CanRotate { get; set; } = false;
+
+ ///
+ /// 장비 ID (도킹/충전 스테이션인 경우)
+ /// 예: "LOADER1", "CLEANER1", "BUFFER1", "CHARGER1"
+ ///
+ public string StationId { get; set; } = string.Empty;
+
+ ///
+ /// 장비 타입 (도킹/충전 스테이션인 경우)
+ ///
+ public StationType? StationType { get; set; } = null;
+
+ ///
+ /// 노드 생성 일자
+ ///
+ public DateTime CreatedDate { get; set; } = DateTime.Now;
+
+ ///
+ /// 노드 수정 일자
+ ///
+ public DateTime ModifiedDate { get; set; } = DateTime.Now;
+
+ ///
+ /// 노드 설명 (추가 정보)
+ ///
+ public string Description { get; set; } = string.Empty;
+
+ ///
+ /// 노드 활성화 여부
+ ///
+ public bool IsActive { get; set; } = true;
+
+ ///
+ /// 노드 색상 (맵 에디터 표시용)
+ ///
+ public Color DisplayColor { get; set; } = Color.Blue;
+
+ ///
+ /// RFID 태그 ID (이 노드에 매핑된 RFID)
+ ///
+ public string RfidId { get; set; } = string.Empty;
+
+ ///
+ /// RFID 상태 (정상, 손상, 교체예정 등)
+ ///
+ public string RfidStatus { get; set; } = "정상";
+
+ ///
+ /// RFID 설치 위치 설명 (현장 작업자용)
+ /// 예: "로더1번 앞", "충전기2번 입구", "복도 교차점" 등
+ ///
+ public string RfidDescription { get; set; } = string.Empty;
+
+ ///
+ /// 라벨 텍스트 (NodeType.Label인 경우 사용)
+ ///
+ public string LabelText { get; set; } = string.Empty;
+
+ ///
+ /// 라벨 폰트 패밀리 (NodeType.Label인 경우 사용)
+ ///
+ public string FontFamily { get; set; } = "Arial";
+
+ ///
+ /// 라벨 폰트 크기 (NodeType.Label인 경우 사용)
+ ///
+ public float FontSize { get; set; } = 12.0f;
+
+ ///
+ /// 라벨 폰트 스타일 (NodeType.Label인 경우 사용)
+ ///
+ public FontStyle FontStyle { get; set; } = FontStyle.Regular;
+
+ ///
+ /// 라벨 전경색 (NodeType.Label인 경우 사용)
+ ///
+ public Color ForeColor { get; set; } = Color.Black;
+
+ ///
+ /// 라벨 배경색 (NodeType.Label인 경우 사용)
+ ///
+ public Color BackColor { get; set; } = Color.Transparent;
+
+ ///
+ /// 라벨 배경 표시 여부 (NodeType.Label인 경우 사용)
+ ///
+ public bool ShowBackground { get; set; } = false;
+
+ ///
+ /// 이미지 파일 경로 (NodeType.Image인 경우 사용)
+ ///
+ public string ImagePath { get; set; } = string.Empty;
+
+ ///
+ /// 이미지 크기 배율 (NodeType.Image인 경우 사용)
+ ///
+ public SizeF Scale { get; set; } = new SizeF(1.0f, 1.0f);
+
+ ///
+ /// 이미지 투명도 (NodeType.Image인 경우 사용, 0.0~1.0)
+ ///
+ public float Opacity { get; set; } = 1.0f;
+
+ ///
+ /// 이미지 회전 각도 (NodeType.Image인 경우 사용, 도 단위)
+ ///
+ public float Rotation { get; set; } = 0.0f;
+
+ ///
+ /// 로딩된 이미지 (런타임에서만 사용, JSON 직렬화 제외)
+ ///
+ [Newtonsoft.Json.JsonIgnore]
+ public Image LoadedImage { get; set; }
+
+ ///
+ /// 기본 생성자
+ ///
+ public MapNode()
+ {
+ }
+
+ ///
+ /// 매개변수 생성자
+ ///
+ /// 노드 ID
+ /// 노드 이름
+ /// 위치
+ /// 노드 타입
+ public MapNode(string nodeId, string name, Point position, NodeType type)
+ {
+ NodeId = nodeId;
+ Name = name;
+ Position = position;
+ Type = type;
+ CreatedDate = DateTime.Now;
+ ModifiedDate = DateTime.Now;
+
+ // 타입별 기본 색상 설정
+ SetDefaultColorByType(type);
+ }
+
+ ///
+ /// 노드 타입에 따른 기본 색상 설정
+ ///
+ /// 노드 타입
+ public void SetDefaultColorByType(NodeType type)
+ {
+ switch (type)
+ {
+ case NodeType.Normal:
+ DisplayColor = Color.Blue;
+ break;
+ case NodeType.Rotation:
+ DisplayColor = Color.Orange;
+ break;
+ case NodeType.Docking:
+ DisplayColor = Color.Green;
+ break;
+ case NodeType.Charging:
+ DisplayColor = Color.Red;
+ break;
+ case NodeType.Label:
+ DisplayColor = Color.Purple;
+ break;
+ case NodeType.Image:
+ DisplayColor = Color.Brown;
+ break;
+ }
+ }
+
+ ///
+ /// 다른 노드와의 연결 추가
+ ///
+ /// 연결할 노드 ID
+ public void AddConnection(string nodeId)
+ {
+ if (!ConnectedNodes.Contains(nodeId))
+ {
+ ConnectedNodes.Add(nodeId);
+ ModifiedDate = DateTime.Now;
+ }
+ }
+
+ ///
+ /// 다른 노드와의 연결 제거
+ ///
+ /// 연결 해제할 노드 ID
+ public void RemoveConnection(string nodeId)
+ {
+ if (ConnectedNodes.Remove(nodeId))
+ {
+ ModifiedDate = DateTime.Now;
+ }
+ }
+
+ ///
+ /// 도킹 스테이션 설정
+ ///
+ /// 장비 ID
+ /// 장비 타입
+ /// 도킹 방향
+ public void SetDockingStation(string stationId, StationType stationType, DockingDirection dockDirection)
+ {
+ Type = NodeType.Docking;
+ StationId = stationId;
+ StationType = stationType;
+ DockDirection = dockDirection;
+ SetDefaultColorByType(NodeType.Docking);
+ ModifiedDate = DateTime.Now;
+ }
+
+ ///
+ /// 충전 스테이션 설정
+ ///
+ /// 충전기 ID
+ public void SetChargingStation(string stationId)
+ {
+ Type = NodeType.Charging;
+ StationId = stationId;
+ StationType = Models.StationType.Charger;
+ DockDirection = DockingDirection.Forward; // 충전기는 항상 전진 도킹
+ SetDefaultColorByType(NodeType.Charging);
+ ModifiedDate = DateTime.Now;
+ }
+
+ ///
+ /// 문자열 표현
+ ///
+ public override string ToString()
+ {
+ return $"{NodeId}: {Name} ({Type}) at ({Position.X}, {Position.Y})";
+ }
+
+ ///
+ /// 리스트박스 표시용 텍스트 (노드ID - 설명 - RFID 순서)
+ ///
+ public string DisplayText
+ {
+ get
+ {
+ var displayText = NodeId;
+
+ if (!string.IsNullOrEmpty(Description))
+ {
+ displayText += $" - {Description}";
+ }
+
+ if (!string.IsNullOrEmpty(RfidId))
+ {
+ displayText += $" - [{RfidId}]";
+ }
+
+ return displayText;
+ }
+ }
+
+ ///
+ /// 노드 복사
+ ///
+ /// 복사된 노드
+ public MapNode Clone()
+ {
+ var clone = new MapNode
+ {
+ NodeId = NodeId,
+ Name = Name,
+ Position = Position,
+ Type = Type,
+ DockDirection = DockDirection,
+ ConnectedNodes = new List(ConnectedNodes),
+ CanRotate = CanRotate,
+ StationId = StationId,
+ StationType = StationType,
+ CreatedDate = CreatedDate,
+ ModifiedDate = ModifiedDate,
+ Description = Description,
+ IsActive = IsActive,
+ DisplayColor = DisplayColor,
+ RfidId = RfidId,
+ RfidStatus = RfidStatus,
+ RfidDescription = RfidDescription,
+ LabelText = LabelText,
+ FontFamily = FontFamily,
+ FontSize = FontSize,
+ FontStyle = FontStyle,
+ ForeColor = ForeColor,
+ BackColor = BackColor,
+ ShowBackground = ShowBackground,
+ ImagePath = ImagePath,
+ Scale = Scale,
+ Opacity = Opacity,
+ Rotation = Rotation
+ };
+ return clone;
+ }
+
+ ///
+ /// 이미지 로드 (256x256 이상일 경우 자동 리사이즈)
+ ///
+ /// 로드 성공 여부
+ public bool LoadImage()
+ {
+ if (Type != NodeType.Image) return false;
+
+ try
+ {
+ if (!string.IsNullOrEmpty(ImagePath) && System.IO.File.Exists(ImagePath))
+ {
+ LoadedImage?.Dispose();
+ var originalImage = Image.FromFile(ImagePath);
+
+ // 이미지 크기 체크 및 리사이즈
+ if (originalImage.Width > 256 || originalImage.Height > 256)
+ {
+ LoadedImage = ResizeImage(originalImage, 256, 256);
+ originalImage.Dispose();
+ }
+ else
+ {
+ LoadedImage = originalImage;
+ }
+
+ return true;
+ }
+ }
+ catch (Exception)
+ {
+ // 이미지 로드 실패
+ }
+ return false;
+ }
+
+ ///
+ /// 이미지 리사이즈 (비율 유지)
+ ///
+ /// 원본 이미지
+ /// 최대 너비
+ /// 최대 높이
+ /// 리사이즈된 이미지
+ private Image ResizeImage(Image image, int maxWidth, int maxHeight)
+ {
+ // 비율 계산
+ double ratioX = (double)maxWidth / image.Width;
+ double ratioY = (double)maxHeight / image.Height;
+ double ratio = Math.Min(ratioX, ratioY);
+
+ // 새로운 크기 계산
+ int newWidth = (int)(image.Width * ratio);
+ int newHeight = (int)(image.Height * ratio);
+
+ // 리사이즈된 이미지 생성
+ var resizedImage = new Bitmap(newWidth, newHeight);
+ using (var graphics = Graphics.FromImage(resizedImage))
+ {
+ graphics.CompositingQuality = CompositingQuality.HighQuality;
+ graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
+ graphics.SmoothingMode = SmoothingMode.HighQuality;
+ graphics.DrawImage(image, 0, 0, newWidth, newHeight);
+ }
+
+ return resizedImage;
+ }
+
+ ///
+ /// 실제 표시될 크기 계산 (이미지 노드인 경우)
+ ///
+ /// 실제 크기
+ public Size GetDisplaySize()
+ {
+ if (Type != NodeType.Image || LoadedImage == null) return Size.Empty;
+
+ return new Size(
+ (int)(LoadedImage.Width * Scale.Width),
+ (int)(LoadedImage.Height * Scale.Height)
+ );
+ }
+
+ ///
+ /// 리소스 정리
+ ///
+ public void Dispose()
+ {
+ LoadedImage?.Dispose();
+ LoadedImage = null;
+ }
+
+ ///
+ /// 경로 찾기에 사용 가능한 노드인지 확인
+ /// (라벨, 이미지 노드는 경로 찾기에서 제외)
+ ///
+ public bool IsNavigationNode()
+ {
+ return Type != NodeType.Label && Type != NodeType.Image && IsActive;
+ }
+
+ ///
+ /// RFID가 할당되어 있는지 확인
+ ///
+ public bool HasRfid()
+ {
+ return !string.IsNullOrEmpty(RfidId);
+ }
+
+ ///
+ /// RFID 정보 설정
+ ///
+ /// RFID ID
+ /// 설치 위치 설명
+ /// RFID 상태
+ public void SetRfidInfo(string rfidId, string rfidDescription = "", string rfidStatus = "정상")
+ {
+ RfidId = rfidId;
+ RfidDescription = rfidDescription;
+ RfidStatus = rfidStatus;
+ ModifiedDate = DateTime.Now;
+ }
+
+ ///
+ /// RFID 정보 삭제
+ ///
+ public void ClearRfidInfo()
+ {
+ RfidId = string.Empty;
+ RfidDescription = string.Empty;
+ RfidStatus = "정상";
+ ModifiedDate = DateTime.Now;
+ }
+
+ ///
+ /// RFID 기반 표시 텍스트 (RFID ID 우선, 없으면 노드ID)
+ ///
+ public string GetRfidDisplayText()
+ {
+ return HasRfid() ? RfidId : NodeId;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Cs_HMI/AGVNavigationCore/Models/RfidMapping.cs b/Cs_HMI/AGVNavigationCore/Models/RfidMapping.cs
new file mode 100644
index 0000000..169c0ab
--- /dev/null
+++ b/Cs_HMI/AGVNavigationCore/Models/RfidMapping.cs
@@ -0,0 +1,79 @@
+using System;
+
+namespace AGVNavigationCore.Models
+{
+ ///
+ /// RFID와 논리적 노드 ID를 매핑하는 클래스
+ /// 물리적 RFID는 의미없는 고유값, 논리적 노드는 맵 에디터에서 관리
+ ///
+ public class RfidMapping
+ {
+ ///
+ /// 물리적 RFID 값 (의미 없는 고유 식별자)
+ /// 예: "1234567890", "ABCDEF1234" 등
+ ///
+ public string RfidId { get; set; } = string.Empty;
+
+ ///
+ /// 논리적 노드 ID (맵 에디터에서 관리)
+ /// 예: "N001", "N002", "LOADER1", "CHARGER1" 등
+ ///
+ public string LogicalNodeId { get; set; } = string.Empty;
+
+ ///
+ /// 매핑 생성 일자
+ ///
+ public DateTime CreatedDate { get; set; } = DateTime.Now;
+
+ ///
+ /// 마지막 수정 일자
+ ///
+ public DateTime ModifiedDate { get; set; } = DateTime.Now;
+
+ ///
+ /// 설치 위치 설명 (현장 작업자용)
+ /// 예: "로더1번 앞", "충전기2번 입구", "복도 교차점" 등
+ ///
+ public string Description { get; set; } = string.Empty;
+
+ ///
+ /// RFID 상태 (정상, 손상, 교체예정 등)
+ ///
+ public string Status { get; set; } = "정상";
+
+ ///
+ /// 매핑 활성화 여부
+ ///
+ public bool IsActive { get; set; } = true;
+
+ ///
+ /// 기본 생성자
+ ///
+ public RfidMapping()
+ {
+ }
+
+ ///
+ /// 매개변수 생성자
+ ///
+ /// 물리적 RFID ID
+ /// 논리적 노드 ID
+ /// 설치 위치 설명
+ public RfidMapping(string rfidId, string logicalNodeId, string description = "")
+ {
+ RfidId = rfidId;
+ LogicalNodeId = logicalNodeId;
+ Description = description;
+ CreatedDate = DateTime.Now;
+ ModifiedDate = DateTime.Now;
+ }
+
+ ///
+ /// 문자열 표현
+ ///
+ public override string ToString()
+ {
+ return $"{RfidId} → {LogicalNodeId} ({Description})";
+ }
+ }
+}
\ No newline at end of file
diff --git a/Cs_HMI/AGVNavigationCore/PathFinding/AGVPathResult.cs b/Cs_HMI/AGVNavigationCore/PathFinding/AGVPathResult.cs
new file mode 100644
index 0000000..33a5b0a
--- /dev/null
+++ b/Cs_HMI/AGVNavigationCore/PathFinding/AGVPathResult.cs
@@ -0,0 +1,244 @@
+using System;
+using System.Collections.Generic;
+using AGVNavigationCore.Models;
+
+namespace AGVNavigationCore.PathFinding
+{
+ ///
+ /// AGV 경로 계산 결과 (방향성 및 명령어 포함)
+ ///
+ public class AGVPathResult
+ {
+ ///
+ /// 경로 찾기 성공 여부
+ ///
+ public bool Success { get; set; }
+
+ ///
+ /// 경로 노드 ID 목록 (시작 → 목적지 순서)
+ ///
+ public List Path { get; set; }
+
+ ///
+ /// AGV 명령어 목록 (이동 방향 시퀀스)
+ ///
+ public List Commands { get; set; }
+
+ ///
+ /// 총 거리
+ ///
+ public float TotalDistance { get; set; }
+
+ ///
+ /// 계산 소요 시간 (밀리초)
+ ///
+ public long CalculationTimeMs { get; set; }
+
+ ///
+ /// 예상 소요 시간 (초)
+ ///
+ public float EstimatedTimeSeconds { get; set; }
+
+ ///
+ /// 회전 횟수
+ ///
+ public int RotationCount { get; set; }
+
+ ///
+ /// 오류 메시지 (실패시)
+ ///
+ public string ErrorMessage { get; set; }
+
+ ///
+ /// 기본 생성자
+ ///
+ public AGVPathResult()
+ {
+ Success = false;
+ Path = new List();
+ Commands = new List();
+ TotalDistance = 0;
+ CalculationTimeMs = 0;
+ EstimatedTimeSeconds = 0;
+ RotationCount = 0;
+ ErrorMessage = string.Empty;
+ }
+
+ ///
+ /// 성공 결과 생성
+ ///
+ /// 경로
+ /// AGV 명령어 목록
+ /// 총 거리
+ /// 계산 시간
+ /// 성공 결과
+ public static AGVPathResult CreateSuccess(List path, List commands, float totalDistance, long calculationTimeMs)
+ {
+ var result = new AGVPathResult
+ {
+ Success = true,
+ Path = new List(path),
+ Commands = new List(commands),
+ TotalDistance = totalDistance,
+ CalculationTimeMs = calculationTimeMs
+ };
+
+ result.CalculateMetrics();
+ return result;
+ }
+
+ ///
+ /// 실패 결과 생성
+ ///
+ /// 오류 메시지
+ /// 계산 시간
+ /// 실패 결과
+ public static AGVPathResult CreateFailure(string errorMessage, long calculationTimeMs)
+ {
+ return new AGVPathResult
+ {
+ Success = false,
+ ErrorMessage = errorMessage,
+ CalculationTimeMs = calculationTimeMs
+ };
+ }
+
+ ///
+ /// 경로 메트릭 계산
+ ///
+ private void CalculateMetrics()
+ {
+ RotationCount = CountRotations();
+ EstimatedTimeSeconds = CalculateEstimatedTime();
+ }
+
+ ///
+ /// 회전 횟수 계산
+ ///
+ private int CountRotations()
+ {
+ int count = 0;
+ foreach (var command in Commands)
+ {
+ if (command == AgvDirection.Left || command == AgvDirection.Right)
+ {
+ count++;
+ }
+ }
+ return count;
+ }
+
+ ///
+ /// 예상 소요 시간 계산
+ ///
+ /// AGV 속도 (픽셀/초, 기본값: 100)
+ /// 회전 시간 (초, 기본값: 3)
+ /// 예상 소요 시간 (초)
+ private float CalculateEstimatedTime(float agvSpeed = 100.0f, float rotationTime = 3.0f)
+ {
+ float moveTime = TotalDistance / agvSpeed;
+ float totalRotationTime = RotationCount * rotationTime;
+ return moveTime + totalRotationTime;
+ }
+
+ ///
+ /// 명령어 요약 생성
+ ///
+ /// 명령어 요약 문자열
+ public string GetCommandSummary()
+ {
+ if (!Success) return "실패";
+
+ var summary = new List();
+ var currentCommand = AgvDirection.Stop;
+ var count = 0;
+
+ foreach (var command in Commands)
+ {
+ if (command == currentCommand)
+ {
+ count++;
+ }
+ else
+ {
+ if (count > 0)
+ {
+ summary.Add($"{GetCommandText(currentCommand)}×{count}");
+ }
+ currentCommand = command;
+ count = 1;
+ }
+ }
+
+ if (count > 0)
+ {
+ summary.Add($"{GetCommandText(currentCommand)}×{count}");
+ }
+
+ return string.Join(" → ", summary);
+ }
+
+ ///
+ /// 명령어 텍스트 반환
+ ///
+ private string GetCommandText(AgvDirection command)
+ {
+ switch (command)
+ {
+ case AgvDirection.Forward: return "전진";
+ case AgvDirection.Backward: return "후진";
+ case AgvDirection.Left: return "좌회전";
+ case AgvDirection.Right: return "우회전";
+ case AgvDirection.Stop: return "정지";
+ default: return command.ToString();
+ }
+ }
+
+ ///
+ /// 상세 경로 정보 반환
+ ///
+ /// 상세 정보 문자열
+ public string GetDetailedInfo()
+ {
+ if (!Success)
+ {
+ return $"경로 계산 실패: {ErrorMessage} (계산시간: {CalculationTimeMs}ms)";
+ }
+
+ return $"경로: {Path.Count}개 노드, 거리: {TotalDistance:F1}px, " +
+ $"회전: {RotationCount}회, 예상시간: {EstimatedTimeSeconds:F1}초, " +
+ $"계산시간: {CalculationTimeMs}ms";
+ }
+
+ ///
+ /// PathResult로 변환 (호환성을 위해)
+ ///
+ /// PathResult 객체
+ public PathResult ToPathResult()
+ {
+ if (Success)
+ {
+ return PathResult.CreateSuccess(Path, TotalDistance, CalculationTimeMs, 0);
+ }
+ else
+ {
+ return PathResult.CreateFailure(ErrorMessage, CalculationTimeMs, 0);
+ }
+ }
+
+ ///
+ /// 문자열 표현
+ ///
+ public override string ToString()
+ {
+ if (Success)
+ {
+ return $"Success: {Path.Count} nodes, {TotalDistance:F1}px, {RotationCount} rotations, {EstimatedTimeSeconds:F1}s";
+ }
+ else
+ {
+ return $"Failed: {ErrorMessage}";
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Cs_HMI/AGVNavigationCore/PathFinding/AGVPathfinder.cs b/Cs_HMI/AGVNavigationCore/PathFinding/AGVPathfinder.cs
new file mode 100644
index 0000000..9aa0342
--- /dev/null
+++ b/Cs_HMI/AGVNavigationCore/PathFinding/AGVPathfinder.cs
@@ -0,0 +1,287 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using AGVNavigationCore.Models;
+
+namespace AGVNavigationCore.PathFinding
+{
+ ///
+ /// AGV 특화 경로 탐색기 (방향성 및 도킹 제약 고려)
+ ///
+ public class AGVPathfinder
+ {
+ private AStarPathfinder _pathfinder;
+ private Dictionary _nodeMap;
+
+ ///
+ /// AGV 현재 방향
+ ///
+ public AgvDirection CurrentDirection { get; set; } = AgvDirection.Forward;
+
+ ///
+ /// 회전 비용 가중치 (회전이 비싼 동작임을 반영)
+ ///
+ public float RotationCostWeight { get; set; } = 50.0f;
+
+ ///
+ /// 도킹 접근 거리 (픽셀 단위)
+ ///
+ public float DockingApproachDistance { get; set; } = 100.0f;
+
+ ///
+ /// 생성자
+ ///
+ public AGVPathfinder()
+ {
+ _pathfinder = new AStarPathfinder();
+ _nodeMap = new Dictionary();
+ }
+
+ ///
+ /// 맵 노드 설정
+ ///
+ /// 맵 노드 목록
+ public void SetMapNodes(List mapNodes)
+ {
+ _pathfinder.SetMapNodes(mapNodes);
+ _nodeMap.Clear();
+
+ foreach (var node in mapNodes ?? new List())
+ {
+ _nodeMap[node.NodeId] = node;
+ }
+ }
+
+ ///
+ /// AGV 경로 계산 (방향성 및 도킹 제약 고려)
+ ///
+ /// 시작 노드 ID
+ /// 목적지 노드 ID
+ /// 목적지 도착 방향 (null이면 자동 결정)
+ /// AGV 경로 계산 결과
+ public AGVPathResult FindAGVPath(string startNodeId, string endNodeId, AgvDirection? targetDirection = null)
+ {
+ var stopwatch = System.Diagnostics.Stopwatch.StartNew();
+
+ try
+ {
+ if (!_nodeMap.ContainsKey(startNodeId))
+ {
+ return AGVPathResult.CreateFailure($"시작 노드를 찾을 수 없습니다: {startNodeId}", stopwatch.ElapsedMilliseconds);
+ }
+
+ if (!_nodeMap.ContainsKey(endNodeId))
+ {
+ return AGVPathResult.CreateFailure($"목적지 노드를 찾을 수 없습니다: {endNodeId}", stopwatch.ElapsedMilliseconds);
+ }
+
+ var endNode = _nodeMap[endNodeId];
+
+ if (IsSpecialNode(endNode))
+ {
+ return FindPathToSpecialNode(startNodeId, endNode, targetDirection, stopwatch);
+ }
+ else
+ {
+ return FindNormalPath(startNodeId, endNodeId, targetDirection, stopwatch);
+ }
+ }
+ catch (Exception ex)
+ {
+ return AGVPathResult.CreateFailure($"AGV 경로 계산 중 오류: {ex.Message}", stopwatch.ElapsedMilliseconds);
+ }
+ }
+
+ ///
+ /// 충전 스테이션으로의 경로 찾기
+ ///
+ /// 시작 노드 ID
+ /// AGV 경로 계산 결과
+ public AGVPathResult FindPathToChargingStation(string startNodeId)
+ {
+ var chargingStations = _nodeMap.Values
+ .Where(n => n.Type == NodeType.Charging && n.IsActive)
+ .Select(n => n.NodeId)
+ .ToList();
+
+ if (chargingStations.Count == 0)
+ {
+ return AGVPathResult.CreateFailure("사용 가능한 충전 스테이션이 없습니다", 0);
+ }
+
+ var nearestResult = _pathfinder.FindNearestPath(startNodeId, chargingStations);
+ if (!nearestResult.Success)
+ {
+ return AGVPathResult.CreateFailure("충전 스테이션으로의 경로를 찾을 수 없습니다", nearestResult.CalculationTimeMs);
+ }
+
+ var targetNodeId = nearestResult.Path.Last();
+ return FindAGVPath(startNodeId, targetNodeId, AgvDirection.Forward);
+ }
+
+ ///
+ /// 특정 타입의 도킹 스테이션으로의 경로 찾기
+ ///
+ /// 시작 노드 ID
+ /// 장비 타입
+ /// AGV 경로 계산 결과
+ public AGVPathResult FindPathToDockingStation(string startNodeId, StationType stationType)
+ {
+ var dockingStations = _nodeMap.Values
+ .Where(n => n.Type == NodeType.Docking && n.StationType == stationType && n.IsActive)
+ .Select(n => n.NodeId)
+ .ToList();
+
+ if (dockingStations.Count == 0)
+ {
+ return AGVPathResult.CreateFailure($"{stationType} 타입의 사용 가능한 도킹 스테이션이 없습니다", 0);
+ }
+
+ var nearestResult = _pathfinder.FindNearestPath(startNodeId, dockingStations);
+ if (!nearestResult.Success)
+ {
+ return AGVPathResult.CreateFailure($"{stationType} 도킹 스테이션으로의 경로를 찾을 수 없습니다", nearestResult.CalculationTimeMs);
+ }
+
+ var targetNodeId = nearestResult.Path.Last();
+ return FindAGVPath(startNodeId, targetNodeId, AgvDirection.Backward);
+ }
+
+ ///
+ /// 일반 노드로의 경로 계산
+ ///
+ private AGVPathResult FindNormalPath(string startNodeId, string endNodeId, AgvDirection? targetDirection, System.Diagnostics.Stopwatch stopwatch)
+ {
+ var result = _pathfinder.FindPath(startNodeId, endNodeId);
+ if (!result.Success)
+ {
+ return AGVPathResult.CreateFailure(result.ErrorMessage, stopwatch.ElapsedMilliseconds);
+ }
+
+ var agvCommands = GenerateAGVCommands(result.Path, targetDirection ?? AgvDirection.Forward);
+ return AGVPathResult.CreateSuccess(result.Path, agvCommands, result.TotalDistance, stopwatch.ElapsedMilliseconds);
+ }
+
+ ///
+ /// 특수 노드(도킹/충전)로의 경로 계산
+ ///
+ private AGVPathResult FindPathToSpecialNode(string startNodeId, MapNode endNode, AgvDirection? targetDirection, System.Diagnostics.Stopwatch stopwatch)
+ {
+ var requiredDirection = GetRequiredDirectionForNode(endNode);
+ var actualTargetDirection = targetDirection ?? requiredDirection;
+
+ var result = _pathfinder.FindPath(startNodeId, endNode.NodeId);
+ if (!result.Success)
+ {
+ return AGVPathResult.CreateFailure(result.ErrorMessage, stopwatch.ElapsedMilliseconds);
+ }
+
+ if (actualTargetDirection != requiredDirection)
+ {
+ return AGVPathResult.CreateFailure($"{endNode.NodeId}는 {requiredDirection} 방향으로만 접근 가능합니다", stopwatch.ElapsedMilliseconds);
+ }
+
+ var agvCommands = GenerateAGVCommands(result.Path, actualTargetDirection);
+ return AGVPathResult.CreateSuccess(result.Path, agvCommands, result.TotalDistance, stopwatch.ElapsedMilliseconds);
+ }
+
+ ///
+ /// 노드가 특수 노드(도킹/충전)인지 확인
+ ///
+ private bool IsSpecialNode(MapNode node)
+ {
+ return node.Type == NodeType.Docking || node.Type == NodeType.Charging;
+ }
+
+ ///
+ /// 노드에 필요한 접근 방향 반환
+ ///
+ private AgvDirection GetRequiredDirectionForNode(MapNode node)
+ {
+ switch (node.Type)
+ {
+ case NodeType.Charging:
+ return AgvDirection.Forward;
+ case NodeType.Docking:
+ return node.DockDirection == DockingDirection.Forward ? AgvDirection.Forward : AgvDirection.Backward;
+ default:
+ return AgvDirection.Forward;
+ }
+ }
+
+ ///
+ /// 경로에서 AGV 명령어 생성
+ ///
+ private List GenerateAGVCommands(List path, AgvDirection targetDirection)
+ {
+ var commands = new List();
+ if (path.Count < 2) return commands;
+
+ var currentDir = CurrentDirection;
+
+ for (int i = 0; i < path.Count - 1; i++)
+ {
+ var currentNodeId = path[i];
+ var nextNodeId = path[i + 1];
+
+ if (_nodeMap.ContainsKey(currentNodeId) && _nodeMap.ContainsKey(nextNodeId))
+ {
+ var currentNode = _nodeMap[currentNodeId];
+ var nextNode = _nodeMap[nextNodeId];
+
+ if (currentNode.CanRotate && ShouldRotate(currentDir, targetDirection))
+ {
+ commands.Add(GetRotationCommand(currentDir, targetDirection));
+ currentDir = targetDirection;
+ }
+
+ commands.Add(currentDir);
+ }
+ }
+
+ return commands;
+ }
+
+ ///
+ /// 회전이 필요한지 판단
+ ///
+ private bool ShouldRotate(AgvDirection current, AgvDirection target)
+ {
+ return current != target && (current == AgvDirection.Forward && target == AgvDirection.Backward ||
+ current == AgvDirection.Backward && target == AgvDirection.Forward);
+ }
+
+ ///
+ /// 회전 명령어 반환
+ ///
+ private AgvDirection GetRotationCommand(AgvDirection from, AgvDirection to)
+ {
+ if (from == AgvDirection.Forward && to == AgvDirection.Backward)
+ return AgvDirection.Right;
+ if (from == AgvDirection.Backward && to == AgvDirection.Forward)
+ return AgvDirection.Right;
+
+ return AgvDirection.Right;
+ }
+
+ ///
+ /// 경로 유효성 검증
+ ///
+ /// 검증할 경로
+ /// 유효성 검증 결과
+ public bool ValidatePath(List path)
+ {
+ if (path == null || path.Count < 2) return true;
+
+ for (int i = 0; i < path.Count - 1; i++)
+ {
+ if (!_pathfinder.AreNodesConnected(path[i], path[i + 1]))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Cs_HMI/AGVNavigationCore/PathFinding/AStarPathfinder.cs b/Cs_HMI/AGVNavigationCore/PathFinding/AStarPathfinder.cs
new file mode 100644
index 0000000..f339c2f
--- /dev/null
+++ b/Cs_HMI/AGVNavigationCore/PathFinding/AStarPathfinder.cs
@@ -0,0 +1,291 @@
+using System;
+using System.Collections.Generic;
+using System.Drawing;
+using System.Linq;
+using AGVNavigationCore.Models;
+
+namespace AGVNavigationCore.PathFinding
+{
+ ///
+ /// A* 알고리즘 기반 경로 탐색기
+ ///
+ public class AStarPathfinder
+ {
+ private Dictionary _nodeMap;
+ private List _mapNodes;
+
+ ///
+ /// 휴리스틱 가중치 (기본값: 1.0)
+ /// 값이 클수록 목적지 방향을 우선시하나 최적 경로를 놓칠 수 있음
+ ///
+ public float HeuristicWeight { get; set; } = 1.0f;
+
+ ///
+ /// 최대 탐색 노드 수 (무한 루프 방지)
+ ///
+ public int MaxSearchNodes { get; set; } = 1000;
+
+ ///
+ /// 생성자
+ ///
+ public AStarPathfinder()
+ {
+ _nodeMap = new Dictionary();
+ _mapNodes = new List();
+ }
+
+ ///
+ /// 맵 노드 설정
+ ///
+ /// 맵 노드 목록
+ public void SetMapNodes(List mapNodes)
+ {
+ _mapNodes = mapNodes ?? new List();
+ _nodeMap.Clear();
+
+ // 1단계: 모든 네비게이션 노드를 PathNode로 변환
+ foreach (var mapNode in _mapNodes)
+ {
+ if (mapNode.IsNavigationNode())
+ {
+ var pathNode = new PathNode(mapNode.NodeId, mapNode.Position);
+ pathNode.ConnectedNodes = new List(mapNode.ConnectedNodes);
+ _nodeMap[mapNode.NodeId] = pathNode;
+ }
+ }
+
+ // 2단계: 양방향 연결 자동 생성 (A→B 연결이 있으면 B→A도 추가)
+ EnsureBidirectionalConnections();
+ }
+
+ ///
+ /// 단방향 연결을 양방향으로 자동 변환
+ /// A→B 연결이 있으면 B→A 연결도 자동 생성
+ ///
+ private void EnsureBidirectionalConnections()
+ {
+ foreach (var nodeId in _nodeMap.Keys.ToList())
+ {
+ var node = _nodeMap[nodeId];
+ foreach (var connectedNodeId in node.ConnectedNodes.ToList())
+ {
+ // 연결된 노드가 존재하고 네비게이션 가능한 노드인지 확인
+ if (_nodeMap.ContainsKey(connectedNodeId))
+ {
+ var connectedNode = _nodeMap[connectedNodeId];
+ // 역방향 연결이 없으면 추가
+ if (!connectedNode.ConnectedNodes.Contains(nodeId))
+ {
+ connectedNode.ConnectedNodes.Add(nodeId);
+ }
+ }
+ }
+ }
+ }
+
+ ///
+ /// 경로 찾기 (A* 알고리즘)
+ ///
+ /// 시작 노드 ID
+ /// 목적지 노드 ID
+ /// 경로 계산 결과
+ public PathResult FindPath(string startNodeId, string endNodeId)
+ {
+ var stopwatch = System.Diagnostics.Stopwatch.StartNew();
+
+ try
+ {
+ if (!_nodeMap.ContainsKey(startNodeId))
+ {
+ return PathResult.CreateFailure($"시작 노드를 찾을 수 없습니다: {startNodeId}", stopwatch.ElapsedMilliseconds, 0);
+ }
+
+ if (!_nodeMap.ContainsKey(endNodeId))
+ {
+ return PathResult.CreateFailure($"목적지 노드를 찾을 수 없습니다: {endNodeId}", stopwatch.ElapsedMilliseconds, 0);
+ }
+
+ if (startNodeId == endNodeId)
+ {
+ return PathResult.CreateSuccess(new List { startNodeId }, 0, stopwatch.ElapsedMilliseconds, 1);
+ }
+
+ var startNode = _nodeMap[startNodeId];
+ var endNode = _nodeMap[endNodeId];
+
+ var openSet = new List();
+ var closedSet = new HashSet();
+ var exploredCount = 0;
+
+ startNode.GCost = 0;
+ startNode.HCost = CalculateHeuristic(startNode, endNode);
+ startNode.Parent = null;
+ openSet.Add(startNode);
+
+ while (openSet.Count > 0 && exploredCount < MaxSearchNodes)
+ {
+ var currentNode = GetLowestFCostNode(openSet);
+ openSet.Remove(currentNode);
+ closedSet.Add(currentNode.NodeId);
+ exploredCount++;
+
+ if (currentNode.NodeId == endNodeId)
+ {
+ var path = ReconstructPath(currentNode);
+ var totalDistance = CalculatePathDistance(path);
+ return PathResult.CreateSuccess(path, totalDistance, stopwatch.ElapsedMilliseconds, exploredCount);
+ }
+
+ foreach (var neighborId in currentNode.ConnectedNodes)
+ {
+ if (closedSet.Contains(neighborId) || !_nodeMap.ContainsKey(neighborId))
+ continue;
+
+ var neighbor = _nodeMap[neighborId];
+ var tentativeGCost = currentNode.GCost + currentNode.DistanceTo(neighbor);
+
+ if (!openSet.Contains(neighbor))
+ {
+ neighbor.Parent = currentNode;
+ neighbor.GCost = tentativeGCost;
+ neighbor.HCost = CalculateHeuristic(neighbor, endNode);
+ openSet.Add(neighbor);
+ }
+ else if (tentativeGCost < neighbor.GCost)
+ {
+ neighbor.Parent = currentNode;
+ neighbor.GCost = tentativeGCost;
+ }
+ }
+ }
+
+ return PathResult.CreateFailure("경로를 찾을 수 없습니다", stopwatch.ElapsedMilliseconds, exploredCount);
+ }
+ catch (Exception ex)
+ {
+ return PathResult.CreateFailure($"경로 계산 중 오류: {ex.Message}", stopwatch.ElapsedMilliseconds, 0);
+ }
+ }
+
+ ///
+ /// 여러 목적지 중 가장 가까운 노드로의 경로 찾기
+ ///
+ /// 시작 노드 ID
+ /// 목적지 후보 노드 ID 목록
+ /// 경로 계산 결과
+ public PathResult FindNearestPath(string startNodeId, List targetNodeIds)
+ {
+ if (targetNodeIds == null || targetNodeIds.Count == 0)
+ {
+ return PathResult.CreateFailure("목적지 노드가 지정되지 않았습니다", 0, 0);
+ }
+
+ PathResult bestResult = null;
+ foreach (var targetId in targetNodeIds)
+ {
+ var result = FindPath(startNodeId, targetId);
+ if (result.Success && (bestResult == null || result.TotalDistance < bestResult.TotalDistance))
+ {
+ bestResult = result;
+ }
+ }
+
+ return bestResult ?? PathResult.CreateFailure("모든 목적지로의 경로를 찾을 수 없습니다", 0, 0);
+ }
+
+ ///
+ /// 휴리스틱 거리 계산 (유클리드 거리)
+ ///
+ private float CalculateHeuristic(PathNode from, PathNode to)
+ {
+ return from.DistanceTo(to) * HeuristicWeight;
+ }
+
+ ///
+ /// F cost가 가장 낮은 노드 선택
+ ///
+ private PathNode GetLowestFCostNode(List nodes)
+ {
+ PathNode lowest = nodes[0];
+ foreach (var node in nodes)
+ {
+ if (node.FCost < lowest.FCost ||
+ (Math.Abs(node.FCost - lowest.FCost) < 0.001f && node.HCost < lowest.HCost))
+ {
+ lowest = node;
+ }
+ }
+ return lowest;
+ }
+
+ ///
+ /// 경로 재구성 (부모 노드를 따라 역추적)
+ ///
+ private List ReconstructPath(PathNode endNode)
+ {
+ var path = new List();
+ var current = endNode;
+
+ while (current != null)
+ {
+ path.Add(current.NodeId);
+ current = current.Parent;
+ }
+
+ path.Reverse();
+ return path;
+ }
+
+ ///
+ /// 경로의 총 거리 계산
+ ///
+ private float CalculatePathDistance(List path)
+ {
+ if (path.Count < 2) return 0;
+
+ float totalDistance = 0;
+ for (int i = 0; i < path.Count - 1; i++)
+ {
+ if (_nodeMap.ContainsKey(path[i]) && _nodeMap.ContainsKey(path[i + 1]))
+ {
+ totalDistance += _nodeMap[path[i]].DistanceTo(_nodeMap[path[i + 1]]);
+ }
+ }
+
+ return totalDistance;
+ }
+
+ ///
+ /// 두 노드가 연결되어 있는지 확인
+ ///
+ /// 노드 1 ID
+ /// 노드 2 ID
+ /// 연결 여부
+ public bool AreNodesConnected(string nodeId1, string nodeId2)
+ {
+ if (!_nodeMap.ContainsKey(nodeId1) || !_nodeMap.ContainsKey(nodeId2))
+ return false;
+
+ return _nodeMap[nodeId1].ConnectedNodes.Contains(nodeId2);
+ }
+
+ ///
+ /// 네비게이션 가능한 노드 목록 반환
+ ///
+ /// 노드 ID 목록
+ public List GetNavigationNodes()
+ {
+ return _nodeMap.Keys.ToList();
+ }
+
+ ///
+ /// 노드 정보 반환
+ ///
+ /// 노드 ID
+ /// 노드 정보 또는 null
+ public PathNode GetNode(string nodeId)
+ {
+ return _nodeMap.ContainsKey(nodeId) ? _nodeMap[nodeId] : null;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Cs_HMI/AGVNavigationCore/PathFinding/PathNode.cs b/Cs_HMI/AGVNavigationCore/PathFinding/PathNode.cs
new file mode 100644
index 0000000..0e668d2
--- /dev/null
+++ b/Cs_HMI/AGVNavigationCore/PathFinding/PathNode.cs
@@ -0,0 +1,101 @@
+using System;
+using System.Drawing;
+
+namespace AGVNavigationCore.PathFinding
+{
+ ///
+ /// A* 알고리즘에서 사용하는 경로 노드
+ ///
+ public class PathNode
+ {
+ ///
+ /// 노드 ID
+ ///
+ public string NodeId { get; set; }
+
+ ///
+ /// 노드 위치
+ ///
+ public Point Position { get; set; }
+
+ ///
+ /// 시작점으로부터의 실제 거리 (G cost)
+ ///
+ public float GCost { get; set; }
+
+ ///
+ /// 목적지까지의 추정 거리 (H cost - 휴리스틱)
+ ///
+ public float HCost { get; set; }
+
+ ///
+ /// 총 비용 (F cost = G cost + H cost)
+ ///
+ public float FCost => GCost + HCost;
+
+ ///
+ /// 부모 노드 (경로 추적용)
+ ///
+ public PathNode Parent { get; set; }
+
+ ///
+ /// 연결된 노드 ID 목록
+ ///
+ public System.Collections.Generic.List ConnectedNodes { get; set; }
+
+ ///
+ /// 생성자
+ ///
+ /// 노드 ID
+ /// 위치
+ public PathNode(string nodeId, Point position)
+ {
+ NodeId = nodeId;
+ Position = position;
+ GCost = 0;
+ HCost = 0;
+ Parent = null;
+ ConnectedNodes = new System.Collections.Generic.List();
+ }
+
+ ///
+ /// 다른 노드까지의 유클리드 거리 계산
+ ///
+ /// 대상 노드
+ /// 거리
+ public float DistanceTo(PathNode other)
+ {
+ float dx = Position.X - other.Position.X;
+ float dy = Position.Y - other.Position.Y;
+ return (float)Math.Sqrt(dx * dx + dy * dy);
+ }
+
+ ///
+ /// 문자열 표현
+ ///
+ public override string ToString()
+ {
+ return $"{NodeId} - F:{FCost:F1} G:{GCost:F1} H:{HCost:F1}";
+ }
+
+ ///
+ /// 같음 비교 (NodeId 기준)
+ ///
+ public override bool Equals(object obj)
+ {
+ if (obj is PathNode other)
+ {
+ return NodeId == other.NodeId;
+ }
+ return false;
+ }
+
+ ///
+ /// 해시코드 (NodeId 기준)
+ ///
+ public override int GetHashCode()
+ {
+ return NodeId?.GetHashCode() ?? 0;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Cs_HMI/AGVNavigationCore/PathFinding/PathResult.cs b/Cs_HMI/AGVNavigationCore/PathFinding/PathResult.cs
new file mode 100644
index 0000000..aa58593
--- /dev/null
+++ b/Cs_HMI/AGVNavigationCore/PathFinding/PathResult.cs
@@ -0,0 +1,107 @@
+using System;
+using System.Collections.Generic;
+
+namespace AGVNavigationCore.PathFinding
+{
+ ///
+ /// 경로 계산 결과
+ ///
+ public class PathResult
+ {
+ ///
+ /// 경로 찾기 성공 여부
+ ///
+ public bool Success { get; set; }
+
+ ///
+ /// 경로 노드 ID 목록 (시작 → 목적지 순서)
+ ///
+ public List Path { get; set; }
+
+ ///
+ /// 총 거리
+ ///
+ public float TotalDistance { get; set; }
+
+ ///
+ /// 계산 소요 시간 (밀리초)
+ ///
+ public long CalculationTimeMs { get; set; }
+
+ ///
+ /// 탐색한 노드 수
+ ///
+ public int ExploredNodeCount { get; set; }
+
+ ///
+ /// 오류 메시지 (실패시)
+ ///
+ public string ErrorMessage { get; set; }
+
+ ///
+ /// 기본 생성자
+ ///
+ public PathResult()
+ {
+ Success = false;
+ Path = new List();
+ TotalDistance = 0;
+ CalculationTimeMs = 0;
+ ExploredNodeCount = 0;
+ ErrorMessage = string.Empty;
+ }
+
+ ///
+ /// 성공 결과 생성
+ ///
+ /// 경로
+ /// 총 거리
+ /// 계산 시간
+ /// 탐색 노드 수
+ /// 성공 결과
+ public static PathResult CreateSuccess(List path, float totalDistance, long calculationTimeMs, int exploredNodeCount)
+ {
+ return new PathResult
+ {
+ Success = true,
+ Path = new List(path),
+ TotalDistance = totalDistance,
+ CalculationTimeMs = calculationTimeMs,
+ ExploredNodeCount = exploredNodeCount
+ };
+ }
+
+ ///
+ /// 실패 결과 생성
+ ///
+ /// 오류 메시지
+ /// 계산 시간
+ /// 탐색 노드 수
+ /// 실패 결과
+ public static PathResult CreateFailure(string errorMessage, long calculationTimeMs, int exploredNodeCount)
+ {
+ return new PathResult
+ {
+ Success = false,
+ ErrorMessage = errorMessage,
+ CalculationTimeMs = calculationTimeMs,
+ ExploredNodeCount = exploredNodeCount
+ };
+ }
+
+ ///
+ /// 문자열 표현
+ ///
+ public override string ToString()
+ {
+ if (Success)
+ {
+ return $"Success: {Path.Count} nodes, {TotalDistance:F1}px, {CalculationTimeMs}ms";
+ }
+ else
+ {
+ return $"Failed: {ErrorMessage}, {CalculationTimeMs}ms";
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Cs_HMI/AGVNavigationCore/PathFinding/RfidBasedPathfinder.cs b/Cs_HMI/AGVNavigationCore/PathFinding/RfidBasedPathfinder.cs
new file mode 100644
index 0000000..b4d6880
--- /dev/null
+++ b/Cs_HMI/AGVNavigationCore/PathFinding/RfidBasedPathfinder.cs
@@ -0,0 +1,275 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using AGVNavigationCore.Models;
+
+namespace AGVNavigationCore.PathFinding
+{
+ ///
+ /// RFID 기반 AGV 경로 탐색기
+ /// 실제 현장에서 AGV가 RFID를 읽어서 위치를 파악하는 방식에 맞춤
+ ///
+ public class RfidBasedPathfinder
+ {
+ private AGVPathfinder _agvPathfinder;
+ private AStarPathfinder _astarPathfinder;
+ private Dictionary _rfidToNodeMap; // RFID -> NodeId
+ private Dictionary _nodeToRfidMap; // NodeId -> RFID
+ private List _mapNodes;
+
+ ///
+ /// AGV 현재 방향
+ ///
+ public AgvDirection CurrentDirection
+ {
+ get => _agvPathfinder.CurrentDirection;
+ set => _agvPathfinder.CurrentDirection = value;
+ }
+
+ ///
+ /// 회전 비용 가중치
+ ///
+ public float RotationCostWeight
+ {
+ get => _agvPathfinder.RotationCostWeight;
+ set => _agvPathfinder.RotationCostWeight = value;
+ }
+
+ ///
+ /// 생성자
+ ///
+ public RfidBasedPathfinder()
+ {
+ _agvPathfinder = new AGVPathfinder();
+ _astarPathfinder = new AStarPathfinder();
+ _rfidToNodeMap = new Dictionary();
+ _nodeToRfidMap = new Dictionary();
+ _mapNodes = new List();
+ }
+
+ ///
+ /// 맵 노드 설정 (MapNode의 RFID 정보 직접 사용)
+ ///
+ /// 맵 노드 목록
+ public void SetMapNodes(List mapNodes)
+ {
+ // 기존 pathfinder에 맵 노드 설정
+ _agvPathfinder.SetMapNodes(mapNodes);
+ _astarPathfinder.SetMapNodes(mapNodes);
+
+ // MapNode의 RFID 정보로 매핑 구성
+ _mapNodes = mapNodes ?? new List();
+ _rfidToNodeMap.Clear();
+ _nodeToRfidMap.Clear();
+
+ foreach (var node in _mapNodes.Where(n => n.IsActive && n.HasRfid()))
+ {
+ _rfidToNodeMap[node.RfidId] = node.NodeId;
+ _nodeToRfidMap[node.NodeId] = node.RfidId;
+ }
+ }
+
+ ///
+ /// RFID 기반 AGV 경로 계산
+ ///
+ /// 시작 RFID
+ /// 목적지 RFID
+ /// 목적지 도착 방향
+ /// RFID 기반 AGV 경로 계산 결과
+ public RfidPathResult FindAGVPath(string startRfidId, string endRfidId, AgvDirection? targetDirection = null)
+ {
+ try
+ {
+ // RFID를 NodeId로 변환
+ if (!_rfidToNodeMap.TryGetValue(startRfidId, out string startNodeId))
+ {
+ return RfidPathResult.CreateFailure($"시작 RFID를 찾을 수 없습니다: {startRfidId}", 0);
+ }
+
+ if (!_rfidToNodeMap.TryGetValue(endRfidId, out string endNodeId))
+ {
+ return RfidPathResult.CreateFailure($"목적지 RFID를 찾을 수 없습니다: {endRfidId}", 0);
+ }
+
+ // NodeId 기반으로 경로 계산
+ var nodeResult = _agvPathfinder.FindAGVPath(startNodeId, endNodeId, targetDirection);
+
+ // 결과를 RFID 기반으로 변환
+ return ConvertToRfidResult(nodeResult, startRfidId, endRfidId);
+ }
+ catch (Exception ex)
+ {
+ return RfidPathResult.CreateFailure($"RFID 기반 경로 계산 중 오류: {ex.Message}", 0);
+ }
+ }
+
+ ///
+ /// 가장 가까운 충전소로의 RFID 기반 경로 찾기
+ ///
+ /// 시작 RFID
+ /// RFID 기반 경로 계산 결과
+ public RfidPathResult FindPathToChargingStation(string startRfidId)
+ {
+ try
+ {
+ if (!_rfidToNodeMap.TryGetValue(startRfidId, out string startNodeId))
+ {
+ return RfidPathResult.CreateFailure($"시작 RFID를 찾을 수 없습니다: {startRfidId}", 0);
+ }
+
+ var nodeResult = _agvPathfinder.FindPathToChargingStation(startNodeId);
+ return ConvertToRfidResult(nodeResult, startRfidId, null);
+ }
+ catch (Exception ex)
+ {
+ return RfidPathResult.CreateFailure($"충전소 경로 계산 중 오류: {ex.Message}", 0);
+ }
+ }
+
+ ///
+ /// 특정 장비 타입의 도킹 스테이션으로의 RFID 기반 경로 찾기
+ ///
+ /// 시작 RFID
+ /// 장비 타입
+ /// RFID 기반 경로 계산 결과
+ public RfidPathResult FindPathToDockingStation(string startRfidId, StationType stationType)
+ {
+ try
+ {
+ if (!_rfidToNodeMap.TryGetValue(startRfidId, out string startNodeId))
+ {
+ return RfidPathResult.CreateFailure($"시작 RFID를 찾을 수 없습니다: {startRfidId}", 0);
+ }
+
+ var nodeResult = _agvPathfinder.FindPathToDockingStation(startNodeId, stationType);
+ return ConvertToRfidResult(nodeResult, startRfidId, null);
+ }
+ catch (Exception ex)
+ {
+ return RfidPathResult.CreateFailure($"도킹 스테이션 경로 계산 중 오류: {ex.Message}", 0);
+ }
+ }
+
+ ///
+ /// 여러 RFID 목적지 중 가장 가까운 곳으로의 경로 찾기
+ ///
+ /// 시작 RFID
+ /// 목적지 후보 RFID 목록
+ /// RFID 기반 경로 계산 결과
+ public RfidPathResult FindNearestPath(string startRfidId, List targetRfidIds)
+ {
+ try
+ {
+ if (!_rfidToNodeMap.TryGetValue(startRfidId, out string startNodeId))
+ {
+ return RfidPathResult.CreateFailure($"시작 RFID를 찾을 수 없습니다: {startRfidId}", 0);
+ }
+
+ // RFID 목록을 NodeId 목록으로 변환
+ var targetNodeIds = new List();
+ foreach (var rfidId in targetRfidIds)
+ {
+ if (_rfidToNodeMap.TryGetValue(rfidId, out string nodeId))
+ {
+ targetNodeIds.Add(nodeId);
+ }
+ }
+
+ if (targetNodeIds.Count == 0)
+ {
+ return RfidPathResult.CreateFailure("유효한 목적지 RFID가 없습니다", 0);
+ }
+
+ var pathResult = _astarPathfinder.FindNearestPath(startNodeId, targetNodeIds);
+ if (!pathResult.Success)
+ {
+ return RfidPathResult.CreateFailure(pathResult.ErrorMessage, pathResult.CalculationTimeMs);
+ }
+
+ // AGV 명령어 생성을 위해 AGV pathfinder 사용
+ var endNodeId = pathResult.Path.Last();
+ var agvResult = _agvPathfinder.FindAGVPath(startNodeId, endNodeId);
+
+ return ConvertToRfidResult(agvResult, startRfidId, null);
+ }
+ catch (Exception ex)
+ {
+ return RfidPathResult.CreateFailure($"최근접 경로 계산 중 오류: {ex.Message}", 0);
+ }
+ }
+
+ ///
+ /// RFID 매핑 상태 확인 (MapNode 기반)
+ ///
+ /// 확인할 RFID
+ /// MapNode 또는 null
+ public MapNode GetRfidMapping(string rfidId)
+ {
+ return _mapNodes.FirstOrDefault(n => n.RfidId == rfidId && n.IsActive && n.HasRfid());
+ }
+
+ ///
+ /// RFID로 NodeId 조회
+ ///
+ /// RFID
+ /// NodeId 또는 null
+ public string GetNodeIdByRfid(string rfidId)
+ {
+ return _rfidToNodeMap.TryGetValue(rfidId, out string nodeId) ? nodeId : null;
+ }
+
+ ///
+ /// NodeId로 RFID 조회
+ ///
+ /// NodeId
+ /// RFID 또는 null
+ public string GetRfidByNodeId(string nodeId)
+ {
+ return _nodeToRfidMap.TryGetValue(nodeId, out string rfidId) ? rfidId : null;
+ }
+
+ ///
+ /// 활성화된 RFID 목록 반환
+ ///
+ /// 활성화된 RFID 목록
+ public List GetActiveRfidList()
+ {
+ return _mapNodes.Where(n => n.IsActive && n.HasRfid()).Select(n => n.RfidId).ToList();
+ }
+
+ ///
+ /// NodeId 기반 결과를 RFID 기반 결과로 변환
+ ///
+ private RfidPathResult ConvertToRfidResult(AGVPathResult nodeResult, string startRfidId, string endRfidId)
+ {
+ if (!nodeResult.Success)
+ {
+ return RfidPathResult.CreateFailure(nodeResult.ErrorMessage, nodeResult.CalculationTimeMs);
+ }
+
+ // NodeId 경로를 RFID 경로로 변환
+ var rfidPath = new List();
+ foreach (var nodeId in nodeResult.Path)
+ {
+ if (_nodeToRfidMap.TryGetValue(nodeId, out string rfidId))
+ {
+ rfidPath.Add(rfidId);
+ }
+ else
+ {
+ // 매핑이 없는 경우 NodeId를 그대로 사용 (경고 로그 필요)
+ rfidPath.Add($"[{nodeId}]");
+ }
+ }
+
+ return RfidPathResult.CreateSuccess(
+ rfidPath,
+ nodeResult.Commands,
+ nodeResult.TotalDistance,
+ nodeResult.CalculationTimeMs,
+ nodeResult.EstimatedTimeSeconds,
+ nodeResult.RotationCount
+ );
+ }
+ }
+}
\ No newline at end of file
diff --git a/Cs_HMI/AGVNavigationCore/PathFinding/RfidPathResult.cs b/Cs_HMI/AGVNavigationCore/PathFinding/RfidPathResult.cs
new file mode 100644
index 0000000..49fb6af
--- /dev/null
+++ b/Cs_HMI/AGVNavigationCore/PathFinding/RfidPathResult.cs
@@ -0,0 +1,229 @@
+using System;
+using System.Collections.Generic;
+using AGVNavigationCore.Models;
+
+namespace AGVNavigationCore.PathFinding
+{
+ ///
+ /// RFID 기반 AGV 경로 계산 결과
+ /// 실제 현장에서 AGV가 RFID를 기준으로 이동하는 방식에 맞춤
+ ///
+ public class RfidPathResult
+ {
+ ///
+ /// 경로 찾기 성공 여부
+ ///
+ public bool Success { get; set; }
+
+ ///
+ /// RFID 경로 목록 (시작 → 목적지 순서)
+ ///
+ public List RfidPath { get; set; }
+
+ ///
+ /// AGV 명령어 목록 (이동 방향 시퀀스)
+ ///
+ public List Commands { get; set; }
+
+ ///
+ /// 총 거리
+ ///
+ public float TotalDistance { get; set; }
+
+ ///
+ /// 계산 소요 시간 (밀리초)
+ ///
+ public long CalculationTimeMs { get; set; }
+
+ ///
+ /// 예상 소요 시간 (초)
+ ///
+ public float EstimatedTimeSeconds { get; set; }
+
+ ///
+ /// 회전 횟수
+ ///
+ public int RotationCount { get; set; }
+
+ ///
+ /// 오류 메시지 (실패시)
+ ///
+ public string ErrorMessage { get; set; }
+
+ ///
+ /// 기본 생성자
+ ///
+ public RfidPathResult()
+ {
+ Success = false;
+ RfidPath = new List();
+ Commands = new List();
+ TotalDistance = 0;
+ CalculationTimeMs = 0;
+ EstimatedTimeSeconds = 0;
+ RotationCount = 0;
+ ErrorMessage = string.Empty;
+ }
+
+ ///
+ /// 성공 결과 생성
+ ///
+ /// RFID 경로
+ /// AGV 명령어 목록
+ /// 총 거리
+ /// 계산 시간
+ /// 예상 소요 시간
+ /// 회전 횟수
+ /// 성공 결과
+ public static RfidPathResult CreateSuccess(
+ List rfidPath,
+ List commands,
+ float totalDistance,
+ long calculationTimeMs,
+ float estimatedTimeSeconds,
+ int rotationCount)
+ {
+ return new RfidPathResult
+ {
+ Success = true,
+ RfidPath = new List(rfidPath),
+ Commands = new List(commands),
+ TotalDistance = totalDistance,
+ CalculationTimeMs = calculationTimeMs,
+ EstimatedTimeSeconds = estimatedTimeSeconds,
+ RotationCount = rotationCount
+ };
+ }
+
+ ///
+ /// 실패 결과 생성
+ ///
+ /// 오류 메시지
+ /// 계산 시간
+ /// 실패 결과
+ public static RfidPathResult CreateFailure(string errorMessage, long calculationTimeMs)
+ {
+ return new RfidPathResult
+ {
+ Success = false,
+ ErrorMessage = errorMessage,
+ CalculationTimeMs = calculationTimeMs
+ };
+ }
+
+ ///
+ /// 명령어 요약 생성
+ ///
+ /// 명령어 요약 문자열
+ public string GetCommandSummary()
+ {
+ if (!Success) return "실패";
+
+ var summary = new List();
+ var currentCommand = AgvDirection.Stop;
+ var count = 0;
+
+ foreach (var command in Commands)
+ {
+ if (command == currentCommand)
+ {
+ count++;
+ }
+ else
+ {
+ if (count > 0)
+ {
+ summary.Add($"{GetCommandText(currentCommand)}×{count}");
+ }
+ currentCommand = command;
+ count = 1;
+ }
+ }
+
+ if (count > 0)
+ {
+ summary.Add($"{GetCommandText(currentCommand)}×{count}");
+ }
+
+ return string.Join(" → ", summary);
+ }
+
+ ///
+ /// 명령어 텍스트 반환
+ ///
+ private string GetCommandText(AgvDirection command)
+ {
+ switch (command)
+ {
+ case AgvDirection.Forward: return "전진";
+ case AgvDirection.Backward: return "후진";
+ case AgvDirection.Left: return "좌회전";
+ case AgvDirection.Right: return "우회전";
+ case AgvDirection.Stop: return "정지";
+ default: return command.ToString();
+ }
+ }
+
+ ///
+ /// RFID 경로 요약 생성
+ ///
+ /// RFID 경로 요약 문자열
+ public string GetRfidPathSummary()
+ {
+ if (!Success || RfidPath.Count == 0) return "경로 없음";
+
+ if (RfidPath.Count <= 3)
+ {
+ return string.Join(" → ", RfidPath);
+ }
+ else
+ {
+ return $"{RfidPath[0]} → ... ({RfidPath.Count - 2}개 경유) → {RfidPath[RfidPath.Count - 1]}";
+ }
+ }
+
+ ///
+ /// 상세 경로 정보 반환
+ ///
+ /// 상세 정보 문자열
+ public string GetDetailedInfo()
+ {
+ if (!Success)
+ {
+ return $"RFID 경로 계산 실패: {ErrorMessage} (계산시간: {CalculationTimeMs}ms)";
+ }
+
+ return $"RFID 경로: {RfidPath.Count}개 지점, 거리: {TotalDistance:F1}px, " +
+ $"회전: {RotationCount}회, 예상시간: {EstimatedTimeSeconds:F1}초, " +
+ $"계산시간: {CalculationTimeMs}ms";
+ }
+
+ ///
+ /// AGV 운영자용 실행 정보 반환
+ ///
+ /// 실행 정보 문자열
+ public string GetExecutionInfo()
+ {
+ if (!Success) return $"실행 불가: {ErrorMessage}";
+
+ return $"[실행준비] {GetRfidPathSummary()}\n" +
+ $"[명령어] {GetCommandSummary()}\n" +
+ $"[예상시간] {EstimatedTimeSeconds:F1}초";
+ }
+
+ ///
+ /// 문자열 표현
+ ///
+ public override string ToString()
+ {
+ if (Success)
+ {
+ return $"Success: {RfidPath.Count} RFIDs, {TotalDistance:F1}px, {RotationCount} rotations, {EstimatedTimeSeconds:F1}s";
+ }
+ else
+ {
+ return $"Failed: {ErrorMessage}";
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Cs_HMI/AGVNavigationCore/Properties/AssemblyInfo.cs b/Cs_HMI/AGVNavigationCore/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000..0fe4149
--- /dev/null
+++ b/Cs_HMI/AGVNavigationCore/Properties/AssemblyInfo.cs
@@ -0,0 +1,36 @@
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("AGVNavigationCore")]
+[assembly: AssemblyDescription("AGV Navigation and Pathfinding Core Library")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("ENIG")]
+[assembly: AssemblyProduct("AGV Navigation System")]
+[assembly: AssemblyCopyright("Copyright © ENIG 2024")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible
+// to COM components. If you need to access a type in this assembly from
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+
+// The following GUID is for the ID of the typelib if this project is exposed to COM
+[assembly: Guid("c5f7a8b2-8d3e-4a1b-9c6e-7f4d5e2a9b1c")]
+
+// Version information for an assembly consists of the following four values:
+//
+// Major Version
+// Minor Version
+// Build Number
+// Revision
+//
+// You can specify all the values or you can default the Build and Revision Numbers
+// by using the '*' as shown below:
+// [assembly: AssemblyVersion("1.0.*")]
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]
\ No newline at end of file
diff --git a/Cs_HMI/AGVNavigationCore/README.md b/Cs_HMI/AGVNavigationCore/README.md
new file mode 100644
index 0000000..ae2efe3
--- /dev/null
+++ b/Cs_HMI/AGVNavigationCore/README.md
@@ -0,0 +1,225 @@
+# AGVNavigationCore 프로젝트 기능 설명
+
+## 📋 개요
+AGVNavigationCore는 AGV(Automated Guided Vehicle) 시스템을 위한 전문적인 경로 계산 및 네비게이션 라이브러리입니다. 실제 AGV의 물리적 제약사항과 운영 요구사항을 고려한 지능형 경로 탐색 기능을 제공합니다.
+
+## 🏗️ 핵심 구조
+
+### **Models 패키지**
+- **MapNode**: 맵의 논리적 노드 정보 (위치, 타입, 연결 정보, RFID 매핑)
+- **RfidMapping**: RFID와 논리적 노드 ID 간의 매핑 정보
+- **Enums**: 노드 타입, AGV 방향, 도킹 방향, 장비 타입 등 열거형 정의
+
+### **PathFinding 패키지**
+경로 계산의 핵심 엔진들이 포함된 패키지
+
+## 🎯 주요 기능
+
+### 1. **기본 경로 탐색 (A* 알고리즘)**
+
+**AStarPathfinder 클래스**:
+- 표준 A* 알고리즘 구현으로 최적 경로 탐색
+- 유클리드 거리 기반 휴리스틱 사용
+- 설정 가능한 파라미터:
+ - `HeuristicWeight`: 휴리스틱 가중치 (기본 1.0)
+ - `MaxSearchNodes`: 최대 탐색 노드 수 (기본 1000개)
+
+**제공 기능**:
+```csharp
+// 단일 경로 탐색
+PathResult FindPath(string startNodeId, string endNodeId)
+
+// 다중 목표 중 최단 경로 탐색
+PathResult FindNearestPath(string startNodeId, List targetNodeIds)
+
+// 노드 연결 상태 확인
+bool AreNodesConnected(string nodeId1, string nodeId2)
+```
+
+### 2. **AGV 전용 지능형 경로 계산**
+
+**AGVPathfinder 클래스**:
+AGV의 실제 움직임 제약사항을 고려한 전문 경로 계산기
+
+**AGV 제약사항 고려**:
+- **방향성 제약**: 전진/후진만 가능, 좌우 이동 불가
+- **회전 제약**: 특정 노드(회전 가능 지점)에서만 180도 회전 가능
+- **도킹 방향**:
+ - 충전기: 전진 도킹만 가능
+ - 장비 (로더, 클리너 등): 후진 도킹만 가능
+
+**전용 기능**:
+```csharp
+// AGV 경로 계산 (방향성 고려)
+AGVPathResult FindAGVPath(string startNodeId, string endNodeId, AgvDirection? targetDirection)
+
+// 가장 가까운 충전소 경로
+AGVPathResult FindPathToChargingStation(string startNodeId)
+
+// 특정 장비 타입 도킹 스테이션 경로
+AGVPathResult FindPathToDockingStation(string startNodeId, StationType stationType)
+```
+
+### 3. **RFID 기반 경로 계산 (실제 운영용)**
+
+**RfidBasedPathfinder 클래스**:
+실제 AGV가 RFID를 읽어서 위치를 파악하는 현장 운영 방식에 최적화
+
+**RFID 기반 제약사항**:
+- **물리적 RFID**: 의미 없는 고유 식별자 (현장 유지보수 편의성)
+- **논리적 매핑**: RFID ↔ NodeId 분리로 맵 변경 시 유연성 확보
+- **실시간 변환**: RFID 입력을 내부적으로 NodeId로 변환하여 처리
+
+**RFID 전용 기능**:
+```csharp
+// RFID 기반 AGV 경로 계산
+RfidPathResult FindAGVPath(string startRfidId, string endRfidId, AgvDirection? targetDirection)
+
+// RFID 기반 충전소 경로
+RfidPathResult FindPathToChargingStation(string startRfidId)
+
+// RFID 기반 도킹 스테이션 경로
+RfidPathResult FindPathToDockingStation(string startRfidId, StationType stationType)
+
+// RFID 매핑 관리
+RfidMapping GetRfidMapping(string rfidId)
+string GetNodeIdByRfid(string rfidId)
+string GetRfidByNodeId(string nodeId)
+```
+
+### 4. **상세한 결과 분석**
+
+**PathResult (기본 결과)**:
+- 성공/실패 여부
+- 노드 ID 시퀀스
+- 총 거리 및 계산 시간
+- 탐색한 노드 수
+
+**AGVPathResult (AGV 전용 결과)**:
+- **실행 가능한 명령어 시퀀스**: `[전진, 전진, 우회전, 후진, 정지]`
+- **상세 메트릭**:
+ - 회전 횟수 계산
+ - 예상 소요 시간 (이동 + 회전 시간)
+ - 명령어 요약 (`전진×3 → 우회전×1 → 후진×2`)
+
+**RfidPathResult (RFID 기반 결과)**:
+- **RFID 경로 시퀀스**: `[RFID001, RFID045, RFID067, RFID123]`
+- **AGV 명령어**: NodeId 기반 결과와 동일한 명령어 시퀀스
+- **현장 친화적 정보**:
+ - RFID 경로 요약 (`RFID001 → ... (2개 경유) → RFID123`)
+ - 실행 정보 (`[실행준비] → [명령어] → [예상시간]`)
+ - 운영자용 상세 정보
+
+### 5. **실시간 검증 및 최적화**
+
+**경로 검증**:
+```csharp
+// 경로 유효성 실시간 검증
+bool ValidatePath(List path)
+
+// 네비게이션 가능 노드 필터링 (라벨/이미지 노드 제외)
+List GetNavigationNodes()
+```
+
+**성능 최적화**:
+- 메모리 효율적인 노드 관리
+- 조기 종료 조건으로 불필요한 탐색 방지
+- 캐시된 거리 계산
+
+## 🔧 설정 가능한 파라미터
+
+**AGV 동작 파라미터**:
+- `CurrentDirection`: AGV 현재 방향
+- `RotationCostWeight`: 회전 비용 가중치 (기본 50.0)
+- `DockingApproachDistance`: 도킹 접근 거리 (기본 100픽셀)
+
+**알고리즘 파라미터**:
+- `HeuristicWeight`: A* 휴리스틱 강도
+- `MaxSearchNodes`: 탐색 제한으로 무한루프 방지
+
+**RFID 매핑 파라미터**:
+- `RfidMappings`: RFID ↔ NodeId 매핑 테이블
+- `IsActive`: 매핑 활성화 상태
+- `Status`: RFID 상태 (정상, 손상, 교체예정)
+
+## 🎯 실제 활용 시나리오
+
+### **시나리오 1: 일반 이동 (NodeId 기반)**
+```
+현재위치(N001) → 목적지(N010)
+결과: [N001, N003, N007, N010] + [전진, 전진, 전진]
+```
+
+### **시나리오 2: 일반 이동 (RFID 기반)**
+```
+현재위치(RFID123) → 목적지(RFID789)
+결과: [RFID123, RFID456, RFID789] + [전진, 우회전, 전진]
+AGV는 RFID를 읽으면서 실제 위치 확인 후 이동
+```
+
+### **시나리오 3: 충전 필요**
+```
+배터리 부족 → 가장 가까운 충전소 자동 탐색
+결과: 충전소까지 최단경로 + 전진 도킹 명령어
+```
+
+### **시나리오 4: 화물 적재**
+```
+로더 스테이션 접근 → 후진 도킹 필수
+결과: 로더까지 경로 + 후진 도킹 시퀀스
+```
+
+## 🌟 차별화 포인트
+
+1. **실제 AGV 제약사항 반영**: 이론적 경로가 아닌 실행 가능한 경로 제공
+2. **명령어 레벨 출력**: 경로뿐만 아니라 실제 AGV 제어 명령어 생성
+3. **RFID 기반 현장 운영**: 물리적 RFID와 논리적 노드 분리로 현장 유지보수성 향상
+4. **다양한 장비 지원**: 충전기, 로더, 클리너, 버퍼 등 각각의 도킹 요구사항 처리
+5. **이중 API 제공**: NodeId 기반(개발용) + RFID 기반(운영용) 동시 지원
+6. **확장성**: 새로운 AGV 타입이나 제약사항 쉽게 추가 가능
+7. **성능 최적화**: 실시간 운영에 적합한 빠른 응답속도
+
+## 🚀 사용 방법
+
+### 기본 사용법
+```csharp
+// 1. 경로 탐색기 초기화
+var pathfinder = new AGVPathfinder();
+pathfinder.SetMapNodes(mapNodes);
+
+// 2. AGV 경로 계산
+var result = pathfinder.FindAGVPath("N001", "N010");
+
+// 3. 결과 확인
+if (result.Success)
+{
+ Console.WriteLine($"경로: {string.Join(" → ", result.Path)}");
+ Console.WriteLine($"명령어: {result.GetCommandSummary()}");
+ Console.WriteLine($"예상시간: {result.EstimatedTimeSeconds}초");
+}
+```
+
+### 충전소 경로 탐색
+```csharp
+var chargingResult = pathfinder.FindPathToChargingStation("N001");
+if (chargingResult.Success)
+{
+ // 충전소까지 자동 이동
+ ExecuteAGVCommands(chargingResult.Commands);
+}
+```
+
+## 📦 의존성
+- .NET Framework 4.8
+- Newtonsoft.Json 13.0.3
+- System.Drawing
+
+## 🔗 통합 프로젝트
+이 라이브러리는 다음 프로젝트에서 사용됩니다:
+- **AGVMapEditor**: 맵 편집 및 경로 시뮬레이션
+- **AGV4**: 메인 AGV 제어 시스템
+- **AGVSimulator**: AGV 동작 시뮬레이터
+
+---
+
+*AGVNavigationCore는 ENIG AGV 시스템의 핵심 네비게이션 엔진입니다.*
\ No newline at end of file
diff --git a/Cs_HMI/AGVNavigationCore/build.bat b/Cs_HMI/AGVNavigationCore/build.bat
new file mode 100644
index 0000000..3a59146
--- /dev/null
+++ b/Cs_HMI/AGVNavigationCore/build.bat
@@ -0,0 +1,29 @@
+@echo off
+echo Building V2GDecoder VC++ Project...
+
+REM Check if Visual Studio 2022 is installed (Professional or Community)
+set MSBUILD_PRO="C:\Program Files\Microsoft Visual Studio\2022\Professional\MSBuild\Current\Bin\MSBuild.exe"
+set MSBUILD_COM="C:\Program Files\Microsoft Visual Studio\2022\Community\MSBuild\Current\Bin\MSBuild.exe"
+set MSBUILD_BT="F:\(VHD) Program Files\Microsoft Visual Studio\2022\MSBuild\Current\Bin\MSBuild.exe"
+
+if exist %MSBUILD_PRO% (
+ echo "Found Visual Studio 2022 Professional"
+ set MSBUILD=%MSBUILD_PRO%
+) else if exist %MSBUILD_COM% (
+ echo "Found Visual Studio 2022 Community"
+ set MSBUILD=%MSBUILD_COM%
+) else if exist %MSBUILD_BT% (
+ echo "Found Visual Studio 2022 BuildTools"
+ set MSBUILD=%MSBUILD_BT%
+) else (
+ echo "Visual Studio 2022 (Professional or Community) not found!"
+ echo "Please install Visual Studio 2022 or update the MSBuild path."
+ pause
+ exit /b 1
+)
+
+REM Build Debug x64 configuration
+echo Building Debug x64 configuration...
+%MSBUILD% AGVNavigationCore.csproj
+
+pause
\ No newline at end of file
diff --git a/Cs_HMI/AGVNavigationCore/packages.config b/Cs_HMI/AGVNavigationCore/packages.config
new file mode 100644
index 0000000..8b1a8d0
--- /dev/null
+++ b/Cs_HMI/AGVNavigationCore/packages.config
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/Cs_HMI/AGVSimulator/AGVSimulator.csproj b/Cs_HMI/AGVSimulator/AGVSimulator.csproj
index c9f0cdc..1a60ad7 100644
--- a/Cs_HMI/AGVSimulator/AGVSimulator.csproj
+++ b/Cs_HMI/AGVSimulator/AGVSimulator.csproj
@@ -4,7 +4,7 @@
Debug
AnyCPU
- {B2C3D4E5-F6G7-8901-BCDE-F23456789012}
+ {B2C3D4E5-0000-0000-0000-000000000000}
WinExe
AGVSimulator
AGVSimulator
@@ -42,14 +42,9 @@
+
-
- UserControl
-
-
- SimulatorCanvas.cs
-
Form
@@ -60,17 +55,19 @@
-
- SimulatorCanvas.cs
-
SimulatorForm.cs
+
+
+ {C5F7A8B2-8D3E-4A1B-9C6E-7F4D5E2A9B1C}
+ AGVNavigationCore
+
{a1b2c3d4-e5f6-7890-abcd-ef1234567890}
AGVMapEditor
diff --git a/Cs_HMI/AGVSimulator/Controls/SimulatorCanvas.cs b/Cs_HMI/AGVSimulator/Controls/SimulatorCanvas.cs
index dfd2402..a48d2b4 100644
--- a/Cs_HMI/AGVSimulator/Controls/SimulatorCanvas.cs
+++ b/Cs_HMI/AGVSimulator/Controls/SimulatorCanvas.cs
@@ -4,6 +4,8 @@ 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
@@ -388,13 +390,13 @@ namespace AGVSimulator.Controls
private void DrawPath(Graphics g)
{
- if (_currentPath?.NodeSequence == null || _currentPath.NodeSequence.Count < 2)
+ if (_currentPath?.Path == null || _currentPath.Path.Count < 2)
return;
- for (int i = 0; i < _currentPath.NodeSequence.Count - 1; i++)
+ for (int i = 0; i < _currentPath.Path.Count - 1; i++)
{
- var currentNodeId = _currentPath.NodeSequence[i];
- var nextNodeId = _currentPath.NodeSequence[i + 1];
+ 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);
@@ -583,11 +585,11 @@ namespace AGVSimulator.Controls
if (_currentPath != null && _currentPath.Success)
{
y += 10;
- g.DrawString($"경로: {_currentPath.NodeSequence.Count}개 노드", font, brush, new PointF(10, y));
+ 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.CalculationTime}ms", font, brush, new PointF(10, y));
+ g.DrawString($"계산시간: {_currentPath.CalculationTimeMs}ms", font, brush, new PointF(10, y));
}
font.Dispose();
diff --git a/Cs_HMI/AGVSimulator/Forms/SimulatorForm.Designer.cs b/Cs_HMI/AGVSimulator/Forms/SimulatorForm.Designer.cs
index 89ce9bb..5945b3d 100644
--- a/Cs_HMI/AGVSimulator/Forms/SimulatorForm.Designer.cs
+++ b/Cs_HMI/AGVSimulator/Forms/SimulatorForm.Designer.cs
@@ -45,20 +45,666 @@ namespace AGVSimulator.Forms
///
private void InitializeComponent()
{
+ this._menuStrip = new System.Windows.Forms.MenuStrip();
+ this.fileToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
+ this.openMapToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
+ this.reloadMapToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
+ this.toolStripSeparator1 = new System.Windows.Forms.ToolStripSeparator();
+ this.launchMapEditorToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
+ this.toolStripSeparator4 = new System.Windows.Forms.ToolStripSeparator();
+ this.exitToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
+ this.simulationToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
+ this.startSimulationToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
+ this.stopSimulationToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
+ this.resetToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
+ this.viewToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
+ this.fitToMapToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
+ this.resetZoomToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
+ this.helpToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
+ this.aboutToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
+ this._toolStrip = new System.Windows.Forms.ToolStrip();
+ this.openMapToolStripButton = new System.Windows.Forms.ToolStripButton();
+ this.reloadMapToolStripButton = new System.Windows.Forms.ToolStripButton();
+ this.launchMapEditorToolStripButton = new System.Windows.Forms.ToolStripButton();
+ this.toolStripSeparator2 = new System.Windows.Forms.ToolStripSeparator();
+ this.startSimulationToolStripButton = new System.Windows.Forms.ToolStripButton();
+ this.stopSimulationToolStripButton = new System.Windows.Forms.ToolStripButton();
+ this.resetToolStripButton = 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();
+ this._statusStrip = new System.Windows.Forms.StatusStrip();
+ this._statusLabel = new System.Windows.Forms.ToolStripStatusLabel();
+ this._coordLabel = new System.Windows.Forms.ToolStripStatusLabel();
+ this._controlPanel = new System.Windows.Forms.Panel();
+ this._statusGroup = new System.Windows.Forms.GroupBox();
+ this._pathLengthLabel = new System.Windows.Forms.Label();
+ this._agvCountLabel = new System.Windows.Forms.Label();
+ this._simulationStatusLabel = new System.Windows.Forms.Label();
+ this._pathGroup = new System.Windows.Forms.GroupBox();
+ this._clearPathButton = new System.Windows.Forms.Button();
+ this._startPathButton = new System.Windows.Forms.Button();
+ this._calculatePathButton = new System.Windows.Forms.Button();
+ this._targetNodeCombo = new System.Windows.Forms.ComboBox();
+ this.targetNodeLabel = new System.Windows.Forms.Label();
+ this._startNodeCombo = new System.Windows.Forms.ComboBox();
+ this.startNodeLabel = new System.Windows.Forms.Label();
+ this._agvControlGroup = new System.Windows.Forms.GroupBox();
+ this._setPositionButton = new System.Windows.Forms.Button();
+ this._rfidTextBox = new System.Windows.Forms.TextBox();
+ this._rfidLabel = 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._menuStrip.SuspendLayout();
+ this._toolStrip.SuspendLayout();
+ this._statusStrip.SuspendLayout();
+ this._controlPanel.SuspendLayout();
+ this._statusGroup.SuspendLayout();
+ this._pathGroup.SuspendLayout();
+ this._agvControlGroup.SuspendLayout();
this.SuspendLayout();
//
+ // _menuStrip
+ //
+ this._menuStrip.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
+ this.fileToolStripMenuItem,
+ this.simulationToolStripMenuItem,
+ this.viewToolStripMenuItem,
+ this.helpToolStripMenuItem});
+ this._menuStrip.Location = new System.Drawing.Point(0, 0);
+ this._menuStrip.Name = "_menuStrip";
+ this._menuStrip.Size = new System.Drawing.Size(1200, 24);
+ this._menuStrip.TabIndex = 0;
+ this._menuStrip.Text = "menuStrip";
+ //
+ // fileToolStripMenuItem
+ //
+ this.fileToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
+ this.openMapToolStripMenuItem,
+ this.reloadMapToolStripMenuItem,
+ this.toolStripSeparator1,
+ this.launchMapEditorToolStripMenuItem,
+ this.toolStripSeparator4,
+ this.exitToolStripMenuItem});
+ this.fileToolStripMenuItem.Name = "fileToolStripMenuItem";
+ this.fileToolStripMenuItem.Size = new System.Drawing.Size(57, 20);
+ this.fileToolStripMenuItem.Text = "파일(&F)";
+ //
+ // openMapToolStripMenuItem
+ //
+ 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.Text = "맵 열기(&O)...";
+ this.openMapToolStripMenuItem.Click += new System.EventHandler(this.OnOpenMap_Click);
+ //
+ // reloadMapToolStripMenuItem
+ //
+ 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.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);
+ //
+ // 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.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);
+ //
+ // 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.Text = "종료(&X)";
+ this.exitToolStripMenuItem.Click += new System.EventHandler(this.OnExit_Click);
+ //
+ // simulationToolStripMenuItem
+ //
+ this.simulationToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
+ this.startSimulationToolStripMenuItem,
+ this.stopSimulationToolStripMenuItem,
+ this.resetToolStripMenuItem});
+ this.simulationToolStripMenuItem.Name = "simulationToolStripMenuItem";
+ this.simulationToolStripMenuItem.Size = new System.Drawing.Size(94, 20);
+ this.simulationToolStripMenuItem.Text = "시뮬레이션(&S)";
+ //
+ // startSimulationToolStripMenuItem
+ //
+ this.startSimulationToolStripMenuItem.Name = "startSimulationToolStripMenuItem";
+ this.startSimulationToolStripMenuItem.ShortcutKeys = System.Windows.Forms.Keys.F5;
+ this.startSimulationToolStripMenuItem.Size = new System.Drawing.Size(145, 22);
+ this.startSimulationToolStripMenuItem.Text = "시작(&S)";
+ this.startSimulationToolStripMenuItem.Click += new System.EventHandler(this.OnStartSimulation_Click);
+ //
+ // stopSimulationToolStripMenuItem
+ //
+ this.stopSimulationToolStripMenuItem.Name = "stopSimulationToolStripMenuItem";
+ this.stopSimulationToolStripMenuItem.ShortcutKeys = System.Windows.Forms.Keys.F6;
+ this.stopSimulationToolStripMenuItem.Size = new System.Drawing.Size(145, 22);
+ this.stopSimulationToolStripMenuItem.Text = "정지(&T)";
+ this.stopSimulationToolStripMenuItem.Click += new System.EventHandler(this.OnStopSimulation_Click);
+ //
+ // resetToolStripMenuItem
+ //
+ this.resetToolStripMenuItem.Name = "resetToolStripMenuItem";
+ this.resetToolStripMenuItem.ShortcutKeys = System.Windows.Forms.Keys.F7;
+ this.resetToolStripMenuItem.Size = new System.Drawing.Size(145, 22);
+ this.resetToolStripMenuItem.Text = "초기화(&R)";
+ this.resetToolStripMenuItem.Click += new System.EventHandler(this.OnReset_Click);
+ //
+ // viewToolStripMenuItem
+ //
+ this.viewToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
+ this.fitToMapToolStripMenuItem,
+ this.resetZoomToolStripMenuItem});
+ this.viewToolStripMenuItem.Name = "viewToolStripMenuItem";
+ this.viewToolStripMenuItem.Size = new System.Drawing.Size(59, 20);
+ this.viewToolStripMenuItem.Text = "보기(&V)";
+ //
+ // fitToMapToolStripMenuItem
+ //
+ this.fitToMapToolStripMenuItem.Name = "fitToMapToolStripMenuItem";
+ this.fitToMapToolStripMenuItem.ShortcutKeys = ((System.Windows.Forms.Keys)((System.Windows.Forms.Keys.Control | System.Windows.Forms.Keys.F)));
+ this.fitToMapToolStripMenuItem.Size = new System.Drawing.Size(182, 22);
+ this.fitToMapToolStripMenuItem.Text = "맵 맞춤(&F)";
+ this.fitToMapToolStripMenuItem.Click += new System.EventHandler(this.OnFitToMap_Click);
+ //
+ // resetZoomToolStripMenuItem
+ //
+ this.resetZoomToolStripMenuItem.Name = "resetZoomToolStripMenuItem";
+ this.resetZoomToolStripMenuItem.ShortcutKeys = ((System.Windows.Forms.Keys)((System.Windows.Forms.Keys.Control | System.Windows.Forms.Keys.D0)));
+ this.resetZoomToolStripMenuItem.Size = new System.Drawing.Size(182, 22);
+ this.resetZoomToolStripMenuItem.Text = "줌 초기화(&Z)";
+ this.resetZoomToolStripMenuItem.Click += new System.EventHandler(this.OnResetZoom_Click);
+ //
+ // helpToolStripMenuItem
+ //
+ this.helpToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
+ this.aboutToolStripMenuItem});
+ this.helpToolStripMenuItem.Name = "helpToolStripMenuItem";
+ this.helpToolStripMenuItem.Size = new System.Drawing.Size(72, 20);
+ this.helpToolStripMenuItem.Text = "도움말(&H)";
+ //
+ // aboutToolStripMenuItem
+ //
+ this.aboutToolStripMenuItem.Name = "aboutToolStripMenuItem";
+ this.aboutToolStripMenuItem.Size = new System.Drawing.Size(123, 22);
+ this.aboutToolStripMenuItem.Text = "정보(&A)...";
+ this.aboutToolStripMenuItem.Click += new System.EventHandler(this.OnAbout_Click);
+ //
+ // _toolStrip
+ //
+ this._toolStrip.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
+ this.openMapToolStripButton,
+ this.reloadMapToolStripButton,
+ this.launchMapEditorToolStripButton,
+ this.toolStripSeparator2,
+ this.startSimulationToolStripButton,
+ this.stopSimulationToolStripButton,
+ this.resetToolStripButton,
+ this.btAllReset,
+ this.toolStripSeparator3,
+ this.fitToMapToolStripButton,
+ this.resetZoomToolStripButton});
+ this._toolStrip.Location = new System.Drawing.Point(0, 24);
+ this._toolStrip.Name = "_toolStrip";
+ this._toolStrip.Size = new System.Drawing.Size(1200, 25);
+ this._toolStrip.TabIndex = 1;
+ this._toolStrip.Text = "toolStrip";
+ //
+ // openMapToolStripButton
+ //
+ this.openMapToolStripButton.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
+ this.openMapToolStripButton.Name = "openMapToolStripButton";
+ this.openMapToolStripButton.Size = new System.Drawing.Size(51, 22);
+ this.openMapToolStripButton.Text = "맵 열기";
+ this.openMapToolStripButton.ToolTipText = "맵 파일을 엽니다";
+ this.openMapToolStripButton.Click += new System.EventHandler(this.OnOpenMap_Click);
+ //
+ // reloadMapToolStripButton
+ //
+ this.reloadMapToolStripButton.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
+ this.reloadMapToolStripButton.Name = "reloadMapToolStripButton";
+ this.reloadMapToolStripButton.Size = new System.Drawing.Size(63, 22);
+ this.reloadMapToolStripButton.Text = "다시열기";
+ this.reloadMapToolStripButton.ToolTipText = "현재 맵을 다시 로드합니다";
+ this.reloadMapToolStripButton.Click += new System.EventHandler(this.OnReloadMap_Click);
+ //
+ // launchMapEditorToolStripButton
+ //
+ this.launchMapEditorToolStripButton.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
+ this.launchMapEditorToolStripButton.Name = "launchMapEditorToolStripButton";
+ this.launchMapEditorToolStripButton.Size = new System.Drawing.Size(71, 22);
+ this.launchMapEditorToolStripButton.Text = "MapEditor";
+ this.launchMapEditorToolStripButton.ToolTipText = "MapEditor를 실행합니다";
+ this.launchMapEditorToolStripButton.Click += new System.EventHandler(this.OnLaunchMapEditor_Click);
+ //
+ // toolStripSeparator2
+ //
+ this.toolStripSeparator2.Name = "toolStripSeparator2";
+ this.toolStripSeparator2.Size = new System.Drawing.Size(6, 25);
+ //
+ // startSimulationToolStripButton
+ //
+ this.startSimulationToolStripButton.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
+ this.startSimulationToolStripButton.Name = "startSimulationToolStripButton";
+ this.startSimulationToolStripButton.Size = new System.Drawing.Size(99, 22);
+ this.startSimulationToolStripButton.Text = "시뮬레이션 시작";
+ this.startSimulationToolStripButton.ToolTipText = "시뮬레이션을 시작합니다";
+ this.startSimulationToolStripButton.Click += new System.EventHandler(this.OnStartSimulation_Click);
+ //
+ // stopSimulationToolStripButton
+ //
+ this.stopSimulationToolStripButton.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
+ this.stopSimulationToolStripButton.Name = "stopSimulationToolStripButton";
+ this.stopSimulationToolStripButton.Size = new System.Drawing.Size(99, 22);
+ this.stopSimulationToolStripButton.Text = "시뮬레이션 정지";
+ this.stopSimulationToolStripButton.ToolTipText = "시뮬레이션을 정지합니다";
+ this.stopSimulationToolStripButton.Click += new System.EventHandler(this.OnStopSimulation_Click);
+ //
+ // resetToolStripButton
+ //
+ this.resetToolStripButton.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
+ this.resetToolStripButton.Name = "resetToolStripButton";
+ this.resetToolStripButton.Size = new System.Drawing.Size(47, 22);
+ this.resetToolStripButton.Text = "초기화";
+ this.resetToolStripButton.ToolTipText = "시뮬레이션을 초기화합니다";
+ this.resetToolStripButton.Click += new System.EventHandler(this.OnReset_Click);
+ //
+ // toolStripSeparator3
+ //
+ this.toolStripSeparator3.Name = "toolStripSeparator3";
+ this.toolStripSeparator3.Size = new System.Drawing.Size(6, 25);
+ //
+ // fitToMapToolStripButton
+ //
+ this.fitToMapToolStripButton.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
+ this.fitToMapToolStripButton.Name = "fitToMapToolStripButton";
+ this.fitToMapToolStripButton.Size = new System.Drawing.Size(51, 22);
+ this.fitToMapToolStripButton.Text = "맵 맞춤";
+ this.fitToMapToolStripButton.ToolTipText = "맵 전체를 화면에 맞춥니다";
+ this.fitToMapToolStripButton.Click += new System.EventHandler(this.OnFitToMap_Click);
+ //
+ // resetZoomToolStripButton
+ //
+ this.resetZoomToolStripButton.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
+ this.resetZoomToolStripButton.Name = "resetZoomToolStripButton";
+ this.resetZoomToolStripButton.Size = new System.Drawing.Size(63, 22);
+ this.resetZoomToolStripButton.Text = "줌 초기화";
+ this.resetZoomToolStripButton.ToolTipText = "줌을 초기화합니다";
+ this.resetZoomToolStripButton.Click += new System.EventHandler(this.OnResetZoom_Click);
+ //
+ // _statusStrip
+ //
+ this._statusStrip.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
+ this._statusLabel,
+ this._coordLabel});
+ this._statusStrip.Location = new System.Drawing.Point(0, 778);
+ this._statusStrip.Name = "_statusStrip";
+ this._statusStrip.Size = new System.Drawing.Size(1200, 22);
+ this._statusStrip.TabIndex = 2;
+ this._statusStrip.Text = "statusStrip";
+ //
+ // _statusLabel
+ //
+ this._statusLabel.Name = "_statusLabel";
+ this._statusLabel.Size = new System.Drawing.Size(31, 17);
+ this._statusLabel.Text = "준비";
+ //
+ // _coordLabel
+ //
+ this._coordLabel.Name = "_coordLabel";
+ this._coordLabel.Size = new System.Drawing.Size(0, 17);
+ //
+ // _controlPanel
+ //
+ this._controlPanel.BackColor = System.Drawing.SystemColors.Control;
+ this._controlPanel.Controls.Add(this._statusGroup);
+ 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.Name = "_controlPanel";
+ this._controlPanel.Size = new System.Drawing.Size(250, 729);
+ this._controlPanel.TabIndex = 3;
+ //
+ // _statusGroup
+ //
+ 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.Name = "_statusGroup";
+ this._statusGroup.Size = new System.Drawing.Size(230, 100);
+ this._statusGroup.TabIndex = 3;
+ this._statusGroup.TabStop = false;
+ this._statusGroup.Text = "상태 정보";
+ //
+ // _pathLengthLabel
+ //
+ this._pathLengthLabel.AutoSize = true;
+ this._pathLengthLabel.Location = new System.Drawing.Point(10, 65);
+ this._pathLengthLabel.Name = "_pathLengthLabel";
+ this._pathLengthLabel.Size = new System.Drawing.Size(71, 12);
+ this._pathLengthLabel.TabIndex = 2;
+ this._pathLengthLabel.Text = "경로 길이: -";
+ //
+ // _agvCountLabel
+ //
+ this._agvCountLabel.AutoSize = true;
+ this._agvCountLabel.Location = new System.Drawing.Point(10, 45);
+ this._agvCountLabel.Name = "_agvCountLabel";
+ this._agvCountLabel.Size = new System.Drawing.Size(60, 12);
+ this._agvCountLabel.TabIndex = 1;
+ this._agvCountLabel.Text = "AGV 수: 0";
+ //
+ // _simulationStatusLabel
+ //
+ this._simulationStatusLabel.AutoSize = true;
+ this._simulationStatusLabel.Location = new System.Drawing.Point(10, 25);
+ this._simulationStatusLabel.Name = "_simulationStatusLabel";
+ this._simulationStatusLabel.Size = new System.Drawing.Size(97, 12);
+ this._simulationStatusLabel.TabIndex = 0;
+ this._simulationStatusLabel.Text = "시뮬레이션: 정지";
+ //
+ // _pathGroup
+ //
+ this._pathGroup.Controls.Add(this._clearPathButton);
+ this._pathGroup.Controls.Add(this._startPathButton);
+ this._pathGroup.Controls.Add(this._calculatePathButton);
+ 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.Name = "_pathGroup";
+ this._pathGroup.Size = new System.Drawing.Size(230, 150);
+ this._pathGroup.TabIndex = 1;
+ this._pathGroup.TabStop = false;
+ this._pathGroup.Text = "경로 제어";
+ //
+ // _clearPathButton
+ //
+ this._clearPathButton.Location = new System.Drawing.Point(150, 120);
+ this._clearPathButton.Name = "_clearPathButton";
+ this._clearPathButton.Size = new System.Drawing.Size(70, 25);
+ this._clearPathButton.TabIndex = 6;
+ this._clearPathButton.Text = "경로 지우기";
+ this._clearPathButton.UseVisualStyleBackColor = true;
+ this._clearPathButton.Click += new System.EventHandler(this.OnClearPath_Click);
+ //
+ // _startPathButton
+ //
+ this._startPathButton.Location = new System.Drawing.Point(80, 120);
+ this._startPathButton.Name = "_startPathButton";
+ this._startPathButton.Size = new System.Drawing.Size(65, 25);
+ this._startPathButton.TabIndex = 5;
+ this._startPathButton.Text = "경로 시작";
+ this._startPathButton.UseVisualStyleBackColor = true;
+ this._startPathButton.Click += new System.EventHandler(this.OnStartPath_Click);
+ //
+ // _calculatePathButton
+ //
+ this._calculatePathButton.Location = new System.Drawing.Point(10, 120);
+ this._calculatePathButton.Name = "_calculatePathButton";
+ this._calculatePathButton.Size = new System.Drawing.Size(65, 25);
+ this._calculatePathButton.TabIndex = 4;
+ this._calculatePathButton.Text = "경로 계산";
+ this._calculatePathButton.UseVisualStyleBackColor = true;
+ this._calculatePathButton.Click += new System.EventHandler(this.OnCalculatePath_Click);
+ //
+ // _targetNodeCombo
+ //
+ this._targetNodeCombo.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
+ this._targetNodeCombo.Location = new System.Drawing.Point(10, 95);
+ this._targetNodeCombo.Name = "_targetNodeCombo";
+ this._targetNodeCombo.Size = new System.Drawing.Size(210, 20);
+ this._targetNodeCombo.TabIndex = 3;
+ //
+ // targetNodeLabel
+ //
+ this.targetNodeLabel.AutoSize = true;
+ this.targetNodeLabel.Location = new System.Drawing.Point(10, 75);
+ this.targetNodeLabel.Name = "targetNodeLabel";
+ this.targetNodeLabel.Size = new System.Drawing.Size(63, 12);
+ this.targetNodeLabel.TabIndex = 2;
+ this.targetNodeLabel.Text = "목표 RFID:";
+ //
+ // _startNodeCombo
+ //
+ this._startNodeCombo.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
+ this._startNodeCombo.Location = new System.Drawing.Point(10, 45);
+ this._startNodeCombo.Name = "_startNodeCombo";
+ this._startNodeCombo.Size = new System.Drawing.Size(210, 20);
+ this._startNodeCombo.TabIndex = 1;
+ //
+ // startNodeLabel
+ //
+ this.startNodeLabel.AutoSize = true;
+ this.startNodeLabel.Location = new System.Drawing.Point(10, 25);
+ this.startNodeLabel.Name = "startNodeLabel";
+ this.startNodeLabel.Size = new System.Drawing.Size(63, 12);
+ this.startNodeLabel.TabIndex = 0;
+ this.startNodeLabel.Text = "시작 RFID:";
+ //
+ // _agvControlGroup
+ //
+ this._agvControlGroup.Controls.Add(this._setPositionButton);
+ this._agvControlGroup.Controls.Add(this._rfidTextBox);
+ this._agvControlGroup.Controls.Add(this._rfidLabel);
+ 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.Name = "_agvControlGroup";
+ this._agvControlGroup.Size = new System.Drawing.Size(230, 180);
+ this._agvControlGroup.TabIndex = 0;
+ this._agvControlGroup.TabStop = false;
+ this._agvControlGroup.Text = "AGV 제어";
+ //
+ // _setPositionButton
+ //
+ this._setPositionButton.Location = new System.Drawing.Point(160, 138);
+ this._setPositionButton.Name = "_setPositionButton";
+ this._setPositionButton.Size = new System.Drawing.Size(60, 25);
+ this._setPositionButton.TabIndex = 7;
+ this._setPositionButton.Text = "위치설정";
+ this._setPositionButton.UseVisualStyleBackColor = true;
+ this._setPositionButton.Click += new System.EventHandler(this.OnSetPosition_Click);
+ //
+ // _rfidTextBox
+ //
+ this._rfidTextBox.Location = new System.Drawing.Point(10, 140);
+ this._rfidTextBox.Name = "_rfidTextBox";
+ this._rfidTextBox.Size = new System.Drawing.Size(140, 21);
+ this._rfidTextBox.TabIndex = 6;
+ this._rfidTextBox.KeyPress += new System.Windows.Forms.KeyPressEventHandler(this.OnRfidTextBox_KeyPress);
+ //
+ // _rfidLabel
+ //
+ this._rfidLabel.AutoSize = true;
+ this._rfidLabel.Location = new System.Drawing.Point(10, 120);
+ this._rfidLabel.Name = "_rfidLabel";
+ this._rfidLabel.Size = new System.Drawing.Size(87, 12);
+ this._rfidLabel.TabIndex = 5;
+ this._rfidLabel.Text = "RFID 현재위치:";
+ //
+ // _stopSimulationButton
+ //
+ this._stopSimulationButton.Location = new System.Drawing.Point(120, 85);
+ this._stopSimulationButton.Name = "_stopSimulationButton";
+ this._stopSimulationButton.Size = new System.Drawing.Size(100, 25);
+ this._stopSimulationButton.TabIndex = 4;
+ this._stopSimulationButton.Text = "시뮬레이션 정지";
+ this._stopSimulationButton.UseVisualStyleBackColor = true;
+ this._stopSimulationButton.Click += new System.EventHandler(this.OnStopSimulation_Click);
+ //
+ // _startSimulationButton
+ //
+ this._startSimulationButton.Location = new System.Drawing.Point(10, 85);
+ this._startSimulationButton.Name = "_startSimulationButton";
+ this._startSimulationButton.Size = new System.Drawing.Size(100, 25);
+ this._startSimulationButton.TabIndex = 3;
+ this._startSimulationButton.Text = "시뮬레이션 시작";
+ this._startSimulationButton.UseVisualStyleBackColor = true;
+ this._startSimulationButton.Click += new System.EventHandler(this.OnStartSimulation_Click);
+ //
+ // _removeAgvButton
+ //
+ this._removeAgvButton.Location = new System.Drawing.Point(120, 55);
+ this._removeAgvButton.Name = "_removeAgvButton";
+ this._removeAgvButton.Size = new System.Drawing.Size(100, 25);
+ this._removeAgvButton.TabIndex = 2;
+ this._removeAgvButton.Text = "AGV 제거";
+ this._removeAgvButton.UseVisualStyleBackColor = true;
+ this._removeAgvButton.Click += new System.EventHandler(this.OnRemoveAGV_Click);
+ //
+ // _addAgvButton
+ //
+ this._addAgvButton.Location = new System.Drawing.Point(10, 55);
+ this._addAgvButton.Name = "_addAgvButton";
+ this._addAgvButton.Size = new System.Drawing.Size(100, 25);
+ this._addAgvButton.TabIndex = 1;
+ this._addAgvButton.Text = "AGV 추가";
+ this._addAgvButton.UseVisualStyleBackColor = true;
+ this._addAgvButton.Click += new System.EventHandler(this.OnAddAGV_Click);
+ //
+ // _agvListCombo
+ //
+ this._agvListCombo.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
+ this._agvListCombo.Location = new System.Drawing.Point(10, 25);
+ this._agvListCombo.Name = "_agvListCombo";
+ this._agvListCombo.Size = new System.Drawing.Size(210, 20);
+ this._agvListCombo.TabIndex = 0;
+ this._agvListCombo.SelectedIndexChanged += new System.EventHandler(this.OnAGVList_SelectedIndexChanged);
+ //
+ // _canvasPanel
+ //
+ this._canvasPanel.Dock = System.Windows.Forms.DockStyle.Fill;
+ this._canvasPanel.Location = new System.Drawing.Point(0, 49);
+ this._canvasPanel.Name = "_canvasPanel";
+ this._canvasPanel.Size = new System.Drawing.Size(950, 729);
+ this._canvasPanel.TabIndex = 4;
+ //
+ // 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);
+ //
// SimulatorForm
//
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 12F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(1200, 800);
+ this.Controls.Add(this._canvasPanel);
+ this.Controls.Add(this._controlPanel);
+ this.Controls.Add(this._statusStrip);
+ this.Controls.Add(this._toolStrip);
+ this.Controls.Add(this._menuStrip);
+ this.MainMenuStrip = this._menuStrip;
this.Name = "SimulatorForm";
+ this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen;
this.Text = "AGV 시뮬레이터";
this.WindowState = System.Windows.Forms.FormWindowState.Maximized;
+ this._menuStrip.ResumeLayout(false);
+ this._menuStrip.PerformLayout();
+ this._toolStrip.ResumeLayout(false);
+ this._toolStrip.PerformLayout();
+ this._statusStrip.ResumeLayout(false);
+ this._statusStrip.PerformLayout();
+ this._controlPanel.ResumeLayout(false);
+ this._statusGroup.ResumeLayout(false);
+ this._statusGroup.PerformLayout();
+ this._pathGroup.ResumeLayout(false);
+ this._pathGroup.PerformLayout();
+ this._agvControlGroup.ResumeLayout(false);
+ this._agvControlGroup.PerformLayout();
this.ResumeLayout(false);
+ this.PerformLayout();
}
#endregion
+
+ private System.Windows.Forms.MenuStrip _menuStrip;
+ private System.Windows.Forms.ToolStripMenuItem fileToolStripMenuItem;
+ private System.Windows.Forms.ToolStripMenuItem openMapToolStripMenuItem;
+ private System.Windows.Forms.ToolStripSeparator toolStripSeparator1;
+ private System.Windows.Forms.ToolStripMenuItem exitToolStripMenuItem;
+ private System.Windows.Forms.ToolStripMenuItem simulationToolStripMenuItem;
+ private System.Windows.Forms.ToolStripMenuItem startSimulationToolStripMenuItem;
+ private System.Windows.Forms.ToolStripMenuItem stopSimulationToolStripMenuItem;
+ private System.Windows.Forms.ToolStripMenuItem resetToolStripMenuItem;
+ private System.Windows.Forms.ToolStripMenuItem viewToolStripMenuItem;
+ private System.Windows.Forms.ToolStripMenuItem fitToMapToolStripMenuItem;
+ private System.Windows.Forms.ToolStripMenuItem resetZoomToolStripMenuItem;
+ private System.Windows.Forms.ToolStripMenuItem helpToolStripMenuItem;
+ private System.Windows.Forms.ToolStripMenuItem aboutToolStripMenuItem;
+ private System.Windows.Forms.ToolStrip _toolStrip;
+ private System.Windows.Forms.ToolStripButton openMapToolStripButton;
+ private System.Windows.Forms.ToolStripSeparator toolStripSeparator2;
+ private System.Windows.Forms.ToolStripButton startSimulationToolStripButton;
+ private System.Windows.Forms.ToolStripButton stopSimulationToolStripButton;
+ private System.Windows.Forms.ToolStripButton resetToolStripButton;
+ private System.Windows.Forms.ToolStripSeparator toolStripSeparator3;
+ private System.Windows.Forms.ToolStripButton fitToMapToolStripButton;
+ private System.Windows.Forms.ToolStripButton resetZoomToolStripButton;
+ private System.Windows.Forms.StatusStrip _statusStrip;
+ private System.Windows.Forms.ToolStripStatusLabel _statusLabel;
+ private System.Windows.Forms.ToolStripStatusLabel _coordLabel;
+ private System.Windows.Forms.Panel _controlPanel;
+ private System.Windows.Forms.GroupBox _agvControlGroup;
+ private System.Windows.Forms.ComboBox _agvListCombo;
+ private System.Windows.Forms.Button _addAgvButton;
+ private System.Windows.Forms.Button _removeAgvButton;
+ private System.Windows.Forms.Button _startSimulationButton;
+ private System.Windows.Forms.Button _stopSimulationButton;
+ private System.Windows.Forms.GroupBox _pathGroup;
+ private System.Windows.Forms.Label startNodeLabel;
+ private System.Windows.Forms.ComboBox _startNodeCombo;
+ private System.Windows.Forms.Label targetNodeLabel;
+ private System.Windows.Forms.ComboBox _targetNodeCombo;
+ private System.Windows.Forms.Button _calculatePathButton;
+ private System.Windows.Forms.Button _startPathButton;
+ private System.Windows.Forms.Button _clearPathButton;
+ private System.Windows.Forms.GroupBox _statusGroup;
+ private System.Windows.Forms.Label _simulationStatusLabel;
+ private System.Windows.Forms.Label _agvCountLabel;
+ private System.Windows.Forms.Label _pathLengthLabel;
+ private System.Windows.Forms.Panel _canvasPanel;
+ private System.Windows.Forms.Label _rfidLabel;
+ private System.Windows.Forms.TextBox _rfidTextBox;
+ private System.Windows.Forms.Button _setPositionButton;
+ 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;
}
}
\ No newline at end of file
diff --git a/Cs_HMI/AGVSimulator/Forms/SimulatorForm.cs b/Cs_HMI/AGVSimulator/Forms/SimulatorForm.cs
index b0c3baa..299d7c1 100644
--- a/Cs_HMI/AGVSimulator/Forms/SimulatorForm.cs
+++ b/Cs_HMI/AGVSimulator/Forms/SimulatorForm.cs
@@ -1,11 +1,13 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Windows.Forms;
using AGVMapEditor.Models;
-using AGVSimulator.Controls;
+using AGVNavigationCore.Models;
+using AGVNavigationCore.Controls;
using AGVSimulator.Models;
using Newtonsoft.Json;
@@ -18,50 +20,17 @@ namespace AGVSimulator.Forms
{
#region Fields
- private SimulatorCanvas _simulatorCanvas;
+ private UnifiedAGVCanvas _simulatorCanvas;
private List _mapNodes;
- private List _rfidMappings;
private NodeResolver _nodeResolver;
private PathCalculator _pathCalculator;
private List _agvList;
private SimulationState _simulationState;
private Timer _simulationTimer;
+ private SimulatorConfig _config;
+ private string _currentMapFilePath;
- // UI Controls
- private MenuStrip _menuStrip;
- private ToolStrip _toolStrip;
- private StatusStrip _statusStrip;
- private Panel _controlPanel;
- private Panel _canvasPanel;
-
- // Control Panel Controls
- private GroupBox _agvControlGroup;
- private ComboBox _agvListCombo;
- private Button _addAgvButton;
- private Button _removeAgvButton;
- private Button _startSimulationButton;
- private Button _stopSimulationButton;
- private Button _resetButton;
-
- private GroupBox _pathGroup;
- private ComboBox _startNodeCombo;
- private ComboBox _targetNodeCombo;
- private Button _calculatePathButton;
- private Button _startPathButton;
- private Button _clearPathButton;
-
- private GroupBox _viewGroup;
- private Button _fitToMapButton;
- private Button _resetZoomButton;
-
- private GroupBox _statusGroup;
- private Label _simulationStatusLabel;
- private Label _agvCountLabel;
- private Label _pathLengthLabel;
-
- // Status Labels
- private ToolStripStatusLabel _statusLabel;
- private ToolStripStatusLabel _coordLabel;
+ // UI Controls - Designer에서 생성됨
#endregion
@@ -88,27 +57,18 @@ namespace AGVSimulator.Forms
private void InitializeForm()
{
- // 폼 설정
- Text = "AGV 시뮬레이터";
- Size = new Size(1200, 800);
- StartPosition = FormStartPosition.CenterScreen;
+ // 설정 로드
+ _config = SimulatorConfig.Load();
// 데이터 초기화
_mapNodes = new List();
- _rfidMappings = new List();
_agvList = new List();
_simulationState = new SimulationState();
+ _currentMapFilePath = string.Empty;
- // UI 컨트롤 생성
- CreateMenuStrip();
- CreateToolStrip();
- CreateStatusStrip();
- CreateControlPanel();
+ // 시뮬레이터 캔버스 생성 (중앙 패널에만)
CreateSimulatorCanvas();
- // 레이아웃 설정
- SetupLayout();
-
// 타이머 초기화
_simulationTimer = new Timer();
_simulationTimer.Interval = 100; // 100ms 간격
@@ -118,260 +78,21 @@ namespace AGVSimulator.Forms
UpdateUI();
}
- private void CreateMenuStrip()
- {
- _menuStrip = new MenuStrip();
-
- // 파일 메뉴
- var fileMenu = new ToolStripMenuItem("파일(&F)");
- fileMenu.DropDownItems.Add(new ToolStripMenuItem("맵 열기(&O)...", null, OnOpenMap_Click) { ShortcutKeys = Keys.Control | Keys.O });
- fileMenu.DropDownItems.Add(new ToolStripSeparator());
- fileMenu.DropDownItems.Add(new ToolStripMenuItem("종료(&X)", null, OnExit_Click) { ShortcutKeys = Keys.Alt | Keys.F4 });
-
- // 시뮬레이션 메뉴
- var simMenu = new ToolStripMenuItem("시뮬레이션(&S)");
- simMenu.DropDownItems.Add(new ToolStripMenuItem("시작(&S)", null, OnStartSimulation_Click) { ShortcutKeys = Keys.F5 });
- simMenu.DropDownItems.Add(new ToolStripMenuItem("정지(&T)", null, OnStopSimulation_Click) { ShortcutKeys = Keys.F6 });
- simMenu.DropDownItems.Add(new ToolStripMenuItem("초기화(&R)", null, OnReset_Click) { ShortcutKeys = Keys.F7 });
-
- // 보기 메뉴
- var viewMenu = new ToolStripMenuItem("보기(&V)");
- viewMenu.DropDownItems.Add(new ToolStripMenuItem("맵 맞춤(&F)", null, OnFitToMap_Click) { ShortcutKeys = Keys.Control | Keys.F });
- viewMenu.DropDownItems.Add(new ToolStripMenuItem("줌 초기화(&Z)", null, OnResetZoom_Click) { ShortcutKeys = Keys.Control | Keys.D0 });
-
- // 도움말 메뉴
- var helpMenu = new ToolStripMenuItem("도움말(&H)");
- helpMenu.DropDownItems.Add(new ToolStripMenuItem("정보(&A)...", null, OnAbout_Click));
-
- _menuStrip.Items.AddRange(new ToolStripItem[] { fileMenu, simMenu, viewMenu, helpMenu });
- Controls.Add(_menuStrip);
- MainMenuStrip = _menuStrip;
- }
- private void CreateToolStrip()
- {
- _toolStrip = new ToolStrip();
-
- _toolStrip.Items.Add(new ToolStripButton("맵 열기", null, OnOpenMap_Click) { ToolTipText = "맵 파일을 엽니다" });
- _toolStrip.Items.Add(new ToolStripSeparator());
- _toolStrip.Items.Add(new ToolStripButton("시뮬레이션 시작", null, OnStartSimulation_Click) { ToolTipText = "시뮬레이션을 시작합니다" });
- _toolStrip.Items.Add(new ToolStripButton("시뮬레이션 정지", null, OnStopSimulation_Click) { ToolTipText = "시뮬레이션을 정지합니다" });
- _toolStrip.Items.Add(new ToolStripButton("초기화", null, OnReset_Click) { ToolTipText = "시뮬레이션을 초기화합니다" });
- _toolStrip.Items.Add(new ToolStripSeparator());
- _toolStrip.Items.Add(new ToolStripButton("맵 맞춤", null, OnFitToMap_Click) { ToolTipText = "맵 전체를 화면에 맞춥니다" });
- _toolStrip.Items.Add(new ToolStripButton("줌 초기화", null, OnResetZoom_Click) { ToolTipText = "줌을 초기화합니다" });
-
- Controls.Add(_toolStrip);
- }
-
- private void CreateStatusStrip()
- {
- _statusStrip = new StatusStrip();
-
- _statusLabel = new ToolStripStatusLabel("준비");
- _coordLabel = new ToolStripStatusLabel();
-
- _statusStrip.Items.AddRange(new ToolStripItem[] { _statusLabel, _coordLabel });
- Controls.Add(_statusStrip);
- }
-
- private void CreateControlPanel()
- {
- _controlPanel = new Panel();
- _controlPanel.Width = 250;
- _controlPanel.Dock = DockStyle.Right;
- _controlPanel.BackColor = SystemColors.Control;
-
- // AGV 제어 그룹
- CreateAGVControlGroup();
-
- // 경로 제어 그룹
- CreatePathControlGroup();
-
- // 뷰 제어 그룹
- CreateViewControlGroup();
-
- // 상태 그룹
- CreateStatusGroup();
-
- Controls.Add(_controlPanel);
- }
-
- private void CreateAGVControlGroup()
- {
- _agvControlGroup = new GroupBox();
- _agvControlGroup.Text = "AGV 제어";
- _agvControlGroup.Location = new Point(10, 10);
- _agvControlGroup.Size = new Size(230, 120);
-
- _agvListCombo = new ComboBox();
- _agvListCombo.DropDownStyle = ComboBoxStyle.DropDownList;
- _agvListCombo.Location = new Point(10, 25);
- _agvListCombo.Size = new Size(210, 21);
- _agvListCombo.SelectedIndexChanged += OnAGVList_SelectedIndexChanged;
-
- _addAgvButton = new Button();
- _addAgvButton.Text = "AGV 추가";
- _addAgvButton.Location = new Point(10, 55);
- _addAgvButton.Size = new Size(100, 25);
- _addAgvButton.Click += OnAddAGV_Click;
-
- _removeAgvButton = new Button();
- _removeAgvButton.Text = "AGV 제거";
- _removeAgvButton.Location = new Point(120, 55);
- _removeAgvButton.Size = new Size(100, 25);
- _removeAgvButton.Click += OnRemoveAGV_Click;
-
- _startSimulationButton = new Button();
- _startSimulationButton.Text = "시뮬레이션 시작";
- _startSimulationButton.Location = new Point(10, 85);
- _startSimulationButton.Size = new Size(100, 25);
- _startSimulationButton.Click += OnStartSimulation_Click;
-
- _stopSimulationButton = new Button();
- _stopSimulationButton.Text = "시뮬레이션 정지";
- _stopSimulationButton.Location = new Point(120, 85);
- _stopSimulationButton.Size = new Size(100, 25);
- _stopSimulationButton.Click += OnStopSimulation_Click;
-
- _agvControlGroup.Controls.AddRange(new Control[] {
- _agvListCombo, _addAgvButton, _removeAgvButton, _startSimulationButton, _stopSimulationButton
- });
-
- _controlPanel.Controls.Add(_agvControlGroup);
- }
-
- private void CreatePathControlGroup()
- {
- _pathGroup = new GroupBox();
- _pathGroup.Text = "경로 제어";
- _pathGroup.Location = new Point(10, 140);
- _pathGroup.Size = new Size(230, 150);
-
- var startLabel = new Label();
- startLabel.Text = "시작 노드:";
- startLabel.Location = new Point(10, 25);
- startLabel.Size = new Size(70, 15);
-
- _startNodeCombo = new ComboBox();
- _startNodeCombo.DropDownStyle = ComboBoxStyle.DropDownList;
- _startNodeCombo.Location = new Point(10, 45);
- _startNodeCombo.Size = new Size(210, 21);
-
- var targetLabel = new Label();
- targetLabel.Text = "목표 노드:";
- targetLabel.Location = new Point(10, 75);
- targetLabel.Size = new Size(70, 15);
-
- _targetNodeCombo = new ComboBox();
- _targetNodeCombo.DropDownStyle = ComboBoxStyle.DropDownList;
- _targetNodeCombo.Location = new Point(10, 95);
- _targetNodeCombo.Size = new Size(210, 21);
-
- _calculatePathButton = new Button();
- _calculatePathButton.Text = "경로 계산";
- _calculatePathButton.Location = new Point(10, 120);
- _calculatePathButton.Size = new Size(65, 25);
- _calculatePathButton.Click += OnCalculatePath_Click;
-
- _startPathButton = new Button();
- _startPathButton.Text = "경로 시작";
- _startPathButton.Location = new Point(80, 120);
- _startPathButton.Size = new Size(65, 25);
- _startPathButton.Click += OnStartPath_Click;
-
- _clearPathButton = new Button();
- _clearPathButton.Text = "경로 지우기";
- _clearPathButton.Location = new Point(150, 120);
- _clearPathButton.Size = new Size(70, 25);
- _clearPathButton.Click += OnClearPath_Click;
-
- _pathGroup.Controls.AddRange(new Control[] {
- startLabel, _startNodeCombo, targetLabel, _targetNodeCombo,
- _calculatePathButton, _startPathButton, _clearPathButton
- });
-
- _controlPanel.Controls.Add(_pathGroup);
- }
-
- private void CreateViewControlGroup()
- {
- _viewGroup = new GroupBox();
- _viewGroup.Text = "화면 제어";
- _viewGroup.Location = new Point(10, 300);
- _viewGroup.Size = new Size(230, 60);
-
- _fitToMapButton = new Button();
- _fitToMapButton.Text = "맵 맞춤";
- _fitToMapButton.Location = new Point(10, 25);
- _fitToMapButton.Size = new Size(100, 25);
- _fitToMapButton.Click += OnFitToMap_Click;
-
- _resetZoomButton = new Button();
- _resetZoomButton.Text = "줌 초기화";
- _resetZoomButton.Location = new Point(120, 25);
- _resetZoomButton.Size = new Size(100, 25);
- _resetZoomButton.Click += OnResetZoom_Click;
-
- _resetButton = new Button();
- _resetButton.Text = "전체 초기화";
- _resetButton.Location = new Point(65, 55);
- _resetButton.Size = new Size(100, 25);
- _resetButton.Click += OnReset_Click;
-
- _viewGroup.Controls.AddRange(new Control[] { _fitToMapButton, _resetZoomButton });
-
- _controlPanel.Controls.Add(_viewGroup);
- }
-
- private void CreateStatusGroup()
- {
- _statusGroup = new GroupBox();
- _statusGroup.Text = "상태 정보";
- _statusGroup.Location = new Point(10, 370);
- _statusGroup.Size = new Size(230, 100);
-
- _simulationStatusLabel = new Label();
- _simulationStatusLabel.Text = "시뮬레이션: 정지";
- _simulationStatusLabel.Location = new Point(10, 25);
- _simulationStatusLabel.Size = new Size(210, 15);
-
- _agvCountLabel = new Label();
- _agvCountLabel.Text = "AGV 수: 0";
- _agvCountLabel.Location = new Point(10, 45);
- _agvCountLabel.Size = new Size(210, 15);
-
- _pathLengthLabel = new Label();
- _pathLengthLabel.Text = "경로 길이: -";
- _pathLengthLabel.Location = new Point(10, 65);
- _pathLengthLabel.Size = new Size(210, 15);
-
- _statusGroup.Controls.AddRange(new Control[] {
- _simulationStatusLabel, _agvCountLabel, _pathLengthLabel
- });
-
- _controlPanel.Controls.Add(_statusGroup);
- }
private void CreateSimulatorCanvas()
{
- _canvasPanel = new Panel();
- _canvasPanel.Dock = DockStyle.Fill;
-
- _simulatorCanvas = new SimulatorCanvas();
+ _simulatorCanvas = new UnifiedAGVCanvas();
_simulatorCanvas.Dock = DockStyle.Fill;
+ _simulatorCanvas.Mode = UnifiedAGVCanvas.CanvasMode.ViewOnly;
_canvasPanel.Controls.Add(_simulatorCanvas);
- Controls.Add(_canvasPanel);
}
private void SetupLayout()
{
- // Z-Order 설정
+ // Z-Order 설정 - 모든 컨트롤이 디자이너에 구현되어 자동 관리됨
_canvasPanel.BringToFront();
- _controlPanel.BringToFront();
- _toolStrip.BringToFront();
- _menuStrip.BringToFront();
}
#endregion
@@ -382,7 +103,7 @@ namespace AGVSimulator.Forms
{
using (var openDialog = new OpenFileDialog())
{
- openDialog.Filter = "맵 파일 (*.json)|*.json|모든 파일 (*.*)|*.*";
+ openDialog.Filter = "AGV Map Files (*.agvmap)|*.agvmap|모든 파일 (*.*)|*.*";
openDialog.Title = "맵 파일 열기";
if (openDialog.ShowDialog() == DialogResult.OK)
@@ -430,30 +151,12 @@ namespace AGVSimulator.Forms
private void OnReset_Click(object sender, EventArgs e)
{
- // 시뮬레이션 정지
- if (_simulationState.IsRunning)
- {
- OnStopSimulation_Click(sender, e);
- }
- // AGV 초기화
- _simulatorCanvas.ClearAGVs();
- _agvList.Clear();
-
- // 경로 초기화
- _simulatorCanvas.CurrentPath = null;
-
- // UI 업데이트
- UpdateAGVComboBox();
- UpdateNodeComboBoxes();
- UpdateUI();
-
- _statusLabel.Text = "초기화 완료";
}
private void OnFitToMap_Click(object sender, EventArgs e)
{
- _simulatorCanvas.FitToMap();
+ _simulatorCanvas.FitToNodes();
}
private void OnResetZoom_Click(object sender, EventArgs e)
@@ -480,7 +183,7 @@ namespace AGVSimulator.Forms
var newAGV = new VirtualAGV(agvId, startPosition);
_agvList.Add(newAGV);
- _simulatorCanvas.AddAGV(newAGV);
+ _simulatorCanvas.AGVList = new List(_agvList.Cast());
UpdateAGVComboBox();
UpdateUI();
@@ -496,8 +199,8 @@ namespace AGVSimulator.Forms
var selectedAGV = _agvListCombo.SelectedItem as VirtualAGV;
if (selectedAGV != null)
{
- _simulatorCanvas.RemoveAGV(selectedAGV.AgvId);
_agvList.Remove(selectedAGV);
+ _simulatorCanvas.AGVList = new List(_agvList.Cast());
UpdateAGVComboBox();
UpdateUI();
@@ -515,7 +218,7 @@ namespace AGVSimulator.Forms
{
if (_startNodeCombo.SelectedItem == null || _targetNodeCombo.SelectedItem == null)
{
- MessageBox.Show("시작 노드와 목표 노드를 선택해주세요.", "알림", MessageBoxButtons.OK, MessageBoxIcon.Information);
+ MessageBox.Show("시작 RFID와 목표 RFID를 선택해주세요.", "알림", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
@@ -524,20 +227,21 @@ namespace AGVSimulator.Forms
if (_pathCalculator == null)
{
- _pathCalculator = new PathCalculator(_mapNodes, _nodeResolver);
+ _pathCalculator = new PathCalculator();
+ _pathCalculator.SetMapData(_mapNodes);
}
- var result = _pathCalculator.CalculatePath(startNode.NodeId, targetNode.NodeId, AgvDirection.Forward);
+ var agvResult = _pathCalculator.FindAGVPath(startNode.NodeId, targetNode.NodeId);
- if (result.Success)
+ if (agvResult.Success)
{
- _simulatorCanvas.CurrentPath = result;
- _pathLengthLabel.Text = $"경로 길이: {result.TotalDistance:F1}";
- _statusLabel.Text = $"경로 계산 완료 ({result.CalculationTime}ms)";
+ _simulatorCanvas.CurrentPath = agvResult.ToPathResult();
+ _pathLengthLabel.Text = $"경로 길이: {agvResult.TotalDistance:F1}";
+ _statusLabel.Text = $"경로 계산 완료 ({agvResult.CalculationTimeMs}ms)";
}
else
{
- MessageBox.Show($"경로를 찾을 수 없습니다:\n{result.ErrorMessage}", "경로 계산 실패",
+ MessageBox.Show($"경로를 찾을 수 없습니다:\n{agvResult.ErrorMessage}", "경로 계산 실패",
MessageBoxButtons.OK, MessageBoxIcon.Warning);
}
}
@@ -568,6 +272,20 @@ namespace AGVSimulator.Forms
_statusLabel.Text = "경로 지움";
}
+ private void OnSetPosition_Click(object sender, EventArgs e)
+ {
+ SetAGVPositionByRfid();
+ }
+
+ private void OnRfidTextBox_KeyPress(object sender, KeyPressEventArgs e)
+ {
+ if (e.KeyChar == (char)Keys.Enter)
+ {
+ SetAGVPositionByRfid();
+ e.Handled = true;
+ }
+ }
+
private void OnSimulationTimer_Tick(object sender, EventArgs e)
{
// 시뮬레이션 업데이트는 각 AGV의 내부 타이머에서 처리됨
@@ -578,51 +296,103 @@ namespace AGVSimulator.Forms
#region Private Methods
+ private void SetAGVPositionByRfid()
+ {
+ // 선택된 AGV 확인
+ var selectedAGV = _agvListCombo.SelectedItem as VirtualAGV;
+ if (selectedAGV == null)
+ {
+ MessageBox.Show("먼저 AGV를 선택해주세요.", "알림", MessageBoxButtons.OK, MessageBoxIcon.Information);
+ return;
+ }
+
+ // RFID 값 확인
+ var rfidId = _rfidTextBox.Text.Trim();
+ if (string.IsNullOrEmpty(rfidId))
+ {
+ MessageBox.Show("RFID 값을 입력해주세요.", "알림", MessageBoxButtons.OK, MessageBoxIcon.Information);
+ return;
+ }
+
+ // RFID에 해당하는 노드 직접 찾기
+ var targetNode = _mapNodes?.FirstOrDefault(n => n.RfidId.Equals(rfidId, StringComparison.OrdinalIgnoreCase));
+ if (targetNode == null)
+ {
+ MessageBox.Show($"RFID '{rfidId}'에 해당하는 노드를 찾을 수 없습니다.\n\n사용 가능한 RFID 목록:\n{GetAvailableRfidList()}",
+ "RFID 찾기 실패", MessageBoxButtons.OK, MessageBoxIcon.Warning);
+ return;
+ }
+
+ // AGV 위치 설정
+ _simulatorCanvas.SetAGVPosition(selectedAGV.AgvId, targetNode.Position);
+
+ _statusLabel.Text = $"{selectedAGV.AgvId} 위치를 RFID '{rfidId}' (노드: {targetNode.NodeId})로 설정했습니다.";
+ _rfidTextBox.Text = ""; // 입력 필드 초기화
+
+ // 시뮬레이터 캔버스의 해당 노드로 이동
+ _simulatorCanvas.PanToNode(targetNode.NodeId);
+ }
+
+ private string GetAvailableRfidList()
+ {
+ if (_mapNodes == null || _mapNodes.Count == 0)
+ return "매핑된 RFID가 없습니다.";
+
+ var nodesWithRfid = _mapNodes.Where(n => n.HasRfid()).ToList();
+ if (nodesWithRfid.Count == 0)
+ return "RFID가 할당된 노드가 없습니다.";
+
+ // 처음 10개의 RFID만 표시
+ var rfidList = nodesWithRfid.Take(10).Select(n => $"- {n.RfidId} → {n.NodeId}");
+ var result = string.Join("\n", rfidList);
+
+ if (nodesWithRfid.Count > 10)
+ result += $"\n... 외 {nodesWithRfid.Count - 10}개";
+
+ return result;
+ }
+
private void LoadMapFile(string filePath)
{
try
{
- var json = File.ReadAllText(filePath);
+ var result = MapLoader.LoadMapFromFile(filePath);
- // 구조체로 직접 역직렬화
- var mapData = JsonConvert.DeserializeObject(json);
-
- if (mapData != null)
+ if (result.Success)
{
- _mapNodes = mapData.MapNodes ?? new List();
- _rfidMappings = mapData.RfidMappings ?? new List();
+ _mapNodes = result.Nodes;
+ _currentMapFilePath = filePath;
+
+ // RFID가 없는 노드들에 자동 할당
+ MapLoader.AssignAutoRfidIds(_mapNodes);
+
+ // 시뮬레이터 캔버스에 맵 설정
+ _simulatorCanvas.Nodes = _mapNodes;
+
+ // 설정에 마지막 맵 파일 경로 저장
+ _config.LastMapFilePath = filePath;
+ if (_config.AutoSave)
+ {
+ _config.Save();
+ }
+
+ // UI 업데이트
+ UpdateNodeComboBoxes();
+ UpdateUI();
+
+ // 맵에 맞춤
+ _simulatorCanvas.FitToNodes();
}
else
{
- _mapNodes = new List();
- _rfidMappings = new List();
+ throw new InvalidOperationException($"맵 파일 로드 실패: {result.ErrorMessage}");
}
-
- // NodeResolver 초기화
- _nodeResolver = new NodeResolver(_rfidMappings, _mapNodes);
-
- // 시뮬레이터 캔버스에 맵 설정
- _simulatorCanvas.MapNodes = _mapNodes;
-
- // UI 업데이트
- UpdateNodeComboBoxes();
- UpdateUI();
-
- // 맵에 맞춤
- _simulatorCanvas.FitToMap();
}
catch (Exception ex)
{
throw new InvalidOperationException($"맵 파일 로드 실패: {ex.Message}", ex);
}
}
-
- // 맵 파일 데이터 구조체
- private class MapFileData
- {
- public List MapNodes { get; set; }
- public List RfidMappings { get; set; }
- }
private void UpdateNodeComboBoxes()
{
@@ -633,13 +403,16 @@ namespace AGVSimulator.Forms
{
foreach (var node in _mapNodes)
{
- _startNodeCombo.Items.Add(node);
- _targetNodeCombo.Items.Add(node);
+ if (node.IsActive && node.HasRfid())
+ {
+ _startNodeCombo.Items.Add(node);
+ _targetNodeCombo.Items.Add(node);
+ }
}
}
- _startNodeCombo.DisplayMember = "NodeId";
- _targetNodeCombo.DisplayMember = "NodeId";
+ _startNodeCombo.DisplayMember = "RfidId";
+ _targetNodeCombo.DisplayMember = "RfidId";
}
private void UpdateAGVComboBox()
@@ -681,8 +454,128 @@ namespace AGVSimulator.Forms
_calculatePathButton.Enabled = _startNodeCombo.SelectedItem != null &&
_targetNodeCombo.SelectedItem != null;
+
+ // RFID 위치 설정 관련
+ var hasSelectedAGV = _agvListCombo.SelectedItem != null;
+ var hasRfidNodes = _mapNodes != null && _mapNodes.Any(n => n.HasRfid());
+
+ _setPositionButton.Enabled = hasSelectedAGV && hasRfidNodes;
+ _rfidTextBox.Enabled = hasSelectedAGV && hasRfidNodes;
+
+ // 맵 다시열기 버튼
+ var hasCurrentMap = !string.IsNullOrEmpty(_currentMapFilePath);
+ reloadMapToolStripMenuItem.Enabled = hasCurrentMap;
+ reloadMapToolStripButton.Enabled = hasCurrentMap;
+ }
+
+ private void OnReloadMap_Click(object sender, EventArgs e)
+ {
+ if (string.IsNullOrEmpty(_currentMapFilePath))
+ {
+ MessageBox.Show("다시 로드할 맵 파일이 없습니다. 먼저 맵을 열어주세요.", "알림",
+ MessageBoxButtons.OK, MessageBoxIcon.Information);
+ return;
+ }
+
+ if (!File.Exists(_currentMapFilePath))
+ {
+ MessageBox.Show($"맵 파일을 찾을 수 없습니다:\n{_currentMapFilePath}", "오류",
+ MessageBoxButtons.OK, MessageBoxIcon.Error);
+ return;
+ }
+
+ try
+ {
+ LoadMapFile(_currentMapFilePath);
+ _statusLabel.Text = $"맵 다시 로드 완료: {Path.GetFileName(_currentMapFilePath)}";
+ }
+ catch (Exception ex)
+ {
+ MessageBox.Show($"맵 파일을 다시 로드할 수 없습니다:\n{ex.Message}", "오류",
+ MessageBoxButtons.OK, MessageBoxIcon.Error);
+ }
+ }
+
+ private void OnLaunchMapEditor_Click(object sender, EventArgs e)
+ {
+ try
+ {
+ // MapEditor 실행 파일 경로 확인
+ string mapEditorPath = _config.MapEditorExecutablePath;
+
+ // 경로가 설정되지 않았거나 파일이 없는 경우 사용자에게 선택을 요청
+ if (string.IsNullOrEmpty(mapEditorPath) || !File.Exists(mapEditorPath))
+ {
+ using (var openDialog = new OpenFileDialog())
+ {
+ openDialog.Filter = "실행 파일 (*.exe)|*.exe|모든 파일 (*.*)|*.*";
+ openDialog.Title = "AGV MapEditor 실행 파일 선택";
+ openDialog.InitialDirectory = Application.StartupPath;
+
+ if (openDialog.ShowDialog() == DialogResult.OK)
+ {
+ mapEditorPath = openDialog.FileName;
+
+ // 설정에 저장
+ _config.MapEditorExecutablePath = mapEditorPath;
+ if (_config.AutoSave)
+ {
+ _config.Save();
+ }
+ }
+ else
+ {
+ return; // 사용자가 취소함
+ }
+ }
+ }
+
+ // MapEditor 실행
+ var startInfo = new System.Diagnostics.ProcessStartInfo
+ {
+ FileName = mapEditorPath,
+ UseShellExecute = true
+ };
+
+ // 현재 로드된 맵 파일이 있으면 파라미터로 전달
+ if (!string.IsNullOrEmpty(_currentMapFilePath) && File.Exists(_currentMapFilePath))
+ {
+ startInfo.Arguments = $"\"{_currentMapFilePath}\"";
+ }
+
+ System.Diagnostics.Process.Start(startInfo);
+ _statusLabel.Text = "MapEditor 실행됨";
+ }
+ catch (Exception ex)
+ {
+ MessageBox.Show($"MapEditor를 실행할 수 없습니다:\n{ex.Message}", "오류",
+ MessageBoxButtons.OK, MessageBoxIcon.Error);
+ }
}
#endregion
+
+ private void btAllReset_Click(object sender, EventArgs e)
+ {
+ // 시뮬레이션 정지
+ if (_simulationState.IsRunning)
+ {
+ OnStopSimulation_Click(sender, e);
+ }
+
+ // AGV 초기화
+ _agvList.Clear();
+ _simulatorCanvas.AGVList = new List();
+
+ // 경로 초기화
+ _simulatorCanvas.CurrentPath = null;
+
+ // UI 업데이트
+ UpdateAGVComboBox();
+ UpdateNodeComboBoxes();
+ UpdateUI();
+
+ _statusLabel.Text = "초기화 완료";
+ }
}
}
\ No newline at end of file
diff --git a/Cs_HMI/AGVSimulator/Forms/SimulatorForm.resx b/Cs_HMI/AGVSimulator/Forms/SimulatorForm.resx
index b45c916..35cb4b7 100644
--- a/Cs_HMI/AGVSimulator/Forms/SimulatorForm.resx
+++ b/Cs_HMI/AGVSimulator/Forms/SimulatorForm.resx
@@ -1,5 +1,64 @@
+
@@ -28,9 +87,9 @@
-
-
-
+
+
+
@@ -39,7 +98,7 @@
-
+
@@ -58,4 +117,13 @@
System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+ 17, 17
+
+
+ 132, 17
+
+
+ 237, 17
+
\ No newline at end of file
diff --git a/Cs_HMI/AGVSimulator/Models/SimulatorConfig.cs b/Cs_HMI/AGVSimulator/Models/SimulatorConfig.cs
new file mode 100644
index 0000000..8450c43
--- /dev/null
+++ b/Cs_HMI/AGVSimulator/Models/SimulatorConfig.cs
@@ -0,0 +1,114 @@
+using System;
+using System.IO;
+using Newtonsoft.Json;
+
+namespace AGVSimulator.Models
+{
+ ///
+ /// 시뮬레이터 환경 설정 클래스
+ ///
+ public class SimulatorConfig
+ {
+ #region Properties
+
+ ///
+ /// MapEditor 실행 파일 경로
+ ///
+ public string MapEditorExecutablePath { get; set; } = string.Empty;
+
+ ///
+ /// 마지막으로 로드한 맵 파일 경로
+ ///
+ public string LastMapFilePath { get; set; } = string.Empty;
+
+ ///
+ /// 설정 파일 자동 저장 여부
+ ///
+ public bool AutoSave { get; set; } = true;
+
+ #endregion
+
+ #region Static Methods
+
+ ///
+ /// 설정 파일 기본 경로
+ ///
+ private static string ConfigFilePath => Path.Combine(
+ Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
+ "AGVSimulator",
+ "config.json");
+
+ ///
+ /// 설정을 파일에서 로드
+ ///
+ /// 로드된 설정 객체
+ public static SimulatorConfig Load()
+ {
+ try
+ {
+ if (File.Exists(ConfigFilePath))
+ {
+ var json = File.ReadAllText(ConfigFilePath);
+ return JsonConvert.DeserializeObject(json) ?? new SimulatorConfig();
+ }
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine($"설정 로드 실패: {ex.Message}");
+ }
+
+ return new SimulatorConfig();
+ }
+
+ ///
+ /// 설정을 파일에 저장
+ ///
+ /// 저장할 설정 객체
+ /// 저장 성공 여부
+ public static bool Save(SimulatorConfig config)
+ {
+ try
+ {
+ var directory = Path.GetDirectoryName(ConfigFilePath);
+ if (!Directory.Exists(directory))
+ {
+ Directory.CreateDirectory(directory);
+ }
+
+ var json = JsonConvert.SerializeObject(config, Formatting.Indented);
+ File.WriteAllText(ConfigFilePath, json);
+ return true;
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine($"설정 저장 실패: {ex.Message}");
+ return false;
+ }
+ }
+
+ #endregion
+
+ #region Instance Methods
+
+ ///
+ /// 현재 설정을 저장
+ ///
+ /// 저장 성공 여부
+ public bool Save()
+ {
+ return Save(this);
+ }
+
+ ///
+ /// MapEditor 실행 파일 경로 유효성 확인
+ ///
+ /// 유효한 경로인지 여부
+ public bool IsMapEditorPathValid()
+ {
+ return !string.IsNullOrEmpty(MapEditorExecutablePath) &&
+ File.Exists(MapEditorExecutablePath);
+ }
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/Cs_HMI/AGVSimulator/Models/VirtualAGV.cs b/Cs_HMI/AGVSimulator/Models/VirtualAGV.cs
index ca3cfd8..1648b09 100644
--- a/Cs_HMI/AGVSimulator/Models/VirtualAGV.cs
+++ b/Cs_HMI/AGVSimulator/Models/VirtualAGV.cs
@@ -3,27 +3,18 @@ using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using AGVMapEditor.Models;
+using AGVNavigationCore.Models;
+using AGVNavigationCore.PathFinding;
+using AGVNavigationCore.Controls;
namespace AGVSimulator.Models
{
- ///
- /// 가상 AGV 상태
- ///
- public enum AGVState
- {
- Idle, // 대기
- Moving, // 이동 중
- Rotating, // 회전 중
- Docking, // 도킹 중
- Charging, // 충전 중
- Error // 오류
- }
///
/// 가상 AGV 클래스
/// 실제 AGV의 동작을 시뮬레이션
///
- public class VirtualAGV
+ public class VirtualAGV : IAGV
{
#region Events
@@ -181,7 +172,7 @@ namespace AGVSimulator.Models
}
_currentPath = path;
- _remainingNodes = new List(path.NodeSequence);
+ _remainingNodes = new List(path.Path);
_currentNodeIndex = 0;
// 시작 노드 위치로 이동
diff --git a/Cs_HMI/AGVSimulator/build.bat b/Cs_HMI/AGVSimulator/build.bat
new file mode 100644
index 0000000..b62566c
--- /dev/null
+++ b/Cs_HMI/AGVSimulator/build.bat
@@ -0,0 +1,29 @@
+@echo off
+echo Building V2GDecoder VC++ Project...
+
+REM Check if Visual Studio 2022 is installed (Professional or Community)
+set MSBUILD_PRO="C:\Program Files\Microsoft Visual Studio\2022\Professional\MSBuild\Current\Bin\MSBuild.exe"
+set MSBUILD_COM="C:\Program Files\Microsoft Visual Studio\2022\Community\MSBuild\Current\Bin\MSBuild.exe"
+set MSBUILD_BT="F:\(VHD) Program Files\Microsoft Visual Studio\2022\MSBuild\Current\Bin\MSBuild.exe"
+
+if exist %MSBUILD_PRO% (
+ echo "Found Visual Studio 2022 Professional"
+ set MSBUILD=%MSBUILD_PRO%
+) else if exist %MSBUILD_COM% (
+ echo "Found Visual Studio 2022 Community"
+ set MSBUILD=%MSBUILD_COM%
+) else if exist %MSBUILD_BT% (
+ echo "Found Visual Studio 2022 BuildTools"
+ set MSBUILD=%MSBUILD_BT%
+) else (
+ echo "Visual Studio 2022 (Professional or Community) not found!"
+ echo "Please install Visual Studio 2022 or update the MSBuild path."
+ pause
+ exit /b 1
+)
+
+REM Build Debug x64 configuration
+echo Building Debug x64 configuration...
+%MSBUILD% AGVSimulator.csproj
+
+pause
\ No newline at end of file
diff --git a/Cs_HMI/CLAUDE.md b/Cs_HMI/CLAUDE.md
index 3264393..8999710 100644
--- a/Cs_HMI/CLAUDE.md
+++ b/Cs_HMI/CLAUDE.md
@@ -1,6 +1,7 @@
# CLAUDE.md
이 파일은 이 저장소의 코드로 작업할 때 Claude Code (claude.ai/code)를 위한 지침을 제공합니다.
+맵데이터는 C:\Data\Source\(5613#) ENIG AGV\Source\Cs_HMI\Data\NewMap.agvmap 파일을 기준으로 사용
## 빌드 및 개발 명령어
diff --git a/Cs_HMI/Data/NewMap.agvmap b/Cs_HMI/Data/NewMap.agvmap
new file mode 100644
index 0000000..35d90c5
--- /dev/null
+++ b/Cs_HMI/Data/NewMap.agvmap
@@ -0,0 +1,648 @@
+{
+ "Nodes": [
+ {
+ "NodeId": "N001",
+ "Name": "N001",
+ "Position": "80, 160",
+ "Type": 2,
+ "DockDirection": null,
+ "ConnectedNodes": [
+ "N002"
+ ],
+ "CanRotate": false,
+ "StationId": "",
+ "StationType": null,
+ "CreatedDate": "2025-09-11T08:34:44.9548285+09:00",
+ "ModifiedDate": "2025-09-11T11:46:18.1785633+09:00",
+ "Description": "TOPS-2",
+ "IsActive": true,
+ "DisplayColor": "Blue",
+ "RfidId": "001",
+ "LabelText": "",
+ "FontFamily": "Arial",
+ "FontSize": 12.0,
+ "FontStyle": 0,
+ "ForeColor": "Black",
+ "BackColor": "Transparent",
+ "ShowBackground": false,
+ "ImagePath": "",
+ "Scale": "1, 1",
+ "Opacity": 1.0,
+ "Rotation": 0.0,
+ "DisplayText": "N001 - TOPS-2 - [001]"
+ },
+ {
+ "NodeId": "N002",
+ "Name": "N002",
+ "Position": "220, 220",
+ "Type": 0,
+ "DockDirection": null,
+ "ConnectedNodes": [
+ "N003"
+ ],
+ "CanRotate": false,
+ "StationId": "",
+ "StationType": null,
+ "CreatedDate": "2025-09-11T08:34:48.2957516+09:00",
+ "ModifiedDate": "2025-09-11T11:46:20.6016371+09:00",
+ "Description": "",
+ "IsActive": true,
+ "DisplayColor": "Blue",
+ "RfidId": "002",
+ "LabelText": "",
+ "FontFamily": "Arial",
+ "FontSize": 12.0,
+ "FontStyle": 0,
+ "ForeColor": "Black",
+ "BackColor": "Transparent",
+ "ShowBackground": false,
+ "ImagePath": "",
+ "Scale": "1, 1",
+ "Opacity": 1.0,
+ "Rotation": 0.0,
+ "DisplayText": "N002 - [002]"
+ },
+ {
+ "NodeId": "N003",
+ "Name": "N003",
+ "Position": "300, 280",
+ "Type": 0,
+ "DockDirection": null,
+ "ConnectedNodes": [
+ "N004"
+ ],
+ "CanRotate": false,
+ "StationId": "",
+ "StationType": null,
+ "CreatedDate": "2025-09-11T08:34:49.2226656+09:00",
+ "ModifiedDate": "2025-09-11T11:46:23.2433989+09:00",
+ "Description": "",
+ "IsActive": true,
+ "DisplayColor": "Blue",
+ "RfidId": "003",
+ "LabelText": "",
+ "FontFamily": "Arial",
+ "FontSize": 12.0,
+ "FontStyle": 0,
+ "ForeColor": "Black",
+ "BackColor": "Transparent",
+ "ShowBackground": false,
+ "ImagePath": "",
+ "Scale": "1, 1",
+ "Opacity": 1.0,
+ "Rotation": 0.0,
+ "DisplayText": "N003 - [003]"
+ },
+ {
+ "NodeId": "N004",
+ "Name": "N004",
+ "Position": "380, 340",
+ "Type": 1,
+ "DockDirection": null,
+ "ConnectedNodes": [
+ "N008",
+ "N011",
+ "N022"
+ ],
+ "CanRotate": true,
+ "StationId": "",
+ "StationType": null,
+ "CreatedDate": "2025-09-11T08:34:50.1681027+09:00",
+ "ModifiedDate": "2025-09-11T11:46:24.8122488+09:00",
+ "Description": "",
+ "IsActive": true,
+ "DisplayColor": "Blue",
+ "RfidId": "004",
+ "LabelText": "",
+ "FontFamily": "Arial",
+ "FontSize": 12.0,
+ "FontStyle": 0,
+ "ForeColor": "Black",
+ "BackColor": "Transparent",
+ "ShowBackground": false,
+ "ImagePath": "",
+ "Scale": "1, 1",
+ "Opacity": 1.0,
+ "Rotation": 0.0,
+ "DisplayText": "N004 - [004]"
+ },
+ {
+ "NodeId": "N006",
+ "Name": "N006",
+ "Position": "520, 220",
+ "Type": 0,
+ "DockDirection": null,
+ "ConnectedNodes": [
+ "N007"
+ ],
+ "CanRotate": false,
+ "StationId": "",
+ "StationType": null,
+ "CreatedDate": "2025-09-11T08:34:51.1111368+09:00",
+ "ModifiedDate": "2025-09-11T11:46:41.6764551+09:00",
+ "Description": "",
+ "IsActive": true,
+ "DisplayColor": "Blue",
+ "RfidId": "013",
+ "LabelText": "",
+ "FontFamily": "Arial",
+ "FontSize": 12.0,
+ "FontStyle": 0,
+ "ForeColor": "Black",
+ "BackColor": "Transparent",
+ "ShowBackground": false,
+ "ImagePath": "",
+ "Scale": "1, 1",
+ "Opacity": 1.0,
+ "Rotation": 0.0,
+ "DisplayText": "N006 - [013]"
+ },
+ {
+ "NodeId": "N007",
+ "Name": "N007",
+ "Position": "600, 180",
+ "Type": 0,
+ "DockDirection": null,
+ "ConnectedNodes": [],
+ "CanRotate": false,
+ "StationId": "",
+ "StationType": null,
+ "CreatedDate": "2025-09-11T08:34:51.9266982+09:00",
+ "ModifiedDate": "2025-09-11T11:46:43.5813583+09:00",
+ "Description": "",
+ "IsActive": true,
+ "DisplayColor": "Blue",
+ "RfidId": "014",
+ "LabelText": "",
+ "FontFamily": "Arial",
+ "FontSize": 12.0,
+ "FontStyle": 0,
+ "ForeColor": "Black",
+ "BackColor": "Transparent",
+ "ShowBackground": false,
+ "ImagePath": "",
+ "Scale": "1, 1",
+ "Opacity": 1.0,
+ "Rotation": 0.0,
+ "DisplayText": "N007 - [014]"
+ },
+ {
+ "NodeId": "N008",
+ "Name": "N008",
+ "Position": "340, 420",
+ "Type": 0,
+ "DockDirection": null,
+ "ConnectedNodes": [
+ "N009"
+ ],
+ "CanRotate": false,
+ "StationId": "",
+ "StationType": null,
+ "CreatedDate": "2025-09-11T08:34:53.9595825+09:00",
+ "ModifiedDate": "2025-09-11T11:46:33.4490347+09:00",
+ "Description": "",
+ "IsActive": true,
+ "DisplayColor": "Blue",
+ "RfidId": "009",
+ "LabelText": "",
+ "FontFamily": "Arial",
+ "FontSize": 12.0,
+ "FontStyle": 0,
+ "ForeColor": "Black",
+ "BackColor": "Transparent",
+ "ShowBackground": false,
+ "ImagePath": "",
+ "Scale": "1, 1",
+ "Opacity": 1.0,
+ "Rotation": 0.0,
+ "DisplayText": "N008 - [009]"
+ },
+ {
+ "NodeId": "N009",
+ "Name": "N009",
+ "Position": "280, 480",
+ "Type": 0,
+ "DockDirection": null,
+ "ConnectedNodes": [
+ "N010"
+ ],
+ "CanRotate": false,
+ "StationId": "",
+ "StationType": null,
+ "CreatedDate": "2025-09-11T08:34:54.5035702+09:00",
+ "ModifiedDate": "2025-09-11T11:46:35.2267268+09:00",
+ "Description": "SSTRON",
+ "IsActive": true,
+ "DisplayColor": "Blue",
+ "RfidId": "010",
+ "LabelText": "",
+ "FontFamily": "Arial",
+ "FontSize": 12.0,
+ "FontStyle": 0,
+ "ForeColor": "Black",
+ "BackColor": "Transparent",
+ "ShowBackground": false,
+ "ImagePath": "",
+ "Scale": "1, 1",
+ "Opacity": 1.0,
+ "Rotation": 0.0,
+ "DisplayText": "N009 - SSTRON - [010]"
+ },
+ {
+ "NodeId": "N010",
+ "Name": "N010",
+ "Position": "180, 540",
+ "Type": 2,
+ "DockDirection": null,
+ "ConnectedNodes": [],
+ "CanRotate": false,
+ "StationId": "",
+ "StationType": null,
+ "CreatedDate": "2025-09-11T08:34:55.0563237+09:00",
+ "ModifiedDate": "2025-09-11T11:46:37.2974468+09:00",
+ "Description": "TOPS-1",
+ "IsActive": true,
+ "DisplayColor": "Blue",
+ "RfidId": "011",
+ "LabelText": "",
+ "FontFamily": "Arial",
+ "FontSize": 12.0,
+ "FontStyle": 0,
+ "ForeColor": "Black",
+ "BackColor": "Transparent",
+ "ShowBackground": false,
+ "ImagePath": "",
+ "Scale": "1, 1",
+ "Opacity": 1.0,
+ "Rotation": 0.0,
+ "DisplayText": "N010 - TOPS-1 - [011]"
+ },
+ {
+ "NodeId": "N011",
+ "Name": "N011",
+ "Position": "460, 420",
+ "Type": 0,
+ "DockDirection": null,
+ "ConnectedNodes": [
+ "N012"
+ ],
+ "CanRotate": false,
+ "StationId": "",
+ "StationType": null,
+ "CreatedDate": "2025-09-11T08:34:55.8875335+09:00",
+ "ModifiedDate": "2025-09-11T11:46:26.5275006+09:00",
+ "Description": "",
+ "IsActive": true,
+ "DisplayColor": "Blue",
+ "RfidId": "005",
+ "LabelText": "",
+ "FontFamily": "Arial",
+ "FontSize": 12.0,
+ "FontStyle": 0,
+ "ForeColor": "Black",
+ "BackColor": "Transparent",
+ "ShowBackground": false,
+ "ImagePath": "",
+ "Scale": "1, 1",
+ "Opacity": 1.0,
+ "Rotation": 0.0,
+ "DisplayText": "N011 - [005]"
+ },
+ {
+ "NodeId": "N012",
+ "Name": "N012",
+ "Position": "540, 480",
+ "Type": 0,
+ "DockDirection": null,
+ "ConnectedNodes": [
+ "N013"
+ ],
+ "CanRotate": false,
+ "StationId": "",
+ "StationType": null,
+ "CreatedDate": "2025-09-11T08:34:56.3678144+09:00",
+ "ModifiedDate": "2025-09-11T11:46:27.9224943+09:00",
+ "Description": "",
+ "IsActive": true,
+ "DisplayColor": "Blue",
+ "RfidId": "006",
+ "LabelText": "",
+ "FontFamily": "Arial",
+ "FontSize": 12.0,
+ "FontStyle": 0,
+ "ForeColor": "Black",
+ "BackColor": "Transparent",
+ "ShowBackground": false,
+ "ImagePath": "",
+ "Scale": "1, 1",
+ "Opacity": 1.0,
+ "Rotation": 0.0,
+ "DisplayText": "N012 - [006]"
+ },
+ {
+ "NodeId": "N013",
+ "Name": "N013",
+ "Position": "620, 520",
+ "Type": 0,
+ "DockDirection": null,
+ "ConnectedNodes": [
+ "N014"
+ ],
+ "CanRotate": false,
+ "StationId": "",
+ "StationType": null,
+ "CreatedDate": "2025-09-11T08:34:56.8390845+09:00",
+ "ModifiedDate": "2025-09-11T11:46:29.5788308+09:00",
+ "Description": "",
+ "IsActive": true,
+ "DisplayColor": "Blue",
+ "RfidId": "007",
+ "LabelText": "",
+ "FontFamily": "Arial",
+ "FontSize": 12.0,
+ "FontStyle": 0,
+ "ForeColor": "Black",
+ "BackColor": "Transparent",
+ "ShowBackground": false,
+ "ImagePath": "",
+ "Scale": "1, 1",
+ "Opacity": 1.0,
+ "Rotation": 0.0,
+ "DisplayText": "N013 - [007]"
+ },
+ {
+ "NodeId": "N014",
+ "Name": "N014",
+ "Position": "720, 580",
+ "Type": 2,
+ "DockDirection": null,
+ "ConnectedNodes": [],
+ "CanRotate": false,
+ "StationId": "",
+ "StationType": null,
+ "CreatedDate": "2025-09-11T08:34:57.2549726+09:00",
+ "ModifiedDate": "2025-09-11T11:46:31.1919274+09:00",
+ "Description": "SS-TRON",
+ "IsActive": true,
+ "DisplayColor": "Blue",
+ "RfidId": "008",
+ "LabelText": "",
+ "FontFamily": "Arial",
+ "FontSize": 12.0,
+ "FontStyle": 0,
+ "ForeColor": "Black",
+ "BackColor": "Transparent",
+ "ShowBackground": false,
+ "ImagePath": "",
+ "Scale": "1, 1",
+ "Opacity": 1.0,
+ "Rotation": 0.0,
+ "DisplayText": "N014 - SS-TRON - [008]"
+ },
+ {
+ "NodeId": "N019",
+ "Name": "N019",
+ "Position": "679, 199",
+ "Type": 3,
+ "DockDirection": null,
+ "ConnectedNodes": [
+ "N007"
+ ],
+ "CanRotate": false,
+ "StationId": "",
+ "StationType": null,
+ "CreatedDate": "2025-09-11T08:35:56.5359098+09:00",
+ "ModifiedDate": "2025-09-11T11:46:45.6967709+09:00",
+ "Description": "Charger",
+ "IsActive": true,
+ "DisplayColor": "Red",
+ "RfidId": "015",
+ "LabelText": "",
+ "FontFamily": "Arial",
+ "FontSize": 12.0,
+ "FontStyle": 0,
+ "ForeColor": "Black",
+ "BackColor": "Transparent",
+ "ShowBackground": false,
+ "ImagePath": "",
+ "Scale": "1, 1",
+ "Opacity": 1.0,
+ "Rotation": 0.0,
+ "DisplayText": "N019 - Charger - [015]"
+ },
+ {
+ "NodeId": "N022",
+ "Name": "N022",
+ "Position": "459, 279",
+ "Type": 0,
+ "DockDirection": null,
+ "ConnectedNodes": [
+ "N006"
+ ],
+ "CanRotate": false,
+ "StationId": "",
+ "StationType": null,
+ "CreatedDate": "2025-09-11T08:36:48.0311551+09:00",
+ "ModifiedDate": "2025-09-11T11:46:39.7262145+09:00",
+ "Description": "",
+ "IsActive": true,
+ "DisplayColor": "Blue",
+ "RfidId": "012",
+ "LabelText": "",
+ "FontFamily": "Arial",
+ "FontSize": 12.0,
+ "FontStyle": 0,
+ "ForeColor": "Black",
+ "BackColor": "Transparent",
+ "ShowBackground": false,
+ "ImagePath": "",
+ "Scale": "1, 1",
+ "Opacity": 1.0,
+ "Rotation": 0.0,
+ "DisplayText": "N022 - [012]"
+ },
+ {
+ "NodeId": "N023",
+ "Name": "N023",
+ "Position": "440, 220",
+ "Type": 0,
+ "DockDirection": null,
+ "ConnectedNodes": [
+ "N024",
+ "N004"
+ ],
+ "CanRotate": false,
+ "StationId": "",
+ "StationType": null,
+ "CreatedDate": "2025-09-11T09:41:36.8738794+09:00",
+ "ModifiedDate": "2025-09-11T11:46:47.8868788+09:00",
+ "Description": "",
+ "IsActive": true,
+ "DisplayColor": "Blue",
+ "RfidId": "016",
+ "LabelText": "",
+ "FontFamily": "Arial",
+ "FontSize": 12.0,
+ "FontStyle": 0,
+ "ForeColor": "Black",
+ "BackColor": "Transparent",
+ "ShowBackground": false,
+ "ImagePath": "",
+ "Scale": "1, 1",
+ "Opacity": 1.0,
+ "Rotation": 0.0,
+ "DisplayText": "N023 - [016]"
+ },
+ {
+ "NodeId": "N024",
+ "Name": "N024",
+ "Position": "500, 160",
+ "Type": 0,
+ "DockDirection": null,
+ "ConnectedNodes": [
+ "N025"
+ ],
+ "CanRotate": false,
+ "StationId": "",
+ "StationType": null,
+ "CreatedDate": "2025-09-11T09:41:37.4551853+09:00",
+ "ModifiedDate": "2025-09-11T11:46:51.7183934+09:00",
+ "Description": "",
+ "IsActive": true,
+ "DisplayColor": "Blue",
+ "RfidId": "017",
+ "LabelText": "",
+ "FontFamily": "Arial",
+ "FontSize": 12.0,
+ "FontStyle": 0,
+ "ForeColor": "Black",
+ "BackColor": "Transparent",
+ "ShowBackground": false,
+ "ImagePath": "",
+ "Scale": "1, 1",
+ "Opacity": 1.0,
+ "Rotation": 0.0,
+ "DisplayText": "N024 - [017]"
+ },
+ {
+ "NodeId": "N025",
+ "Name": "N025",
+ "Position": "600, 120",
+ "Type": 0,
+ "DockDirection": null,
+ "ConnectedNodes": [
+ "N026"
+ ],
+ "CanRotate": false,
+ "StationId": "",
+ "StationType": null,
+ "CreatedDate": "2025-09-11T09:41:38.0142374+09:00",
+ "ModifiedDate": "2025-09-11T11:46:54.3289018+09:00",
+ "Description": "",
+ "IsActive": true,
+ "DisplayColor": "Blue",
+ "RfidId": "018",
+ "LabelText": "",
+ "FontFamily": "Arial",
+ "FontSize": 12.0,
+ "FontStyle": 0,
+ "ForeColor": "Black",
+ "BackColor": "Transparent",
+ "ShowBackground": false,
+ "ImagePath": "",
+ "Scale": "1, 1",
+ "Opacity": 1.0,
+ "Rotation": 0.0,
+ "DisplayText": "N025 - [018]"
+ },
+ {
+ "NodeId": "N026",
+ "Name": "N026",
+ "Position": "660, 100",
+ "Type": 3,
+ "DockDirection": null,
+ "ConnectedNodes": [],
+ "CanRotate": false,
+ "StationId": "",
+ "StationType": null,
+ "CreatedDate": "2025-09-11T09:41:38.5834487+09:00",
+ "ModifiedDate": "2025-09-11T11:46:57.0288799+09:00",
+ "Description": "Charger",
+ "IsActive": true,
+ "DisplayColor": "Blue",
+ "RfidId": "019",
+ "LabelText": "",
+ "FontFamily": "Arial",
+ "FontSize": 12.0,
+ "FontStyle": 0,
+ "ForeColor": "Black",
+ "BackColor": "Transparent",
+ "ShowBackground": false,
+ "ImagePath": "",
+ "Scale": "1, 1",
+ "Opacity": 1.0,
+ "Rotation": 0.0,
+ "DisplayText": "N026 - Charger - [019]"
+ },
+ {
+ "NodeId": "LBL001",
+ "Name": "Amkor Technology Korea",
+ "Position": "160, 80",
+ "Type": 4,
+ "DockDirection": null,
+ "ConnectedNodes": [],
+ "CanRotate": false,
+ "StationId": "",
+ "StationType": null,
+ "CreatedDate": "2025-09-11T11:08:22.4048927+09:00",
+ "ModifiedDate": "2025-09-11T11:08:22.4048927+09:00",
+ "Description": "",
+ "IsActive": true,
+ "DisplayColor": "Purple",
+ "RfidId": "",
+ "LabelText": "Amkor Technology Korea",
+ "FontFamily": "Arial",
+ "FontSize": 12.0,
+ "FontStyle": 0,
+ "ForeColor": "Black",
+ "BackColor": "Transparent",
+ "ShowBackground": false,
+ "ImagePath": "",
+ "Scale": "1, 1",
+ "Opacity": 1.0,
+ "Rotation": 0.0,
+ "DisplayText": "LBL001"
+ },
+ {
+ "NodeId": "IMG001",
+ "Name": "logo",
+ "Position": "700, 320",
+ "Type": 5,
+ "DockDirection": null,
+ "ConnectedNodes": [],
+ "CanRotate": false,
+ "StationId": "",
+ "StationType": null,
+ "CreatedDate": "2025-09-11T11:08:44.7897541+09:00",
+ "ModifiedDate": "2025-09-11T11:08:44.7897541+09:00",
+ "Description": "",
+ "IsActive": true,
+ "DisplayColor": "Brown",
+ "RfidId": "",
+ "LabelText": "",
+ "FontFamily": "Arial",
+ "FontSize": 12.0,
+ "FontStyle": 0,
+ "ForeColor": "Black",
+ "BackColor": "Transparent",
+ "ShowBackground": false,
+ "ImagePath": "C:\\Data\\Users\\Pictures\\logo.png",
+ "Scale": "1, 1",
+ "Opacity": 1.0,
+ "Rotation": 0.0,
+ "DisplayText": "IMG001"
+ }
+ ],
+ "CreatedDate": "2025-09-11T11:46:57.8091998+09:00",
+ "Version": "1.0"
+}
\ No newline at end of file