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); } } // 현재 선택된 경로 그리기 (AGVPathResult 활용) if (_currentPath != null) { DrawPath(g, _currentPath, Color.Purple); // AGVPathResult의 모터방향 정보가 있다면 향상된 경로 그리기 // 현재는 기본 PathResult를 사용하므로 향후 AGVPathResult로 업그레이드 시 활성화 // TODO: AGVPathfinder 사용시 AGVPathResult로 업그레이드 } } 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(); } /// /// AGV 경로 및 모터방향 정보를 시각화 /// /// Graphics 객체 /// AGV 경로 계산 결과 private void DrawAGVPath(Graphics g, AGVPathResult agvResult) { if (agvResult?.NodeMotorInfos == null || agvResult.NodeMotorInfos.Count == 0) return; // 노드별 모터방향 정보를 기반으로 향상된 경로 표시 for (int i = 0; i < agvResult.NodeMotorInfos.Count - 1; i++) { var currentMotorInfo = agvResult.NodeMotorInfos[i]; var nextMotorInfo = agvResult.NodeMotorInfos[i + 1]; var currentNode = _nodes?.FirstOrDefault(n => n.NodeId == currentMotorInfo.NodeId); var nextNode = _nodes?.FirstOrDefault(n => n.NodeId == nextMotorInfo.NodeId); if (currentNode != null && nextNode != null) { // 모터방향에 따른 색상 결정 var motorDirection = currentMotorInfo.MotorDirection; Color pathColor = motorDirection == AgvDirection.Forward ? Color.Green : Color.Orange; // 강조된 경로 선 그리기 var enhancedPen = new Pen(pathColor, 6) { DashStyle = DashStyle.Solid }; g.DrawLine(enhancedPen, 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, motorDirection); // 노드 옆에 모터방향 텍스트 표시 DrawMotorDirectionLabel(g, currentNode.Position, motorDirection); enhancedPen.Dispose(); } } // 마지막 노드의 모터방향 표시 if (agvResult.NodeMotorInfos.Count > 0) { var lastMotorInfo = agvResult.NodeMotorInfos[agvResult.NodeMotorInfos.Count - 1]; var lastNode = _nodes?.FirstOrDefault(n => n.NodeId == lastMotorInfo.NodeId); if (lastNode != null) { DrawMotorDirectionLabel(g, lastNode.Position, lastMotorInfo.MotorDirection); } } } /// /// 모터방향 레이블 표시 /// /// Graphics 객체 /// 노드 위치 /// 모터방향 private void DrawMotorDirectionLabel(Graphics g, Point nodePosition, AgvDirection motorDirection) { string motorText = motorDirection == AgvDirection.Forward ? "전진" : "후진"; Color textColor = motorDirection == AgvDirection.Forward ? Color.DarkGreen : Color.DarkOrange; var font = new Font("맑은 고딕", 8, FontStyle.Bold); var brush = new SolidBrush(textColor); // 노드 우측 상단에 모터방향 텍스트 표시 var textPosition = new Point(nodePosition.X + NODE_RADIUS + 2, nodePosition.Y - NODE_RADIUS - 2); g.DrawString(motorText, font, brush, textPosition); font.Dispose(); brush.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 == _destinationNode) { // 금색 테두리로 목적지 강조 g.DrawEllipse(_destinationNodePen, rect); // 펄싱 효과를 위한 추가 원 그리기 var pulseRect = new Rectangle(rect.X - 3, rect.Y - 3, rect.Width + 6, rect.Height + 6); g.DrawEllipse(new Pen(Color.Gold, 2) { DashStyle = DashStyle.Dash }, pulseRect); } // 호버된 노드 강조 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 == _destinationNode) { // 금색 테두리로 목적지 강조 g.DrawPolygon(_destinationNodePen, points); // 펄싱 효과를 위한 추가 오각형 그리기 var pulsePoints = new Point[5]; for (int i = 0; i < 5; i++) { var angle = (Math.PI * 2 * i / 5) - Math.PI / 2; pulsePoints[i] = new Point( (int)(center.X + (radius + 4) * Math.Cos(angle)), (int)(center.Y + (radius + 4) * Math.Sin(angle)) ); } g.DrawPolygon(new Pen(Color.Gold, 2) { DashStyle = DashStyle.Dash }, pulsePoints); } // 호버된 노드 강조 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 == _destinationNode) { // 금색 테두리로 목적지 강조 g.DrawPolygon(_destinationNodePen, points); // 펄싱 효과를 위한 추가 삼각형 그리기 var pulsePoints = new Point[3]; for (int i = 0; i < 3; i++) { var angle = (Math.PI * 2 * i / 3) - Math.PI / 2; pulsePoints[i] = new Point( (int)(center.X + (radius + 4) * Math.Cos(angle)), (int)(center.Y + (radius + 4) * Math.Sin(angle)) ); } g.DrawPolygon(new Pen(Color.Gold, 2) { DashStyle = DashStyle.Dash }, pulsePoints); } // 호버된 노드 강조 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 ); // 방사형 그라디언트 효과를 위한 브러시 생성 using (var gradientBrush = new System.Drawing.Drawing2D.PathGradientBrush( new Point[] { new Point(rect.Left, rect.Top), new Point(rect.Right, rect.Top), new Point(rect.Right, rect.Bottom), new Point(rect.Left, rect.Bottom) })) { gradientBrush.CenterPoint = new PointF(position.X, position.Y); gradientBrush.CenterColor = GetAGVCenterColor(state); gradientBrush.SurroundColors = new Color[] { GetAGVOuterColor(state), GetAGVOuterColor(state), GetAGVOuterColor(state), GetAGVOuterColor(state) }; // 원형으로 AGV 본체 그리기 (3D 효과) g.FillEllipse(gradientBrush, rect); // 외곽선 (두께 조절) using (var outerPen = new Pen(Color.DarkGray, 3)) { g.DrawEllipse(outerPen, rect); } // 내부 링 (입체감) var innerSize = AGV_SIZE - 8; var innerRect = new Rectangle( position.X - innerSize / 2, position.Y - innerSize / 2, innerSize, innerSize ); using (var innerPen = new Pen(GetAGVInnerRingColor(state), 2)) { g.DrawEllipse(innerPen, innerRect); } // 중앙 상태 표시등 (더 크고 화려하게) var indicatorSize = 10; var indicatorRect = new Rectangle( position.X - indicatorSize / 2, position.Y - indicatorSize / 2, indicatorSize, indicatorSize ); // 표시등 글로우 효과 using (var glowBrush = new System.Drawing.Drawing2D.PathGradientBrush( new Point[] { new Point(indicatorRect.Left, indicatorRect.Top), new Point(indicatorRect.Right, indicatorRect.Top), new Point(indicatorRect.Right, indicatorRect.Bottom), new Point(indicatorRect.Left, indicatorRect.Bottom) })) { glowBrush.CenterPoint = new PointF(position.X, position.Y); glowBrush.CenterColor = Color.White; glowBrush.SurroundColors = new Color[] { GetStatusIndicatorColor(state), GetStatusIndicatorColor(state), GetStatusIndicatorColor(state), GetStatusIndicatorColor(state) }; g.FillEllipse(glowBrush, indicatorRect); g.DrawEllipse(new Pen(Color.DarkGray, 1), indicatorRect); } // 원형 센서 패턴 (방사형 배치) DrawCircularSensors(g, position, state); } // 리프트 그리기 (이동 경로 기반) DrawAGVLiftAdvanced(g, agv); // 모니터 그리기 (리프트 반대편) DrawAGVMonitor(g, agv); // 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 DrawAGVLiftAdvanced(Graphics g, IAGV agv) { const int liftLength = 28; // 리프트 길이 (더욱 크게) const int liftWidth = 16; // 리프트 너비 (더욱 크게) const int liftDistance = AGV_SIZE / 2 + 2; // AGV 본체 면에 바로 붙도록 var currentPos = agv.CurrentPosition; var targetPos = agv.TargetPosition; var dockingDirection = agv.DockingDirection; var currentDirection = agv.CurrentDirection; // 경로 예측 기반 LiftCalculator 사용 Point fallbackTarget = targetPos ?? new Point(currentPos.X + 1, currentPos.Y); // 기본 타겟 var liftInfo = AGVNavigationCore.Utils.LiftCalculator.CalculateLiftInfoWithPathPrediction( currentPos, fallbackTarget, currentDirection, _nodes); float liftAngle = (float)liftInfo.AngleRadians; // 리프트 위치 계산 (각도 기반) Point liftPosition = new Point( (int)(currentPos.X + liftDistance * Math.Cos(liftAngle)), (int)(currentPos.Y + liftDistance * Math.Sin(liftAngle)) ); // 방향을 알 수 있는지 확인 bool hasDirection = targetPos.HasValue && targetPos.Value != currentPos; // 방향에 따른 리프트 색상 및 스타일 결정 Color liftColor; Color borderColor; // 모터 방향과 경로 상태에 따른 색상 결정 (더 눈에 띄게) switch (currentDirection) { case AgvDirection.Forward: liftColor = Color.Yellow; // 전진 - 노란색 (잘 보임) borderColor = Color.Orange; break; case AgvDirection.Backward: liftColor = Color.Cyan; // 후진 - 시안색 (잘 보임) borderColor = Color.DarkCyan; break; default: liftColor = Color.LightGray; // 방향 불명 - 회색 borderColor = Color.Gray; break; } // 경로 예측 결과에 따른 색상 조정 if (liftInfo.CalculationMethod.Contains("경로 예측")) { // 경로 예측이 성공한 경우 더 선명한 색상 if (currentDirection == AgvDirection.Forward) { liftColor = Color.Gold; // 전진 예측 - 골드 borderColor = Color.DarkGoldenrod; } else if (currentDirection == AgvDirection.Backward) { liftColor = Color.DeepSkyBlue; // 후진 예측 - 딥 스카이 블루 borderColor = Color.Navy; } } else if (liftInfo.CalculationMethod.Contains("갈래길")) { // 갈래길에서는 약간 흐린 색상 liftColor = Color.FromArgb(200, liftColor.R, liftColor.G, liftColor.B); borderColor = Color.FromArgb(150, borderColor.R, borderColor.G, borderColor.B); } // Graphics 상태 저장 var oldTransform = g.Transform; // 리프트 중심으로 회전 변환 적용 g.TranslateTransform(liftPosition.X, liftPosition.Y); g.RotateTransform((float)(liftAngle * 180.0 / Math.PI)); // 회전된 좌표계에서 리프트 그리기 (중심이 원점) var liftRect = new Rectangle( -liftLength / 2, // 중심 기준 왼쪽 -liftWidth / 2, // 중심 기준 위쪽 liftLength, liftWidth ); using (var liftBrush = new SolidBrush(liftColor)) using (var liftPen = new Pen(borderColor, 3)) // 두껍게 테두리 { // 둥근 사각형으로 리프트 그리기 DrawRoundedRectangle(g, liftBrush, liftPen, liftRect, 4); // 추가 강조 테두리 (더 잘 보이게) using (var emphasisPen = new Pen(Color.Black, 1)) { DrawRoundedRectangle(g, null, emphasisPen, liftRect, 4); } } if (hasDirection) { // 리프트 세부 표현 (방향 표시용 선들) using (var detailPen = new Pen(Color.Gray, 1)) { int lineSpacing = 4; for (int i = -liftLength / 2 + 3; i < liftLength / 2 - 3; i += lineSpacing) { g.DrawLine(detailPen, i, -liftWidth / 2 + 2, i, liftWidth / 2 - 2); } } // 이동 방향 표시 화살표 (리프트 끝쪽) using (var arrowPen = new Pen(borderColor, 2)) { var arrowSize = 3; var arrowX = liftLength / 2 - 4; // 화살표 모양 g.DrawLine(arrowPen, arrowX, 0, arrowX + arrowSize, -arrowSize); g.DrawLine(arrowPen, arrowX, 0, arrowX + arrowSize, arrowSize); } } else { // 방향을 모를 때 '?' 표시 using (var questionBrush = new SolidBrush(Color.DarkGray)) using (var questionFont = new Font("Arial", 9, FontStyle.Bold)) { var questionSize = g.MeasureString("?", questionFont); var questionPoint = new PointF( -questionSize.Width / 2, -questionSize.Height / 2 ); g.DrawString("?", questionFont, questionBrush, questionPoint); } } // Graphics 상태 복원 g.Transform = oldTransform; } /// /// 둥근 모서리 사각형 그리기 /// private void DrawRoundedRectangle(Graphics g, Brush fillBrush, Pen borderPen, Rectangle rect, int cornerRadius) { if (cornerRadius <= 0) { if (fillBrush != null) g.FillRectangle(fillBrush, rect); if (borderPen != null) g.DrawRectangle(borderPen, rect); return; } using (var path = new System.Drawing.Drawing2D.GraphicsPath()) { // 둥근 모서리 경로 생성 path.AddArc(rect.X, rect.Y, cornerRadius * 2, cornerRadius * 2, 180, 90); path.AddArc(rect.Right - cornerRadius * 2, rect.Y, cornerRadius * 2, cornerRadius * 2, 270, 90); path.AddArc(rect.Right - cornerRadius * 2, rect.Bottom - cornerRadius * 2, cornerRadius * 2, cornerRadius * 2, 0, 90); path.AddArc(rect.X, rect.Bottom - cornerRadius * 2, cornerRadius * 2, cornerRadius * 2, 90, 90); path.CloseFigure(); // 채우기와 테두리 그리기 (null 체크 추가) if (fillBrush != null) g.FillPath(fillBrush, path); if (borderPen != null) g.DrawPath(borderPen, path); } } private void DrawAGVLiftDebugInfo(Graphics g, IAGV agv) { var currentPos = agv.CurrentPosition; var targetPos = agv.TargetPosition; // 디버그 정보 (개발용) if (targetPos.HasValue) { // 이동 방향 벡터 그리기 (얇은 점선) using (var debugPen = new Pen(Color.Red, 1) { DashStyle = System.Drawing.Drawing2D.DashStyle.Dot }) { g.DrawLine(debugPen, currentPos, targetPos.Value); } } } private void DrawTemporaryConnection(Graphics g) { if (_connectionStartNode != null && _connectionEndPoint != Point.Empty) { g.DrawLine(_tempConnectionPen, _connectionStartNode.Position, _connectionEndPoint); } } /// /// AGV 중앙 색상 (방사형 그라디언트) /// private Color GetAGVCenterColor(AGVState state) { switch (state) { case AGVState.Moving: return Color.White; case AGVState.Charging: return Color.LightCyan; case AGVState.Error: return Color.LightPink; case AGVState.Docking: return Color.LightYellow; default: return Color.WhiteSmoke; } } /// /// AGV 외곽 색상 (방사형 그라디언트) /// private Color GetAGVOuterColor(AGVState state) { switch (state) { case AGVState.Moving: return Color.DarkGreen; case AGVState.Charging: return Color.DarkBlue; case AGVState.Error: return Color.DarkRed; case AGVState.Docking: return Color.DarkOrange; default: return Color.DarkGray; } } /// /// AGV 내부 링 색상 /// private Color GetAGVInnerRingColor(AGVState state) { switch (state) { case AGVState.Moving: return Color.Green; case AGVState.Charging: return Color.Blue; case AGVState.Error: return Color.Red; case AGVState.Docking: return Color.Orange; default: return Color.Gray; } } /// /// AGV 중앙 표시등 색상 /// private Color GetStatusIndicatorColor(AGVState state) { switch (state) { case AGVState.Moving: return Color.Lime; case AGVState.Charging: return Color.Blue; case AGVState.Error: return Color.Red; case AGVState.Docking: return Color.Yellow; default: return Color.Gray; } } /// /// AGV 원형 센서 패턴 (방사형 배치) /// private void DrawCircularSensors(Graphics g, Point center, AGVState state) { var sensorSize = 3; var sensorColor = state == AGVState.Error ? Color.Red : Color.Silver; var radius = AGV_SIZE / 2 - 6; // 8방향으로 센서 배치 (45도씩) for (int i = 0; i < 8; i++) { double angle = i * Math.PI / 4; // 45도씩 int sensorX = (int)(center.X + radius * Math.Cos(angle)); int sensorY = (int)(center.Y + radius * Math.Sin(angle)); var sensorRect = new Rectangle( sensorX - sensorSize / 2, sensorY - sensorSize / 2, sensorSize, sensorSize ); using (var sensorBrush = new SolidBrush(sensorColor)) { g.FillEllipse(sensorBrush, sensorRect); g.DrawEllipse(new Pen(Color.Black, 1), sensorRect); } } // 내부 원형 패턴 (장식) var innerRadius = AGV_SIZE / 2 - 12; for (int i = 0; i < 4; i++) { double angle = i * Math.PI / 2 + Math.PI / 4; // 45도 오프셋된 4방향 int dotX = (int)(center.X + innerRadius * Math.Cos(angle)); int dotY = (int)(center.Y + innerRadius * Math.Sin(angle)); var dotRect = new Rectangle(dotX - 1, dotY - 1, 2, 2); using (var dotBrush = new SolidBrush(GetAGVInnerRingColor(state))) { g.FillEllipse(dotBrush, dotRect); } } } /// /// AGV 모니터 그리기 (리프트 반대편) /// private void DrawAGVMonitor(Graphics g, IAGV agv) { const int monitorWidth = 12; // 모니터 너비 (가로로 더 길게) const int monitorHeight = 24; // 모니터 높이 (세로는 줄임) const int monitorDistance = AGV_SIZE / 2 + 2; // AGV 본체에서 거리 (리프트와 동일) var currentPos = agv.CurrentPosition; var targetPos = agv.TargetPosition; var dockingDirection = agv.DockingDirection; var currentDirection = agv.CurrentDirection; // 리프트 방향 계산 (모니터는 정반대) Point fallbackTarget = targetPos ?? new Point(currentPos.X + 50, currentPos.Y); var liftInfo = AGVNavigationCore.Utils.LiftCalculator.CalculateLiftInfoWithPathPrediction( currentPos, fallbackTarget, currentDirection, _nodes); bool hasDirection = liftInfo != null; double monitorAngle = hasDirection ? liftInfo.AngleRadians + Math.PI : 0; // 리프트 반대 방향 (180도 회전) // Graphics 변환 설정 var oldTransform = g.Transform; var transform = oldTransform.Clone(); transform.Translate(currentPos.X, currentPos.Y); transform.Rotate((float)(monitorAngle * 180.0 / Math.PI)); g.Transform = transform; // 모니터 위치 (AGV 면에 붙도록) var monitorRect = new Rectangle( monitorDistance - monitorWidth / 2, -monitorHeight / 2, monitorWidth, monitorHeight ); if (hasDirection) { // 모니터 상태에 따른 색상 var monitorBackColor = GetMonitorBackColor(agv.CurrentState); var monitorBorderColor = Color.DarkGray; // 모니터 본체 (둥근 모서리) using (var monitorBrush = new SolidBrush(monitorBackColor)) using (var borderPen = new Pen(monitorBorderColor, 2)) { DrawRoundedRectangle(g, monitorBrush, borderPen, monitorRect, 2); } // 모니터 화면 (내부 사각형) var screenRect = new Rectangle( monitorRect.X + 2, monitorRect.Y + 2, monitorRect.Width - 4, monitorRect.Height - 4 ); using (var screenBrush = new SolidBrush(GetMonitorScreenColor(agv.CurrentState))) { g.FillRectangle(screenBrush, screenRect); } // 화면 패턴 (상태별) DrawMonitorScreen(g, screenRect, agv.CurrentState); // 모니터 스탠드 var standRect = new Rectangle( monitorDistance - 2, monitorHeight / 2 - 1, 4, 3 ); using (var standBrush = new SolidBrush(Color.Gray)) { g.FillRectangle(standBrush, standRect); } } else { // 방향을 모를 때 기본 모니터 (스카이블루로) using (var monitorBrush = new SolidBrush(Color.SkyBlue)) using (var borderPen = new Pen(Color.DarkGray, 2)) { DrawRoundedRectangle(g, monitorBrush, borderPen, monitorRect, 2); } } // Graphics 상태 복원 g.Transform = oldTransform; } /// /// 모니터 배경 색상 (스카이블루 계통으로 통일) /// private Color GetMonitorBackColor(AGVState state) { // 모든 상태에서 스카이블루 계통으로 통일하여 AGV와 구분 return Color.SkyBlue; } /// /// 모니터 화면 색상 (상태별로 구분) /// private Color GetMonitorScreenColor(AGVState state) { switch (state) { case AGVState.Moving: return Color.Navy; // 이동: 네이비 case AGVState.Charging: return Color.DarkBlue; // 충전: 다크블루 case AGVState.Error: return Color.DarkRed; // 에러: 다크레드 case AGVState.Docking: return Color.DarkGreen; // 도킹: 다크그린 default: return Color.DarkSlateGray; // 대기: 다크슬레이트그레이 } } /// /// 모니터 화면 패턴 그리기 /// private void DrawMonitorScreen(Graphics g, Rectangle screenRect, AGVState state) { using (var patternPen = new Pen(Color.White, 1)) { switch (state) { case AGVState.Moving: // 이동 중: 작은 점들 (데이터 표시) for (int i = 1; i < screenRect.Width; i += 3) { for (int j = 1; j < screenRect.Height; j += 3) { g.DrawRectangle(patternPen, screenRect.X + i, screenRect.Y + j, 1, 1); } } break; case AGVState.Charging: // 충전 중: 배터리 아이콘 var batteryRect = new Rectangle( screenRect.X + screenRect.Width / 2 - 3, screenRect.Y + screenRect.Height / 2 - 2, 6, 4 ); g.DrawRectangle(patternPen, batteryRect); g.DrawLine(patternPen, batteryRect.Right, batteryRect.Y + 1, batteryRect.Right, batteryRect.Bottom - 1); break; case AGVState.Error: // 에러: X 마크 g.DrawLine(patternPen, screenRect.X + 2, screenRect.Y + 2, screenRect.Right - 2, screenRect.Bottom - 2); g.DrawLine(patternPen, screenRect.Right - 2, screenRect.Y + 2, screenRect.X + 2, screenRect.Bottom - 2); break; case AGVState.Docking: // 도킹: 화살표 var centerX = screenRect.X + screenRect.Width / 2; var centerY = screenRect.Y + screenRect.Height / 2; g.DrawLine(patternPen, centerX - 4, centerY, centerX + 2, centerY); g.DrawLine(patternPen, centerX, centerY - 2, centerX + 2, centerY); g.DrawLine(patternPen, centerX, centerY + 2, centerX + 2, centerY); break; default: // 대기: 중앙 점 g.DrawRectangle(patternPen, screenRect.X + screenRect.Width / 2 - 1, screenRect.Y + screenRect.Height / 2 - 1, 2, 2); break; } } } 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 } }