From de0e39e0309ec937a147afc9cb0aa79a1e86d97b Mon Sep 17 00:00:00 2001 From: ChiKyun Kim Date: Thu, 11 Sep 2025 16:41:52 +0900 Subject: [PATCH] refactor: Consolidate RFID mapping and add bidirectional pathfinding MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Major improvements to AGV navigation system: • Consolidated RFID management into MapNode, removing duplicate RfidMapping class • Enhanced MapNode with RFID metadata fields (RfidStatus, RfidDescription) • Added automatic bidirectional connection generation in pathfinding algorithms • Updated all components to use unified MapNode-based RFID system • Added command line argument support for AGVMapEditor auto-loading files • Fixed pathfinding failures by ensuring proper node connectivity Technical changes: - Removed RfidMapping class and dependencies across all projects - Updated AStarPathfinder with EnsureBidirectionalConnections() method - Modified MapLoader to use AssignAutoRfidIds() for RFID automation - Enhanced UnifiedAGVCanvas, SimulatorForm, and MainForm for MapNode integration - Improved data consistency and reduced memory footprint 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- Cs_HMI/AGVMapEditor/AGVMapEditor.csproj | 25 +- .../Controls/MapCanvas.Designer.cs | 59 - Cs_HMI/AGVMapEditor/Controls/MapCanvas.cs | 608 -------- Cs_HMI/AGVMapEditor/Controls/MapCanvas.resx | 61 - .../MapCanvas_Interactive.Designer.cs | 34 + .../Controls/MapCanvas_Interactive.cs | 1358 +++++++++++++++++ .../AGVMapEditor/Forms/MainForm.Designer.cs | 218 +-- Cs_HMI/AGVMapEditor/Forms/MainForm.cs | 504 ++++-- Cs_HMI/AGVMapEditor/Forms/MainForm.resx | 67 +- Cs_HMI/AGVMapEditor/Models/Enums.cs | 6 +- Cs_HMI/AGVMapEditor/Models/MapData.cs | 15 + Cs_HMI/AGVMapEditor/Models/MapImage.cs | 210 +++ Cs_HMI/AGVMapEditor/Models/MapLabel.cs | 125 ++ Cs_HMI/AGVMapEditor/Models/MapNode.cs | 199 ++- .../Models/NodePropertyWrapper.cs | 499 ++++++ Cs_HMI/AGVMapEditor/Models/NodeResolver.cs | 1 + Cs_HMI/AGVMapEditor/Models/PathCalculator.cs | 549 +++---- Cs_HMI/AGVMapEditor/Models/RfidMapping.cs | 1 + Cs_HMI/AGVMapEditor/Program.cs | 6 +- Cs_HMI/AGVMapEditor/build.bat | 29 + .../AGVNavigationCore.csproj | 101 ++ .../Controls/UnifiedAGVCanvas.Designer.cs | 41 + .../Controls/UnifiedAGVCanvas.Events.cs | 801 ++++++++++ .../Controls/UnifiedAGVCanvas.Mouse.cs | 605 ++++++++ .../Controls/UnifiedAGVCanvas.cs | 536 +++++++ Cs_HMI/AGVNavigationCore/Models/Enums.cs | 68 + Cs_HMI/AGVNavigationCore/Models/MapLoader.cs | 144 ++ Cs_HMI/AGVNavigationCore/Models/MapNode.cs | 486 ++++++ .../AGVNavigationCore/Models/RfidMapping.cs | 79 + .../PathFinding/AGVPathResult.cs | 244 +++ .../PathFinding/AGVPathfinder.cs | 287 ++++ .../PathFinding/AStarPathfinder.cs | 291 ++++ .../AGVNavigationCore/PathFinding/PathNode.cs | 101 ++ .../PathFinding/PathResult.cs | 107 ++ .../PathFinding/RfidBasedPathfinder.cs | 275 ++++ .../PathFinding/RfidPathResult.cs | 229 +++ .../Properties/AssemblyInfo.cs | 36 + Cs_HMI/AGVNavigationCore/README.md | 225 +++ Cs_HMI/AGVNavigationCore/build.bat | 29 + Cs_HMI/AGVNavigationCore/packages.config | 4 + Cs_HMI/AGVSimulator/AGVSimulator.csproj | 17 +- .../AGVSimulator/Controls/SimulatorCanvas.cs | 14 +- .../Forms/SimulatorForm.Designer.cs | 646 ++++++++ Cs_HMI/AGVSimulator/Forms/SimulatorForm.cs | 605 +++----- Cs_HMI/AGVSimulator/Forms/SimulatorForm.resx | 76 +- Cs_HMI/AGVSimulator/Models/SimulatorConfig.cs | 114 ++ Cs_HMI/AGVSimulator/Models/VirtualAGV.cs | 19 +- Cs_HMI/AGVSimulator/build.bat | 29 + Cs_HMI/CLAUDE.md | 1 + Cs_HMI/Data/NewMap.agvmap | 648 ++++++++ 50 files changed, 9578 insertions(+), 1854 deletions(-) delete mode 100644 Cs_HMI/AGVMapEditor/Controls/MapCanvas.Designer.cs delete mode 100644 Cs_HMI/AGVMapEditor/Controls/MapCanvas.cs delete mode 100644 Cs_HMI/AGVMapEditor/Controls/MapCanvas.resx create mode 100644 Cs_HMI/AGVMapEditor/Controls/MapCanvas_Interactive.Designer.cs create mode 100644 Cs_HMI/AGVMapEditor/Controls/MapCanvas_Interactive.cs create mode 100644 Cs_HMI/AGVMapEditor/Models/MapData.cs create mode 100644 Cs_HMI/AGVMapEditor/Models/MapImage.cs create mode 100644 Cs_HMI/AGVMapEditor/Models/MapLabel.cs create mode 100644 Cs_HMI/AGVMapEditor/Models/NodePropertyWrapper.cs create mode 100644 Cs_HMI/AGVMapEditor/build.bat create mode 100644 Cs_HMI/AGVNavigationCore/AGVNavigationCore.csproj create mode 100644 Cs_HMI/AGVNavigationCore/Controls/UnifiedAGVCanvas.Designer.cs create mode 100644 Cs_HMI/AGVNavigationCore/Controls/UnifiedAGVCanvas.Events.cs create mode 100644 Cs_HMI/AGVNavigationCore/Controls/UnifiedAGVCanvas.Mouse.cs create mode 100644 Cs_HMI/AGVNavigationCore/Controls/UnifiedAGVCanvas.cs create mode 100644 Cs_HMI/AGVNavigationCore/Models/Enums.cs create mode 100644 Cs_HMI/AGVNavigationCore/Models/MapLoader.cs create mode 100644 Cs_HMI/AGVNavigationCore/Models/MapNode.cs create mode 100644 Cs_HMI/AGVNavigationCore/Models/RfidMapping.cs create mode 100644 Cs_HMI/AGVNavigationCore/PathFinding/AGVPathResult.cs create mode 100644 Cs_HMI/AGVNavigationCore/PathFinding/AGVPathfinder.cs create mode 100644 Cs_HMI/AGVNavigationCore/PathFinding/AStarPathfinder.cs create mode 100644 Cs_HMI/AGVNavigationCore/PathFinding/PathNode.cs create mode 100644 Cs_HMI/AGVNavigationCore/PathFinding/PathResult.cs create mode 100644 Cs_HMI/AGVNavigationCore/PathFinding/RfidBasedPathfinder.cs create mode 100644 Cs_HMI/AGVNavigationCore/PathFinding/RfidPathResult.cs create mode 100644 Cs_HMI/AGVNavigationCore/Properties/AssemblyInfo.cs create mode 100644 Cs_HMI/AGVNavigationCore/README.md create mode 100644 Cs_HMI/AGVNavigationCore/build.bat create mode 100644 Cs_HMI/AGVNavigationCore/packages.config create mode 100644 Cs_HMI/AGVSimulator/Models/SimulatorConfig.cs create mode 100644 Cs_HMI/AGVSimulator/build.bat create mode 100644 Cs_HMI/Data/NewMap.agvmap 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