using System; using System.Collections.Generic; using System.Drawing; using System.Linq; using System.Windows.Forms; using AGVMapEditor.Models; using AGVNavigationCore.Models; using AGVNavigationCore.PathFinding; using AGVSimulator.Models; namespace AGVSimulator.Controls { /// /// AGV 시뮬레이션 시각화 캔버스 /// public partial class SimulatorCanvas : UserControl { #region Fields private List _mapNodes; private List _agvList; private PathResult _currentPath; // 그래픽 설정 private float _zoom = 1.0f; private Point _panOffset = Point.Empty; private bool _isPanning = false; private Point _lastMousePos = Point.Empty; // 색상 설정 private readonly Brush _normalNodeBrush = Brushes.LightBlue; private readonly Brush _rotationNodeBrush = Brushes.Yellow; private readonly Brush _dockingNodeBrush = Brushes.Orange; private readonly Brush _chargingNodeBrush = Brushes.Green; private readonly Brush _agvBrush = Brushes.Red; private readonly Brush _pathBrush = Brushes.Purple; private readonly Pen _connectionPen = new Pen(Color.Gray, 2); private readonly Pen _pathPen = new Pen(Color.Purple, 3); private readonly Pen _agvPen = new Pen(Color.Red, 3); // 크기 설정 private const int NODE_SIZE = 20; private const int AGV_SIZE = 30; private const int CONNECTION_ARROW_SIZE = 8; #endregion #region Properties /// /// 맵 노드 목록 /// public List MapNodes { get => _mapNodes; set { _mapNodes = value; Invalidate(); } } /// /// AGV 목록 /// public List AGVList { get => _agvList; set { _agvList = value; Invalidate(); } } /// /// 현재 경로 /// public PathResult CurrentPath { get => _currentPath; set { _currentPath = value; Invalidate(); } } #endregion #region Constructor public SimulatorCanvas() { InitializeComponent(); InitializeCanvas(); } #endregion #region Initialization private void InitializeCanvas() { _mapNodes = new List(); _agvList = new List(); SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint | ControlStyles.DoubleBuffer | ControlStyles.ResizeRedraw, true); BackColor = Color.White; // 마우스 이벤트 연결 MouseDown += OnMouseDown; MouseMove += OnMouseMove; MouseUp += OnMouseUp; MouseWheel += OnMouseWheel; } #endregion #region Public Methods /// /// AGV 추가 /// public void AddAGV(VirtualAGV agv) { if (_agvList == null) _agvList = new List(); _agvList.Add(agv); // AGV 이벤트 연결 agv.PositionChanged += OnAGVPositionChanged; agv.StateChanged += OnAGVStateChanged; Invalidate(); } /// /// AGV 제거 /// public void RemoveAGV(string agvId) { var agv = _agvList?.FirstOrDefault(a => a.AgvId == agvId); if (agv != null) { // 이벤트 연결 해제 agv.PositionChanged -= OnAGVPositionChanged; agv.StateChanged -= OnAGVStateChanged; _agvList.Remove(agv); Invalidate(); } } /// /// 모든 AGV 제거 /// public void ClearAGVs() { if (_agvList != null) { foreach (var agv in _agvList) { agv.PositionChanged -= OnAGVPositionChanged; agv.StateChanged -= OnAGVStateChanged; } _agvList.Clear(); Invalidate(); } } /// /// 확대/축소 초기화 /// public void ResetZoom() { _zoom = 1.0f; _panOffset = Point.Empty; Invalidate(); } /// /// 맵 전체 맞춤 /// public void FitToMap() { if (_mapNodes == null || _mapNodes.Count == 0) return; var minX = _mapNodes.Min(n => n.Position.X); var maxX = _mapNodes.Max(n => n.Position.X); var minY = _mapNodes.Min(n => n.Position.Y); var maxY = _mapNodes.Max(n => n.Position.Y); var mapWidth = maxX - minX + 100; // 여백 추가 var mapHeight = maxY - minY + 100; var zoomX = (float)Width / mapWidth; var zoomY = (float)Height / mapHeight; _zoom = Math.Min(zoomX, zoomY) * 0.9f; // 약간의 여백 _panOffset = new Point( (int)((Width - mapWidth * _zoom) / 2 - minX * _zoom), (int)((Height - mapHeight * _zoom) / 2 - minY * _zoom) ); Invalidate(); } #endregion #region Event Handlers private void OnAGVPositionChanged(object sender, Point newPosition) { Invalidate(); // AGV 위치 변경시 화면 갱신 } private void OnAGVStateChanged(object sender, AGVState newState) { Invalidate(); // AGV 상태 변경시 화면 갱신 } private void OnMouseDown(object sender, MouseEventArgs e) { if (e.Button == MouseButtons.Right) { _isPanning = true; _lastMousePos = e.Location; Cursor = Cursors.Hand; } } private void OnMouseMove(object sender, MouseEventArgs e) { if (_isPanning) { var deltaX = e.X - _lastMousePos.X; var deltaY = e.Y - _lastMousePos.Y; _panOffset = new Point( _panOffset.X + deltaX, _panOffset.Y + deltaY ); _lastMousePos = e.Location; Invalidate(); } } private void OnMouseUp(object sender, MouseEventArgs e) { if (e.Button == MouseButtons.Right) { _isPanning = false; Cursor = Cursors.Default; } } private void OnMouseWheel(object sender, MouseEventArgs e) { var zoomFactor = e.Delta > 0 ? 1.1f : 0.9f; var newZoom = _zoom * zoomFactor; if (newZoom >= 0.1f && newZoom <= 10.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)) ); _zoom = newZoom; Invalidate(); } } #endregion #region Painting protected override void OnPaint(PaintEventArgs e) { base.OnPaint(e); var g = e.Graphics; g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias; // 변환 행렬 설정 g.TranslateTransform(_panOffset.X, _panOffset.Y); g.ScaleTransform(_zoom, _zoom); // 배경 그리드 그리기 DrawGrid(g); // 맵 노드 연결선 그리기 DrawNodeConnections(g); // 경로 그리기 if (_currentPath != null && _currentPath.Success) { DrawPath(g); } // 맵 노드 그리기 DrawMapNodes(g); // AGV 그리기 DrawAGVs(g); // 정보 표시 (변환 해제) g.ResetTransform(); DrawInfo(g); } private void DrawGrid(Graphics g) { var gridSize = 50; var pen = new Pen(Color.LightGray, 1); var startX = -(int)(_panOffset.X / _zoom / gridSize) * gridSize; var startY = -(int)(_panOffset.Y / _zoom / gridSize) * gridSize; var endX = startX + (int)(Width / _zoom) + gridSize; var endY = startY + (int)(Height / _zoom) + gridSize; for (int x = startX; x <= endX; x += gridSize) { g.DrawLine(pen, x, startY, x, endY); } for (int y = startY; y <= endY; y += gridSize) { g.DrawLine(pen, startX, y, endX, y); } pen.Dispose(); } private void DrawNodeConnections(Graphics g) { if (_mapNodes == null) return; foreach (var node in _mapNodes) { if (node.ConnectedNodes != null) { foreach (var connectedNodeId in node.ConnectedNodes) { var connectedNode = _mapNodes.FirstOrDefault(n => n.NodeId == connectedNodeId); if (connectedNode != null) { DrawConnection(g, node.Position, connectedNode.Position); } } } } } private void DrawConnection(Graphics g, Point from, Point to) { g.DrawLine(_connectionPen, from, to); // 방향 화살표 그리기 var angle = Math.Atan2(to.Y - from.Y, to.X - from.X); var arrowX = to.X - CONNECTION_ARROW_SIZE * Math.Cos(angle); var arrowY = to.Y - CONNECTION_ARROW_SIZE * Math.Sin(angle); var arrowPoint1 = new PointF( (float)(arrowX - CONNECTION_ARROW_SIZE * Math.Cos(angle - Math.PI / 6)), (float)(arrowY - CONNECTION_ARROW_SIZE * Math.Sin(angle - Math.PI / 6)) ); var arrowPoint2 = new PointF( (float)(arrowX - CONNECTION_ARROW_SIZE * Math.Cos(angle + Math.PI / 6)), (float)(arrowY - CONNECTION_ARROW_SIZE * Math.Sin(angle + Math.PI / 6)) ); g.DrawLine(_connectionPen, to, arrowPoint1); g.DrawLine(_connectionPen, to, arrowPoint2); } private void DrawPath(Graphics g) { if (_currentPath?.Path == null || _currentPath.Path.Count < 2) return; for (int i = 0; i < _currentPath.Path.Count - 1; i++) { var currentNodeId = _currentPath.Path[i]; var nextNodeId = _currentPath.Path[i + 1]; var currentNode = _mapNodes?.FirstOrDefault(n => n.NodeId == currentNodeId); var nextNode = _mapNodes?.FirstOrDefault(n => n.NodeId == nextNodeId); if (currentNode != null && nextNode != null) { g.DrawLine(_pathPen, currentNode.Position, nextNode.Position); } } } private void DrawMapNodes(Graphics g) { if (_mapNodes == null) return; foreach (var node in _mapNodes) { DrawMapNode(g, node); } } private void DrawMapNode(Graphics g, MapNode node) { var brush = GetNodeBrush(node.Type); var rect = new Rectangle( node.Position.X - NODE_SIZE / 2, node.Position.Y - NODE_SIZE / 2, NODE_SIZE, NODE_SIZE ); // 노드 그리기 if (node.Type == NodeType.Rotation) { g.FillEllipse(brush, rect); // 회전 노드는 원형 } else { g.FillRectangle(brush, rect); // 일반 노드는 사각형 } g.DrawRectangle(Pens.Black, rect); // 노드 ID 표시 var font = new Font("Arial", 8); var textSize = g.MeasureString(node.NodeId, font); var textPos = new PointF( node.Position.X - textSize.Width / 2, node.Position.Y + NODE_SIZE / 2 + 2 ); g.DrawString(node.NodeId, font, Brushes.Black, textPos); font.Dispose(); } private Brush GetNodeBrush(NodeType nodeType) { switch (nodeType) { case NodeType.Rotation: return _rotationNodeBrush; case NodeType.Docking: return _dockingNodeBrush; case NodeType.Charging: return _chargingNodeBrush; default: return _normalNodeBrush; } } private void DrawAGVs(Graphics g) { if (_agvList == null) return; foreach (var agv in _agvList) { DrawAGV(g, agv); } } private void DrawAGV(Graphics g, VirtualAGV agv) { var position = agv.CurrentPosition; var rect = new Rectangle( position.X - AGV_SIZE / 2, position.Y - AGV_SIZE / 2, AGV_SIZE, AGV_SIZE ); // AGV 상태에 따른 색상 변경 var brush = GetAGVBrush(agv.CurrentState); // AGV 본체 그리기 g.FillEllipse(brush, rect); g.DrawEllipse(_agvPen, rect); // 방향 표시 DrawAGVDirection(g, position, agv.CurrentDirection); // AGV ID 표시 var font = new Font("Arial", 10, FontStyle.Bold); var textSize = g.MeasureString(agv.AgvId, font); var textPos = new PointF( position.X - textSize.Width / 2, position.Y + AGV_SIZE / 2 + 5 ); g.DrawString(agv.AgvId, font, Brushes.Black, textPos); font.Dispose(); } private Brush GetAGVBrush(AGVState state) { switch (state) { case AGVState.Moving: return Brushes.Blue; case AGVState.Rotating: return Brushes.Yellow; case AGVState.Docking: return Brushes.Orange; case AGVState.Charging: return Brushes.Green; case AGVState.Error: return Brushes.Red; default: return Brushes.Gray; // Idle } } private void DrawAGVDirection(Graphics g, Point position, AgvDirection direction) { var arrowSize = 10; var pen = new Pen(Color.White, 2); switch (direction) { case AgvDirection.Forward: // 위쪽 화살표 g.DrawLine(pen, position.X, position.Y - arrowSize, position.X, position.Y + arrowSize); g.DrawLine(pen, position.X, position.Y - arrowSize, position.X - 5, position.Y - arrowSize + 5); g.DrawLine(pen, position.X, position.Y - arrowSize, position.X + 5, position.Y - arrowSize + 5); break; case AgvDirection.Backward: // 아래쪽 화살표 g.DrawLine(pen, position.X, position.Y - arrowSize, position.X, position.Y + arrowSize); g.DrawLine(pen, position.X, position.Y + arrowSize, position.X - 5, position.Y + arrowSize - 5); g.DrawLine(pen, position.X, position.Y + arrowSize, position.X + 5, position.Y + arrowSize - 5); break; case AgvDirection.Left: // 왼쪽 화살표 g.DrawLine(pen, position.X - arrowSize, position.Y, position.X + arrowSize, position.Y); g.DrawLine(pen, position.X - arrowSize, position.Y, position.X - arrowSize + 5, position.Y - 5); g.DrawLine(pen, position.X - arrowSize, position.Y, position.X - arrowSize + 5, position.Y + 5); break; case AgvDirection.Right: // 오른쪽 화살표 g.DrawLine(pen, position.X - arrowSize, position.Y, position.X + arrowSize, position.Y); g.DrawLine(pen, position.X + arrowSize, position.Y, position.X + arrowSize - 5, position.Y - 5); g.DrawLine(pen, position.X + arrowSize, position.Y, position.X + arrowSize - 5, position.Y + 5); break; } pen.Dispose(); } private void DrawInfo(Graphics g) { var font = new Font("Arial", 10); var brush = Brushes.Black; var y = 10; // 줌 레벨 표시 g.DrawString($"줌: {_zoom:P0}", font, brush, new PointF(10, y)); y += 20; // AGV 정보 표시 if (_agvList != null) { g.DrawString($"AGV 수: {_agvList.Count}", font, brush, new PointF(10, y)); y += 20; foreach (var agv in _agvList) { var info = $"{agv.AgvId}: {agv.CurrentState} ({agv.CurrentPosition.X},{agv.CurrentPosition.Y})"; g.DrawString(info, font, brush, new PointF(10, y)); y += 15; } } // 경로 정보 표시 if (_currentPath != null && _currentPath.Success) { y += 10; g.DrawString($"경로: {_currentPath.Path.Count}개 노드", font, brush, new PointF(10, y)); y += 15; g.DrawString($"거리: {_currentPath.TotalDistance:F1}", font, brush, new PointF(10, y)); y += 15; g.DrawString($"계산시간: {_currentPath.CalculationTimeMs}ms", font, brush, new PointF(10, y)); } font.Dispose(); } #endregion #region Cleanup private void CleanupResources() { // AGV 이벤트 연결 해제 if (_agvList != null) { foreach (var agv in _agvList) { agv.PositionChanged -= OnAGVPositionChanged; agv.StateChanged -= OnAGVStateChanged; } } // 리소스 정리 _connectionPen?.Dispose(); _pathPen?.Dispose(); _agvPen?.Dispose(); } #endregion } }