- Replace NodeID with RFID values in path display for better field mapping
- Add ComboBoxItem<T> class for {rfid} - [{node}] format in combo boxes
- Implement GetRfidByNodeId helper method for NodeID to RFID conversion
- Enhanced UpdatePathDebugInfo to show both RFID and NodeID information
- Improved path visualization with RFID-based route display
- Users can now easily match displayed paths with physical RFID tags
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
1448 lines
56 KiB
C#
1448 lines
56 KiB
C#
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();
|
|
}
|
|
|
|
/// <summary>
|
|
/// AGV 경로 및 모터방향 정보를 시각화
|
|
/// </summary>
|
|
/// <param name="g">Graphics 객체</param>
|
|
/// <param name="agvResult">AGV 경로 계산 결과</param>
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 모터방향 레이블 표시
|
|
/// </summary>
|
|
/// <param name="g">Graphics 객체</param>
|
|
/// <param name="nodePosition">노드 위치</param>
|
|
/// <param name="motorDirection">모터방향</param>
|
|
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;
|
|
}
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// 이동 경로 기반 개선된 리프트 그리기 (각도 기반 동적 디자인)
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 둥근 모서리 사각형 그리기
|
|
/// </summary>
|
|
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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// AGV 중앙 색상 (방사형 그라디언트)
|
|
/// </summary>
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// AGV 외곽 색상 (방사형 그라디언트)
|
|
/// </summary>
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// AGV 내부 링 색상
|
|
/// </summary>
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// AGV 중앙 표시등 색상
|
|
/// </summary>
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// AGV 원형 센서 패턴 (방사형 배치)
|
|
/// </summary>
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// AGV 모니터 그리기 (리프트 반대편)
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 모니터 배경 색상 (스카이블루 계통으로 통일)
|
|
/// </summary>
|
|
private Color GetMonitorBackColor(AGVState state)
|
|
{
|
|
// 모든 상태에서 스카이블루 계통으로 통일하여 AGV와 구분
|
|
return Color.SkyBlue;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 모니터 화면 색상 (상태별로 구분)
|
|
/// </summary>
|
|
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; // 대기: 다크슬레이트그레이
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 모니터 화면 패턴 그리기
|
|
/// </summary>
|
|
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
|
|
}
|
|
} |