using AGVNavigationCore.Models; using AGVNavigationCore.PathFinding; using AGVNavigationCore.PathFinding.Core; using System; using System.Collections.Generic; using System.Drawing; using System.Drawing.Drawing2D; using System.Linq; using System.Runtime.Remoting.Channels; using System.Windows.Forms; namespace AGVNavigationCore.Controls { public partial class UnifiedAGVCanvas { #region Paint Events private static int _paintCounter = 0; private void UnifiedAGVCanvas_Paint(object sender, PaintEventArgs e) { // 디버그: Paint 이벤트 실행 확인 if (_isDragging) { _paintCounter++; //System.Diagnostics.Debug.WriteLine($"Paint #{_paintCounter} 실행! 현재 노드 위치: ({_selectedNode?.Position.X}, {_selectedNode?.Position.Y})"); } var g = e.Graphics; // 🔥 배경색 그리기 (변환 행렬 적용 전에 전체 화면을 배경색으로 채움) g.Clear(this.BackColor); g.SmoothingMode = SmoothingMode.AntiAlias; g.InterpolationMode = InterpolationMode.High; // 변환 행렬 설정 (줌 및 팬) // 순서: Translate 먼저, Scale 나중 (Append로 순서 보장) var transform = new Matrix(); transform.Translate(_panOffset.X, _panOffset.Y); transform.Scale(_zoomFactor, _zoomFactor, System.Drawing.Drawing2D.MatrixOrder.Append); g.Transform = transform; try { // 그리드 그리기 if (_showGrid) { DrawGrid(g); } // 노드 연결선 그리기 (가장 먼저 - 텍스트가 가려지지 않게) DrawConnections(g); // 경로 그리기 DrawPaths(g); // 임시 연결선 그리기 (편집 모드) if (_canvasMode == CanvasMode.Edit && _isConnectionMode) { DrawTemporaryConnection(g); } // 노드 그리기 DrawMarkSensors(g); DrawImages(g); // 추가: 이미지 노드 DrawNodesOnly(g); // 드래그 고스트 그리기 (노드 위에 표시되도록 나중에 그리기) if (_isDragging && _selectedNode != null) { DrawDragGhost(g); } // 마그넷 방향 텍스트 그리기 (노드 위에 표시) DrawMagnetDirections(g); // AGV 그리기 DrawAGVs(g); // 노드 라벨 그리기 (가장 나중 - 선이 텍스트를 가리지 않게) DrawNodeLabels(g); DrawLabels(g); // 추가: 텍스트 라벨 } finally { g.Transform = new Matrix(); // 변환 행렬 리셋 } // UI 정보 그리기 (변환 없이) //if (_showGrid) DrawUIInfo(g); // 동기화 화면 그리기 (변환 없이, 최상위) if (_canvasMode == CanvasMode.Sync) { DrawSyncScreen(g); } //예측문자는 디버깅시에만 표시한다. if (string.IsNullOrEmpty(PredictMessage) == false) { g.DrawString(this.PredictMessage, this.Font, Brushes.White, 10, 100); } DrawSystemMessage(g); DrawAlertMessage(g); } private void DrawMagnetDirections(Graphics g) { if (_nodes == null) return; using (var font = new Font("Arial", 8, FontStyle.Bold)) using (var brushS = new SolidBrush(Color.Magenta)) using (var brushL = new SolidBrush(Color.Green)) using (var brushR = new SolidBrush(Color.Blue)) using (var brushBg = new SolidBrush(Color.FromArgb(180, 255, 255, 255))) { foreach (var node in _nodes) { if (node.MagnetDirections != null && node.MagnetDirections.Count > 0) { foreach (var kvp in node.MagnetDirections) { var targetId = kvp.Key; var dir = kvp.Value; var targetNode = _nodes.FirstOrDefault(n => n.Id == targetId); if (targetNode != null) { // 방향 텍스트 위치 계산 (출발 -> 도착 벡터의 일정 거리 지점) var start = node.Position; var end = targetNode.Position; var angle = Math.Atan2(end.Y - start.Y, end.X - start.X); // 박스(텍스트) 중심 위치: 약 40px 거리 var boxDist = 40; var boxX = start.X + boxDist * Math.Cos(angle); var boxY = start.Y + boxDist * Math.Sin(angle); string text = dir.ToString(); Color color = Color.Blue; if (dir == MagnetPosition.L) color = Color.LimeGreen; else if (dir == MagnetPosition.R) color = Color.Red; // 화살표 및 텍스트 설정 using (var arrowBrush = new SolidBrush(color)) using (var arrowPen = new Pen(color, 2)) // 두께 약간 증가 using (var textBrush = new SolidBrush(color)) { // 1. 화살표 그리기 (박스를 가로지르는 선) // 시작점: 노드 근처 (25px) // 끝점: 박스 너머 (55px) var arrowStartDist = 25; var arrowEndDist = 55; var pStart = new PointF((float)(start.X + arrowStartDist * Math.Cos(angle)), (float)(start.Y + arrowStartDist * Math.Sin(angle))); var pEnd = new PointF((float)(start.X + arrowEndDist * Math.Cos(angle)), (float)(start.Y + arrowEndDist * Math.Sin(angle))); // 화살표 선 그리기 g.DrawLine(arrowPen, pStart, pEnd); // 화살표 머리 그리기 (끝점에) var arrowSize = 6; var pHead1 = new PointF((float)(pEnd.X + arrowSize * Math.Cos(angle)), (float)(pEnd.Y + arrowSize * Math.Sin(angle))); // 뾰족한 끝 // 삼각형 머리 (채우기) var pBackL = new PointF((float)(pEnd.X + arrowSize * Math.Cos(angle + 2.5)), (float)(pEnd.Y + arrowSize * Math.Sin(angle + 2.5))); var pBackR = new PointF((float)(pEnd.X + arrowSize * Math.Cos(angle - 2.5)), (float)(pEnd.Y + arrowSize * Math.Sin(angle - 2.5))); // pHead1이 가장 먼 쪽이 되도록 조정 (pEnd가 삼각형의 뒷부분 중심이 되도록) // pEnd에서 시작해서 앞으로 나가는 삼각형 // pTip = pEnd + size * angle // pBackL = pEnd + size/2 * angle_back_L (약간 뒤로) // 현재 코드는 pEnd를 중심으로, pHead1이 앞, pBackL/R이 뒤... 가 아니라 // pHead1, pBackK, pBackR로 삼각형을 그림. // pHead1이 팁. g.FillPolygon(arrowBrush, new PointF[] { pHead1, pBackL, pBackR }); // 2. 텍스트 그리기 (화살표 위에 박스, 그 위에 텍스트) var textSize = g.MeasureString(text, font); var textPoint = new PointF((float)(boxX - textSize.Width / 2), (float)(boxY - textSize.Height / 2)); //편집모드에서만 글자를 표시한다. if (Mode == CanvasMode.Edit) { // 텍스트 배경 (반투명 - 선이 은은하게 보이도록 투명도 조절하거나, 가독성을 위해 불투명하게 처리) // 사용자가 "박스를 가로지르는" 느낌을 원했으므로 선이 보여야 함. 하지만 텍스트 가독성도 필요. // 배경을 아주 옅게 (Alpha 100정도) 처리하여 선이 보이게 함. using (var translucentBg = new SolidBrush(Color.FromArgb(120, 255, 255, 255))) { g.FillRectangle(translucentBg, textPoint.X - 1, textPoint.Y - 1, textSize.Width + 2, textSize.Height + 2); } g.DrawString(text, font, textBrush, textPoint); } } } } } } } } void DrawSystemMessage(Graphics g) { if (!showalertsystem || String.IsNullOrEmpty(this._systemmesage)) return; // 상단 중앙에 반투명 빨간색 배경 바 표시 int barHeight = 40; int barWidth = Math.Min(600, this.Width - 40); // 최대 600px, 좌우 여백 20px int barX = (this.Width - barWidth) / 2; int barY = 20; // 둥근 사각형 배경 using (var path = new GraphicsPath()) { int radius = 10; path.AddArc(barX, barY, radius * 2, radius * 2, 180, 90); path.AddArc(barX + barWidth - radius * 2, barY, radius * 2, radius * 2, 270, 90); path.AddArc(barX + barWidth - radius * 2, barY + barHeight - radius * 2, radius * 2, radius * 2, 0, 90); path.AddArc(barX, barY + barHeight - radius * 2, radius * 2, radius * 2, 90, 90); path.CloseFigure(); using (var brush = new SolidBrush(Color.FromArgb(200, 255, 69, 58))) // 진한 붉은색 (Apple Red 계열) { g.FillPath(brush, path); } using (var pen = new Pen(Color.FromArgb(255, 255, 255), 2)) { g.DrawPath(pen, path); } } // 텍스트 깜박임 효과 (배경은 유지하고 텍스트만 깜박임) using (var font = new Font("Malgun Gothic", 12, FontStyle.Bold)) using (var brush = new SolidBrush(Color.White)) { var textSize = g.MeasureString(_systemmesage, font); g.DrawString(_systemmesage, font, brush, barX + (barWidth - textSize.Width) / 2, barY + (barHeight - textSize.Height) / 2); } // 경고 아이콘 그리기 (왼쪽) // 간단한 느낌표 아이콘 int iconX = barX + 15; int iconY = barY + barHeight / 2; using (var brush = new SolidBrush(Color.White)) { g.FillEllipse(brush, iconX - 2, iconY - 8, 4, 16); // body g.FillEllipse(brush, iconX - 2, iconY + 10, 4, 4); // dot } } void DrawAlertMessage(Graphics g) { if (!showinfo || String.IsNullOrEmpty(this._infomessage)) return; // 상단 중앙에 반투명 빨간색 배경 바 표시 int barHeight = 40; int barWidth = Math.Min(600, this.Width - 40); // 최대 600px, 좌우 여백 20px int barX = (this.Width - barWidth) / 2; int barY = 20+50; // 둥근 사각형 배경 using (var path = new GraphicsPath()) { int radius = 10; path.AddArc(barX, barY, radius * 2, radius * 2, 180, 90); path.AddArc(barX + barWidth - radius * 2, barY, radius * 2, radius * 2, 270, 90); path.AddArc(barX + barWidth - radius * 2, barY + barHeight - radius * 2, radius * 2, radius * 2, 0, 90); path.AddArc(barX, barY + barHeight - radius * 2, radius * 2, radius * 2, 90, 90); path.CloseFigure(); using (var brush = new SolidBrush(Color.FromArgb(255, Color.Lime))) { g.FillPath(brush, path); } using (var pen = new Pen(Color.FromArgb(255, 255, 255), 2)) { g.DrawPath(pen, path); } } // 텍스트 깜박임 효과 (배경은 유지하고 텍스트만 깜박임) using (var font = new Font("Malgun Gothic", 12, FontStyle.Bold)) using (var brush = new SolidBrush(Color.Black)) { var textSize = g.MeasureString(_infomessage, font); g.DrawString(_infomessage, font, brush, barX + (barWidth - textSize.Width) / 2, barY + (barHeight - textSize.Height) / 2); } // 경고 아이콘 그리기 (왼쪽) // 간단한 느낌표 아이콘 int iconX = barX + 15; int iconY = barY + barHeight / 2; using (var brush = new SolidBrush(Color.Black)) { g.FillEllipse(brush, iconX - 2, iconY - 8, 4, 16); // body g.FillEllipse(brush, iconX - 2, iconY + 10, 4, 4); // dot } } private void DrawSyncScreen(Graphics g) { // 반투명 검은색 배경 using (var brush = new SolidBrush(Color.FromArgb(200, 0, 0, 0))) { g.FillRectangle(brush, this.ClientRectangle); } // 중앙에 메시지 표시 var center = new Point(Width / 2, Height / 2); // 메시지 폰트 using (var fontTitle = new Font("Malgun Gothic", 24, FontStyle.Bold)) using (var fontDetail = new Font("Malgun Gothic", 14)) using (var brushText = new SolidBrush(Color.White)) { // 메인 메시지 var sizeTitle = g.MeasureString(_syncMessage, fontTitle); g.DrawString(_syncMessage, fontTitle, brushText, center.X - sizeTitle.Width / 2, center.Y - sizeTitle.Height / 2 - 60); // 진행률 바 배경 int barWidth = 500; int barHeight = 30; int barX = center.X - barWidth / 2; int barY = center.Y + 10; using (var brushBarBg = new SolidBrush(Color.FromArgb(64, 64, 64))) { g.FillRectangle(brushBarBg, barX, barY, barWidth, barHeight); } g.DrawRectangle(Pens.Gray, barX, barY, barWidth, barHeight); // 진행률 바 채우기 if (_syncProgress > 0) { using (var brushProgress = new SolidBrush(Color.LimeGreen)) { int fillWidth = (int)((barWidth - 4) * _syncProgress); if (fillWidth > 0) g.FillRectangle(brushProgress, barX + 2, barY + 2, fillWidth, barHeight - 4); } } // 진행률 텍스트 string progressText = $"{(_syncProgress * 100):F0}%"; var sizeProgress = g.MeasureString(progressText, fontDetail); g.DrawString(progressText, fontDetail, brushText, center.X - sizeProgress.Width / 2, barY + 5); // 상세 메시지 if (!string.IsNullOrEmpty(_syncDetail)) { var sizeDetail = g.MeasureString(_syncDetail, fontDetail); g.DrawString(_syncDetail, fontDetail, brushText, center.X - sizeDetail.Width / 2, barY + barHeight + 20); } } } private void DrawGrid(Graphics g) { if (!_showGrid) return; var gridSize = (int)(GRID_SIZE * _zoomFactor); if (gridSize < 5) return; // 너무 작으면 그리지 않음 // 화면 전체를 덮는 월드 좌표 범위 계산 var topLeft = ScreenToWorld(new Point(0, 0)); var bottomRight = ScreenToWorld(new Point(Width, Height)); // 그리드 시작 위치 (월드 좌표에서 GRID_SIZE의 배수로 정렬) int startX = (topLeft.X / GRID_SIZE) * GRID_SIZE - GRID_SIZE; int startY = (topLeft.Y / GRID_SIZE) * GRID_SIZE - GRID_SIZE; int endX = bottomRight.X + GRID_SIZE; int endY = bottomRight.Y + GRID_SIZE; // 그리드 펜 (가는 선과 굵은 선) using (var thinPen = new Pen(Color.FromArgb(200, 200, 200), 1)) using (var thickPen = new Pen(Color.FromArgb(150, 150, 150), 1)) { // 수직선 그리기 for (int x = startX; x <= endX; x += GRID_SIZE) { var pen = (x % (GRID_SIZE * 5) == 0) ? thickPen : thinPen; g.DrawLine(pen, x, startY, x, endY); } // 수평선 그리기 for (int y = startY; y <= endY; y += GRID_SIZE) { var pen = (y % (GRID_SIZE * 5) == 0) ? thickPen : thinPen; g.DrawLine(pen, startX, y, endX, y); } } } private void DrawConnections(Graphics g) { if (_nodes == null) return; // 1. 일반 연결 그리기 foreach (var node in _nodes) { if (node.ConnectedMapNodes != null) { foreach (var targetNode in node.ConnectedMapNodes) { if (targetNode == null) continue; // 강조된 연결은 나중에 그리기 위해 건너뜀 if (IsConnectionHighlighted(node.Id, targetNode.Id)) continue; DrawConnection(g, node, targetNode); } } } // 1.1 강조된 연결 그리기 (항상 위에 표시되도록) if (_highlightedConnection.HasValue) { var n1 = _nodes.FirstOrDefault(n => n.Id == _highlightedConnection.Value.FromNodeId); var n2 = _nodes.FirstOrDefault(n => n.Id == _highlightedConnection.Value.ToNodeId); if (n1 != null && n2 != null) { DrawConnection(g, n1, n2); } } // 2. 마그넷 그리기 (별도 리스트 사용) if (_magnets != null) { foreach (var magnet in _magnets) { DrawMagnet(g, magnet); } } } private void DrawConnection(Graphics g, MapNode fromNode, MapNode toNode) { var startPoint = fromNode.Position; var endPoint = toNode.Position; // 강조된 연결인지 확인 bool isHighlighted = IsConnectionHighlighted(fromNode.Id, toNode.Id); // 펜 선택 Pen pen = isHighlighted ? _highlightedConnectionPen : _connectionPen; g.DrawLine(pen, startPoint, endPoint); } private void DrawMagnet(Graphics g, MapMagnet magnet) { if (magnet == null) return; // 마그넷 좌표 var startPoint = magnet.StartPoint; var endPoint = magnet.EndPoint; if (magnet.ControlPoint != null) { // Quadratic Bezier Curve (2차 베지어 곡선) // GDI+ DrawBezier는 Cubic(3차)이므로 2차 -> 3차 변환 필요 // QP0 = Start, QP1 = Control, QP2 = End // CP0 = QP0 // CP1 = QP0 + (2/3) * (QP1 - QP0) // CP2 = QP2 + (2/3) * (QP1 - QP2) // CP3 = QP2 float qp0x = startPoint.X; float qp0y = startPoint.Y; float qp1x = (float)magnet.ControlPoint.X; float qp1y = (float)magnet.ControlPoint.Y; float qp2x = endPoint.X; float qp2y = endPoint.Y; float cp1x = qp0x + (2.0f / 3.0f) * (qp1x - qp0x); float cp1y = qp0y + (2.0f / 3.0f) * (qp1y - qp0y); float cp2x = qp2x + (2.0f / 3.0f) * (qp1x - qp2x); float cp2y = qp2y + (2.0f / 3.0f) * (qp1y - qp2y); g.DrawBezier(_magnetPen, qp0x, qp0y, cp1x, cp1y, cp2x, cp2y, qp2x, qp2y); } else { // 직선 그리기 g.DrawLine(_magnetPen, startPoint, endPoint); } // 호버되거나 선택된 마그넷 강조 (선택: Red, 호버: Orange) bool isHovered = (magnet == _hoveredNode); bool isSelected = (magnet == _selectedNode); if (isHovered || isSelected) { Color highlightColor = isSelected ? Color.Red : Color.Orange; // 선택된 상태에서 호버되면? -> 선택 색상 우선 (Red) 또는 명확한 구분 필요 // 여기서는 선택이 더 중요하므로 Red 유지 using (var highlightPen = new Pen(highlightColor, 19) { StartCap = LineCap.Round, EndCap = LineCap.Round }) { if (magnet.ControlPoint != null) { // Bezier calculation duplicate float qp0x = startPoint.X; float qp0y = startPoint.Y; float qp1x = (float)magnet.ControlPoint.X; float qp1y = (float)magnet.ControlPoint.Y; float qp2x = endPoint.X; float qp2y = endPoint.Y; float cp1x = qp0x + (2.0f / 3.0f) * (qp1x - qp0x); float cp1y = qp0y + (2.0f / 3.0f) * (qp1y - qp0y); float cp2x = qp2x + (2.0f / 3.0f) * (qp1x - qp2x); float cp2y = qp2y + (2.0f / 3.0f) * (qp1y - qp2y); g.DrawBezier(highlightPen, qp0x, qp0y, cp1x, cp1y, cp2x, cp2y, qp2x, qp2y); } else { g.DrawLine(highlightPen, startPoint, endPoint); } } } // 선택된 마그넷 핸들 그리기 if (magnet == _selectedNode && _canvasMode == CanvasMode.Edit) { using (var handleBrush = new SolidBrush(Color.White)) using (var handlePen = new Pen(Color.Black, 1)) { float size = HANDLE_SIZE / _zoomFactor; float half = size / 2; // 시작점, 끝점 핸들 g.FillRectangle(handleBrush, startPoint.X - half, startPoint.Y - half, size, size); g.DrawRectangle(handlePen, startPoint.X - half, startPoint.Y - half, size, size); g.FillRectangle(handleBrush, endPoint.X - half, endPoint.Y - half, size, size); g.DrawRectangle(handlePen, endPoint.X - half, endPoint.Y - half, size, size); // 제어점 핸들 (곡선일 경우) if (magnet.ControlPoint != null) { var cp = magnet.ControlPoint; g.FillRectangle(handleBrush, (float)cp.X - half, (float)cp.Y - half, size, size); g.DrawRectangle(handlePen, (float)cp.X - half, (float)cp.Y - half, size, size); } } } } private void DrawMarkSensors(Graphics g) { if (_marks == null) return; // _marks 리스트 사용 int sensorSize = 12; // 크기 설정 foreach (var mark in _marks) { int lineLength = (int)mark.Length; // 저장된 길이 사용 int halfLength = lineLength / 2; Point p = mark.Position; double radians = mark.Rotation * Math.PI / 180.0; // 회전 각도에 따른 시점과 종점 계산 // 마크는 직선 형태로 표시됨 int dx = (int)(halfLength * Math.Cos(radians)); int dy = (int)(halfLength * Math.Sin(radians)); Point p1 = new Point(p.X - dx, p.Y - dy); Point p2 = new Point(p.X + dx, p.Y + dy); g.DrawLine(_markPen, p1, p2); // 호버된 마크 강조 if (mark == _hoveredNode) { using (var highlightPen = new Pen(Color.Orange, 5)) { g.DrawLine(highlightPen, p1, p2); } } // 선택된 마크 핸들 그리기 if (mark == _selectedNode && _canvasMode == CanvasMode.Edit) { using (var handleBrush = new SolidBrush(Color.White)) using (var handlePen = new Pen(Color.Black, 1)) { float size = HANDLE_SIZE / _zoomFactor; float half = size / 2; g.FillRectangle(handleBrush, p1.X - half, p1.Y - half, size, size); g.DrawRectangle(handlePen, p1.X - half, p1.Y - half, size, size); g.FillRectangle(handleBrush, p2.X - half, p2.Y - half, size, size); g.DrawRectangle(handlePen, p2.X - half, p2.Y - half, size, size); } } } } /// /// 연결이 강조 표시되어야 하는지 확인 /// private bool IsConnectionHighlighted(string nodeId1, string nodeId2) { if (!_highlightedConnection.HasValue) return false; var highlighted = _highlightedConnection.Value; // 사전순으로 정렬하여 비교 (연결이 단일 방향으로 저장되므로) string from, to; if (string.Compare(nodeId1, nodeId2, StringComparison.Ordinal) <= 0) { from = nodeId1; to = nodeId2; } else { from = nodeId2; to = nodeId1; } return highlighted.FromNodeId == from && highlighted.ToNodeId == to; } private void DrawDirectionArrow(Graphics g, Point point, double angle, AgvDirection direction) { // 정삼각형 화살표 - 크기 축소 (8 픽셀) var arrowSize = 8; // 정삼각형의 3개 점 계산 // 끝점 (방향 가르키는 점) var arrowTipPoint = new Point( (int)(point.X + arrowSize * Math.Cos(angle)), (int)(point.Y + arrowSize * Math.Sin(angle)) ); // 좌측 점 (120도 차이) var arrowPoint1 = new Point( (int)(point.X + arrowSize * Math.Cos(angle + 2 * Math.PI / 3)), (int)(point.Y + arrowSize * Math.Sin(angle + 2 * Math.PI / 3)) ); // 우측 점 (240도 차이) var arrowPoint2 = new Point( (int)(point.X + arrowSize * Math.Cos(angle + 4 * Math.PI / 3)), (int)(point.Y + arrowSize * Math.Sin(angle + 4 * Math.PI / 3)) ); var arrowColor = direction == AgvDirection.Forward ? Color.Blue : Color.Yellow; var arrowBrush = new SolidBrush(arrowColor); // 정삼각형으로 화살표 그리기 (내부 채움) var trianglePoints = new Point[] { arrowTipPoint, arrowPoint1, arrowPoint2 }; g.FillPolygon(arrowBrush, trianglePoints); // 윤곽선 그리기 var arrowPen = new Pen(arrowColor, 1f); g.DrawPolygon(arrowPen, trianglePoints); arrowBrush.Dispose(); 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); // 경로 내 교차로 강조 표시 HighlightJunctionsInPath(g, _currentPath); // AGVPathResult의 모터방향 정보가 있다면 향상된 경로 그리기 // 현재는 기본 PathResult를 사용하므로 향후 AGVPathResult로 업그레이드 시 활성화 // TODO: AGVPathfinder 사용시 AGVPathResult로 업그레이드 } } private void DrawPath(Graphics g, AGVPathResult path, Color color) { if (path?.Path == null || path.Path.Count < 2) return; // 현재 선택된 경로인 경우 보라색 + 투명도 50% + 선 두께 2배 Color lineColor = color; float lineThickness = 4; if (color == Color.Purple) { // 투명도 50% (Alpha = 128 / 255) lineColor = Color.FromArgb(128, color.R, color.G, color.B); lineThickness = 8; // 2배 증가 } var pathPen = new Pen(lineColor, lineThickness) { DashStyle = DashStyle.Dash }; // 왕복 경로 감지: 방문한 노드 추적 var visitedSegments = new Dictionary(); for (int i = 0; i < path.Path.Count - 1; i++) { var currentNode = path.Path[i]; var nextNode = path.Path[i + 1]; if (currentNode == null || nextNode == null) continue; var currentNodeId = currentNode.Id; var nextNodeId = nextNode.Id; // 왕복 구간 키 생성 (양방향 모두 같은 키) var segmentKey = string.Compare(currentNodeId, nextNodeId) < 0 ? $"{currentNodeId}_{nextNodeId}" : $"{nextNodeId}_{currentNodeId}"; if (!visitedSegments.ContainsKey(segmentKey)) visitedSegments[segmentKey] = 0; visitedSegments[segmentKey]++; if (currentNode != null && nextNode != null) { // 왕복 경로면 더 진한 색상으로 표시 if (visitedSegments[segmentKey] > 1 && color == Color.Purple) { // 왕복 경로는 투명도를 낮춤 (더 진하게) var darkPathColor = Color.FromArgb(200, color.R, color.G, color.B); var darkPathPen = new Pen(darkPathColor, lineThickness) { DashStyle = DashStyle.Dash }; g.DrawLine(darkPathPen, currentNode.Position, nextNode.Position); darkPathPen.Dispose(); } else { // 일반 경로 선 그리기 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); // 상세 경로 정보가 있으면 해당 방향 사용, 없으면 Forward AgvDirection arrowDir = AgvDirection.Forward; if (path.DetailedPath != null && i < path.DetailedPath.Count) { arrowDir = path.DetailedPath[i].MotorDirection; } DrawDirectionArrow(g, midPoint, angle, arrowDir); } } pathPen.Dispose(); } /// /// 경로에 포함된 교차로(3개 이상의 노드가 연결된 노드)를 파란색으로 강조 표시 /// /// /// 경로에 포함된 특정 노드(Gateway 등)를 강조 표시 /// HighlightNodeId가 설정된 경우 해당 노드만 표시하고, 없으면 기존대로 교차로 표시(또는 표시 안함) /// 사용자가 "교차로 대신 게이트웨이만 강조"를 원하므로 우선순위 적용 /// private void HighlightJunctionsInPath(Graphics g, AGVPathResult path) { if (path?.Path == null || _nodes == null || _nodes.Count == 0) return; // 1. HighlightNodeId가 설정되어 있다면 해당 노드만 강조 if (!string.IsNullOrEmpty(HighlightNodeId)) { var targetNode = path.Path.FirstOrDefault(n => n.Id == HighlightNodeId); if (targetNode != null) { DrawJunctionHighlight(g, targetNode, true); // true = Gateway 강조 색상 사용 } // HighlightNodeId가 설정된 경우 다른 교차로는 표시하지 않음 (사용자 요청) return; } // 2. 설정이 없다면 기존 로직 (교차로 표시) 유지 여부 결정 // 사용자가 "게이트웨이만 강조해줘"라고 했으므로, 혼란을 피하기 위해 // HighlightNodeId가 없을 때는 아무것도 표시하지 않거나, 필요한 경우 복구. // 현재는 사용자 요청에 따라 Gateway 지정이 안된 경우(일반 경로)에는 교차로 강조를 끄는 것이 맞아 보임. // 하지만 일반 주행시에도 교차로 정보가 필요할 수 있으니 일단 둡니다. // 단, Gateway 로직을 타는 경우(HighlightNodeId가 Set됨)에는 위에서 return 되므로 OK. /* const int JUNCTION_CONNECTIONS = 3; foreach (var node in path.Path) { if (node == null) continue; if (node.ConnectedMapNodes != null && node.ConnectedMapNodes.Count >= JUNCTION_CONNECTIONS) { DrawJunctionHighlight(g, node, false); } } */ } /// /// 노드 강조 표시 /// private void DrawJunctionHighlight(Graphics g, MapNode junctionNode, bool isGateway) { if (junctionNode == null) return; int radius = isGateway ? 23 : 18; // 게이트웨이는 좀 더 크게 // 색상 결정: Gateway=진한 주황/골드, 일반 교차로=기존 파랑 Color fillColor = isGateway ? Color.FromArgb(100, 255, 140, 0) : Color.FromArgb(80, 70, 130, 200); Color penColor = isGateway ? Color.OrangeRed : Color.FromArgb(150, 100, 150, 220); using (var highlightBrush = new SolidBrush(fillColor)) using (var highlightPen = new Pen(penColor, 3)) { g.FillEllipse( highlightBrush, junctionNode.Position.X - radius, junctionNode.Position.Y - radius, radius * 2, radius * 2 ); // 테두리 점선 효과 (Gateway 인 경우) if (isGateway) highlightPen.DashStyle = DashStyle.Dot; g.DrawEllipse( highlightPen, junctionNode.Position.X - radius, junctionNode.Position.Y - radius, radius * 2, radius * 2 ); } } /// /// 교차로 라벨을 표시 (선택사항) /// private void DrawJunctionLabel(Graphics g, MapNode junctionNode) { if (junctionNode == null) return; using (var font = new Font("Arial", 9, FontStyle.Bold)) using (var brush = new SolidBrush(Color.Blue)) { var text = "교차로"; var textSize = g.MeasureString(text, font); // 노드 위쪽에 라벨 표시 var labelX = junctionNode.Position.X - textSize.Width / 2; var labelY = junctionNode.Position.Y - 35; // 배경 박스 그리기 using (var bgBrush = new SolidBrush(Color.FromArgb(220, 255, 255, 200))) { g.FillRectangle( bgBrush, labelX - 3, labelY - 3, textSize.Width + 6, textSize.Height + 6 ); } g.DrawString(text, font, brush, labelX, labelY); } } /// /// 드래그 고스트 그리기 - 원래 위치에 반투명 노드 표시 /// private void DrawDragGhost(Graphics g) { if (!_isDragging) return; if (_selectedNode != null) { // 반투명 효과를 위한 브러시 생성 Brush ghostBrush = new SolidBrush(Color.FromArgb(120, 200, 200, 200)); // 반투명 회색 // 고스트 노드 그리기 switch (_selectedNode.Type) { case NodeType.Normal: var item = _selectedNode as MapNode; if (item.StationType == StationType.Charger) DrawTriangleGhost(g, ghostBrush); else DrawPentagonGhost(g, ghostBrush); break; case NodeType.Label: DrawLabelGhost(g, SelectedLabel); break; case NodeType.Image: DrawImageGhost(g, SelectedImage); break; default: //mark, magnet DrawCircleGhost(g, ghostBrush); break; } ghostBrush?.Dispose(); } } private void DrawCircleGhost(Graphics g, Brush ghostBrush) { var rect = new Rectangle( _dragStartPosition.X - NODE_RADIUS, _dragStartPosition.Y - NODE_RADIUS, NODE_SIZE, NODE_SIZE ); g.FillEllipse(ghostBrush, rect); // 회색 점선 테두리 g.DrawEllipse(new Pen(Color.FromArgb(180, 128, 128, 128), 2) { DashStyle = DashStyle.Dash }, rect); // 빨간색 외곽 테두리 (디버깅용 - 고스트가 확실히 보이도록) var outerRect = new Rectangle(rect.X - 2, rect.Y - 2, rect.Width + 4, rect.Height + 4); g.DrawEllipse(new Pen(Color.FromArgb(200, 255, 0, 0), 1), outerRect); } private void DrawPentagonGhost(Graphics g, Brush ghostBrush) { var points = new Point[5]; for (int i = 0; i < 5; i++) { var angle = (Math.PI * 2 * i / 5) - Math.PI / 2; points[i] = new Point( (int)(_dragStartPosition.X + NODE_RADIUS * Math.Cos(angle)), (int)(_dragStartPosition.Y + NODE_RADIUS * Math.Sin(angle)) ); } g.FillPolygon(ghostBrush, points); // 회색 점선 테두리 g.DrawPolygon(new Pen(Color.FromArgb(180, 128, 128, 128), 2) { DashStyle = DashStyle.Dash }, points); // 빨간색 외곽 테두리 (디버깅용) var outerPoints = new Point[5]; for (int i = 0; i < 5; i++) { var angle = (Math.PI * 2 * i / 5) - Math.PI / 2; outerPoints[i] = new Point( (int)(_dragStartPosition.X + (NODE_RADIUS + 3) * Math.Cos(angle)), (int)(_dragStartPosition.Y + (NODE_RADIUS + 3) * Math.Sin(angle)) ); } g.DrawPolygon(new Pen(Color.FromArgb(200, 255, 0, 0), 1), outerPoints); } private void DrawTriangleGhost(Graphics g, Brush ghostBrush) { var points = new Point[3]; for (int i = 0; i < 3; i++) { var angle = (Math.PI * 2 * i / 3) - Math.PI / 2; points[i] = new Point( (int)(_dragStartPosition.X + NODE_RADIUS * Math.Cos(angle)), (int)(_dragStartPosition.Y + NODE_RADIUS * Math.Sin(angle)) ); } g.FillPolygon(ghostBrush, points); // 회색 점선 테두리 g.DrawPolygon(new Pen(Color.FromArgb(180, 128, 128, 128), 2) { DashStyle = DashStyle.Dash }, points); // 빨간색 외곽 테두리 (디버깅용) var outerPoints = new Point[3]; for (int i = 0; i < 3; i++) { var angle = (Math.PI * 2 * i / 3) - Math.PI / 2; outerPoints[i] = new Point( (int)(_dragStartPosition.X + (NODE_RADIUS + 3) * Math.Cos(angle)), (int)(_dragStartPosition.Y + (NODE_RADIUS + 3) * Math.Sin(angle)) ); } g.DrawPolygon(new Pen(Color.FromArgb(200, 255, 0, 0), 1), outerPoints); } private void DrawLabelGhost(Graphics g, MapLabel label) { var text = string.IsNullOrEmpty(label.Text) ? label.Id : label.Text; using (var font = new Font(label.FontFamily, label.FontSize, label.FontStyle)) using (var textBrush = new SolidBrush(Color.FromArgb(120, label.ForeColor))) { var textSize = g.MeasureString(text, font); var textPoint = new Point( (int)(_dragStartPosition.X - textSize.Width / 2), (int)(_dragStartPosition.Y - textSize.Height / 2) ); if (label.BackColor != Color.Transparent) { using (var backgroundBrush = new SolidBrush(Color.FromArgb(120, label.BackColor))) { var backgroundRect = new Rectangle( textPoint.X - label.Padding, textPoint.Y - label.Padding, (int)textSize.Width + (label.Padding * 2), (int)textSize.Height + (label.Padding * 2) ); g.FillRectangle(backgroundBrush, backgroundRect); g.DrawRectangle(new Pen(Color.FromArgb(180, 128, 128, 128), 2) { DashStyle = DashStyle.Dash }, backgroundRect); } } g.DrawString(text, font, textBrush, textPoint); // 배경이 없어도 테두리 표시 if (label.BackColor != Color.Transparent) { var borderRect = new Rectangle( textPoint.X - 2, textPoint.Y - 2, (int)textSize.Width + 4, (int)textSize.Height + 4 ); g.DrawRectangle(new Pen(Color.FromArgb(180, 128, 128, 128), 2) { DashStyle = DashStyle.Dash }, borderRect); } // 빨간색 외곽 테두리 (디버깅용) var outerRect = new Rectangle( textPoint.X - 4, textPoint.Y - 4, (int)textSize.Width + 8, (int)textSize.Height + 8 ); g.DrawRectangle(new Pen(Color.FromArgb(200, 255, 0, 0), 1), outerRect); } } private void DrawImageGhost(Graphics g, MapImage image) { var displaySize = image.GetDisplaySize(); if (displaySize.IsEmpty) displaySize = new Size(50, 50); var imageRect = new Rectangle( _dragStartPosition.X - displaySize.Width / 2, _dragStartPosition.Y - displaySize.Height / 2, displaySize.Width, displaySize.Height ); // 반투명 회색 사각형 using (var ghostBrush = new SolidBrush(Color.FromArgb(120, 200, 200, 200))) { g.FillRectangle(ghostBrush, imageRect); g.DrawRectangle(new Pen(Color.FromArgb(180, 128, 128, 128), 2) { DashStyle = DashStyle.Dash }, imageRect); } // 빨간색 외곽 테두리 (디버깅용) var outerRect = new Rectangle(imageRect.X - 2, imageRect.Y - 2, imageRect.Width + 4, imageRect.Height + 4); g.DrawRectangle(new Pen(Color.FromArgb(200, 255, 0, 0), 1), outerRect); } private void DrawNodeLabels(Graphics g) { if (_nodes == null) return; foreach (var node in _nodes) { // 일반 노드 라벨 그리기 DrawNodeLabel(g, node); } } private void DrawNodesOnly(Graphics g) { if (_nodes == null) return; foreach (var node in _nodes) { var brush = GetNodeBrush(node); switch (node.StationType) { case StationType.Loader: case StationType.Cleaner: case StationType.Plating: case StationType.Buffer: DrawPentagonNodeShape(g, node, brush); break; case StationType.Charger: DrawTriangleNodeShape(g, node, brush); break; case StationType.Limit: DrawRectangleNodeShape(g, node, brush); break; default: DrawCircleNodeShape(g, node, brush); break; } } } private void DrawRectangleNodeShape(Graphics g, MapNode node, Brush brush) { // 드래그 중인 노드는 약간 크게 그리기 bool isDraggingThisNode = _isDragging && node == _selectedNode; int sizeAdjustment = isDraggingThisNode ? 4 : 0; var rect = new Rectangle( node.Position.X - NODE_RADIUS - sizeAdjustment, node.Position.Y - NODE_RADIUS - sizeAdjustment, NODE_SIZE + sizeAdjustment * 2, NODE_SIZE + sizeAdjustment * 2 ); // 드래그 중인 노드의 그림자 효과 if (isDraggingThisNode) { var shadowRect = new Rectangle(rect.X + 3, rect.Y + 3, rect.Width, rect.Height); using (var shadowBrush = new SolidBrush(Color.FromArgb(100, 0, 0, 0))) { g.FillRectangle(shadowBrush, shadowRect); } } // 노드 그리기 g.FillRectangle(brush, rect); g.DrawRectangle(Pens.Black, rect); // 드래그 중인 노드 강조 (가장 강력한 효과) if (isDraggingThisNode) { // 청록색 두꺼운 테두리 g.DrawRectangle(new Pen(Color.Cyan, 3), rect); // 펄스 효과 var pulseRect = new Rectangle(rect.X - 4, rect.Y - 4, rect.Width + 8, rect.Height + 8); g.DrawRectangle(new Pen(Color.FromArgb(150, 0, 255, 255), 2) { DashStyle = DashStyle.Dash }, pulseRect); } // 선택된 노드 강조 (단일 또는 다중) else if (node == _selectedNode || (_selectedNodes != null && _selectedNodes.Contains(node))) { g.DrawRectangle(_selectedNodePen, rect); } // 목적지 노드 강조 if (node == _destinationNode) { // 금색 테두리로 목적지 강조 g.DrawRectangle(_destinationNodePen, rect); // 펄싱 효과를 위한 추가 원 그리기 var pulseRect = new Rectangle(rect.X - 3, rect.Y - 3, rect.Width + 6, rect.Height + 6); g.DrawRectangle(new Pen(Color.Gold, 2) { DashStyle = DashStyle.Dash }, pulseRect); } // 호버된 노드 강조 (드래그 중이 아닐 때만) if (node == _hoveredNode && !isDraggingThisNode) { var hoverRect = new Rectangle(rect.X - 2, rect.Y - 2, rect.Width + 4, rect.Height + 4); g.DrawRectangle(new Pen(Color.Orange, 2), hoverRect); } // RFID 중복 노드 표시 (빨간 X자) if (_duplicateRfidNodes.Contains(node.Id)) { DrawDuplicateRfidMarker(g, node); } // CanCross 가능 노드 표시 (교차지점으로 사용 가능) if (node.DisableCross == true) { var crossRect = new Rectangle(rect.X - 3, rect.Y - 3, rect.Width + 6, rect.Height + 6); g.DrawRectangle(new Pen(Color.DeepSkyBlue, 3), crossRect); } g.DrawLine(Pens.Black, rect.X, rect.Y, rect.Right, rect.Bottom); g.DrawLine(Pens.Black, rect.Right, rect.Top, rect.X, rect.Bottom); } private void DrawCircleNodeShape(Graphics g, MapNode node, Brush brush) { // 드래그 중인 노드는 약간 크게 그리기 bool isDraggingThisNode = _isDragging && node == _selectedNode; int sizeAdjustment = isDraggingThisNode ? 4 : 0; var rect = new Rectangle( node.Position.X - NODE_RADIUS - sizeAdjustment, node.Position.Y - NODE_RADIUS - sizeAdjustment, NODE_SIZE + sizeAdjustment * 2, NODE_SIZE + sizeAdjustment * 2 ); // 드래그 중인 노드의 그림자 효과 if (isDraggingThisNode) { var shadowRect = new Rectangle(rect.X + 3, rect.Y + 3, rect.Width, rect.Height); using (var shadowBrush = new SolidBrush(Color.FromArgb(100, 0, 0, 0))) { g.FillEllipse(shadowBrush, shadowRect); } } // 노드 그리기 g.FillEllipse(brush, rect); g.DrawEllipse(Pens.Black, rect); // 드래그 중인 노드 강조 (가장 강력한 효과) if (isDraggingThisNode) { // 청록색 두꺼운 테두리 g.DrawEllipse(new Pen(Color.Cyan, 3), rect); // 펄스 효과 var pulseRect = new Rectangle(rect.X - 4, rect.Y - 4, rect.Width + 8, rect.Height + 8); g.DrawEllipse(new Pen(Color.FromArgb(150, 0, 255, 255), 2) { DashStyle = DashStyle.Dash }, pulseRect); } // 선택된 노드 강조 (단일 또는 다중) else if (node == _selectedNode || (_selectedNodes != null && _selectedNodes.Contains(node))) { 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 && !isDraggingThisNode) { var hoverRect = new Rectangle(rect.X - 2, rect.Y - 2, rect.Width + 4, rect.Height + 4); g.DrawEllipse(new Pen(Color.Orange, 2), hoverRect); } // RFID 중복 노드 표시 (빨간 X자) if (_duplicateRfidNodes.Contains(node.Id)) { DrawDuplicateRfidMarker(g, node); } // CanCross 가능 노드 표시 (교차지점으로 사용 가능) if (node.DisableCross == true) { var crossRect = new Rectangle(rect.X - 3, rect.Y - 3, rect.Width + 6, rect.Height + 6); g.DrawEllipse(new Pen(Color.DeepSkyBlue, 3), crossRect); } } private void DrawPentagonNodeShape(Graphics g, MapNode node, Brush brush) { // 드래그 중인 노드는 약간 크게 그리기 bool isDraggingThisNode = _isDragging && node == _selectedNode; int radiusAdjustment = isDraggingThisNode ? 4 : 0; var radius = NODE_RADIUS + radiusAdjustment; 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)) ); } // 드래그 중인 노드의 그림자 효과 if (isDraggingThisNode) { var shadowPoints = new Point[5]; for (int i = 0; i < 5; i++) { shadowPoints[i] = new Point(points[i].X + 3, points[i].Y + 3); } using (var shadowBrush = new SolidBrush(Color.FromArgb(100, 0, 0, 0))) { g.FillPolygon(shadowBrush, shadowPoints); } } // 5각형 그리기 g.FillPolygon(brush, points); g.DrawPolygon(Pens.Black, points); // 드래그 중인 노드 강조 (가장 강력한 효과) if (isDraggingThisNode) { // 청록색 두꺼운 테두리 g.DrawPolygon(new Pen(Color.Cyan, 3), 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 + 5) * Math.Cos(angle)), (int)(center.Y + (radius + 5) * Math.Sin(angle)) ); } g.DrawPolygon(new Pen(Color.FromArgb(150, 0, 255, 255), 2) { DashStyle = DashStyle.Dash }, pulsePoints); } // 선택된 노드 강조 else 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 && !isDraggingThisNode) { // 확장된 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); } // RFID 중복 노드 표시 (빨간 X자) if (_duplicateRfidNodes.Contains(node.Id)) { DrawDuplicateRfidMarker(g, node); } // CanCross 가능 노드 표시 (교차지점으로 사용 가능) if (node.DisableCross == false) { var crossPoints = new Point[5]; for (int i = 0; i < 5; i++) { var angle = (Math.PI * 2 * i / 5) - Math.PI / 2; crossPoints[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, 3), crossPoints); } } private void DrawTriangleNodeShape(Graphics g, MapNode node, Brush brush) { // 드래그 중인 노드는 약간 크게 그리기 bool isDraggingThisNode = _isDragging && node == _selectedNode; int radiusAdjustment = isDraggingThisNode ? 4 : 0; var radius = NODE_RADIUS + radiusAdjustment; 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)) ); } // 드래그 중인 노드의 그림자 효과 if (isDraggingThisNode) { var shadowPoints = new Point[3]; for (int i = 0; i < 3; i++) { shadowPoints[i] = new Point(points[i].X + 3, points[i].Y + 3); } using (var shadowBrush = new SolidBrush(Color.FromArgb(100, 0, 0, 0))) { g.FillPolygon(shadowBrush, shadowPoints); } } // 삼각형 그리기 g.FillPolygon(brush, points); g.DrawPolygon(Pens.Black, points); // 드래그 중인 노드 강조 (가장 강력한 효과) if (isDraggingThisNode) { // 청록색 두꺼운 테두리 g.DrawPolygon(new Pen(Color.Cyan, 3), 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 + 5) * Math.Cos(angle)), (int)(center.Y + (radius + 5) * Math.Sin(angle)) ); } g.DrawPolygon(new Pen(Color.FromArgb(150, 0, 255, 255), 2) { DashStyle = DashStyle.Dash }, pulsePoints); } // 선택된 노드 강조 else 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 && !isDraggingThisNode) { // 확장된 삼각형 계산 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); } // RFID 중복 노드 표시 (빨간 X자) if (_duplicateRfidNodes.Contains(node.Id)) { DrawDuplicateRfidMarker(g, node); } // CanCross 가능 노드 표시 (교차지점으로 사용 가능) if (node.DisableCross == false) { var crossPoints = new Point[3]; for (int i = 0; i < 3; i++) { var angle = (Math.PI * 2 * i / 3) - Math.PI / 2; crossPoints[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, 3), crossPoints); } } private void DrawNodeLabel(Graphics g, MapNode node) { Color textColor = Color.White; // 위쪽에 표시할 이름 (노드의 Name 속성) string TopIDText = node.HasRfid() ? node.RfidId.ToString("0000") : $"[{node.Id}]"; // 아래쪽에 표시할 값 (RFID 우선, 없으면 노드ID) string BottomLabelText = node.Text; // 🔥 노드의 폰트 설정 사용 (0 이하일 경우 기본값 7.0f 사용) var topFont = new Font("Arial", 9, FontStyle.Bold); var btmFont = new Font("Arial", node.NodeTextFontSize, FontStyle.Bold); // 메인 텍스트 크기 측정 var TopSize = g.MeasureString(TopIDText, topFont); var BtmSize = g.MeasureString(BottomLabelText, btmFont); // 메인 텍스트 위치 (RFID는 노드 위쪽) var topPoint = new Point( (int)(node.Position.X - TopSize.Width / 2), (int)(node.Position.Y - NODE_RADIUS - TopSize.Height - 2) ); // 설명 텍스트 위치 (설명은 노드 아래쪽) var btmPoint = new Point( (int)(node.Position.X - BtmSize.Width / 2), (int)(node.Position.Y + NODE_RADIUS + 2) ); // 설명 텍스트 그리기 (설명이 있는 경우에만) if (!string.IsNullOrEmpty(BottomLabelText)) { // 🔥 노드의 말풍선 글자색 사용 (NameBubbleForeColor) Color fgColor = Color.Black; Color bgColor = Color.White; switch (node.StationType) { case StationType.Charger: fgColor = Color.White; bgColor = Color.Tomato; break; case StationType.Buffer: fgColor = Color.Black; bgColor = Color.White; break; case StationType.Plating: fgColor = Color.Black; bgColor = Color.DeepSkyBlue; break; case StationType.Loader: case StationType.Cleaner: fgColor = Color.Black; bgColor = Color.Gold; break; default: fgColor = Color.Black; break; } var rectpaddingx = 4; var rectpaddingy = 2; var roundRect = new Rectangle((int)(btmPoint.X - rectpaddingx), (int)(btmPoint.Y), (int)BtmSize.Width + rectpaddingx * 2, (int)BtmSize.Height + rectpaddingy * 2); // 라운드 사각형 그리기 (노드 이름 말풍선 배경색 사용) using (var backgroundBrush = new SolidBrush(bgColor)) { DrawRoundedRectangle(g, backgroundBrush, roundRect, 3); // 모서리 반지름 3px } // 라운드 사각형 테두리 그리기 (진한 빨간색) using (var borderPen = new Pen(Color.DimGray, 1)) { DrawRoundedRectangleBorder(g, borderPen, roundRect, 3); } using (var descBrush = new SolidBrush(node.NodeTextForeColor)) { g.DrawString(BottomLabelText, btmFont, descBrush, roundRect, new StringFormat { Alignment = StringAlignment.Center, LineAlignment = StringAlignment.Center, }); } } // 메인 텍스트 그리기 (RFID 중복인 경우 특별 처리) if (node.HasRfid() && _duplicateRfidNodes.Contains(node.Id)) { // 중복 RFID 노드: 빨간 배경의 라운드 사각형 DrawDuplicateRfidLabel(g, TopIDText, topPoint, topFont); } else { // 일반 텍스트 그리기 using (var textBrush = new SolidBrush(textColor)) { g.DrawString(TopIDText, topFont, textBrush, topPoint); } } topFont.Dispose(); btmFont.Dispose(); } private void DrawLabels(Graphics g) { if (_labels == null) return; foreach (var label in _labels) { DrawLabel(g, label); } } private void DrawImages(Graphics g) { if (_images == null) return; foreach (var image in _images) { DrawImage(g, image); } } private void DrawLabel(Graphics g, MapLabel label) { // 드래그 중인 라벨 확인 (TODO: NodeBase 선택/드래그 로직 통합 시 수정 필요) bool isDraggingThisLabel = _isDragging && label == SelectedLabel; var text = string.IsNullOrEmpty(label.Text) ? label.Id : label.Text; // 폰트 설정 using (var font = new Font(label.FontFamily, label.FontSize, label.FontStyle)) using (var textBrush = new SolidBrush(label.ForeColor)) // MapLabel.ForeColor (NodeTextForeColor -> ForeColor) { // 텍스트 크기 측정 var textSize = g.MeasureString(text, font); var textPoint = new Point( (int)(label.Position.X - textSize.Width / 2), (int)(label.Position.Y - textSize.Height / 2) ); // 드래그 중일 때 그림자 효과 if (isDraggingThisLabel) { var shadowPoint = new Point(textPoint.X + 3, textPoint.Y + 3); using (var shadowBrush = new SolidBrush(Color.FromArgb(100, 0, 0, 0))) { g.DrawString(text, font, shadowBrush, shadowPoint); } } // 배경 그리기 (설정된 경우) if (label.BackColor != Color.Transparent) { using (var backgroundBrush = new SolidBrush(label.BackColor)) { var backgroundRect = new Rectangle( textPoint.X - label.Padding, textPoint.Y - label.Padding, (int)textSize.Width + (label.Padding * 2), (int)textSize.Height + (label.Padding * 2) ); g.FillRectangle(backgroundBrush, backgroundRect); g.DrawRectangle(Pens.Black, backgroundRect); } } // 텍스트 그리기 g.DrawString(text, font, textBrush, textPoint); // 드래그 중인 노드 강조 if (isDraggingThisLabel) { var dragPadding = label.Padding + 4; var dragRect = new Rectangle( textPoint.X - dragPadding, textPoint.Y - dragPadding, (int)textSize.Width + (dragPadding * 2), (int)textSize.Height + (dragPadding * 2) ); g.DrawRectangle(new Pen(Color.Cyan, 3), dragRect); } // 선택된 노드 강조 else if (label == SelectedLabel) { var selectionPadding = label.Padding + 2; var selectionRect = new Rectangle( textPoint.X - selectionPadding, textPoint.Y - selectionPadding, (int)textSize.Width + (selectionPadding * 2), (int)textSize.Height + (selectionPadding * 2) ); g.DrawRectangle(_selectedNodePen, selectionRect); } // 호버된 라벨 강조 else if (label == _hoveredNode) { var hoverPadding = label.Padding + 2; var hoverRect = new Rectangle( textPoint.X - hoverPadding, textPoint.Y - hoverPadding, (int)textSize.Width + (hoverPadding * 2), (int)textSize.Height + (hoverPadding * 2) ); g.DrawRectangle(new Pen(Color.Orange, 2), hoverRect); } } } private void DrawImage(Graphics g, MapImage image) { bool isDraggingThisImage = _isDragging && image == SelectedImage; // 이미지 로드 (필요시) if (image.LoadedImage == null && !string.IsNullOrEmpty(image.ImagePath)) { image.LoadImage(); } if (image.LoadedImage != null) { // 실제 표시 크기 계산 var displaySize = image.GetDisplaySize(); if (displaySize.IsEmpty) displaySize = new Size(50, 50); // 기본 크기 // 드래그 중일 때 약간 크게 표시 if (isDraggingThisImage) { displaySize = new Size((int)(displaySize.Width * 1.1), (int)(displaySize.Height * 1.1)); } var imageRect = new Rectangle( image.Position.X - displaySize.Width / 2, image.Position.Y - displaySize.Height / 2, displaySize.Width, displaySize.Height ); // 드래그 중일 때 그림자 효과 if (isDraggingThisImage) { var shadowRect = new Rectangle(imageRect.X + 3, imageRect.Y + 3, imageRect.Width, imageRect.Height); using (var shadowBrush = new SolidBrush(Color.FromArgb(100, 0, 0, 0))) { g.FillRectangle(shadowBrush, shadowRect); } } // 회전이 있는 경우 if (image.Rotation != 0) { var oldTransform = g.Transform; g.TranslateTransform(image.Position.X, image.Position.Y); g.RotateTransform(image.Rotation); g.TranslateTransform(-image.Position.X, -image.Position.Y); DrawImageContent(g, image, imageRect); g.Transform = oldTransform; } else { DrawImageContent(g, image, imageRect); } // 선택/드래그 효과 if (isDraggingThisImage) { g.DrawRectangle(new Pen(Color.Cyan, 3), imageRect); } else if (image == SelectedImage) { g.DrawRectangle(_selectedNodePen, imageRect); } // 호버된 이미지 강조 else if (image == _hoveredNode) { g.DrawRectangle(new Pen(Color.Orange, 2), imageRect); } } else { // 이미지가 없는 경우 표시 로직 (기존과 유사하게 구현) var rect = new Rectangle(image.Position.X - 25, image.Position.Y - 25, 50, 50); g.FillRectangle(Brushes.LightGray, rect); g.DrawRectangle(Pens.Black, rect); using (var font = new Font("Arial", 8)) g.DrawString("No Image", font, Brushes.Black, rect); if (image == SelectedImage) g.DrawRectangle(_selectedNodePen, rect); // 호버된 이미지 강조 (No Image case) else if (image == _hoveredNode) { g.DrawRectangle(new Pen(Color.Orange, 2), rect); } } } private void DrawImageContent(Graphics g, MapImage image, Rectangle rect) { // 투명도 적용하여 이미지 그리기 if (image.Opacity < 1.0f) { using (var imageAttributes = new System.Drawing.Imaging.ImageAttributes()) { var colorMatrix = new System.Drawing.Imaging.ColorMatrix(); colorMatrix.Matrix33 = image.Opacity; imageAttributes.SetColorMatrix(colorMatrix, System.Drawing.Imaging.ColorMatrixFlag.Default, System.Drawing.Imaging.ColorAdjustType.Bitmap); g.DrawImage(image.LoadedImage, rect, 0, 0, image.LoadedImage.Width, image.LoadedImage.Height, GraphicsUnit.Pixel, imageAttributes); } } else { g.DrawImage(image.LoadedImage, rect); } } private Brush GetNodeBrush(MapNode node) { // 🔥 노드의 DisplayColor를 배경색으로 사용 // RFID가 없는 노드는 DisplayColor를 50% 투명도로 표시 bool hasRfid = node.HasRfid(); Color bgColor = Color.Transparent; switch (node.StationType) { case StationType.Normal: if (node.CanTurnLeft || node.CanTurnRight) bgColor = Color.Violet; else bgColor = Color.DeepSkyBlue; break; case StationType.Charger: bgColor = Color.Tomato; break; case StationType.Loader: case StationType.Cleaner: bgColor = Color.Gold; break; case StationType.Plating: bgColor = Color.DeepSkyBlue; break; case StationType.Buffer: bgColor = Color.WhiteSmoke; break; case StationType.Limit: bgColor = Color.Red; break; default: bgColor = Color.White; break; } return new SolidBrush(bgColor); } 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.PrevPosition; 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.PrevPosition; // 디버그 정보 (개발용) 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) { // Stop-Mark 상태 (Error 상태와 유사하게 처리하거나 별도 처리) // 여기서는 AGVState에 StopMark가 없으므로, 외부에서 상태를 Error로 설정하거나 // 별도의 플래그를 확인해야 함. 하지만 IAGV 인터페이스에는 플래그가 없음. // 따라서 fMain에서 상태를 Error로 설정하는 것을 권장. 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; // Stop-Mark 시 Error 상태 사용 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.PrevPosition; 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); } // 줌 및 스케일 정보 (동적 계산) // 스케일: 1픽셀 = GRID_SIZE / _zoomFactor mm // 예: GRID_SIZE=10, zoom=1.0 → 1:10, zoom=0.1 → 1:100 double scaleRatio = GRID_SIZE / _zoomFactor; var zoomText = $"Zoom: {_zoomFactor:P0}"; var scaleText = $"스케일: 1:{scaleRatio:F0}"; using (var font = new Font("맑은 고딕", 10, FontStyle.Bold)) using (var bgBrush = new SolidBrush(Color.FromArgb(220, Color.White))) { // 줌 정보 (좌하단) var zoomSize = g.MeasureString(zoomText, font); var zoomRect = new RectangleF(10, Height - zoomSize.Height - 15, zoomSize.Width + 10, zoomSize.Height + 5); g.FillRectangle(bgBrush, zoomRect); g.DrawRectangle(Pens.Gray, zoomRect.X, zoomRect.Y, zoomRect.Width, zoomRect.Height); g.DrawString(zoomText, font, Brushes.Black, zoomRect.X + 5, zoomRect.Y + 2); // 스케일 정보 (줌 정보 위에) var scaleSize = g.MeasureString(scaleText, font); var scaleRect = new RectangleF(10, Height - zoomSize.Height - scaleSize.Height - 25, scaleSize.Width + 10, scaleSize.Height + 5); g.FillRectangle(bgBrush, scaleRect); g.DrawRectangle(Pens.Gray, scaleRect.X, scaleRect.Y, scaleRect.Width, scaleRect.Height); g.DrawString(scaleText, font, Brushes.Black, scaleRect.X + 5, scaleRect.Y + 2); // 팬 정보 (스케일 정보 위에) var panText = $"Pan: {_panOffset.X:F1}, {_panOffset.Y:F1}"; var panSize = g.MeasureString(panText, font); var panRect = new RectangleF(10, Height - zoomSize.Height - scaleSize.Height - panSize.Height - 35, panSize.Width + 10, panSize.Height + 5); g.FillRectangle(bgBrush, panRect); g.DrawRectangle(Pens.Gray, panRect.X, panRect.Y, panRect.Width, panRect.Height); g.DrawString(panText, font, Brushes.Black, panRect.X + 5, panRect.Y + 2); } // 측정 정보 (우하단 - 사용자 정의 정보가 있을 경우) if (!string.IsNullOrEmpty(_measurementInfo)) { using (var font = new Font("맑은 고딕", 9)) using (var textBrush = new SolidBrush(Color.Black)) using (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); } } } /// /// RFID 중복 노드에 빨간 X자 표시를 그림 /// private void DrawDuplicateRfidMarker(Graphics g, MapNode node) { // X자를 그리기 위한 펜 (빨간색, 굵기 3) using (var pen = new Pen(Color.Red, 3)) { var center = node.Position; var size = NODE_RADIUS; // 노드 반지름 크기로 X자 크기 설정 // X자의 두 대각선 그리기 // 좌상 → 우하 대각선 g.DrawLine(pen, center.X - size, center.Y - size, center.X + size, center.Y + size); // 우상 → 좌하 대각선 g.DrawLine(pen, center.X + size, center.Y - size, center.X - size, center.Y + size); } } /// /// 중복 RFID 값을 빨간 배경의 라운드 사각형으로 표시 /// private void DrawDuplicateRfidLabel(Graphics g, string text, Point position, Font font) { // 텍스트 크기 측정 var textSize = g.MeasureString(text, font); // 라운드 사각형 영역 계산 (텍스트보다 약간 크게) var padding = 2; var rectWidth = (int)textSize.Width + padding * 2; var rectHeight = (int)textSize.Height + padding * 2; var rectX = position.X - padding; var rectY = position.Y - padding - 5; var roundRect = new Rectangle(rectX, rectY, rectWidth, rectHeight); // 라운드 사각형 그리기 (빨간 배경) using (var backgroundBrush = new SolidBrush(Color.Red)) { DrawRoundedRectangle(g, backgroundBrush, roundRect, 6); // 모서리 반지름 6px } // 라운드 사각형 테두리 그리기 (진한 빨간색) using (var borderPen = new Pen(Color.DarkRed, 1)) { DrawRoundedRectangleBorder(g, borderPen, roundRect, 6); } // 흰색 텍스트 그리기 using (var textBrush = new SolidBrush(Color.White)) { g.DrawString(text, font, textBrush, roundRect, new StringFormat { Alignment = StringAlignment.Center, LineAlignment = StringAlignment.Center, }); } } /// /// 라운드 사각형 채우기 /// private void DrawRoundedRectangle(Graphics g, Brush brush, Rectangle rect, int radius) { using (var path = new GraphicsPath()) { path.AddArc(rect.X, rect.Y, radius * 2, radius * 2, 180, 90); path.AddArc(rect.Right - radius * 2, rect.Y, radius * 2, radius * 2, 270, 90); path.AddArc(rect.Right - radius * 2, rect.Bottom - radius * 2, radius * 2, radius * 2, 0, 90); path.AddArc(rect.X, rect.Bottom - radius * 2, radius * 2, radius * 2, 90, 90); path.CloseFigure(); g.FillPath(brush, path); } } /// /// 라운드 사각형 테두리 그리기 /// private void DrawRoundedRectangleBorder(Graphics g, Pen pen, Rectangle rect, int radius) { using (var path = new GraphicsPath()) { path.AddArc(rect.X, rect.Y, radius * 2, radius * 2, 180, 90); path.AddArc(rect.Right - radius * 2, rect.Y, radius * 2, radius * 2, 270, 90); path.AddArc(rect.Right - radius * 2, rect.Bottom - radius * 2, radius * 2, radius * 2, 0, 90); path.AddArc(rect.X, rect.Bottom - radius * 2, radius * 2, radius * 2, 90, 90); path.CloseFigure(); g.DrawPath(pen, path); } } private Rectangle GetVisibleBounds() { // Graphics Transform: Screen = (World + Pan) * Zoom // World = Screen / Zoom - Pan var left = (int)(0 / _zoomFactor - _panOffset.X); var top = (int)(0 / _zoomFactor - _panOffset.Y); var right = (int)(Width / _zoomFactor - _panOffset.X); var bottom = (int)(Height / _zoomFactor - _panOffset.Y); return new Rectangle(left, top, right - left, bottom - top); } #endregion } }