2588 lines
106 KiB
C#
2588 lines
106 KiB
C#
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);
|
|
}
|
|
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 DrawAlertMessage(Graphics g)
|
|
{
|
|
if (!showalert || String.IsNullOrEmpty(this._alertmesage)) 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);
|
|
}
|
|
}
|
|
|
|
// 텍스트 깜박임 효과 (배경은 유지하고 텍스트만 깜박임)
|
|
if (_isAlertBlinkOn)
|
|
{
|
|
using (var font = new Font("Malgun Gothic", 12, FontStyle.Bold))
|
|
using (var brush = new SolidBrush(Color.White))
|
|
{
|
|
var textSize = g.MeasureString(_alertmesage, font);
|
|
g.DrawString(_alertmesage, 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
|
|
}
|
|
}
|
|
}
|
|
|
|
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;
|
|
DrawConnection(g, node, targetNode);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
|
|
// 호버된 마그넷 강조
|
|
if (magnet == _hoveredNode)
|
|
{
|
|
using (var highlightPen = new Pen(Color.Orange, 19))
|
|
{
|
|
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);
|
|
}
|
|
}
|
|
// Redraw normal to keep it on top? No, highlight is usually outer.
|
|
// If I draw highlight AFTER, it covers.
|
|
// But DrawMagnet is void. If I draw highlight after, it's fine if I want it to glow.
|
|
// Actually _magnetPen is Width 15, very thick.
|
|
// If I draw highlight Width 19 *before* normal, it acts as border.
|
|
// But this method draws normal first.
|
|
// So I should refactor to calculate path first, then draw?
|
|
// Or just draw highlight on top with alpha?
|
|
// Let's draw highlight on top with non-filled center? No, it's a line.
|
|
// I'll draw highlight on top for now, maybe with alpha.
|
|
}
|
|
}
|
|
|
|
private void DrawMarkSensors(Graphics g)
|
|
{
|
|
if (_marks == null) return; // _marks 리스트 사용
|
|
|
|
int sensorSize = 12; // 크기 설정
|
|
int lineLength = 20; // 선 길이 설정
|
|
int halfLength = lineLength / 2;
|
|
|
|
foreach (var mark in _marks)
|
|
{
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 연결이 강조 표시되어야 하는지 확인
|
|
/// </summary>
|
|
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<string, int>();
|
|
|
|
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();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 경로에 포함된 교차로(3개 이상의 노드가 연결된 노드)를 파란색으로 강조 표시
|
|
/// </summary>
|
|
/// <summary>
|
|
/// 경로에 포함된 특정 노드(Gateway 등)를 강조 표시
|
|
/// HighlightNodeId가 설정된 경우 해당 노드만 표시하고, 없으면 기존대로 교차로 표시(또는 표시 안함)
|
|
/// 사용자가 "교차로 대신 게이트웨이만 강조"를 원하므로 우선순위 적용
|
|
/// </summary>
|
|
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);
|
|
}
|
|
}
|
|
*/
|
|
}
|
|
|
|
/// <summary>
|
|
/// 노드 강조 표시
|
|
/// </summary>
|
|
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
|
|
);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 교차로 라벨을 표시 (선택사항)
|
|
/// </summary>
|
|
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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 드래그 고스트 그리기 - 원래 위치에 반투명 노드 표시
|
|
/// </summary>
|
|
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.Charger1 || item.StationType == StationType.Charger2)
|
|
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.UnLoader:
|
|
case StationType.Clearner:
|
|
case StationType.Buffer:
|
|
DrawPentagonNodeShape(g, node, brush);
|
|
break;
|
|
case StationType.Charger1:
|
|
case StationType.Charger2:
|
|
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", 12, 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.Charger1:
|
|
case StationType.Charger2:
|
|
fgColor = Color.White;
|
|
bgColor = Color.Tomato;
|
|
break;
|
|
case StationType.Buffer:
|
|
fgColor = Color.Black;
|
|
bgColor = Color.White;
|
|
break;
|
|
case StationType.Clearner:
|
|
fgColor = Color.Black;
|
|
bgColor = Color.DeepSkyBlue;
|
|
break;
|
|
case StationType.Loader:
|
|
case StationType.UnLoader:
|
|
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(fgColor))
|
|
{
|
|
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: bgColor = Color.DeepSkyBlue; break;
|
|
case StationType.Charger1: bgColor = Color.Tomato; break;
|
|
case StationType.Charger2: bgColor = Color.Tomato; break;
|
|
case StationType.Loader:
|
|
case StationType.UnLoader: bgColor = Color.Gold; break;
|
|
case StationType.Clearner: 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;
|
|
}
|
|
}
|
|
|
|
|
|
/// <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.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;
|
|
}
|
|
|
|
/// <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.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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// AGV 중앙 색상 (방사형 그라디언트)
|
|
/// </summary>
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <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; // Stop-Mark 시 Error 상태 사용
|
|
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.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;
|
|
}
|
|
|
|
/// <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);
|
|
}
|
|
|
|
// 줌 및 스케일 정보 (동적 계산)
|
|
// 스케일: 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);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// RFID 중복 노드에 빨간 X자 표시를 그림
|
|
/// </summary>
|
|
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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 중복 RFID 값을 빨간 배경의 라운드 사각형으로 표시
|
|
/// </summary>
|
|
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,
|
|
});
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 라운드 사각형 채우기
|
|
/// </summary>
|
|
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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 라운드 사각형 테두리 그리기
|
|
/// </summary>
|
|
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
|
|
}
|
|
} |