using System; using System.Drawing; using System.Drawing.Drawing2D; using System.Linq; using System.Windows.Forms; using AGVNavigationCore.Models; using AGVNavigationCore.PathFinding; namespace AGVNavigationCore.Controls { public partial class UnifiedAGVCanvas { #region Paint Events private void UnifiedAGVCanvas_Paint(object sender, PaintEventArgs e) { var g = e.Graphics; g.SmoothingMode = SmoothingMode.AntiAlias; g.InterpolationMode = InterpolationMode.High; // 변환 행렬 설정 (줌 및 팬) var transform = new Matrix(); transform.Scale(_zoomFactor, _zoomFactor); transform.Translate(_panOffset.X, _panOffset.Y); g.Transform = transform; try { // 그리드 그리기 if (_showGrid) { DrawGrid(g); } // 노드 연결선 그리기 DrawConnections(g); // 경로 그리기 DrawPaths(g); // 노드 그리기 DrawNodes(g); // AGV 그리기 DrawAGVs(g); // 임시 연결선 그리기 (편집 모드) if (_canvasMode == CanvasMode.Edit && _isConnectionMode) { DrawTemporaryConnection(g); } } finally { g.Transform = new Matrix(); // 변환 행렬 리셋 } // UI 정보 그리기 (변환 없이) DrawUIInfo(g); } private void DrawGrid(Graphics g) { if (!_showGrid) return; var bounds = GetVisibleBounds(); var gridSize = (int)(GRID_SIZE * _zoomFactor); if (gridSize < 5) return; // 너무 작으면 그리지 않음 for (int x = bounds.Left; x < bounds.Right; x += GRID_SIZE) { if (x % (GRID_SIZE * 5) == 0) g.DrawLine(new Pen(Color.Gray, 1), x, bounds.Top, x, bounds.Bottom); else g.DrawLine(_gridPen, x, bounds.Top, x, bounds.Bottom); } for (int y = bounds.Top; y < bounds.Bottom; y += GRID_SIZE) { if (y % (GRID_SIZE * 5) == 0) g.DrawLine(new Pen(Color.Gray, 1), bounds.Left, y, bounds.Right, y); else g.DrawLine(_gridPen, bounds.Left, y, bounds.Right, y); } } private void DrawConnections(Graphics g) { if (_nodes == null) return; foreach (var node in _nodes) { if (node.ConnectedNodes == null) continue; foreach (var connectedNodeId in node.ConnectedNodes) { var targetNode = _nodes.FirstOrDefault(n => n.NodeId == connectedNodeId); if (targetNode == null) continue; DrawConnection(g, node, targetNode); } } } private void DrawConnection(Graphics g, MapNode fromNode, MapNode toNode) { var startPoint = fromNode.Position; var endPoint = toNode.Position; // 연결선만 그리기 (단순한 도로 연결, 방향성 없음) g.DrawLine(_connectionPen, startPoint, endPoint); } private void DrawDirectionArrow(Graphics g, Point point, double angle, AgvDirection direction) { var arrowSize = CONNECTION_ARROW_SIZE; var arrowAngle = Math.PI / 6; // 30도 var cos = Math.Cos(angle); var sin = Math.Sin(angle); var arrowPoint1 = new Point( (int)(point.X - arrowSize * Math.Cos(angle - arrowAngle)), (int)(point.Y - arrowSize * Math.Sin(angle - arrowAngle)) ); var arrowPoint2 = new Point( (int)(point.X - arrowSize * Math.Cos(angle + arrowAngle)), (int)(point.Y - arrowSize * Math.Sin(angle + arrowAngle)) ); var arrowColor = direction == AgvDirection.Forward ? Color.Blue : Color.Red; var arrowPen = new Pen(arrowColor, 2); g.DrawLine(arrowPen, point, arrowPoint1); g.DrawLine(arrowPen, point, arrowPoint2); arrowPen.Dispose(); } private void DrawPaths(Graphics g) { // 모든 경로 그리기 if (_allPaths != null) { foreach (var path in _allPaths) { DrawPath(g, path, Color.LightBlue); } } // 현재 선택된 경로 그리기 if (_currentPath != null) { DrawPath(g, _currentPath, Color.Purple); } } private void DrawPath(Graphics g, PathResult path, Color color) { if (path?.Path == null || path.Path.Count < 2) return; var pathPen = new Pen(color, 4) { DashStyle = DashStyle.Dash }; for (int i = 0; i < path.Path.Count - 1; i++) { var currentNodeId = path.Path[i]; var nextNodeId = path.Path[i + 1]; var currentNode = _nodes?.FirstOrDefault(n => n.NodeId == currentNodeId); var nextNode = _nodes?.FirstOrDefault(n => n.NodeId == nextNodeId); if (currentNode != null && nextNode != null) { // 경로 선 그리기 g.DrawLine(pathPen, currentNode.Position, nextNode.Position); // 경로 방향 표시 (계산된 경로의 경우에만 방향 화살표 표시) var midPoint = new Point( (currentNode.Position.X + nextNode.Position.X) / 2, (currentNode.Position.Y + nextNode.Position.Y) / 2 ); var angle = Math.Atan2(nextNode.Position.Y - currentNode.Position.Y, nextNode.Position.X - currentNode.Position.X); DrawDirectionArrow(g, midPoint, angle, AgvDirection.Forward); } } pathPen.Dispose(); } private void DrawNodes(Graphics g) { if (_nodes == null) return; foreach (var node in _nodes) { DrawNode(g, node); } } private void DrawNode(Graphics g, MapNode node) { switch (node.Type) { case NodeType.Label: DrawLabelNode(g, node); break; case NodeType.Image: DrawImageNode(g, node); break; default: DrawCircularNode(g, node); break; } } private void DrawCircularNode(Graphics g, MapNode node) { var brush = GetNodeBrush(node); switch (node.Type) { case NodeType.Docking: DrawPentagonNode(g, node, brush); break; case NodeType.Charging: DrawTriangleNode(g, node, brush); break; default: DrawCircleNode(g, node, brush); break; } } private void DrawCircleNode(Graphics g, MapNode node, Brush brush) { var rect = new Rectangle( node.Position.X - NODE_RADIUS, node.Position.Y - NODE_RADIUS, NODE_SIZE, NODE_SIZE ); // 노드 그리기 g.FillEllipse(brush, rect); g.DrawEllipse(Pens.Black, rect); // 선택된 노드 강조 if (node == _selectedNode) { g.DrawEllipse(_selectedNodePen, rect); } // 호버된 노드 강조 if (node == _hoveredNode) { var hoverRect = new Rectangle(rect.X - 2, rect.Y - 2, rect.Width + 4, rect.Height + 4); g.DrawEllipse(new Pen(Color.Orange, 2), hoverRect); } DrawNodeLabel(g, node); } private void DrawPentagonNode(Graphics g, MapNode node, Brush brush) { var radius = NODE_RADIUS; var center = node.Position; // 5각형 꼭짓점 계산 (위쪽부터 시계방향) var points = new Point[5]; for (int i = 0; i < 5; i++) { var angle = (Math.PI * 2 * i / 5) - Math.PI / 2; // -90도부터 시작 (위쪽) points[i] = new Point( (int)(center.X + radius * Math.Cos(angle)), (int)(center.Y + radius * Math.Sin(angle)) ); } // 5각형 그리기 g.FillPolygon(brush, points); g.DrawPolygon(Pens.Black, points); // 선택된 노드 강조 if (node == _selectedNode) { g.DrawPolygon(_selectedNodePen, points); } // 호버된 노드 강조 if (node == _hoveredNode) { // 확장된 5각형 계산 var hoverPoints = new Point[5]; for (int i = 0; i < 5; i++) { var angle = (Math.PI * 2 * i / 5) - Math.PI / 2; hoverPoints[i] = new Point( (int)(center.X + (radius + 3) * Math.Cos(angle)), (int)(center.Y + (radius + 3) * Math.Sin(angle)) ); } g.DrawPolygon(new Pen(Color.Orange, 2), hoverPoints); } DrawNodeLabel(g, node); } private void DrawTriangleNode(Graphics g, MapNode node, Brush brush) { var radius = NODE_RADIUS; var center = node.Position; // 삼각형 꼭짓점 계산 (위쪽 꼭짓점부터 시계방향) var points = new Point[3]; for (int i = 0; i < 3; i++) { var angle = (Math.PI * 2 * i / 3) - Math.PI / 2; // -90도부터 시작 (위쪽) points[i] = new Point( (int)(center.X + radius * Math.Cos(angle)), (int)(center.Y + radius * Math.Sin(angle)) ); } // 삼각형 그리기 g.FillPolygon(brush, points); g.DrawPolygon(Pens.Black, points); // 선택된 노드 강조 if (node == _selectedNode) { g.DrawPolygon(_selectedNodePen, points); } // 호버된 노드 강조 if (node == _hoveredNode) { // 확장된 삼각형 계산 var hoverPoints = new Point[3]; for (int i = 0; i < 3; i++) { var angle = (Math.PI * 2 * i / 3) - Math.PI / 2; hoverPoints[i] = new Point( (int)(center.X + (radius + 3) * Math.Cos(angle)), (int)(center.Y + (radius + 3) * Math.Sin(angle)) ); } g.DrawPolygon(new Pen(Color.Orange, 2), hoverPoints); } DrawNodeLabel(g, node); } private void DrawNodeLabel(Graphics g, MapNode node) { string displayText; Color textColor; string descriptionText; // 위쪽에 표시할 설명 (노드의 Description 속성) descriptionText = string.IsNullOrEmpty(node.Description) ? "" : node.Description; // 아래쪽에 표시할 값 (RFID 우선, 없으면 노드ID) if (node.HasRfid()) { // RFID가 있는 경우: 순수 RFID 값만 표시 (진한 색상) displayText = node.RfidId; textColor = Color.Black; } else { // RFID가 없는 경우: 노드 ID 표시 (연한 색상) displayText = node.NodeId; textColor = Color.Gray; } var font = new Font("Arial", 8, FontStyle.Bold); var descFont = new Font("Arial", 6, FontStyle.Regular); // 메인 텍스트 크기 측정 var textSize = g.MeasureString(displayText, font); var descSize = g.MeasureString(descriptionText, descFont); // 설명 텍스트 위치 (노드 위쪽) var descPoint = new Point( (int)(node.Position.X - descSize.Width / 2), (int)(node.Position.Y - NODE_RADIUS - descSize.Height - 2) ); // 메인 텍스트 위치 (노드 아래쪽) var textPoint = new Point( (int)(node.Position.X - textSize.Width / 2), (int)(node.Position.Y + NODE_RADIUS + 2) ); // 설명 텍스트 그리기 (설명이 있는 경우에만) if (!string.IsNullOrEmpty(descriptionText)) { using (var descBrush = new SolidBrush(Color.FromArgb(120, Color.Black))) { g.DrawString(descriptionText, descFont, descBrush, descPoint); } } // 메인 텍스트 그리기 using (var textBrush = new SolidBrush(textColor)) { g.DrawString(displayText, font, textBrush, textPoint); } font.Dispose(); descFont.Dispose(); } private void DrawLabelNode(Graphics g, MapNode node) { var text = string.IsNullOrEmpty(node.LabelText) ? node.NodeId : node.LabelText; // 폰트 설정 var font = new Font(node.FontFamily, node.FontSize, node.FontStyle); var textBrush = new SolidBrush(node.ForeColor); // 텍스트 크기 측정 var textSize = g.MeasureString(text, font); var textPoint = new Point( (int)(node.Position.X - textSize.Width / 2), (int)(node.Position.Y - textSize.Height / 2) ); // 배경 그리기 (설정된 경우) if (node.ShowBackground) { var backgroundBrush = new SolidBrush(node.BackColor); var backgroundRect = new Rectangle( textPoint.X - 2, textPoint.Y - 2, (int)textSize.Width + 4, (int)textSize.Height + 4 ); g.FillRectangle(backgroundBrush, backgroundRect); g.DrawRectangle(Pens.Black, backgroundRect); backgroundBrush.Dispose(); } // 텍스트 그리기 g.DrawString(text, font, textBrush, textPoint); // 선택된 노드 강조 if (node == _selectedNode) { var selectionRect = new Rectangle( textPoint.X - 4, textPoint.Y - 4, (int)textSize.Width + 8, (int)textSize.Height + 8 ); g.DrawRectangle(_selectedNodePen, selectionRect); } // 호버된 노드 강조 if (node == _hoveredNode) { var hoverRect = new Rectangle( textPoint.X - 6, textPoint.Y - 6, (int)textSize.Width + 12, (int)textSize.Height + 12 ); g.DrawRectangle(new Pen(Color.Orange, 2), hoverRect); } font.Dispose(); textBrush.Dispose(); } private void DrawImageNode(Graphics g, MapNode node) { // 이미지 로드 (필요시) if (node.LoadedImage == null && !string.IsNullOrEmpty(node.ImagePath)) { node.LoadImage(); } if (node.LoadedImage != null) { // 실제 표시 크기 계산 var displaySize = node.GetDisplaySize(); if (displaySize.IsEmpty) displaySize = new Size(50, 50); // 기본 크기 var imageRect = new Rectangle( node.Position.X - displaySize.Width / 2, node.Position.Y - displaySize.Height / 2, displaySize.Width, displaySize.Height ); // 회전이 있는 경우 if (node.Rotation != 0) { var oldTransform = g.Transform; g.TranslateTransform(node.Position.X, node.Position.Y); g.RotateTransform(node.Rotation); g.TranslateTransform(-node.Position.X, -node.Position.Y); // 투명도 적용하여 이미지 그리기 if (node.Opacity < 1.0f) { var imageAttributes = new System.Drawing.Imaging.ImageAttributes(); var colorMatrix = new System.Drawing.Imaging.ColorMatrix(); colorMatrix.Matrix33 = node.Opacity; imageAttributes.SetColorMatrix(colorMatrix, System.Drawing.Imaging.ColorMatrixFlag.Default, System.Drawing.Imaging.ColorAdjustType.Bitmap); g.DrawImage(node.LoadedImage, imageRect, 0, 0, node.LoadedImage.Width, node.LoadedImage.Height, GraphicsUnit.Pixel, imageAttributes); imageAttributes.Dispose(); } else { g.DrawImage(node.LoadedImage, imageRect); } g.Transform = oldTransform; } else { // 투명도 적용하여 이미지 그리기 if (node.Opacity < 1.0f) { var imageAttributes = new System.Drawing.Imaging.ImageAttributes(); var colorMatrix = new System.Drawing.Imaging.ColorMatrix(); colorMatrix.Matrix33 = node.Opacity; imageAttributes.SetColorMatrix(colorMatrix, System.Drawing.Imaging.ColorMatrixFlag.Default, System.Drawing.Imaging.ColorAdjustType.Bitmap); g.DrawImage(node.LoadedImage, imageRect, 0, 0, node.LoadedImage.Width, node.LoadedImage.Height, GraphicsUnit.Pixel, imageAttributes); imageAttributes.Dispose(); } else { g.DrawImage(node.LoadedImage, imageRect); } } // 선택된 노드 강조 if (node == _selectedNode) { g.DrawRectangle(_selectedNodePen, imageRect); } // 호버된 노드 강조 if (node == _hoveredNode) { var hoverRect = new Rectangle(imageRect.X - 2, imageRect.Y - 2, imageRect.Width + 4, imageRect.Height + 4); g.DrawRectangle(new Pen(Color.Orange, 2), hoverRect); } } else { // 이미지가 없는 경우 기본 사각형으로 표시 var rect = new Rectangle( node.Position.X - 25, node.Position.Y - 25, 50, 50 ); g.FillRectangle(Brushes.LightGray, rect); g.DrawRectangle(Pens.Black, rect); // "이미지 없음" 텍스트 var font = new Font("Arial", 8); var text = "No Image"; var textSize = g.MeasureString(text, font); var textPoint = new Point( (int)(node.Position.X - textSize.Width / 2), (int)(node.Position.Y - textSize.Height / 2) ); g.DrawString(text, font, Brushes.Black, textPoint); font.Dispose(); // 선택된 노드 강조 if (node == _selectedNode) { g.DrawRectangle(_selectedNodePen, rect); } // 호버된 노드 강조 if (node == _hoveredNode) { var hoverRect = new Rectangle(rect.X - 2, rect.Y - 2, rect.Width + 4, rect.Height + 4); g.DrawRectangle(new Pen(Color.Orange, 2), hoverRect); } } } private Brush GetNodeBrush(MapNode node) { switch (node.Type) { case NodeType.Normal: return _normalNodeBrush; case NodeType.Rotation: return _rotationNodeBrush; case NodeType.Docking: return _dockingNodeBrush; case NodeType.Charging: return _chargingNodeBrush; case NodeType.Label: return new SolidBrush(Color.Purple); case NodeType.Image: return new SolidBrush(Color.Brown); default: return _normalNodeBrush; } } private void DrawAGVs(Graphics g) { if (_agvList == null) return; foreach (var agv in _agvList) { if (_agvPositions.ContainsKey(agv.AgvId)) { DrawAGV(g, agv); } } } private void DrawAGV(Graphics g, IAGV agv) { if (!_agvPositions.ContainsKey(agv.AgvId)) return; var position = _agvPositions[agv.AgvId]; var direction = _agvDirections.ContainsKey(agv.AgvId) ? _agvDirections[agv.AgvId] : AgvDirection.Forward; var state = _agvStates.ContainsKey(agv.AgvId) ? _agvStates[agv.AgvId] : AGVState.Idle; // AGV 색상 결정 var brush = GetAGVBrush(state); // AGV 사각형 그리기 var rect = new Rectangle( position.X - AGV_SIZE / 2, position.Y - AGV_SIZE / 2, AGV_SIZE, AGV_SIZE ); g.FillRectangle(brush, rect); g.DrawRectangle(_agvPen, rect); // 방향 표시 (화살표) DrawAGVDirection(g, position, direction); // AGV ID 표시 var font = new Font("Arial", 10, FontStyle.Bold); var textSize = g.MeasureString(agv.AgvId, font); var textPoint = new Point( (int)(position.X - textSize.Width / 2), (int)(position.Y - AGV_SIZE / 2 - textSize.Height - 2) ); g.DrawString(agv.AgvId, font, Brushes.Black, textPoint); // 배터리 레벨 표시 var batteryText = $"{agv.BatteryLevel:F0}%"; var batterySize = g.MeasureString(batteryText, font); var batteryPoint = new Point( (int)(position.X - batterySize.Width / 2), (int)(position.Y + AGV_SIZE / 2 + 2) ); g.DrawString(batteryText, font, Brushes.Black, batteryPoint); font.Dispose(); } private Brush GetAGVBrush(AGVState state) { switch (state) { case AGVState.Idle: return Brushes.LightGray; case AGVState.Moving: return Brushes.LightGreen; case AGVState.Rotating: return Brushes.Yellow; case AGVState.Docking: return Brushes.Orange; case AGVState.Charging: return Brushes.Blue; case AGVState.Error: return Brushes.Red; default: return Brushes.LightGray; } } private void DrawAGVDirection(Graphics g, Point position, AgvDirection direction) { var arrowSize = 10; Point[] arrowPoints = null; switch (direction) { case AgvDirection.Forward: arrowPoints = new Point[] { new Point(position.X + arrowSize, position.Y), new Point(position.X - arrowSize/2, position.Y - arrowSize/2), new Point(position.X - arrowSize/2, position.Y + arrowSize/2) }; break; case AgvDirection.Backward: arrowPoints = new Point[] { new Point(position.X - arrowSize, position.Y), new Point(position.X + arrowSize/2, position.Y - arrowSize/2), new Point(position.X + arrowSize/2, position.Y + arrowSize/2) }; break; } if (arrowPoints != null) { g.FillPolygon(Brushes.White, arrowPoints); g.DrawPolygon(Pens.Black, arrowPoints); } } private void DrawTemporaryConnection(Graphics g) { if (_connectionStartNode != null && _connectionEndPoint != Point.Empty) { g.DrawLine(_tempConnectionPen, _connectionStartNode.Position, _connectionEndPoint); } } private void DrawUIInfo(Graphics g) { // 회사 로고 if (_companyLogo != null) { var logoRect = new Rectangle(10, 10, 100, 50); g.DrawImage(_companyLogo, logoRect); } // 측정 정보 if (!string.IsNullOrEmpty(_measurementInfo)) { var font = new Font("Arial", 9); var textBrush = new SolidBrush(Color.Black); var backgroundBrush = new SolidBrush(Color.FromArgb(200, Color.White)); var textSize = g.MeasureString(_measurementInfo, font); var textRect = new Rectangle( Width - (int)textSize.Width - 20, Height - (int)textSize.Height - 20, (int)textSize.Width + 10, (int)textSize.Height + 10 ); g.FillRectangle(backgroundBrush, textRect); g.DrawRectangle(Pens.Gray, textRect); g.DrawString(_measurementInfo, font, textBrush, textRect.X + 5, textRect.Y + 5); font.Dispose(); textBrush.Dispose(); backgroundBrush.Dispose(); } // 줌 정보 var zoomText = $"Zoom: {_zoomFactor:P0}"; var zoomFont = new Font("Arial", 10, FontStyle.Bold); var zoomSize = g.MeasureString(zoomText, zoomFont); var zoomPoint = new Point(10, Height - (int)zoomSize.Height - 10); g.FillRectangle(new SolidBrush(Color.FromArgb(200, Color.White)), zoomPoint.X - 5, zoomPoint.Y - 5, zoomSize.Width + 10, zoomSize.Height + 10); g.DrawString(zoomText, zoomFont, Brushes.Black, zoomPoint); zoomFont.Dispose(); } private Rectangle GetVisibleBounds() { var left = (int)(-_panOffset.X / _zoomFactor); var top = (int)(-_panOffset.Y / _zoomFactor); var right = (int)((Width - _panOffset.X) / _zoomFactor); var bottom = (int)((Height - _panOffset.Y) / _zoomFactor); return new Rectangle(left, top, right - left, bottom - top); } #endregion } }