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 @@ - +