using System; using System.Collections.Generic; using System.Drawing; using System.Drawing.Drawing2D; using System.Linq; using System.Windows.Forms; using AGVMapEditor.Models; namespace AGVMapEditor.Controls { /// /// 맵 편집을 위한 그래픽 캔버스 컨트롤 /// public partial class MapCanvas : UserControl { #region Constants private const int NODE_SIZE = 20; private const int NODE_RADIUS = NODE_SIZE / 2; private const int GRID_SIZE = 20; private const float CONNECTION_WIDTH = 2.0f; #endregion #region Fields private List _nodes; private MapNode _selectedNode; private MapNode _hoveredNode; private bool _isDragging; private Point _dragOffset; private Point _lastMousePosition; // 그리드 및 줌 관련 private bool _showGrid = true; private float _zoomFactor = 1.0f; private Point _panOffset = Point.Empty; // 브러쉬 및 펜 private Brush _normalNodeBrush; private Brush _rotationNodeBrush; private Brush _dockingNodeBrush; private Brush _chargingNodeBrush; private Brush _selectedNodeBrush; private Brush _hoveredNodeBrush; private Pen _connectionPen; private Pen _gridPen; #endregion #region Events /// /// 노드가 선택되었을 때 발생하는 이벤트 /// public event EventHandler NodeSelected; /// /// 노드가 이동되었을 때 발생하는 이벤트 /// public event EventHandler NodeMoved; /// /// 배경이 클릭되었을 때 발생하는 이벤트 /// public event EventHandler BackgroundClicked; #endregion #region Constructor public MapCanvas() : this(new List()) { } public MapCanvas(List nodes) { InitializeComponent(); InitializeGraphics(); SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint | ControlStyles.DoubleBuffer | ControlStyles.ResizeRedraw, true); _nodes = nodes ?? new List(); // 이벤트 연결 this.MouseDown += MapCanvas_MouseDown; this.MouseMove += MapCanvas_MouseMove; this.MouseUp += MapCanvas_MouseUp; this.MouseWheel += MapCanvas_MouseWheel; this.KeyDown += MapCanvas_KeyDown; // 포커스를 받을 수 있도록 설정 this.TabStop = true; } #endregion #region Graphics Initialization private void InitializeGraphics() { _normalNodeBrush = new SolidBrush(Color.Blue); _rotationNodeBrush = new SolidBrush(Color.Orange); _dockingNodeBrush = new SolidBrush(Color.Green); _chargingNodeBrush = new SolidBrush(Color.Red); _selectedNodeBrush = new SolidBrush(Color.Yellow); _hoveredNodeBrush = new SolidBrush(Color.LightBlue); _connectionPen = new Pen(Color.Gray, CONNECTION_WIDTH); _gridPen = new Pen(Color.LightGray, 1.0f) { DashStyle = DashStyle.Dot }; } #endregion #region Properties /// /// 그리드 표시 여부 /// public bool ShowGrid { get { return _showGrid; } set { _showGrid = value; Invalidate(); } } /// /// 줌 팩터 /// public float ZoomFactor { get { return _zoomFactor; } set { _zoomFactor = Math.Max(0.1f, Math.Min(5.0f, value)); Invalidate(); } } /// /// 선택된 노드 /// public MapNode SelectedNode { get { return _selectedNode; } set { if (_selectedNode != value) { _selectedNode = value; Invalidate(); } } } #endregion #region Mouse Events private void MapCanvas_MouseDown(object sender, MouseEventArgs e) { this.Focus(); // 키보드 이벤트를 받기 위해 포커스 설정 var worldPoint = ScreenToWorld(e.Location); var hitNode = GetNodeAt(worldPoint); if (e.Button == MouseButtons.Left) { if (hitNode != null) { // 노드 선택 및 드래그 시작 SelectNode(hitNode); _isDragging = true; _dragOffset = new Point(worldPoint.X - hitNode.Position.X, worldPoint.Y - hitNode.Position.Y); } else { // 배경 클릭 SelectNode(null); BackgroundClicked?.Invoke(this, worldPoint); } } else if (e.Button == MouseButtons.Right) { // 우클릭 메뉴 (추후 구현) ShowContextMenu(worldPoint, hitNode); } _lastMousePosition = e.Location; } private void MapCanvas_MouseMove(object sender, MouseEventArgs e) { var worldPoint = ScreenToWorld(e.Location); if (_isDragging && _selectedNode != null) { // 노드 드래그 var newPosition = new Point(worldPoint.X - _dragOffset.X, worldPoint.Y - _dragOffset.Y); // 그리드에 맞춤 (Ctrl 키를 누르지 않은 경우) if (!ModifierKeys.HasFlag(Keys.Control)) { newPosition.X = (newPosition.X / GRID_SIZE) * GRID_SIZE; newPosition.Y = (newPosition.Y / GRID_SIZE) * GRID_SIZE; } _selectedNode.Position = newPosition; Invalidate(); } else if (e.Button == MouseButtons.Middle || (e.Button == MouseButtons.Left && ModifierKeys.HasFlag(Keys.Space))) { // 팬 (화면 이동) var deltaX = e.X - _lastMousePosition.X; var deltaY = e.Y - _lastMousePosition.Y; _panOffset.X += deltaX; _panOffset.Y += deltaY; Invalidate(); } else { // 호버 효과 var hitNode = GetNodeAt(worldPoint); if (_hoveredNode != hitNode) { _hoveredNode = hitNode; Invalidate(); } } _lastMousePosition = e.Location; } private void MapCanvas_MouseUp(object sender, MouseEventArgs e) { if (_isDragging && _selectedNode != null) { NodeMoved?.Invoke(this, _selectedNode); } _isDragging = false; } private void MapCanvas_MouseWheel(object sender, MouseEventArgs e) { // 줌 var delta = e.Delta > 0 ? 1.1f : 0.9f; ZoomFactor *= delta; } #endregion #region Keyboard Events private void MapCanvas_KeyDown(object sender, KeyEventArgs e) { switch (e.KeyCode) { case Keys.Delete: // 선택된 노드 삭제 (메인 폼에서 처리하도록 이벤트 발생) if (_selectedNode != null) { // 삭제 확인 후 처리는 메인 폼에서 } break; case Keys.G: // 그리드 토글 ShowGrid = !ShowGrid; break; case Keys.Home: // 뷰 리셋 ZoomFactor = 1.0f; _panOffset = Point.Empty; Invalidate(); break; } } #endregion #region Coordinate Conversion /// /// 스크린 좌표를 월드 좌표로 변환 /// private Point ScreenToWorld(Point screenPoint) { var worldX = (int)((screenPoint.X - _panOffset.X) / _zoomFactor); var worldY = (int)((screenPoint.Y - _panOffset.Y) / _zoomFactor); return new Point(worldX, worldY); } /// /// 월드 좌표를 스크린 좌표로 변환 /// private Point WorldToScreen(Point worldPoint) { var screenX = (int)(worldPoint.X * _zoomFactor + _panOffset.X); var screenY = (int)(worldPoint.Y * _zoomFactor + _panOffset.Y); return new Point(screenX, screenY); } #endregion #region Node Management /// /// 지정된 위치의 노드 검색 /// private MapNode GetNodeAt(Point worldPoint) { foreach (var node in _nodes) { var distance = Math.Sqrt(Math.Pow(worldPoint.X - node.Position.X, 2) + Math.Pow(worldPoint.Y - node.Position.Y, 2)); if (distance <= NODE_RADIUS) { return node; } } return null; } /// /// 노드 선택 /// private void SelectNode(MapNode node) { if (_selectedNode != node) { _selectedNode = node; NodeSelected?.Invoke(this, node); Invalidate(); } } /// /// 우클릭 컨텍스트 메뉴 표시 /// private void ShowContextMenu(Point worldPoint, MapNode node) { var contextMenu = new ContextMenuStrip(); if (node != null) { contextMenu.Items.Add("노드 삭제", null, (s, e) => { /* 삭제 처리 */ }); contextMenu.Items.Add(new ToolStripSeparator()); contextMenu.Items.Add("일반 노드로 변경", null, (s, e) => ChangeNodeType(node, NodeType.Normal)); contextMenu.Items.Add("회전 지점으로 변경", null, (s, e) => ChangeNodeType(node, NodeType.Rotation)); contextMenu.Items.Add("도킹 스테이션으로 변경", null, (s, e) => ChangeNodeType(node, NodeType.Docking)); contextMenu.Items.Add("충전 스테이션으로 변경", null, (s, e) => ChangeNodeType(node, NodeType.Charging)); } else { contextMenu.Items.Add("새 노드 추가", null, (s, e) => { /* 노드 추가 처리 */ }); } var screenPoint = WorldToScreen(worldPoint); contextMenu.Show(this, screenPoint); } /// /// 노드 타입 변경 /// private void ChangeNodeType(MapNode node, NodeType newType) { node.Type = newType; node.SetDefaultColorByType(newType); Invalidate(); } #endregion #region Painting protected override void OnPaint(PaintEventArgs e) { base.OnPaint(e); var g = e.Graphics; g.SmoothingMode = SmoothingMode.AntiAlias; // 변환 매트릭스 적용 g.ResetTransform(); g.TranslateTransform(_panOffset.X, _panOffset.Y); g.ScaleTransform(_zoomFactor, _zoomFactor); // 배경 그리드 그리기 if (_showGrid) { DrawGrid(g); } // 연결선 그리기 DrawConnections(g); // 노드 그리기 DrawNodes(g); // 선택된 노드의 연결 정보 강조 표시 if (_selectedNode != null) { DrawSelectedNodeConnections(g); } } private void DrawGrid(Graphics g) { var bounds = GetVisibleWorldBounds(); // 수직선 for (int x = (bounds.Left / GRID_SIZE) * GRID_SIZE; x <= bounds.Right; x += GRID_SIZE) { g.DrawLine(_gridPen, x, bounds.Top, x, bounds.Bottom); } // 수평선 for (int y = (bounds.Top / GRID_SIZE) * GRID_SIZE; y <= bounds.Bottom; y += GRID_SIZE) { g.DrawLine(_gridPen, bounds.Left, y, bounds.Right, y); } } private void DrawConnections(Graphics g) { foreach (var node in _nodes) { foreach (var connectedNodeId in node.ConnectedNodes) { var connectedNode = _nodes.FirstOrDefault(n => n.NodeId == connectedNodeId); if (connectedNode != null) { g.DrawLine(_connectionPen, node.Position, connectedNode.Position); } } } } private void DrawNodes(Graphics g) { foreach (var node in _nodes) { DrawNode(g, node); } } private void DrawNode(Graphics g, MapNode node) { var brush = GetNodeBrush(node); var rect = new Rectangle( node.Position.X - NODE_RADIUS, node.Position.Y - NODE_RADIUS, NODE_SIZE, NODE_SIZE); // 노드 그리기 switch (node.Type) { case NodeType.Normal: g.FillEllipse(brush, rect); break; case NodeType.Rotation: g.FillRectangle(brush, rect); break; case NodeType.Docking: g.FillRectangle(brush, rect); // 도킹 방향 표시 DrawDockingDirection(g, node); break; case NodeType.Charging: g.FillEllipse(brush, rect); // 충전 표시 (+) DrawChargingSymbol(g, node); break; } // 노드 테두리 g.DrawEllipse(Pens.Black, rect); // 노드 이름 표시 if (_zoomFactor > 0.5f) // 줌이 충분히 큰 경우만 텍스트 표시 { var font = new Font("Arial", 8 * _zoomFactor); var textRect = new RectangleF( node.Position.X - 30, node.Position.Y + NODE_RADIUS + 2, 60, 15); var format = new StringFormat { Alignment = StringAlignment.Center, LineAlignment = StringAlignment.Near }; g.DrawString(node.Name, font, Brushes.Black, textRect, format); } } private Brush GetNodeBrush(MapNode node) { if (node == _selectedNode) return _selectedNodeBrush; if (node == _hoveredNode) return _hoveredNodeBrush; switch (node.Type) { case NodeType.Normal: return _normalNodeBrush; case NodeType.Rotation: return _rotationNodeBrush; case NodeType.Docking: return _dockingNodeBrush; case NodeType.Charging: return _chargingNodeBrush; default: return _normalNodeBrush; } } private void DrawDockingDirection(Graphics g, MapNode node) { if (node.DockDirection == null) return; var arrowSize = 8; var arrowPen = new Pen(Color.White, 2); Point arrowStart, arrowEnd; if (node.DockDirection == DockingDirection.Forward) { arrowStart = new Point(node.Position.X - arrowSize/2, node.Position.Y); arrowEnd = new Point(node.Position.X + arrowSize/2, node.Position.Y); } else // Backward { arrowStart = new Point(node.Position.X + arrowSize/2, node.Position.Y); arrowEnd = new Point(node.Position.X - arrowSize/2, node.Position.Y); } g.DrawLine(arrowPen, arrowStart, arrowEnd); // 화살표 머리 var headSize = 3; var headPoints = new Point[] { arrowEnd, new Point(arrowEnd.X - headSize, arrowEnd.Y - headSize), new Point(arrowEnd.X - headSize, arrowEnd.Y + headSize) }; g.FillPolygon(Brushes.White, headPoints); } private void DrawChargingSymbol(Graphics g, MapNode node) { var symbolSize = 6; var symbolPen = new Pen(Color.White, 2); // + 모양 g.DrawLine(symbolPen, node.Position.X - symbolSize/2, node.Position.Y, node.Position.X + symbolSize/2, node.Position.Y); g.DrawLine(symbolPen, node.Position.X, node.Position.Y - symbolSize/2, node.Position.X, node.Position.Y + symbolSize/2); } private void DrawSelectedNodeConnections(Graphics g) { var highlightPen = new Pen(Color.Yellow, CONNECTION_WIDTH + 2); foreach (var connectedNodeId in _selectedNode.ConnectedNodes) { var connectedNode = _nodes.FirstOrDefault(n => n.NodeId == connectedNodeId); if (connectedNode != null) { g.DrawLine(highlightPen, _selectedNode.Position, connectedNode.Position); } } } #endregion #region Helper Methods /// /// 현재 보이는 월드 영역 계산 /// private Rectangle GetVisibleWorldBounds() { var topLeft = ScreenToWorld(new Point(0, 0)); var bottomRight = ScreenToWorld(new Point(Width, Height)); return new Rectangle( topLeft.X, topLeft.Y, bottomRight.X - topLeft.X, bottomRight.Y - topLeft.Y); } #endregion } }