diff --git a/Cs_HMI/AGVMapEditor/AGVMapEditor.csproj b/Cs_HMI/AGVMapEditor/AGVMapEditor.csproj
index 8c38a9d..a30a039 100644
--- a/Cs_HMI/AGVMapEditor/AGVMapEditor.csproj
+++ b/Cs_HMI/AGVMapEditor/AGVMapEditor.csproj
@@ -49,6 +49,7 @@
+
@@ -63,11 +64,20 @@
+
+ True
+ True
+ Resources.resx
+
MainForm.cs
+
+ ResXFileCodeGenerator
+ Resources.Designer.cs
+
diff --git a/Cs_HMI/AGVMapEditor/Controls/MapCanvas_Interactive.Designer.cs b/Cs_HMI/AGVMapEditor/Controls/MapCanvas_Interactive.Designer.cs
deleted file mode 100644
index ce5ed68..0000000
--- a/Cs_HMI/AGVMapEditor/Controls/MapCanvas_Interactive.Designer.cs
+++ /dev/null
@@ -1,34 +0,0 @@
-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
deleted file mode 100644
index a3b9b3a..0000000
--- a/Cs_HMI/AGVMapEditor/Controls/MapCanvas_Interactive.cs
+++ /dev/null
@@ -1,1358 +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;
-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 d0b406c..6fe95f5 100644
--- a/Cs_HMI/AGVMapEditor/Forms/MainForm.Designer.cs
+++ b/Cs_HMI/AGVMapEditor/Forms/MainForm.Designer.cs
@@ -28,113 +28,38 @@ namespace AGVMapEditor.Forms
///
private void InitializeComponent()
{
- this.menuStrip1 = new System.Windows.Forms.MenuStrip();
- this.fileToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
- this.newToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
- this.openToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
- 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();
+ System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(MainForm));
this.statusStrip1 = new System.Windows.Forms.StatusStrip();
this.toolStripStatusLabel1 = new System.Windows.Forms.ToolStripStatusLabel();
this.splitContainer1 = new System.Windows.Forms.SplitContainer();
this.tabControl1 = new System.Windows.Forms.TabControl();
this.tabPageNodes = new System.Windows.Forms.TabPage();
this.listBoxNodes = new System.Windows.Forms.ListBox();
- this._propertyGrid = new System.Windows.Forms.PropertyGrid();
this.label1 = new System.Windows.Forms.Label();
- this.menuStrip1.SuspendLayout();
+ this.tabPage1 = new System.Windows.Forms.TabPage();
+ this.lstNodeConnection = new System.Windows.Forms.ListBox();
+ this.toolStrip1 = new System.Windows.Forms.ToolStrip();
+ this.btNodeRemove = new System.Windows.Forms.ToolStripButton();
+ this._propertyGrid = new System.Windows.Forms.PropertyGrid();
+ this.toolStrip2 = new System.Windows.Forms.ToolStrip();
+ this.btnNew = new System.Windows.Forms.ToolStripButton();
+ this.btnOpen = new System.Windows.Forms.ToolStripButton();
+ this.btnReopen = new System.Windows.Forms.ToolStripButton();
+ this.btnClose = new System.Windows.Forms.ToolStripButton();
+ this.toolStripSeparator3 = new System.Windows.Forms.ToolStripSeparator();
+ this.btnSave = new System.Windows.Forms.ToolStripButton();
+ this.btnSaveAs = new System.Windows.Forms.ToolStripButton();
this.statusStrip1.SuspendLayout();
((System.ComponentModel.ISupportInitialize)(this.splitContainer1)).BeginInit();
this.splitContainer1.Panel1.SuspendLayout();
this.splitContainer1.SuspendLayout();
this.tabControl1.SuspendLayout();
this.tabPageNodes.SuspendLayout();
+ this.tabPage1.SuspendLayout();
+ this.toolStrip1.SuspendLayout();
+ this.toolStrip2.SuspendLayout();
this.SuspendLayout();
//
- // menuStrip1
- //
- this.menuStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
- this.fileToolStripMenuItem});
- this.menuStrip1.Location = new System.Drawing.Point(0, 0);
- this.menuStrip1.Name = "menuStrip1";
- this.menuStrip1.Size = new System.Drawing.Size(1200, 24);
- this.menuStrip1.TabIndex = 0;
- this.menuStrip1.Text = "menuStrip1";
- //
- // fileToolStripMenuItem
- //
- this.fileToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
- this.newToolStripMenuItem,
- this.openToolStripMenuItem,
- this.closeToolStripMenuItem,
- this.toolStripSeparator1,
- this.saveToolStripMenuItem,
- this.saveAsToolStripMenuItem,
- this.toolStripSeparator2,
- this.exitToolStripMenuItem});
- this.fileToolStripMenuItem.Name = "fileToolStripMenuItem";
- this.fileToolStripMenuItem.Size = new System.Drawing.Size(57, 20);
- this.fileToolStripMenuItem.Text = "파일(&F)";
- //
- // newToolStripMenuItem
- //
- 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(198, 22);
- this.newToolStripMenuItem.Text = "새로 만들기(&N)";
- this.newToolStripMenuItem.Click += new System.EventHandler(this.newToolStripMenuItem_Click);
- //
- // openToolStripMenuItem
- //
- 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(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(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(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(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(195, 6);
- //
- // exitToolStripMenuItem
- //
- this.exitToolStripMenuItem.Name = "exitToolStripMenuItem";
- this.exitToolStripMenuItem.Size = new System.Drawing.Size(198, 22);
- this.exitToolStripMenuItem.Text = "종료(&X)";
- this.exitToolStripMenuItem.Click += new System.EventHandler(this.exitToolStripMenuItem_Click);
- //
// statusStrip1
//
this.statusStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
@@ -154,36 +79,37 @@ namespace AGVMapEditor.Forms
// splitContainer1
//
this.splitContainer1.Dock = System.Windows.Forms.DockStyle.Fill;
- this.splitContainer1.Location = new System.Drawing.Point(0, 24);
+ this.splitContainer1.Location = new System.Drawing.Point(0, 25);
this.splitContainer1.Name = "splitContainer1";
//
// splitContainer1.Panel1
//
this.splitContainer1.Panel1.Controls.Add(this.tabControl1);
+ this.splitContainer1.Panel1.Controls.Add(this._propertyGrid);
this.splitContainer1.Panel1MinSize = 300;
- this.splitContainer1.Size = new System.Drawing.Size(1200, 727);
+ this.splitContainer1.Size = new System.Drawing.Size(1200, 726);
this.splitContainer1.SplitterDistance = 300;
this.splitContainer1.TabIndex = 2;
//
// tabControl1
//
this.tabControl1.Controls.Add(this.tabPageNodes);
+ this.tabControl1.Controls.Add(this.tabPage1);
this.tabControl1.Dock = System.Windows.Forms.DockStyle.Fill;
this.tabControl1.Location = new System.Drawing.Point(0, 0);
this.tabControl1.Name = "tabControl1";
this.tabControl1.SelectedIndex = 0;
- this.tabControl1.Size = new System.Drawing.Size(300, 727);
+ this.tabControl1.Size = new System.Drawing.Size(300, 335);
this.tabControl1.TabIndex = 0;
//
// tabPageNodes
//
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";
this.tabPageNodes.Padding = new System.Windows.Forms.Padding(3);
- this.tabPageNodes.Size = new System.Drawing.Size(292, 701);
+ this.tabPageNodes.Size = new System.Drawing.Size(292, 309);
this.tabPageNodes.TabIndex = 0;
this.tabPageNodes.Text = "노드 관리";
this.tabPageNodes.UseVisualStyleBackColor = true;
@@ -195,17 +121,9 @@ namespace AGVMapEditor.Forms
this.listBoxNodes.ItemHeight = 12;
this.listBoxNodes.Location = new System.Drawing.Point(3, 3);
this.listBoxNodes.Name = "listBoxNodes";
- this.listBoxNodes.Size = new System.Drawing.Size(286, 245);
+ this.listBoxNodes.Size = new System.Drawing.Size(286, 303);
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;
@@ -215,6 +133,132 @@ namespace AGVMapEditor.Forms
this.label1.TabIndex = 0;
this.label1.Text = "노드 목록";
//
+ // tabPage1
+ //
+ this.tabPage1.Controls.Add(this.lstNodeConnection);
+ this.tabPage1.Controls.Add(this.toolStrip1);
+ this.tabPage1.Location = new System.Drawing.Point(4, 22);
+ this.tabPage1.Name = "tabPage1";
+ this.tabPage1.Padding = new System.Windows.Forms.Padding(3);
+ this.tabPage1.Size = new System.Drawing.Size(292, 310);
+ this.tabPage1.TabIndex = 1;
+ this.tabPage1.Text = "연결 관리";
+ this.tabPage1.UseVisualStyleBackColor = true;
+ //
+ // lstNodeConnection
+ //
+ this.lstNodeConnection.Dock = System.Windows.Forms.DockStyle.Fill;
+ this.lstNodeConnection.FormattingEnabled = true;
+ this.lstNodeConnection.ItemHeight = 12;
+ this.lstNodeConnection.Location = new System.Drawing.Point(3, 3);
+ this.lstNodeConnection.Name = "lstNodeConnection";
+ this.lstNodeConnection.Size = new System.Drawing.Size(286, 279);
+ this.lstNodeConnection.TabIndex = 2;
+ //
+ // toolStrip1
+ //
+ this.toolStrip1.Dock = System.Windows.Forms.DockStyle.Bottom;
+ this.toolStrip1.GripStyle = System.Windows.Forms.ToolStripGripStyle.Hidden;
+ this.toolStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
+ this.btNodeRemove});
+ this.toolStrip1.Location = new System.Drawing.Point(3, 282);
+ this.toolStrip1.Name = "toolStrip1";
+ this.toolStrip1.Size = new System.Drawing.Size(286, 25);
+ this.toolStrip1.TabIndex = 3;
+ this.toolStrip1.Text = "toolStrip1";
+ //
+ // btNodeRemove
+ //
+ this.btNodeRemove.Image = ((System.Drawing.Image)(resources.GetObject("btNodeRemove.Image")));
+ this.btNodeRemove.ImageTransparentColor = System.Drawing.Color.Magenta;
+ this.btNodeRemove.Name = "btNodeRemove";
+ this.btNodeRemove.Size = new System.Drawing.Size(70, 22);
+ this.btNodeRemove.Text = "Remove";
+ this.btNodeRemove.Click += new System.EventHandler(this.btNodeRemove_Click);
+ //
+ // _propertyGrid
+ //
+ this._propertyGrid.Dock = System.Windows.Forms.DockStyle.Bottom;
+ this._propertyGrid.Location = new System.Drawing.Point(0, 335);
+ this._propertyGrid.Name = "_propertyGrid";
+ this._propertyGrid.Size = new System.Drawing.Size(300, 391);
+ this._propertyGrid.TabIndex = 6;
+ //
+ // toolStrip2
+ //
+ this.toolStrip2.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
+ this.btnNew,
+ this.btnOpen,
+ this.btnReopen,
+ this.btnClose,
+ this.toolStripSeparator3,
+ this.btnSave,
+ this.btnSaveAs});
+ this.toolStrip2.Location = new System.Drawing.Point(0, 0);
+ this.toolStrip2.Name = "toolStrip2";
+ this.toolStrip2.Size = new System.Drawing.Size(1200, 25);
+ this.toolStrip2.TabIndex = 0;
+ this.toolStrip2.Text = "toolStrip2";
+ //
+ // btnNew
+ //
+ this.btnNew.Image = ((System.Drawing.Image)(resources.GetObject("btnNew.Image")));
+ this.btnNew.Name = "btnNew";
+ this.btnNew.Size = new System.Drawing.Size(104, 22);
+ this.btnNew.Text = "새로만들기(&N)";
+ this.btnNew.ToolTipText = "새로 만들기 (Ctrl+N)";
+ this.btnNew.Click += new System.EventHandler(this.btnNew_Click);
+ //
+ // btnOpen
+ //
+ this.btnOpen.Image = ((System.Drawing.Image)(resources.GetObject("btnOpen.Image")));
+ this.btnOpen.Name = "btnOpen";
+ this.btnOpen.Size = new System.Drawing.Size(68, 22);
+ this.btnOpen.Text = "열기(&O)";
+ this.btnOpen.ToolTipText = "열기 (Ctrl+O)";
+ this.btnOpen.Click += new System.EventHandler(this.btnOpen_Click);
+ //
+ // btnReopen
+ //
+ this.btnReopen.Image = ((System.Drawing.Image)(resources.GetObject("btnReopen.Image")));
+ this.btnReopen.Name = "btnReopen";
+ this.btnReopen.Size = new System.Drawing.Size(90, 22);
+ this.btnReopen.Text = "다시열기(&R)";
+ this.btnReopen.ToolTipText = "현재 파일 다시 열기";
+ this.btnReopen.Click += new System.EventHandler(this.btnReopen_Click);
+ //
+ // btnClose
+ //
+ this.btnClose.Image = ((System.Drawing.Image)(resources.GetObject("btnClose.Image")));
+ this.btnClose.Name = "btnClose";
+ this.btnClose.Size = new System.Drawing.Size(75, 22);
+ this.btnClose.Text = "파일닫기";
+ this.btnClose.ToolTipText = "닫기";
+ this.btnClose.Click += new System.EventHandler(this.btnClose_Click);
+ //
+ // toolStripSeparator3
+ //
+ this.toolStripSeparator3.Name = "toolStripSeparator3";
+ this.toolStripSeparator3.Size = new System.Drawing.Size(6, 25);
+ //
+ // btnSave
+ //
+ this.btnSave.Image = ((System.Drawing.Image)(resources.GetObject("btnSave.Image")));
+ this.btnSave.Name = "btnSave";
+ this.btnSave.Size = new System.Drawing.Size(66, 22);
+ this.btnSave.Text = "저장(&S)";
+ this.btnSave.ToolTipText = "저장 (Ctrl+S)";
+ this.btnSave.Click += new System.EventHandler(this.btnSave_Click);
+ //
+ // btnSaveAs
+ //
+ this.btnSaveAs.Image = ((System.Drawing.Image)(resources.GetObject("btnSaveAs.Image")));
+ this.btnSaveAs.Name = "btnSaveAs";
+ this.btnSaveAs.Size = new System.Drawing.Size(123, 22);
+ this.btnSaveAs.Text = "다른이름으로저장";
+ this.btnSaveAs.ToolTipText = "다른 이름으로 저장";
+ this.btnSaveAs.Click += new System.EventHandler(this.btnSaveAs_Click);
+ //
// MainForm
//
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 12F);
@@ -222,15 +266,12 @@ namespace AGVMapEditor.Forms
this.ClientSize = new System.Drawing.Size(1200, 773);
this.Controls.Add(this.splitContainer1);
this.Controls.Add(this.statusStrip1);
- this.Controls.Add(this.menuStrip1);
- this.MainMenuStrip = this.menuStrip1;
+ this.Controls.Add(this.toolStrip2);
this.Name = "MainForm";
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen;
this.Text = "AGV Map Editor";
this.FormClosing += new System.Windows.Forms.FormClosingEventHandler(this.MainForm_FormClosing);
this.Load += new System.EventHandler(this.MainForm_Load);
- this.menuStrip1.ResumeLayout(false);
- this.menuStrip1.PerformLayout();
this.statusStrip1.ResumeLayout(false);
this.statusStrip1.PerformLayout();
this.splitContainer1.Panel1.ResumeLayout(false);
@@ -239,6 +280,12 @@ namespace AGVMapEditor.Forms
this.tabControl1.ResumeLayout(false);
this.tabPageNodes.ResumeLayout(false);
this.tabPageNodes.PerformLayout();
+ this.tabPage1.ResumeLayout(false);
+ this.tabPage1.PerformLayout();
+ this.toolStrip1.ResumeLayout(false);
+ this.toolStrip1.PerformLayout();
+ this.toolStrip2.ResumeLayout(false);
+ this.toolStrip2.PerformLayout();
this.ResumeLayout(false);
this.PerformLayout();
@@ -246,16 +293,6 @@ namespace AGVMapEditor.Forms
#endregion
- private System.Windows.Forms.MenuStrip menuStrip1;
- private System.Windows.Forms.ToolStripMenuItem fileToolStripMenuItem;
- private System.Windows.Forms.ToolStripMenuItem newToolStripMenuItem;
- private System.Windows.Forms.ToolStripMenuItem openToolStripMenuItem;
- 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;
private System.Windows.Forms.ToolStripStatusLabel toolStripStatusLabel1;
private System.Windows.Forms.SplitContainer splitContainer1;
@@ -264,5 +301,17 @@ namespace AGVMapEditor.Forms
private System.Windows.Forms.ListBox listBoxNodes;
private System.Windows.Forms.Label label1;
private System.Windows.Forms.PropertyGrid _propertyGrid;
+ private System.Windows.Forms.TabPage tabPage1;
+ private System.Windows.Forms.ListBox lstNodeConnection;
+ private System.Windows.Forms.ToolStrip toolStrip1;
+ private System.Windows.Forms.ToolStripButton btNodeRemove;
+ private System.Windows.Forms.ToolStrip toolStrip2;
+ private System.Windows.Forms.ToolStripButton btnNew;
+ private System.Windows.Forms.ToolStripButton btnOpen;
+ private System.Windows.Forms.ToolStripButton btnReopen;
+ private System.Windows.Forms.ToolStripButton btnClose;
+ private System.Windows.Forms.ToolStripSeparator toolStripSeparator3;
+ private System.Windows.Forms.ToolStripButton btnSave;
+ private System.Windows.Forms.ToolStripButton btnSaveAs;
}
}
\ No newline at end of file
diff --git a/Cs_HMI/AGVMapEditor/Forms/MainForm.cs b/Cs_HMI/AGVMapEditor/Forms/MainForm.cs
index 9dde1b9..d1bb41c 100644
--- a/Cs_HMI/AGVMapEditor/Forms/MainForm.cs
+++ b/Cs_HMI/AGVMapEditor/Forms/MainForm.cs
@@ -27,6 +27,35 @@ namespace AGVMapEditor.Forms
// 파일 경로
private string _currentMapFile = string.Empty;
private bool _hasChanges = false;
+ private bool _hasCommandLineArgs = false;
+
+ // 노드 연결 정보를 표현하는 클래스
+ public class NodeConnectionInfo
+ {
+ public string FromNodeId { get; set; }
+ public string FromNodeName { get; set; }
+ public string FromRfidId { get; set; }
+ public string ToNodeId { get; set; }
+ public string ToNodeName { get; set; }
+ public string ToRfidId { get; set; }
+ public string ConnectionType { get; set; }
+
+ public override string ToString()
+ {
+ // RFID가 있으면 RFID(노드이름), 없으면 NodeID(노드이름) 형태로 표시
+ string fromDisplay = !string.IsNullOrEmpty(FromRfidId)
+ ? $"{FromRfidId}({FromNodeName})"
+ : $"---({FromNodeId})";
+
+ string toDisplay = !string.IsNullOrEmpty(ToRfidId)
+ ? $"{ToRfidId}({ToNodeName})"
+ : $"---({ToNodeId})";
+
+ // 양방향 연결은 ↔ 기호 사용
+ string arrow = ConnectionType == "양방향" ? "↔" : "→";
+ return $"{fromDisplay} {arrow} {toDisplay}";
+ }
+ }
#endregion
@@ -47,6 +76,7 @@ namespace AGVMapEditor.Forms
// 명령줄 인수로 파일이 전달되었으면 자동으로 열기
if (args != null && args.Length > 0)
{
+ _hasCommandLineArgs = true;
string filePath = args[0];
if (System.IO.File.Exists(filePath))
{
@@ -54,14 +84,16 @@ namespace AGVMapEditor.Forms
}
else
{
- MessageBox.Show($"지정된 파일을 찾을 수 없습니다: {filePath}", "파일 오류",
+ MessageBox.Show($"지정된 파일을 찾을 수 없습니다: {filePath}", "파일 오류",
MessageBoxButtons.OK, MessageBoxIcon.Warning);
}
}
+ // 명령줄 인수가 없는 경우는 Form_Load에서 마지막 맵 파일 자동 로드 확인
}
#endregion
+
#region Initialization
private void InitializeData()
@@ -82,6 +114,7 @@ namespace AGVMapEditor.Forms
_mapCanvas.NodeSelected += OnNodeSelected;
_mapCanvas.NodeMoved += OnNodeMoved;
_mapCanvas.NodeDeleted += OnNodeDeleted;
+ _mapCanvas.ConnectionDeleted += OnConnectionDeleted;
_mapCanvas.MapChanged += OnMapChanged;
// 스플리터 패널에 맵 캔버스 추가
@@ -148,31 +181,38 @@ namespace AGVMapEditor.Forms
btnDelete.Location = new Point(495, 3);
btnDelete.Click += (s, e) => _mapCanvas.CurrentEditMode = UnifiedAGVCanvas.EditMode.Delete;
+ // 연결 삭제 버튼
+ var btnDeleteConnection = new Button();
+ btnDeleteConnection.Text = "연결삭제 (X)";
+ btnDeleteConnection.Size = new Size(80, 28);
+ btnDeleteConnection.Location = new Point(570, 3);
+ btnDeleteConnection.Click += (s, e) => _mapCanvas.CurrentEditMode = UnifiedAGVCanvas.EditMode.DeleteConnection;
+
// 구분선
var separator1 = new Label();
separator1.Text = "|";
separator1.Size = new Size(10, 28);
- separator1.Location = new Point(570, 3);
+ separator1.Location = new Point(655, 3);
separator1.TextAlign = ContentAlignment.MiddleCenter;
// 그리드 토글 버튼
var btnToggleGrid = new Button();
btnToggleGrid.Text = "그리드";
btnToggleGrid.Size = new Size(60, 28);
- btnToggleGrid.Location = new Point(585, 3);
+ btnToggleGrid.Location = new Point(670, 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.Location = new Point(735, 3);
btnFitMap.Click += (s, e) => _mapCanvas.FitToNodes();
// 툴바에 버튼들 추가
toolbarPanel.Controls.AddRange(new Control[]
{
- btnSelect, btnMove, btnAddNode, btnAddLabel, btnAddImage, btnConnect, btnDelete, separator1, btnToggleGrid, btnFitMap
+ btnSelect, btnMove, btnAddNode, btnAddLabel, btnAddImage, btnConnect, btnDelete, btnDeleteConnection, separator1, btnToggleGrid, btnFitMap
});
// 스플리터 패널에 툴바 추가 (맨 위에)
@@ -186,9 +226,18 @@ namespace AGVMapEditor.Forms
private void MainForm_Load(object sender, EventArgs e)
{
+
RefreshNodeList();
// 속성 변경 시 이벤트 연결
_propertyGrid.PropertyValueChanged += PropertyGrid_PropertyValueChanged;
+
+ // 명령줄 인수가 없는 경우에만 마지막 맵 파일 자동 로드 확인
+ if (!_hasCommandLineArgs)
+ {
+ this.Show();
+ Application.DoEvents();
+ CheckAndLoadLastMapFile();
+ }
}
private void OnNodeAdded(object sender, MapNode node)
@@ -228,6 +277,14 @@ namespace AGVMapEditor.Forms
UpdateNodeProperties(); // 연결 정보 업데이트
}
+ private void OnConnectionDeleted(object sender, (MapNode From, MapNode To) connection)
+ {
+ _hasChanges = true;
+ UpdateTitle();
+ RefreshNodeConnectionList();
+ UpdateNodeProperties(); // 연결 정보 업데이트
+ }
+
private void OnMapChanged(object sender, EventArgs e)
{
_hasChanges = true;
@@ -242,9 +299,9 @@ namespace AGVMapEditor.Forms
#endregion
- #region Menu Event Handlers
+ #region ToolStrip Button Event Handlers
- private void newToolStripMenuItem_Click(object sender, EventArgs e)
+ private void btnNew_Click(object sender, EventArgs e)
{
if (CheckSaveChanges())
{
@@ -252,7 +309,7 @@ namespace AGVMapEditor.Forms
}
}
- private void openToolStripMenuItem_Click(object sender, EventArgs e)
+ private void btnOpen_Click(object sender, EventArgs e)
{
if (CheckSaveChanges())
{
@@ -260,28 +317,70 @@ namespace AGVMapEditor.Forms
}
}
- private void saveToolStripMenuItem_Click(object sender, EventArgs e)
+ private void btnReopen_Click(object sender, EventArgs e)
{
- SaveMap();
+ if (string.IsNullOrEmpty(_currentMapFile))
+ {
+ MessageBox.Show("다시 열 파일이 없습니다.", "알림", MessageBoxButtons.OK, MessageBoxIcon.Information);
+ return;
+ }
+
+ if (!File.Exists(_currentMapFile))
+ {
+ MessageBox.Show($"파일을 찾을 수 없습니다: {_currentMapFile}", "오류", MessageBoxButtons.OK, MessageBoxIcon.Error);
+ return;
+ }
+
+ if (CheckSaveChanges())
+ {
+ LoadMapFromFile(_currentMapFile);
+ UpdateStatusBar($"파일을 다시 열었습니다: {Path.GetFileName(_currentMapFile)}");
+ }
}
- private void saveAsToolStripMenuItem_Click(object sender, EventArgs e)
- {
- SaveAsMap();
- }
-
- private void closeToolStripMenuItem_Click(object sender, EventArgs e)
+ private void btnClose_Click(object sender, EventArgs e)
{
CloseMap();
}
- private void exitToolStripMenuItem_Click(object sender, EventArgs e)
+ private void btnSave_Click(object sender, EventArgs e)
+ {
+ SaveMap();
+ }
+
+ private void btnSaveAs_Click(object sender, EventArgs e)
+ {
+ SaveAsMap();
+ }
+
+ private void btnExit_Click(object sender, EventArgs e)
{
this.Close();
}
#endregion
+ #region Keyboard Shortcuts
+
+ protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
+ {
+ switch (keyData)
+ {
+ case Keys.Control | Keys.N:
+ btnNew_Click(null, null);
+ return true;
+ case Keys.Control | Keys.O:
+ btnOpen_Click(null, null);
+ return true;
+ case Keys.Control | Keys.S:
+ btnSave_Click(null, null);
+ return true;
+ }
+ return base.ProcessCmdKey(ref msg, keyData);
+ }
+
+ #endregion
+
#region Button Event Handlers
private void btnAddNode_Click(object sender, EventArgs e)
@@ -529,27 +628,55 @@ namespace AGVMapEditor.Forms
private void LoadMapFromFile(string filePath)
{
var result = MapLoader.LoadMapFromFile(filePath);
-
+
if (result.Success)
{
_mapNodes = result.Nodes;
-
+
// 맵 캔버스에 데이터 설정
_mapCanvas.Nodes = _mapNodes;
// RfidMappings 제거됨 - MapNode에 통합
+
+ // 현재 파일 경로 업데이트
+ _currentMapFile = filePath;
+ _hasChanges = false;
+
+ // 설정에 마지막 맵 파일 경로 저장
+ EditorSettings.Instance.UpdateLastMapFile(filePath);
+
+ UpdateTitle();
+ UpdateNodeList();
+ RefreshNodeConnectionList();
+
+ // 맵 로드 후 자동으로 맵에 맞춤
+ _mapCanvas.FitToNodes();
+
+ UpdateStatusBar($"맵 파일을 성공적으로 로드했습니다: {Path.GetFileName(filePath)}");
}
else
{
- MessageBox.Show($"맵 파일 로딩 실패: {result.ErrorMessage}", "오류",
+ MessageBox.Show($"맵 파일 로딩 실패: {result.ErrorMessage}", "오류",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
private void SaveMapToFile(string filePath)
{
- if (!MapLoader.SaveMapToFile(filePath, _mapNodes))
+ if (MapLoader.SaveMapToFile(filePath, _mapNodes))
{
- MessageBox.Show("맵 파일 저장 실패", "오류",
+ // 현재 파일 경로 업데이트
+ _currentMapFile = filePath;
+ _hasChanges = false;
+
+ // 설정에 마지막 맵 파일 경로 저장
+ EditorSettings.Instance.UpdateLastMapFile(filePath);
+
+ UpdateTitle();
+ UpdateStatusBar($"맵 파일을 성공적으로 저장했습니다: {Path.GetFileName(filePath)}");
+ }
+ else
+ {
+ MessageBox.Show("맵 파일 저장 실패", "오류",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
@@ -559,8 +686,8 @@ namespace AGVMapEditor.Forms
///
private void UpdateRfidMappings()
{
- // 네비게이션 노드들에 RFID 자동 할당
- MapLoader.AssignAutoRfidIds(_mapNodes);
+ // RFID 자동 할당 제거 - 사용자가 직접 입력한 값 유지
+ // MapLoader.AssignAutoRfidIds(_mapNodes);
}
private bool CheckSaveChanges()
@@ -584,6 +711,29 @@ namespace AGVMapEditor.Forms
return true;
}
+ ///
+ /// 마지막 맵 파일이 있는지 확인하고 사용자에게 로드할지 물어봄
+ ///
+ private void CheckAndLoadLastMapFile()
+ {
+ var settings = EditorSettings.Instance;
+
+ if (settings.AutoLoadLastMapFile && settings.HasValidLastMapFile())
+ {
+ string fileName = Path.GetFileName(settings.LastMapFilePath);
+ var result = MessageBox.Show(
+ $"마지막으로 사용한 맵 파일을 찾았습니다:\n\n{fileName}\n\n이 파일을 열까요?",
+ "마지막 맵 파일 로드",
+ MessageBoxButtons.YesNo,
+ MessageBoxIcon.Question);
+
+ if (result == DialogResult.Yes)
+ {
+ LoadMapFromFile(settings.LastMapFilePath);
+ }
+ }
+ }
+
#endregion
#region UI Updates
@@ -591,6 +741,7 @@ namespace AGVMapEditor.Forms
private void RefreshAll()
{
RefreshNodeList();
+ RefreshNodeConnectionList();
RefreshMapCanvas();
ClearNodeProperties();
}
@@ -672,12 +823,12 @@ namespace AGVMapEditor.Forms
e.Graphics.FillRectangle(brush, e.Bounds);
}
- // 텍스트 그리기 (노드ID - 설명 - RFID 순서)
+ // 텍스트 그리기 (노드ID - 노드명 - RFID 순서)
var displayText = node.NodeId;
-
- if (!string.IsNullOrEmpty(node.Description))
+
+ if (!string.IsNullOrEmpty(node.Name))
{
- displayText += $" - {node.Description}";
+ displayText += $" - {node.Name}";
}
if (!string.IsNullOrEmpty(node.RfidId))
@@ -694,6 +845,92 @@ namespace AGVMapEditor.Forms
e.DrawFocusRectangle();
}
+ private void RefreshNodeConnectionList()
+ {
+ var connections = new List();
+ var processedPairs = new HashSet();
+
+ // 모든 노드의 연결 정보를 수집 (중복 방지)
+ foreach (var fromNode in _mapNodes)
+ {
+ foreach (var toNodeId in fromNode.ConnectedNodes)
+ {
+ var toNode = _mapNodes.FirstOrDefault(n => n.NodeId == toNodeId);
+ if (toNode != null)
+ {
+ // 중복 체크 (단일 연결만 표시)
+ string pairKey1 = $"{fromNode.NodeId}-{toNode.NodeId}";
+ string pairKey2 = $"{toNode.NodeId}-{fromNode.NodeId}";
+
+ if (!processedPairs.Contains(pairKey1) && !processedPairs.Contains(pairKey2))
+ {
+ // 사전 순으로 정렬하여 일관성 있게 표시
+ var (firstNode, secondNode) = string.Compare(fromNode.NodeId, toNode.NodeId) < 0
+ ? (fromNode, toNode)
+ : (toNode, fromNode);
+
+ connections.Add(new NodeConnectionInfo
+ {
+ FromNodeId = firstNode.NodeId,
+ FromNodeName = firstNode.Name,
+ FromRfidId = firstNode.RfidId,
+ ToNodeId = secondNode.NodeId,
+ ToNodeName = secondNode.Name,
+ ToRfidId = secondNode.RfidId,
+ ConnectionType = "양방향" // 모든 연결이 양방향
+ });
+
+ processedPairs.Add(pairKey1);
+ processedPairs.Add(pairKey2);
+ }
+ }
+ }
+ }
+
+ // 리스트박스에 표시
+ lstNodeConnection.DataSource = null;
+ lstNodeConnection.DataSource = connections;
+ lstNodeConnection.DisplayMember = "ToString";
+
+ // 리스트박스 클릭 이벤트 연결
+ lstNodeConnection.SelectedIndexChanged -= LstNodeConnection_SelectedIndexChanged;
+ lstNodeConnection.SelectedIndexChanged += LstNodeConnection_SelectedIndexChanged;
+
+ // 더블클릭 이벤트 연결 (연결 삭제)
+ lstNodeConnection.DoubleClick -= LstNodeConnection_DoubleClick;
+ lstNodeConnection.DoubleClick += LstNodeConnection_DoubleClick;
+ }
+
+
+ private void LstNodeConnection_SelectedIndexChanged(object sender, EventArgs e)
+ {
+ if (lstNodeConnection.SelectedItem is NodeConnectionInfo connectionInfo)
+ {
+ // 캔버스에서 해당 연결선 강조 표시
+ _mapCanvas?.HighlightConnection(connectionInfo.FromNodeId, connectionInfo.ToNodeId);
+
+ // 연결된 노드들을 맵에서 하이라이트 표시 (선택적)
+ var fromNode = _mapNodes.FirstOrDefault(n => n.NodeId == connectionInfo.FromNodeId);
+ if (fromNode != null)
+ {
+ _selectedNode = fromNode;
+ UpdateNodeProperties();
+ _mapCanvas?.Invalidate();
+ }
+ }
+ else
+ {
+ // 선택 해제 시 강조 표시 제거
+ _mapCanvas?.ClearHighlightedConnection();
+ }
+ }
+
+ private void LstNodeConnection_DoubleClick(object sender, EventArgs e)
+ {
+ // 더블클릭으로 연결 삭제
+ DeleteSelectedConnection();
+ }
+
private void RefreshMapCanvas()
{
_mapCanvas?.Invalidate();
@@ -735,6 +972,31 @@ namespace AGVMapEditor.Forms
this.Text = title;
}
+ ///
+ /// 노드 목록을 업데이트
+ ///
+ private void UpdateNodeList()
+ {
+ if (listBoxNodes != null)
+ {
+ listBoxNodes.DataSource = null;
+ listBoxNodes.DataSource = _mapNodes;
+ listBoxNodes.DisplayMember = "DisplayText";
+ }
+ }
+
+ ///
+ /// 상태바에 메시지 표시
+ ///
+ /// 표시할 메시지
+ private void UpdateStatusBar(string message)
+ {
+ if (toolStripStatusLabel1 != null)
+ {
+ toolStripStatusLabel1.Text = message;
+ }
+ }
+
#endregion
#region Form Events
@@ -754,10 +1016,27 @@ namespace AGVMapEditor.Forms
private void PropertyGrid_PropertyValueChanged(object s, PropertyValueChangedEventArgs e)
{
+ // RFID 값 변경시 중복 검사
+ if (e.ChangedItem.PropertyDescriptor.Name == "RFID")
+ {
+ string newRfidValue = e.ChangedItem.Value?.ToString();
+ if (!string.IsNullOrEmpty(newRfidValue) && CheckRfidDuplicate(newRfidValue))
+ {
+ // 중복된 RFID 값 발견
+ MessageBox.Show($"RFID 값 '{newRfidValue}'이(가) 이미 다른 노드에서 사용 중입니다.\n입력값을 되돌립니다.",
+ "RFID 중복 오류", MessageBoxButtons.OK, MessageBoxIcon.Warning);
+
+ // 원래 값으로 되돌리기 - PropertyGrid의 SelectedObject 사용
+ e.ChangedItem.PropertyDescriptor.SetValue(_propertyGrid.SelectedObject, e.OldValue);
+ _propertyGrid.Refresh();
+ return;
+ }
+ }
+
// 속성이 변경되었을 때 자동으로 변경사항 표시
_hasChanges = true;
UpdateTitle();
-
+
// 현재 선택된 노드를 기억
var currentSelectedNode = _selectedNode;
@@ -775,12 +1054,107 @@ namespace AGVMapEditor.Forms
}
}
+ ///
+ /// RFID 값 중복 검사
+ ///
+ /// 검사할 RFID 값
+ /// 중복되면 true, 아니면 false
+ private bool CheckRfidDuplicate(string rfidValue)
+ {
+ if (string.IsNullOrEmpty(rfidValue) || _mapNodes == null)
+ return false;
+
+ // 현재 편집 중인 노드 제외하고 중복 검사
+ string currentNodeId = null;
+ var selectedObject = _propertyGrid.SelectedObject;
+
+ // 다양한 PropertyWrapper 타입 처리
+ if (selectedObject is NodePropertyWrapper nodeWrapper)
+ {
+ currentNodeId = nodeWrapper.WrappedNode?.NodeId;
+ }
+ else if (selectedObject is LabelNodePropertyWrapper labelWrapper)
+ {
+ currentNodeId = labelWrapper.WrappedNode?.NodeId;
+ }
+ else if (selectedObject is ImageNodePropertyWrapper imageWrapper)
+ {
+ currentNodeId = imageWrapper.WrappedNode?.NodeId;
+ }
+
+ int duplicateCount = 0;
+ foreach (var node in _mapNodes)
+ {
+ // 현재 편집 중인 노드는 제외
+ if (node.NodeId == currentNodeId)
+ continue;
+
+ // 같은 RFID 값을 가진 노드가 있는지 확인
+ if (!string.IsNullOrEmpty(node.RfidId) && node.RfidId.Equals(rfidValue, StringComparison.OrdinalIgnoreCase))
+ {
+ duplicateCount++;
+ break; // 하나라도 발견되면 중복
+ }
+ }
+
+ return duplicateCount > 0;
+ }
+
#endregion
#region Data Model for Serialization
#endregion
-
+
+ private void btNodeRemove_Click(object sender, EventArgs e)
+ {
+ DeleteSelectedConnection();
+ }
+
+ private void DeleteSelectedConnection()
+ {
+ if (lstNodeConnection.SelectedItem is NodeConnectionInfo connectionInfo)
+ {
+ var result = MessageBox.Show(
+ $"다음 연결을 삭제하시겠습니까?\n{connectionInfo}",
+ "연결 삭제 확인",
+ MessageBoxButtons.YesNo,
+ MessageBoxIcon.Question);
+
+ if (result == DialogResult.Yes)
+ {
+ // 단일 연결 삭제
+ var fromNode = _mapNodes.FirstOrDefault(n => n.NodeId == connectionInfo.FromNodeId);
+ var toNode = _mapNodes.FirstOrDefault(n => n.NodeId == connectionInfo.ToNodeId);
+
+ if (fromNode != null && toNode != null)
+ {
+ // 단일 연결 삭제 (어느 방향에 저장되어 있는지 확인 후 삭제)
+ if (fromNode.ConnectedNodes.Contains(toNode.NodeId))
+ {
+ fromNode.RemoveConnection(toNode.NodeId);
+ }
+ else if (toNode.ConnectedNodes.Contains(fromNode.NodeId))
+ {
+ toNode.RemoveConnection(fromNode.NodeId);
+ }
+
+ _hasChanges = true;
+
+ RefreshNodeConnectionList();
+ RefreshMapCanvas();
+ UpdateNodeProperties();
+ UpdateTitle();
+
+ toolStripStatusLabel1.Text = $"연결 삭제됨: {connectionInfo.FromNodeId} ↔ {connectionInfo.ToNodeId}";
+ }
+ }
+ }
+ else
+ {
+ MessageBox.Show("삭제할 연결을 선택하세요.", "알림", MessageBoxButtons.OK, MessageBoxIcon.Information);
+ }
+ }
}
}
\ No newline at end of file
diff --git a/Cs_HMI/AGVMapEditor/Forms/MainForm.resx b/Cs_HMI/AGVMapEditor/Forms/MainForm.resx
index 69e20d7..0500881 100644
--- a/Cs_HMI/AGVMapEditor/Forms/MainForm.resx
+++ b/Cs_HMI/AGVMapEditor/Forms/MainForm.resx
@@ -117,10 +117,112 @@
System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
-
- 17, 17
-
132, 17
+
+ 249, 17
+
+
+ 249, 17
+
+
+
+
+ iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8
+ YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAIDSURBVDhPpZLrS5NhGMb3j4SWh0oRQVExD4gonkDpg4hG
+ YKxG6WBogkMZKgPNCEVJFBGdGETEvgwyO9DJE5syZw3PIlPEE9pgBCLZ5XvdMB8Ew8gXbl54nuf63dd9
+ 0OGSnwCahxbPRNPAPMw9Xpg6ZmF46kZZ0xSKzJPIrhpDWsVnpBhGkKx3nAX8Pv7z1zg8OoY/cITdn4fw
+ bf/C0kYAN3Ma/w3gWfZL5kzTKBxjWyK2DftwI9tyMYCZKXbNHaD91bLYJrDXsYbrWfUKwJrPE9M2M1Oc
+ VzOOpHI7Jr376Hi9ogHqFIANO0/MmmmbmSmm9a8ze+I4MrNWAdjtoJgWcx+PSzg166yZZ8xM8XvXDix9
+ c4jIqFYAjoriBV9AhEPv1mH/sonogha0afbZMMZz+yreTGyhpusHwtNNCsA5U1zS4BLxzJIfg299qO32
+ Ir7UJtZfftyATqeT+8o2D8JSjQrAJblrncYL7ZJ2+bfaFnC/1S1NjL3diRat7qrO7wLRP3HjWsojBeCo
+ mDEo5mNjuweFGvjWg2EBhCbpkW78htSHHwRyNdmgAFzPEee2iFkzayy2OLXzT4gr6UdUnlXrullsxxQ+
+ kx0g8BTA3aZlButjSTyjODq/WcQcW/B/Je4OQhLvKQDnzN1mp0nnkvAhR8VuMzNrpm1mpjgkoVwB/v8D
+ TgDQASA1MVpwzwAAAABJRU5ErkJggg==
+
+
+
+ 356, 17
+
+
+
+ R0lGODlhEAAQAIUoAOLp8ElVa0NLXIivyJXK/D5FVYm77N7n7ykxQ5rA1svM0YS8O4C4OJXJSInAP5fL
+ S362N4/ERJLHR5DFRdXb5JbKStXn8HqzM4vAQJ7O+1Jhe1dwkezx9nKsLeXt9XaXtKTR+7HX/PL2+sDf
+ /bvc/avU+7ba/P///////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH/C05FVFNDQVBFMi4wAwEBAAAh+QQBAAAoACwAAAAAEAAQ
+ AAAIswBPCBxIcCCKgycsnOjQ4USCAQM+SPxwAqHABw0GetjowcCGigsrSIjgoOGIkyIMeBTYYQKGBRBM
+ kiAhoqYGggwuDBxBwoSJECJuOlxokqfPECWCChxAcOZPpEkDLBVxsufPEiVAgPAg9cQHDlWvZgWRwYMA
+ gV+dQtWaIQOAs145WEXKlgCBAwXQiui5tq1dDnlPbKAggsNGAIgBHOCgAAHaDZA1aAgQQICAAgUQCC3I
+ mWBAADs=
+
+
+
+
+ iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29m
+ dHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAILSURBVDhPpY9LaxNRGIa7cO/On+CqFdFmRHQpiCs3
+ ohtvIAW1WUiUQkXQLnoBIVAaRUQshVLBFotGBJfWRTW17dhEM5mkteklNMFm7pecubx+mWomQhBLYZ75
+ zjnwPu85bQD2RMvD3dDycDcEv9zbTr44c5mtfbzaxBWWf3dyWXjTfoDY/xfJgH0NgZjkbN/3aOnTV2dn
+ 7VgVbMx219Znu+0/lOZ7rc3UnWLu9ZHzoWA6IvuuBXsjDmNlGMbqKMziFHxHIpcO39UIZWfvVAGP1cRp
+ LhMKXnKqxxQKJ6AXRqB+68N26iYqc1FI6X5ISw8g8XdRXewhYnSzMsRXnVoomIwwx9yClEmgujCE8ofb
+ MMufqVX93V6fcngDep44yVmhYOIYC97v2YQJuEYQdE0BdikBa70f5o8eGPnr0IWLcPUsxPFIk2CMq/mu
+ CVf9BEd6D/ZzCrWtZxR8CHP1HsxCFEbuEvTvZ6GlT8HVeIijzYKnnO0xFWw7iVp5nFofwVqj1pV6axf0
+ 7AVomTNQv56AsnAYjvoF4pNmQSJi+Uyi8BjszWFYxT4Kx2CI1yh8jlpPQ+WPQ5k/BHnuIAlSqGcaglz8
+ aInJBVlffuHp+efQxMfQhDi07ADUzH2o6V4oSzEofBTK4g3YlRmfMnJDIAy03xKGOiaEwY4KTeufDAZT
+ ocxIQ7AXWh7+P2j7BY3RGzIVTOkAAAAAAElFTkSuQmCC
+
+
+
+
+ R0lGODlhEAAQAIQfAJfL/OTs9HWVsW6aUqnT+6bnZldwkYiux7TZ/O3z+UlVa/P2+ZfTW36wWJDLV4m7
+ 69nn78bi/qjL3qDP+VJhe4rAVa7S40NLXJ3bYJrA1ikxQz5FVdDU22OPRf///////yH/C05FVFNDQVBF
+ Mi4wAwEBAAAh+QQBAAAfACwAAAAAEAAQAAAIwQA9CBxIcOCHgx4gWLAgIUOGAwcESBTgAaEFCAEGaBwQ
+ IGOABwYqerCQsYBJBho7JHgAUqCEDjAxYGBQgYHKBAsoCMzQIUIEmA6CdkCAIOfOBT5/MnBQYSgBozCj
+ SoVJ4KkCDx1MFhhKFEFVAhMCXM1aAANMoh2qTgh7AWvZmQ6igp0AIEDbDg0aLA06YC4AABA2eBjgYcHG
+ vmv/Akgg2IMBDgsSdJwcAEICDhoECjDAmQIFBQouXNiwQYPOgqgLBgQAOw==
+
+
+
+
+ iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29m
+ dHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHlSURBVDhPpZJbSxtBGIb3VhQULVoqUunRtEpCEmmM
+ 0UJi2ZiE1qYHg1ZKbUsP/9MLD+DfKGZDScJmd7M783RmY9LdpjelFy/DDPO837zfNwbwXxo/NAwrPL6W
+ iMv6835so7ZNHj+Ab+/g6yHyywHycwP58S3yw2uCh3fwDaMTZcbh70dglpDZLDKdRqRSiLU1gkKO4OgF
+ /fu3cfTdmEEUPmzA5SWyXkckk4jVVYJqFXlxgV8r4+/v4t1dwr42GRoMnqwqa5huF1otglqNoFIBS0Xv
+ dJBnZ/STj+i/LNIN0YhBmDeTQezthfDQJJSGlUl/ext3YQHv+VPaUQPdYflp/3de9eQhGErB3tYW7vw8
+ ztwcbrXwF4PjN4O8iQRBuRwz0NW9fB5ndpbe9DRuOTduIN7XCVZWCExzlFmvGqbdRl5d4ajJ2JOTuM/W
+ +Rk1ULNFqBH5T9LI8/NB1WYTb3MTd2MjhKUyEScn9JZu4pQycQNP/TA9X79RwTeLiNPTENZ5ezMzOLq5
+ CnbWUzg7WTqLN2gZxo+RgZb6HJaeb//VDl7i3iivPTWFPTGBvagaqCq3I3DMQEt9DstevoVXK+Du5nHN
+ XJjXKWVxiukxWCtmoKW6a+kOD6WzailQS40mfj+2+Xdh/ALnlbiDsb03NQAAAABJRU5ErkJggg==
+
+
+
+
+ R0lGODlhEAAQAIQAAJXD9Iasxm6MqnSn2lZtjVaRyEpXbYu767TX/2KZztvr/4Gy5KrT/3ut32+gzlFh
+ e+r0/0RNX9/u/9Ln+8Xg//n8/4e36CkxQz9GVkSCvKjL35/N/Je91K7T5bDS4////yH/C05FVFNDQVBF
+ Mi4wAwEBAAAh+QQAAAAAACwAAAAAEAAQAAAIuQA/CBxIsKDACRwScggQwIGAhwIICBDYQcEEgwg+bNjw
+ QKCHCQgkQBgpQcKBCg0AEBCoAaRIkhIsVBigUiAHCgwkKNjJU8GAAx0/3NwIAMABCwsaDHCwIGgAChuK
+ HjiQdMDSAQYEPpWKtKqDBA6yfgiAwGhXpUsTJIgg0AGCo0nRfi1QgO0HAQyQNpCrtkAGDAIFbKi69GsC
+ un8FEohqdEFavxkyXAhMoPKDBwYMRIiAAcOFoAZDCwwIADs=
+
+
+
+
+ iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29m
+ dHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAMPSURBVDhPZZL9S1NhFMfvT/Vv9BeEUURkRIQWZSGh
+ URRRRsZMM0UqzbSyWdq0d0gtVmlZJJHkC7FJS92rtd5cmzbnu1PX5tzutuduu3ffzi69aQ98OOc+3Ps5
+ 5zzP5dIqeo/sVpm1mXXmBQIEy6wn6iwsU2ViGbUGW+oZ/a1Nio4VALjlcDuq9f3eIPMuhATRxwv4jTeY
+ hMHhDqLi9SRy7zrDqYqOlf8JMmoMnmAkJprGY7BMxmEej8MwFkWvU8AbewhCHOgciqCifQLHbjsjm/M7
+ l0i4XbXGSCAchXVKJOJ4L0ui6BsRoLGHSSDB6hZhmYlB2T2JrWW61iWCjFojSwo+keDTdFIiYmAiBr0r
+ ip7hCMbmQ/IoPItj3h9GemUf9bRMsBiKYpCqDM5K+CJLkl3E5C601EWXjUfH1wAWQwK2VenZEsHOGiPz
+ k8Axm8A3EtjcEr6SrPCeDeVqG4obPqPqiR2K61bk3bTidNNnlDR+wVGVGasOvFzP7SCBjwRDcwk4CDuJ
+ Bmck5NZ/QLuVpyJAh2lKjv+uxzoPjtRawG1XGpmXj8IufyzJJDs5eMUE9Tu//HLhnQ9yTC46CgQEoO7V
+ NHKumcClVxnwg+48WfmPgMY4VGOmKn7ERAlxMQFJSkAkYnQrcdqrfu5C1oVecFvO65l7kWFvp4DsLoYs
+ ilmvBRphAI1vPKh86sLFVhfOtzhx7tEwyojSh0MoVQ/hkFIHblNpL5vyMRT2SVA7gPt2oIFijsqC211u
+ XHo2SUygomUU5Y9HUPbQibPqYZQ02bCvUgtuQ7EuPPYjnFDo6ODexnHsrSiTfUmPe90eeHge7oCP8MO9
+ 6McM5b4wj7IH35BW1A5ubV7PqCcgBMe8TBr1RDDyiz0X+tHU7ccc74FStwcNllNotBRTno350DTKH7iw
+ Me8FuHUntPkpuZqeNce1vpTjGhCMntney0ZoPiZvwQXTwlU4Qs1wRJopr6O9cVQ3u7D5RNvfP2o5axUa
+ w35lPxSqQRTdmEBB/QhOqr6j8JoDBSor0k62YfXhVvwE3mQsoPunpBAAAAAASUVORK5CYII=
+
+
\ No newline at end of file
diff --git a/Cs_HMI/AGVMapEditor/Models/EditorSettings.cs b/Cs_HMI/AGVMapEditor/Models/EditorSettings.cs
new file mode 100644
index 0000000..4071a87
--- /dev/null
+++ b/Cs_HMI/AGVMapEditor/Models/EditorSettings.cs
@@ -0,0 +1,162 @@
+using System;
+using System.IO;
+using Newtonsoft.Json;
+
+namespace AGVMapEditor.Models
+{
+ ///
+ /// AGV 맵 에디터의 환경설정을 관리하는 클래스
+ ///
+ public class EditorSettings
+ {
+ #region Properties
+
+ ///
+ /// 마지막으로 열었던 맵 파일의 경로
+ ///
+ public string LastMapFilePath { get; set; } = string.Empty;
+
+ ///
+ /// 프로그램 시작시 마지막 맵 파일을 자동으로 로드할지 여부
+ ///
+ public bool AutoLoadLastMapFile { get; set; } = true;
+
+ ///
+ /// 설정이 마지막으로 저장된 시간
+ ///
+ public DateTime LastSaved { get; set; } = DateTime.Now;
+
+ ///
+ /// 기본 맵 파일 저장 디렉토리
+ ///
+ public string DefaultMapDirectory { get; set; } = string.Empty;
+
+ #endregion
+
+ #region Constants
+
+ private static readonly string SettingsFileName = "EditorSettings.json";
+ private static readonly string SettingsDirectory = Path.Combine(
+ Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
+ "AGVMapEditor");
+
+ private static readonly string SettingsFilePath = Path.Combine(SettingsDirectory, SettingsFileName);
+
+ #endregion
+
+ #region Static Instance
+
+ private static EditorSettings _instance;
+ private static readonly object _lock = new object();
+
+ ///
+ /// 싱글톤 인스턴스
+ ///
+ public static EditorSettings Instance
+ {
+ get
+ {
+ if (_instance == null)
+ {
+ lock (_lock)
+ {
+ if (_instance == null)
+ {
+ _instance = LoadSettings();
+ }
+ }
+ }
+ return _instance;
+ }
+ }
+
+ #endregion
+
+ #region Methods
+
+ ///
+ /// 설정을 파일에서 로드
+ ///
+ private static EditorSettings LoadSettings()
+ {
+ try
+ {
+ if (File.Exists(SettingsFilePath))
+ {
+ string jsonContent = File.ReadAllText(SettingsFilePath);
+ var settings = JsonConvert.DeserializeObject(jsonContent);
+ return settings ?? new EditorSettings();
+ }
+ }
+ catch (Exception ex)
+ {
+ // 설정 로드 실패시 기본 설정 사용
+ System.Diagnostics.Debug.WriteLine($"설정 로드 실패: {ex.Message}");
+ }
+
+ return new EditorSettings();
+ }
+
+ ///
+ /// 설정을 파일에 저장
+ ///
+ public void Save()
+ {
+ try
+ {
+ // 디렉토리가 없으면 생성
+ if (!Directory.Exists(SettingsDirectory))
+ {
+ Directory.CreateDirectory(SettingsDirectory);
+ }
+
+ LastSaved = DateTime.Now;
+ string jsonContent = JsonConvert.SerializeObject(this, Formatting.Indented);
+ File.WriteAllText(SettingsFilePath, jsonContent);
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine($"설정 저장 실패: {ex.Message}");
+ }
+ }
+
+ ///
+ /// 마지막 맵 파일 경로 업데이트
+ ///
+ /// 맵 파일 경로
+ public void UpdateLastMapFile(string filePath)
+ {
+ if (!string.IsNullOrEmpty(filePath) && File.Exists(filePath))
+ {
+ LastMapFilePath = filePath;
+
+ // 기본 디렉토리도 업데이트
+ DefaultMapDirectory = Path.GetDirectoryName(filePath);
+
+ Save();
+ }
+ }
+
+ ///
+ /// 마지막 맵 파일이 존재하는지 확인
+ ///
+ public bool HasValidLastMapFile()
+ {
+ return !string.IsNullOrEmpty(LastMapFilePath) && File.Exists(LastMapFilePath);
+ }
+
+ ///
+ /// 설정 초기화
+ ///
+ public void Reset()
+ {
+ LastMapFilePath = string.Empty;
+ AutoLoadLastMapFile = true;
+ DefaultMapDirectory = string.Empty;
+ LastSaved = DateTime.Now;
+ Save();
+ }
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/Cs_HMI/AGVMapEditor/Models/Enums.cs b/Cs_HMI/AGVMapEditor/Models/Enums.cs
deleted file mode 100644
index 36baf9f..0000000
--- a/Cs_HMI/AGVMapEditor/Models/Enums.cs
+++ /dev/null
@@ -1,68 +0,0 @@
-using System;
-
-namespace AGVMapEditor.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/AGVMapEditor/Models/MapNode.cs b/Cs_HMI/AGVMapEditor/Models/MapNode.cs
deleted file mode 100644
index 570a256..0000000
--- a/Cs_HMI/AGVMapEditor/Models/MapNode.cs
+++ /dev/null
@@ -1,422 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Drawing;
-
-namespace AGVMapEditor.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;
-
- ///
- /// 라벨 텍스트 (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,
- 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
index 4eda91f..ab48a7a 100644
--- a/Cs_HMI/AGVMapEditor/Models/NodePropertyWrapper.cs
+++ b/Cs_HMI/AGVMapEditor/Models/NodePropertyWrapper.cs
@@ -39,6 +39,11 @@ namespace AGVMapEditor.Models
_mapNodes = mapNodes;
}
+ ///
+ /// 래핑된 MapNode 인스턴스 접근
+ ///
+ public MapNode WrappedNode => _node;
+
[Category("기본 정보")]
[DisplayName("노드 ID")]
[Description("노드의 고유 식별자")]
@@ -207,6 +212,11 @@ namespace AGVMapEditor.Models
_mapNodes = mapNodes;
}
+ ///
+ /// 래핑된 MapNode 인스턴스 접근
+ ///
+ public MapNode WrappedNode => _node;
+
[Category("기본 정보")]
[DisplayName("노드 ID")]
[Description("노드의 고유 식별자")]
@@ -350,6 +360,11 @@ namespace AGVMapEditor.Models
_mapNodes = mapNodes;
}
+ ///
+ /// 래핑된 MapNode 인스턴스 접근
+ ///
+ public MapNode WrappedNode => _node;
+
[Category("기본 정보")]
[DisplayName("노드 ID")]
[Description("노드의 고유 식별자")]
@@ -450,18 +465,6 @@ namespace AGVMapEditor.Models
- [Category("고급")]
- [DisplayName("설명")]
- [Description("노드에 대한 추가 설명")]
- public string Description
- {
- get => _node.Description;
- set
- {
- _node.Description = value ?? "";
- _node.ModifiedDate = DateTime.Now;
- }
- }
[Category("고급")]
[DisplayName("활성화")]
diff --git a/Cs_HMI/AGVMapEditor/Models/PathCalculator.cs b/Cs_HMI/AGVMapEditor/Models/PathCalculator.cs
index 27aa135..f3324eb 100644
--- a/Cs_HMI/AGVMapEditor/Models/PathCalculator.cs
+++ b/Cs_HMI/AGVMapEditor/Models/PathCalculator.cs
@@ -49,6 +49,8 @@ namespace AGVMapEditor.Models
_astarPathfinder.SetMapNodes(mapNodes);
// RfidPathfinder는 MapNode의 RFID 정보를 직접 사용
_rfidPathfinder.SetMapNodes(mapNodes);
+ // 도킹 조건 검색용 내부 노드 목록 업데이트
+ UpdateInternalMapNodes(mapNodes);
}
///
@@ -63,6 +65,32 @@ namespace AGVMapEditor.Models
return _agvPathfinder.FindAGVPath(startNodeId, endNodeId, targetDirection);
}
+ ///
+ /// AGV 경로 계산 (옵션 지정 가능)
+ ///
+ /// 시작 노드 ID
+ /// 목적지 노드 ID
+ /// 목적지 도착 방향
+ /// 경로 탐색 옵션
+ /// AGV 경로 계산 결과
+ public AGVPathResult FindAGVPath(string startNodeId, string endNodeId, AgvDirection? targetDirection, PathfindingOptions options)
+ {
+ return _agvPathfinder.FindAGVPath(startNodeId, endNodeId, targetDirection, options);
+ }
+
+ ///
+ /// AGV 경로 계산 (현재 방향 및 PathfindingOptions 지원)
+ ///
+ /// 시작 노드 ID
+ /// 목적지 노드 ID
+ /// 현재 AGV 방향
+ /// 목적지 도착 방향
+ /// 경로 탐색 옵션
+ /// AGV 경로 계산 결과
+ public AGVPathResult FindAGVPath(string startNodeId, string endNodeId, AgvDirection? currentDirection, AgvDirection? targetDirection, PathfindingOptions options)
+ {
+ return _agvPathfinder.FindAGVPath(startNodeId, endNodeId, currentDirection, targetDirection, options);
+ }
///
/// 충전 스테이션으로의 경로 찾기
@@ -264,5 +292,141 @@ namespace AGVMapEditor.Models
{
_rfidPathfinder.RotationCostWeight = weight;
}
+
+ #region 도킹 조건 검색 기능
+
+ // 내부 노드 목록 저장
+ private List _mapNodes;
+
+ ///
+ /// 맵 노드 설정 (도킹 조건 검색용)
+ ///
+ private void UpdateInternalMapNodes(List mapNodes)
+ {
+ _mapNodes = mapNodes;
+ }
+
+ ///
+ /// 도킹 방향 기반 노드 검색
+ ///
+ /// 도킹 방향
+ /// 해당 도킹 방향의 노드 목록
+ public List GetNodesByDockingDirection(DockingDirection dockingDirection)
+ {
+ if (_mapNodes == null) return new List();
+
+ var result = new List();
+
+ foreach (var node in _mapNodes)
+ {
+ if (!node.IsActive) continue;
+
+ var nodeDockingDirection = GetNodeDockingDirection(node);
+ if (nodeDockingDirection == dockingDirection)
+ {
+ result.Add(node);
+ }
+ }
+
+ return result;
+ }
+
+ ///
+ /// 노드의 도킹 방향 결정
+ ///
+ /// 노드
+ /// 도킹 방향
+ public DockingDirection GetNodeDockingDirection(MapNode node)
+ {
+ switch (node.Type)
+ {
+ case NodeType.Charging:
+ return DockingDirection.Forward; // 충전기: 전진 도킹
+ case NodeType.Docking:
+ return DockingDirection.Backward; // 장비: 후진 도킹
+ default:
+ return DockingDirection.Forward; // 기본값: 전진
+ }
+ }
+
+ ///
+ /// 도킹 방향과 장비 타입에 맞는 노드들로의 경로 검색
+ ///
+ /// 시작 RFID
+ /// 필요한 도킹 방향
+ /// 장비 타입 (선택사항)
+ /// 경로 계산 결과 목록 (거리 순 정렬)
+ public List FindPathsByDockingCondition(string startRfidId, DockingDirection dockingDirection, StationType? stationType = null)
+ {
+ var targetNodes = GetNodesByDockingDirection(dockingDirection);
+ var results = new List();
+
+ // 장비 타입 필터링 (필요시)
+ if (stationType.HasValue && dockingDirection == DockingDirection.Backward)
+ {
+ // 후진 도킹이면서 특정 장비 타입이 지정된 경우
+ // 이 부분은 추후 StationMapping 정보가 있을 때 구현
+ // 현재는 모든 도킹 노드를 대상으로 함
+ }
+
+ foreach (var targetNode in targetNodes)
+ {
+ if (!targetNode.HasRfid()) continue;
+
+ try
+ {
+ var pathResult = _rfidPathfinder.FindAGVPath(startRfidId, targetNode.RfidId);
+ if (pathResult.Success)
+ {
+ results.Add(pathResult);
+ }
+ }
+ catch (Exception ex)
+ {
+ // 개별 경로 계산 실패는 무시하고 계속 진행
+ System.Diagnostics.Debug.WriteLine($"Path calculation failed from {startRfidId} to {targetNode.RfidId}: {ex.Message}");
+ }
+ }
+
+ // 거리 순으로 정렬
+ return results.OrderBy(r => r.TotalDistance).ToList();
+ }
+
+ ///
+ /// 가장 가까운 충전기 경로 찾기 (전진 도킹)
+ ///
+ /// 시작 RFID
+ /// 가장 가까운 충전기로의 경로
+ public RfidPathResult FindNearestChargingStationPath(string startRfidId)
+ {
+ var chargingPaths = FindPathsByDockingCondition(startRfidId, DockingDirection.Forward);
+ var chargingNodes = chargingPaths.Where(p => p.Success).ToList();
+
+ return chargingNodes.FirstOrDefault() ?? new RfidPathResult
+ {
+ Success = false,
+ ErrorMessage = "충전 가능한 충전기를 찾을 수 없습니다."
+ };
+ }
+
+ ///
+ /// 가장 가까운 장비 도킹 경로 찾기 (후진 도킹)
+ ///
+ /// 시작 RFID
+ /// 장비 타입 (선택사항)
+ /// 가장 가까운 장비로의 경로
+ public RfidPathResult FindNearestEquipmentPath(string startRfidId, StationType? stationType = null)
+ {
+ var equipmentPaths = FindPathsByDockingCondition(startRfidId, DockingDirection.Backward, stationType);
+ var equipmentNodes = equipmentPaths.Where(p => p.Success).ToList();
+
+ return equipmentNodes.FirstOrDefault() ?? new RfidPathResult
+ {
+ Success = false,
+ ErrorMessage = $"도킹 가능한 장비를 찾을 수 없습니다. ({stationType?.ToString() ?? "모든 타입"})"
+ };
+ }
+
+ #endregion
}
}
\ No newline at end of file
diff --git a/Cs_HMI/AGVMapEditor/Models/PathNode.cs b/Cs_HMI/AGVMapEditor/Models/PathNode.cs
deleted file mode 100644
index 983cde8..0000000
--- a/Cs_HMI/AGVMapEditor/Models/PathNode.cs
+++ /dev/null
@@ -1,144 +0,0 @@
-using System;
-using System.Collections.Generic;
-
-namespace AGVMapEditor.Models
-{
- ///
- /// A* 알고리즘에서 사용되는 경로 노드
- ///
- public class PathNode : IComparable
- {
- ///
- /// 맵 노드 ID
- ///
- public string NodeId { get; set; } = string.Empty;
-
- ///
- /// AGV의 현재 방향 (이 노드에 도달했을 때의 방향)
- ///
- public AgvDirection Direction { get; set; } = AgvDirection.Forward;
-
- ///
- /// 시작점에서 이 노드까지의 실제 비용 (G)
- ///
- public float GCost { get; set; } = float.MaxValue;
-
- ///
- /// 이 노드에서 목표까지의 추정 비용 (H)
- ///
- public float HCost { get; set; } = 0;
-
- ///
- /// 총 비용 (F = G + H)
- ///
- public float FCost => GCost + HCost;
-
- ///
- /// 이전 노드 (경로 추적용)
- ///
- public PathNode Parent { get; set; } = null;
-
- ///
- /// 회전 횟수 (방향 전환 비용 계산용)
- ///
- public int RotationCount { get; set; } = 0;
-
- ///
- /// 이 노드에 도달하기 위한 이동 명령 시퀀스
- ///
- public List MovementSequence { get; set; } = new List();
-
- ///
- /// 기본 생성자
- ///
- public PathNode()
- {
- }
-
- ///
- /// 매개변수 생성자
- ///
- /// 노드 ID
- /// AGV 방향
- public PathNode(string nodeId, AgvDirection direction)
- {
- NodeId = nodeId;
- Direction = direction;
- }
-
- ///
- /// 우선순위 큐를 위한 비교 (FCost 기준)
- ///
- public int CompareTo(PathNode other)
- {
- if (other == null) return 1;
-
- int compare = FCost.CompareTo(other.FCost);
- if (compare == 0)
- {
- // FCost가 같으면 HCost가 낮은 것을 우선
- compare = HCost.CompareTo(other.HCost);
- }
- if (compare == 0)
- {
- // 그것도 같으면 회전 횟수가 적은 것을 우선
- compare = RotationCount.CompareTo(other.RotationCount);
- }
-
- return compare;
- }
-
- ///
- /// 노드 상태 복사
- ///
- public PathNode Clone()
- {
- return new PathNode
- {
- NodeId = NodeId,
- Direction = Direction,
- GCost = GCost,
- HCost = HCost,
- Parent = Parent,
- RotationCount = RotationCount,
- MovementSequence = new List(MovementSequence)
- };
- }
-
- ///
- /// 고유 키 생성 (노드ID + 방향)
- ///
- public string GetKey()
- {
- return $"{NodeId}_{Direction}";
- }
-
- ///
- /// 문자열 표현
- ///
- public override string ToString()
- {
- return $"{NodeId}({Direction}) F:{FCost:F1} G:{GCost:F1} H:{HCost:F1} R:{RotationCount}";
- }
-
- ///
- /// 해시코드 (딕셔너리 키용)
- ///
- public override int GetHashCode()
- {
- return GetKey().GetHashCode();
- }
-
- ///
- /// 동등성 비교
- ///
- public override bool Equals(object obj)
- {
- if (obj is PathNode other)
- {
- return NodeId == other.NodeId && Direction == other.Direction;
- }
- return false;
- }
- }
-}
\ No newline at end of file
diff --git a/Cs_HMI/AGVMapEditor/Models/PathResult.cs b/Cs_HMI/AGVMapEditor/Models/PathResult.cs
deleted file mode 100644
index 993c2c7..0000000
--- a/Cs_HMI/AGVMapEditor/Models/PathResult.cs
+++ /dev/null
@@ -1,277 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-
-namespace AGVMapEditor.Models
-{
- ///
- /// 경로 계산 결과
- ///
- public class PathResult
- {
- ///
- /// 경로 계산 성공 여부
- ///
- public bool Success { get; set; } = false;
-
- ///
- /// 경로상의 노드 ID 시퀀스
- ///
- public List NodeSequence { get; set; } = new List();
-
- ///
- /// AGV 이동 명령 시퀀스
- ///
- public List MovementSequence { get; set; } = new List();
-
- ///
- /// 총 이동 거리 (비용)
- ///
- public float TotalDistance { get; set; } = 0;
-
- ///
- /// 총 회전 횟수
- ///
- public int TotalRotations { get; set; } = 0;
-
- ///
- /// 예상 소요 시간 (초)
- ///
- public float EstimatedTime { get; set; } = 0;
-
- ///
- /// 시작 노드 ID
- ///
- public string StartNodeId { get; set; } = string.Empty;
-
- ///
- /// 목표 노드 ID
- ///
- public string TargetNodeId { get; set; } = string.Empty;
-
- ///
- /// 시작시 AGV 방향
- ///
- public AgvDirection StartDirection { get; set; } = AgvDirection.Forward;
-
- ///
- /// 도착시 AGV 방향
- ///
- public AgvDirection EndDirection { get; set; } = AgvDirection.Forward;
-
- ///
- /// 경로 계산에 걸린 시간 (밀리초)
- ///
- public long CalculationTime { get; set; } = 0;
-
- ///
- /// 오류 메시지 (실패시)
- ///
- public string ErrorMessage { get; set; } = string.Empty;
-
- ///
- /// 경로상의 상세 정보 (디버깅용)
- ///
- public List DetailedPath { get; set; } = new List();
-
- ///
- /// 회전이 발생하는 노드들
- ///
- public List RotationNodes { get; set; } = new List();
-
- ///
- /// 기본 생성자
- ///
- public PathResult()
- {
- }
-
- ///
- /// 성공 결과 생성자
- ///
- public PathResult(List path, string startNodeId, string targetNodeId, AgvDirection startDirection)
- {
- if (path == null || path.Count == 0)
- {
- Success = false;
- ErrorMessage = "빈 경로입니다.";
- return;
- }
-
- Success = true;
- StartNodeId = startNodeId;
- TargetNodeId = targetNodeId;
- StartDirection = startDirection;
- DetailedPath = new List(path);
-
- // 노드 시퀀스 구성
- NodeSequence = path.Select(p => p.NodeId).ToList();
-
- // 이동 명령 시퀀스 구성
- MovementSequence = new List();
- for (int i = 0; i < path.Count; i++)
- {
- MovementSequence.AddRange(path[i].MovementSequence);
- }
-
- // 통계 계산
- if (path.Count > 0)
- {
- TotalDistance = path[path.Count - 1].GCost;
- EndDirection = path[path.Count - 1].Direction;
- }
-
- TotalRotations = MovementSequence.Count(cmd =>
- cmd == AgvDirection.Left || cmd == AgvDirection.Right);
-
- // 회전 노드 추출
- var previousDirection = startDirection;
- for (int i = 0; i < path.Count; i++)
- {
- if (path[i].Direction != previousDirection)
- {
- RotationNodes.Add(path[i].NodeId);
- }
- previousDirection = path[i].Direction;
- }
-
- // 예상 소요 시간 계산 (단순 추정)
- EstimatedTime = CalculateEstimatedTime();
- }
-
- ///
- /// 실패 결과 생성자
- ///
- public PathResult(string errorMessage)
- {
- Success = false;
- ErrorMessage = errorMessage;
- }
-
- ///
- /// 예상 소요 시간 계산
- ///
- private float CalculateEstimatedTime()
- {
- // 기본 이동 속도 및 회전 시간 가정
- const float MOVE_SPEED = 1.0f; // 단위/초
- const float ROTATION_TIME = 2.0f; // 초/회전
-
- float moveTime = TotalDistance / MOVE_SPEED;
- float rotationTime = TotalRotations * ROTATION_TIME;
-
- return moveTime + rotationTime;
- }
-
- ///
- /// 경로 요약 정보
- ///
- public string GetSummary()
- {
- if (!Success)
- {
- return $"경로 계산 실패: {ErrorMessage}";
- }
-
- return $"경로: {NodeSequence.Count}개 노드, " +
- $"거리: {TotalDistance:F1}, " +
- $"회전: {TotalRotations}회, " +
- $"예상시간: {EstimatedTime:F1}초";
- }
-
- ///
- /// 상세 경로 정보
- ///
- public List GetDetailedSteps()
- {
- var steps = new List();
-
- if (!Success)
- {
- steps.Add($"경로 계산 실패: {ErrorMessage}");
- return steps;
- }
-
- steps.Add($"시작: {StartNodeId} (방향: {StartDirection})");
-
- for (int i = 0; i < DetailedPath.Count; i++)
- {
- var node = DetailedPath[i];
- var step = $"{i + 1}. {node.NodeId}";
-
- if (node.MovementSequence.Count > 0)
- {
- step += $" [명령: {string.Join(",", node.MovementSequence)}]";
- }
-
- step += $" (F:{node.FCost:F1}, 방향:{node.Direction})";
- steps.Add(step);
- }
-
- steps.Add($"도착: {TargetNodeId} (최종 방향: {EndDirection})");
-
- return steps;
- }
-
- ///
- /// RFID 시퀀스 추출 (실제 AGV 제어용)
- ///
- public List GetRfidSequence(NodeResolver nodeResolver)
- {
- var rfidSequence = new List();
-
- foreach (var nodeId in NodeSequence)
- {
- var rfidId = nodeResolver.GetRfidByNodeId(nodeId);
- if (!string.IsNullOrEmpty(rfidId))
- {
- rfidSequence.Add(rfidId);
- }
- }
-
- return rfidSequence;
- }
-
- ///
- /// 경로 유효성 검증
- ///
- public bool ValidatePath(List mapNodes)
- {
- if (!Success || NodeSequence.Count == 0)
- return false;
-
- // 모든 노드가 존재하는지 확인
- foreach (var nodeId in NodeSequence)
- {
- if (!mapNodes.Any(n => n.NodeId == nodeId))
- {
- ErrorMessage = $"존재하지 않는 노드: {nodeId}";
- return false;
- }
- }
-
- // 연결성 확인
- for (int i = 0; i < NodeSequence.Count - 1; i++)
- {
- var currentNode = mapNodes.FirstOrDefault(n => n.NodeId == NodeSequence[i]);
- var nextNodeId = NodeSequence[i + 1];
-
- if (currentNode != null && !currentNode.ConnectedNodes.Contains(nextNodeId))
- {
- ErrorMessage = $"연결되지 않은 노드: {currentNode.NodeId} → {nextNodeId}";
- return false;
- }
- }
-
- return true;
- }
-
- ///
- /// JSON 직렬화를 위한 문자열 변환
- ///
- public override string ToString()
- {
- return GetSummary();
- }
- }
-}
\ No newline at end of file
diff --git a/Cs_HMI/AGVMapEditor/Models/RfidMapping.cs b/Cs_HMI/AGVMapEditor/Models/RfidMapping.cs
deleted file mode 100644
index 8cb949c..0000000
--- a/Cs_HMI/AGVMapEditor/Models/RfidMapping.cs
+++ /dev/null
@@ -1,80 +0,0 @@
-using System;
-using AGVNavigationCore.Models;
-
-namespace AGVMapEditor.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/AGVMapEditor/Properties/Resources.Designer.cs b/Cs_HMI/AGVMapEditor/Properties/Resources.Designer.cs
new file mode 100644
index 0000000..9493d6f
--- /dev/null
+++ b/Cs_HMI/AGVMapEditor/Properties/Resources.Designer.cs
@@ -0,0 +1,63 @@
+//------------------------------------------------------------------------------
+//
+// 이 코드는 도구를 사용하여 생성되었습니다.
+// 런타임 버전:4.0.30319.42000
+//
+// 파일 내용을 변경하면 잘못된 동작이 발생할 수 있으며, 코드를 다시 생성하면
+// 이러한 변경 내용이 손실됩니다.
+//
+//------------------------------------------------------------------------------
+
+namespace AGVMapEditor.Properties {
+ using System;
+
+
+ ///
+ /// 지역화된 문자열 등을 찾기 위한 강력한 형식의 리소스 클래스입니다.
+ ///
+ // 이 클래스는 ResGen 또는 Visual Studio와 같은 도구를 통해 StronglyTypedResourceBuilder
+ // 클래스에서 자동으로 생성되었습니다.
+ // 멤버를 추가하거나 제거하려면 .ResX 파일을 편집한 다음 /str 옵션을 사용하여 ResGen을
+ // 다시 실행하거나 VS 프로젝트를 다시 빌드하십시오.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class Resources {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal Resources() {
+ }
+
+ ///
+ /// 이 클래스에서 사용하는 캐시된 ResourceManager 인스턴스를 반환합니다.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("AGVMapEditor.Properties.Resources", typeof(Resources).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ ///
+ /// 이 강력한 형식의 리소스 클래스를 사용하여 모든 리소스 조회에 대해 현재 스레드의 CurrentUICulture 속성을
+ /// 재정의합니다.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+ }
+}
diff --git a/Cs_HMI/AGVSimulator/Controls/SimulatorCanvas.resx b/Cs_HMI/AGVMapEditor/Properties/Resources.resx
similarity index 90%
rename from Cs_HMI/AGVSimulator/Controls/SimulatorCanvas.resx
rename to Cs_HMI/AGVMapEditor/Properties/Resources.resx
index c50400a..1af7de1 100644
--- a/Cs_HMI/AGVSimulator/Controls/SimulatorCanvas.resx
+++ b/Cs_HMI/AGVMapEditor/Properties/Resources.resx
@@ -1,4 +1,4 @@
-
+