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