using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Linq;
using System.Windows.Forms;
using AGVMapEditor.Models;
using AGVNavigationCore.Models;
namespace AGVMapEditor.Controls
{
///
/// 대화형 맵 편집 캔버스 컨트롤
/// 마우스로 노드 추가, 드래그 이동, 연결 등의 기능 제공
///
public partial class MapCanvasInteractive : UserControl
{
#region Constants
private const int NODE_SIZE = 24;
private const int NODE_RADIUS = NODE_SIZE / 2;
private const int GRID_SIZE = 20;
private const float CONNECTION_WIDTH = 2.0f;
private const int SNAP_DISTANCE = 10;
#endregion
#region Enums
///
/// 편집 모드 열거형
///
public enum EditMode
{
Select, // 선택 모드
Move, // 이동 모드
AddNode, // 노드 추가 모드
Connect, // 연결 모드
Delete, // 삭제 모드
AddLabel, // 라벨 추가 모드
AddImage // 이미지 추가 모드
}
#endregion
#region Fields
private List _nodes;
private MapNode _selectedNode;
// UI 요소들
private Image _companyLogo;
private string _companyLogoPath = string.Empty;
private string _measurementInfo = "스케일: 1:100\n면적: 1000㎡\n최종 수정: " + DateTime.Now.ToString("yyyy-MM-dd");
private MapNode _hoveredNode;
private bool _isDragging;
private Point _dragOffset;
private Point _lastMousePosition;
// 연결 모드 관련
private bool _isConnectionMode;
private MapNode _connectionStartNode;
private Point _connectionEndPoint;
// 편집 모드
private EditMode _editMode = EditMode.Select;
// 그리드 및 줌 관련
private bool _showGrid = true;
private float _zoomFactor = 1.0f;
private Point _panOffset = Point.Empty;
private bool _isPanning;
// 자동 증가 카운터
private int _nodeCounter = 1;
// 브러쉬 및 펜
private Brush _normalNodeBrush;
private Brush _rotationNodeBrush;
private Brush _dockingNodeBrush;
private Brush _chargingNodeBrush;
private Brush _selectedNodeBrush;
private Brush _hoveredNodeBrush;
private Brush _gridBrush;
private Pen _connectionPen;
private Pen _gridPen;
private Pen _tempConnectionPen;
private Pen _selectedNodePen;
// 컨텍스트 메뉴
private ContextMenuStrip _contextMenu;
#endregion
#region Properties
///
/// 현재 편집 모드
///
public EditMode CurrentEditMode
{
get => _editMode;
set
{
_editMode = value;
// 모드 변경시 연결 모드 해제
if (_editMode != EditMode.Connect)
{
CancelConnection();
}
Cursor = GetCursorForMode(_editMode);
Invalidate();
}
}
///
/// 그리드 표시 여부
///
public bool ShowGrid
{
get => _showGrid;
set
{
_showGrid = value;
Invalidate();
}
}
///
/// 줌 팩터
///
public float ZoomFactor
{
get => _zoomFactor;
set
{
_zoomFactor = Math.Max(0.1f, Math.Min(5.0f, value));
Invalidate();
}
}
///
/// 선택된 노드
///
public MapNode SelectedNode => _selectedNode;
///
/// 노드 목록
///
public List Nodes
{
get => _nodes;
set
{
_nodes = value ?? new List();
UpdateNodeCounter();
Invalidate();
}
}
#endregion
#region Events
///
/// 노드가 추가되었을 때 발생하는 이벤트
///
public event EventHandler NodeAdded;
///
/// 노드가 선택되었을 때 발생하는 이벤트
///
public event EventHandler NodeSelected;
///
/// 노드가 이동되었을 때 발생하는 이벤트
///
public event EventHandler NodeMoved;
///
/// 노드가 삭제되었을 때 발생하는 이벤트
///
public event EventHandler NodeDeleted;
///
/// 노드 연결이 생성되었을 때 발생하는 이벤트
///
public event EventHandler<(MapNode From, MapNode To)> ConnectionCreated;
///
/// 맵이 변경되었을 때 발생하는 이벤트
///
public event EventHandler MapChanged;
#endregion
#region Constructor
public MapCanvasInteractive()
{
InitializeComponent();
// Set Optimized Double Buffer to reduce flickering
this.SetStyle(ControlStyles.UserPaint, true);
this.SetStyle(ControlStyles.AllPaintingInWmPaint, true);
this.SetStyle(ControlStyles.OptimizedDoubleBuffer, true);
this.SetStyle(ControlStyles.SupportsTransparentBackColor, true);
// Redraw when resized
this.SetStyle(ControlStyles.ResizeRedraw, true);
this.Resize += arLabel_Resize;
InitializeCanvas();
}
void arLabel_Resize(object sender, EventArgs e)
{
Invalidate();
}
private void InitializeCanvas()
{
_nodes = new List();
// 더블 버퍼링 및 기타 스타일 설정
SetStyle(ControlStyles.AllPaintingInWmPaint |
ControlStyles.UserPaint |
ControlStyles.DoubleBuffer |
ControlStyles.ResizeRedraw, true);
// 포커스를 받을 수 있도록 설정
TabStop = true;
InitializeGraphics();
InitializeContextMenu();
// 이벤트 연결
MouseDown += OnMouseDown;
MouseMove += OnMouseMove;
MouseUp += OnMouseUp;
MouseWheel += OnMouseWheel;
KeyDown += OnKeyDown;
BackColor = Color.White;
}
#endregion
#region Graphics Initialization
private void InitializeGraphics()
{
_normalNodeBrush = new SolidBrush(Color.LightBlue);
_rotationNodeBrush = new SolidBrush(Color.Yellow);
_dockingNodeBrush = new SolidBrush(Color.Orange);
_chargingNodeBrush = new SolidBrush(Color.Green);
_selectedNodeBrush = new SolidBrush(Color.Red);
_hoveredNodeBrush = new SolidBrush(Color.Pink);
_gridBrush = new SolidBrush(Color.LightGray);
_connectionPen = new Pen(Color.Gray, CONNECTION_WIDTH);
_gridPen = new Pen(Color.LightGray, 1.0f) { DashStyle = DashStyle.Dot };
_tempConnectionPen = new Pen(Color.Blue, 2.0f) { DashStyle = DashStyle.Dash };
_selectedNodePen = new Pen(Color.Black, 2.0f);
}
private void InitializeContextMenu()
{
_contextMenu = new ContextMenuStrip();
var addNodeItem = new ToolStripMenuItem("노드 추가");
addNodeItem.Click += (s, e) => SetEditMode(EditMode.AddNode);
var connectItem = new ToolStripMenuItem("노드 연결");
connectItem.Click += (s, e) => SetEditMode(EditMode.Connect);
var deleteItem = new ToolStripMenuItem("삭제");
deleteItem.Click += (s, e) => SetEditMode(EditMode.Delete);
var separator1 = new ToolStripSeparator();
var normalNodeItem = new ToolStripMenuItem("일반 노드로 변경");
normalNodeItem.Click += (s, e) => ChangeSelectedNodeType(NodeType.Normal);
var rotationNodeItem = new ToolStripMenuItem("회전 노드로 변경");
rotationNodeItem.Click += (s, e) => ChangeSelectedNodeType(NodeType.Rotation);
var dockingNodeItem = new ToolStripMenuItem("도킹 노드로 변경");
dockingNodeItem.Click += (s, e) => ChangeSelectedNodeType(NodeType.Docking);
var chargingNodeItem = new ToolStripMenuItem("충전 노드로 변경");
chargingNodeItem.Click += (s, e) => ChangeSelectedNodeType(NodeType.Charging);
_contextMenu.Items.AddRange(new ToolStripItem[]
{
addNodeItem, connectItem, deleteItem, separator1,
normalNodeItem, rotationNodeItem, dockingNodeItem, chargingNodeItem
});
ContextMenuStrip = _contextMenu;
}
#endregion
#region Public Methods
///
/// 편집 모드 설정
///
public void SetEditMode(EditMode mode)
{
CurrentEditMode = mode;
}
///
/// 노드 추가
///
public MapNode AddNode(Point location, NodeType nodeType = NodeType.Normal)
{
var screenLocation = ScreenToWorld(location);
var nodeId = $"N{_nodeCounter:D3}";
_nodeCounter++;
var newNode = new MapNode
{
NodeId = nodeId,
Name = nodeId,
Position = screenLocation,
Type = nodeType,
ConnectedNodes = new List(),
CanRotate = nodeType == NodeType.Rotation
};
_nodes.Add(newNode);
SelectNode(newNode);
NodeAdded?.Invoke(this, newNode);
MapChanged?.Invoke(this, EventArgs.Empty);
Invalidate();
return newNode;
}
///
/// 노드 삭제
///
public void DeleteNode(MapNode node)
{
if (node == null) return;
// 다른 노드들의 연결에서 이 노드 제거
foreach (var otherNode in _nodes)
{
if (otherNode.ConnectedNodes.Contains(node.NodeId))
{
otherNode.ConnectedNodes.Remove(node.NodeId);
}
}
_nodes.Remove(node);
if (_selectedNode == node)
{
_selectedNode = null;
}
NodeDeleted?.Invoke(this, node);
MapChanged?.Invoke(this, EventArgs.Empty);
Invalidate();
}
///
/// 두 노드를 연결
///
public void ConnectNodes(MapNode fromNode, MapNode toNode)
{
if (fromNode == null || toNode == null || fromNode == toNode) return;
// 라벨이나 이미지 노드는 연결 불가
if (fromNode.Type == NodeType.Label || fromNode.Type == NodeType.Image ||
toNode.Type == NodeType.Label || toNode.Type == NodeType.Image)
{
return;
}
if (!fromNode.ConnectedNodes.Contains(toNode.NodeId))
{
fromNode.ConnectedNodes.Add(toNode.NodeId);
ConnectionCreated?.Invoke(this, (fromNode, toNode));
MapChanged?.Invoke(this, EventArgs.Empty);
Invalidate();
}
}
///
/// 화면 좌표를 월드 좌표로 변환
///
public Point ScreenToWorld(Point screenPoint)
{
return new Point(
(int)((screenPoint.X - _panOffset.X) / _zoomFactor),
(int)((screenPoint.Y - _panOffset.Y) / _zoomFactor)
);
}
///
/// 월드 좌표를 화면 좌표로 변환
///
public Point WorldToScreen(Point worldPoint)
{
return new Point(
(int)(worldPoint.X * _zoomFactor + _panOffset.X),
(int)(worldPoint.Y * _zoomFactor + _panOffset.Y)
);
}
///
/// 맵 전체 맞춤
///
public void FitToMap()
{
if (_nodes == null || _nodes.Count == 0) return;
var minX = _nodes.Min(n => n.Position.X) - 50;
var maxX = _nodes.Max(n => n.Position.X) + 50;
var minY = _nodes.Min(n => n.Position.Y) - 50;
var maxY = _nodes.Max(n => n.Position.Y) + 50;
var mapWidth = maxX - minX;
var mapHeight = maxY - minY;
var zoomX = (float)Width / mapWidth;
var zoomY = (float)Height / mapHeight;
_zoomFactor = Math.Min(zoomX, zoomY) * 0.9f;
_panOffset = new Point(
(int)((Width - mapWidth * _zoomFactor) / 2 - minX * _zoomFactor),
(int)((Height - mapHeight * _zoomFactor) / 2 - minY * _zoomFactor)
);
Invalidate();
}
#endregion
#region Mouse Event Handlers
private void OnMouseDown(object sender, MouseEventArgs e)
{
Focus();
var worldPoint = ScreenToWorld(e.Location);
var clickedNode = GetNodeAtPoint(worldPoint);
if (e.Button == MouseButtons.Left)
{
switch (_editMode)
{
case EditMode.Select:
HandleSelectModeMouseDown(e, worldPoint, clickedNode);
break;
case EditMode.Move:
HandleMoveModeMouseDown(e, worldPoint, clickedNode);
break;
case EditMode.AddNode:
HandleAddNodeModeMouseDown(e, worldPoint, clickedNode);
break;
case EditMode.Connect:
HandleConnectModeMouseDown(e, worldPoint, clickedNode);
break;
case EditMode.Delete:
HandleDeleteModeMouseDown(e, worldPoint, clickedNode);
break;
case EditMode.AddLabel:
HandleAddLabelModeMouseDown(e, worldPoint);
break;
case EditMode.AddImage:
HandleAddImageModeMouseDown(e, worldPoint);
break;
}
}
_lastMousePosition = e.Location;
}
private void OnMouseMove(object sender, MouseEventArgs e)
{
var worldPoint = ScreenToWorld(e.Location);
var nodeAtPoint = GetNodeAtPoint(worldPoint);
// 호버 상태 업데이트
if (_hoveredNode != nodeAtPoint)
{
_hoveredNode = nodeAtPoint;
Invalidate();
}
switch (_editMode)
{
case EditMode.Select:
HandleSelectModeMouseMove(e, worldPoint);
break;
case EditMode.Move:
HandleMoveModeMouseMove(e, worldPoint);
break;
case EditMode.Connect:
HandleConnectModeMouseMove(e, worldPoint);
break;
}
// 패닝 처리 (가운데 마우스 버튼)
if (e.Button == MouseButtons.Middle)
{
var deltaX = e.X - _lastMousePosition.X;
var deltaY = e.Y - _lastMousePosition.Y;
_panOffset = new Point(_panOffset.X + deltaX, _panOffset.Y + deltaY);
Invalidate();
}
_lastMousePosition = e.Location;
}
private void OnMouseUp(object sender, MouseEventArgs e)
{
_isDragging = false;
_isPanning = false;
Cursor = GetCursorForMode(_editMode);
}
private void OnMouseWheel(object sender, MouseEventArgs e)
{
var zoomFactor = e.Delta > 0 ? 1.1f : 0.9f;
var newZoom = _zoomFactor * zoomFactor;
if (newZoom >= 0.1f && newZoom <= 5.0f)
{
var mouseX = e.X - _panOffset.X;
var mouseY = e.Y - _panOffset.Y;
_panOffset = new Point(
(int)(_panOffset.X - mouseX * (zoomFactor - 1)),
(int)(_panOffset.Y - mouseY * (zoomFactor - 1))
);
_zoomFactor = newZoom;
Invalidate();
}
}
#endregion
#region Mode-Specific Handlers
private void HandleSelectModeMouseDown(MouseEventArgs e, Point worldPoint, MapNode clickedNode)
{
if (clickedNode != null)
{
SelectNode(clickedNode);
}
else
{
SelectNode(null);
}
}
private void HandleSelectModeMouseMove(MouseEventArgs e, Point worldPoint)
{
// 선택 모드에서는 드래그 이동 비활성화
}
private void HandleAddNodeModeMouseDown(MouseEventArgs e, Point worldPoint, MapNode clickedNode)
{
if (clickedNode == null)
{
var snappedPoint = _showGrid ? SnapToGrid(worldPoint) : worldPoint;
AddNode(WorldToScreen(snappedPoint));
}
}
private void HandleConnectModeMouseDown(MouseEventArgs e, Point worldPoint, MapNode clickedNode)
{
if (clickedNode != null)
{
// 라벨이나 이미지 노드는 연결 불가
if (clickedNode.Type == NodeType.Label || clickedNode.Type == NodeType.Image)
{
return;
}
if (_connectionStartNode == null)
{
// 연결 시작
_connectionStartNode = clickedNode;
_isConnectionMode = true;
SelectNode(clickedNode);
}
else if (_connectionStartNode != clickedNode)
{
// 연결 완료
ConnectNodes(_connectionStartNode, clickedNode);
CancelConnection();
}
else
{
// 같은 노드 클릭시 연결 취소
CancelConnection();
}
}
else
{
CancelConnection();
}
}
private void HandleConnectModeMouseMove(MouseEventArgs e, Point worldPoint)
{
if (_isConnectionMode)
{
_connectionEndPoint = worldPoint;
Invalidate();
}
}
private void HandleMoveModeMouseDown(MouseEventArgs e, Point worldPoint, MapNode clickedNode)
{
if (clickedNode != null)
{
SelectNode(clickedNode);
_isDragging = true;
_dragOffset = new Point(worldPoint.X - clickedNode.Position.X, worldPoint.Y - clickedNode.Position.Y);
Cursor = Cursors.SizeAll;
}
else
{
SelectNode(null);
}
}
private void HandleMoveModeMouseMove(MouseEventArgs e, Point worldPoint)
{
if (_isDragging && _selectedNode != null)
{
var newPosition = new Point(worldPoint.X - _dragOffset.X, worldPoint.Y - _dragOffset.Y);
// 그리드에 스냅
if (_showGrid)
{
newPosition = SnapToGrid(newPosition);
}
_selectedNode.Position = newPosition;
NodeMoved?.Invoke(this, _selectedNode);
MapChanged?.Invoke(this, EventArgs.Empty);
Invalidate();
}
}
private void HandleDeleteModeMouseDown(MouseEventArgs e, Point worldPoint, MapNode clickedNode)
{
if (clickedNode != null)
{
DeleteNode(clickedNode);
}
}
#endregion
#region Helper Methods
private MapNode GetNodeAtPoint(Point point)
{
foreach (var node in _nodes)
{
var screenPos = WorldToScreen(node.Position);
var distance = Math.Sqrt(Math.Pow(point.X - node.Position.X, 2) + Math.Pow(point.Y - node.Position.Y, 2));
if (distance <= NODE_RADIUS)
{
return node;
}
}
return null;
}
private void SelectNode(MapNode node)
{
_selectedNode = node;
NodeSelected?.Invoke(this, node);
Invalidate();
}
private Point SnapToGrid(Point point)
{
return new Point(
(int)(Math.Round((double)point.X / GRID_SIZE) * GRID_SIZE),
(int)(Math.Round((double)point.Y / GRID_SIZE) * GRID_SIZE)
);
}
private Cursor GetCursorForMode(EditMode mode)
{
switch (mode)
{
case EditMode.Move: return Cursors.SizeAll;
case EditMode.AddNode: return Cursors.Cross;
case EditMode.Connect: return Cursors.Hand;
case EditMode.Delete: return Cursors.No;
default: return Cursors.Default;
}
}
private void CancelConnection()
{
_isConnectionMode = false;
_connectionStartNode = null;
Invalidate();
}
private void ChangeSelectedNodeType(NodeType newType)
{
if (_selectedNode != null)
{
_selectedNode.Type = newType;
_selectedNode.CanRotate = newType == NodeType.Rotation;
MapChanged?.Invoke(this, EventArgs.Empty);
Invalidate();
}
}
private void HandleAddLabelModeMouseDown(MouseEventArgs e, Point worldPoint)
{
// 라벨 텍스트 입력 다이얼로그
var text = Microsoft.VisualBasic.Interaction.InputBox("라벨 텍스트를 입력하세요:", "라벨 추가", "새 라벨");
if (!string.IsNullOrEmpty(text))
{
var nodeId = GenerateNodeId("LBL");
var labelNode = new MapNode(nodeId, text, worldPoint, NodeType.Label);
labelNode.LabelText = text;
_nodes.Add(labelNode);
_selectedNode = labelNode;
NodeAdded?.Invoke(this, labelNode);
NodeSelected?.Invoke(this, labelNode);
MapChanged?.Invoke(this, EventArgs.Empty);
// 라벨 추가 후 자동으로 선택 모드로 전환
CurrentEditMode = EditMode.Select;
Invalidate();
}
}
private void HandleAddImageModeMouseDown(MouseEventArgs e, Point worldPoint)
{
// 이미지 파일 선택 다이얼로그
var openFileDialog = new OpenFileDialog
{
Filter = "Image Files|*.jpg;*.jpeg;*.png;*.bmp;*.gif;*.tiff|All Files|*.*",
Title = "이미지 파일 선택"
};
if (openFileDialog.ShowDialog() == DialogResult.OK)
{
var nodeId = GenerateNodeId("IMG");
var imageNode = new MapNode(nodeId, System.IO.Path.GetFileNameWithoutExtension(openFileDialog.FileName), worldPoint, NodeType.Image);
imageNode.ImagePath = openFileDialog.FileName;
if (imageNode.LoadImage())
{
_nodes.Add(imageNode);
_selectedNode = imageNode;
NodeAdded?.Invoke(this, imageNode);
NodeSelected?.Invoke(this, imageNode);
MapChanged?.Invoke(this, EventArgs.Empty);
// 이미지 추가 후 자동으로 선택 모드로 전환
CurrentEditMode = EditMode.Select;
Invalidate();
}
else
{
MessageBox.Show("이미지 로드에 실패했습니다.", "오류", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
}
private string GenerateNodeId(string prefix)
{
int counter = 1;
string nodeId;
do
{
nodeId = $"{prefix}{counter:D3}";
counter++;
} while (_nodes.Any(n => n.NodeId == nodeId));
return nodeId;
}
private void UpdateNodeCounter()
{
if (_nodes != null && _nodes.Count > 0)
{
// 기존 노드 중 가장 큰 번호 찾기
var maxNumber = 0;
foreach (var node in _nodes)
{
if (node.NodeId.StartsWith("N") && int.TryParse(node.NodeId.Substring(1), out var number))
{
maxNumber = Math.Max(maxNumber, number);
}
}
_nodeCounter = maxNumber + 1;
}
}
private Brush GetNodeBrush(MapNode node)
{
if (node == _selectedNode) return _selectedNodeBrush;
if (node == _hoveredNode) return _hoveredNodeBrush;
switch (node.Type)
{
case NodeType.Rotation: return _rotationNodeBrush;
case NodeType.Docking: return _dockingNodeBrush;
case NodeType.Charging: return _chargingNodeBrush;
default: return _normalNodeBrush;
}
}
#endregion
#region Keyboard Handlers
private void OnKeyDown(object sender, KeyEventArgs e)
{
switch (e.KeyCode)
{
case Keys.Delete:
if (_selectedNode != null)
{
DeleteNode(_selectedNode);
}
break;
case Keys.Escape:
CancelConnection();
SelectNode(null);
break;
case Keys.A:
if (e.Control)
{
SetEditMode(EditMode.AddNode);
}
break;
case Keys.C:
if (e.Control)
{
SetEditMode(EditMode.Connect);
}
break;
case Keys.D:
if (e.Control)
{
SetEditMode(EditMode.Delete);
}
break;
case Keys.S:
if (e.Control)
{
SetEditMode(EditMode.Select);
}
break;
case Keys.M:
if (e.Control)
{
SetEditMode(EditMode.Move);
}
break;
}
}
#endregion
#region Painting
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
var g = e.Graphics;
g.SmoothingMode = SmoothingMode.AntiAlias;
// 변환 행렬 설정
g.TranslateTransform(_panOffset.X, _panOffset.Y);
g.ScaleTransform(_zoomFactor, _zoomFactor);
// 그리드 그리기
if (_showGrid)
{
DrawGrid(g);
}
// 노드 연결선 그리기
DrawConnections(g);
// 임시 연결선 그리기 (연결 모드일 때)
if (_isConnectionMode && _connectionStartNode != null)
{
DrawTempConnection(g);
}
// 노드 그리기 (라벨, 이미지 포함)
DrawNodes(g);
// 변환 행렬 리셋 후 UI 요소 그리기
g.ResetTransform();
DrawUI(g);
}
private void DrawGrid(Graphics g)
{
var startX = -(int)(_panOffset.X / _zoomFactor / GRID_SIZE) * GRID_SIZE;
var startY = -(int)(_panOffset.Y / _zoomFactor / GRID_SIZE) * GRID_SIZE;
var endX = startX + (int)(Width / _zoomFactor) + GRID_SIZE;
var endY = startY + (int)(Height / _zoomFactor) + GRID_SIZE;
for (int x = startX; x <= endX; x += GRID_SIZE)
{
g.DrawLine(_gridPen, x, startY, x, endY);
}
for (int y = startY; y <= endY; y += GRID_SIZE)
{
g.DrawLine(_gridPen, startX, y, endX, y);
}
}
private void DrawConnections(Graphics g)
{
foreach (var node in _nodes)
{
foreach (var connectedNodeId in node.ConnectedNodes)
{
var connectedNode = _nodes.FirstOrDefault(n => n.NodeId == connectedNodeId);
if (connectedNode != null)
{
g.DrawLine(_connectionPen, node.Position, connectedNode.Position);
// 방향 화살표 그리기
DrawArrow(g, node.Position, connectedNode.Position);
}
}
}
}
private void DrawTempConnection(Graphics g)
{
var screenEndPoint = ScreenToWorld(WorldToScreen(_connectionEndPoint));
g.DrawLine(_tempConnectionPen, _connectionStartNode.Position, screenEndPoint);
}
private void DrawNodes(Graphics g)
{
foreach (var node in _nodes)
{
var brush = GetNodeBrush(node);
var rect = new Rectangle(
node.Position.X - NODE_RADIUS,
node.Position.Y - NODE_RADIUS,
NODE_SIZE,
NODE_SIZE
);
// 노드 모양에 따른 그리기
switch (node.Type)
{
case NodeType.Label:
// 라벨 노드 - 텍스트 렌더링
DrawLabelNode(g, node);
continue; // 일반 노드 텍스트 렌더링 건너뛰기
case NodeType.Image:
// 이미지 노드 - 이미지 렌더링
DrawImageNode(g, node);
continue; // 일반 노드 텍스트 렌더링 건너뛰기
case NodeType.Rotation:
// 회전 노드 - 원형
g.FillEllipse(brush, rect);
g.DrawEllipse(_selectedNodePen, rect);
break;
case NodeType.Docking:
// 도킹 노드 - 오각형
DrawPentagon(g, brush, _selectedNodePen, node.Position);
break;
case NodeType.Charging:
// 충전 노드 - 삼각형
DrawTriangle(g, brush, _selectedNodePen, node.Position);
break;
default:
// 일반 노드 - 사각형
g.FillRectangle(brush, rect);
g.DrawRectangle(_selectedNodePen, rect);
break;
}
// 선택된 노드는 테두리 강조
if (node == _selectedNode)
{
var selectedPen = new Pen(Color.Red, 3);
switch (node.Type)
{
case NodeType.Rotation:
g.DrawEllipse(selectedPen, rect);
break;
case NodeType.Docking:
DrawPentagonOutline(g, selectedPen, node.Position);
break;
case NodeType.Charging:
DrawTriangleOutline(g, selectedPen, node.Position);
break;
default:
g.DrawRectangle(selectedPen, rect);
break;
}
selectedPen.Dispose();
}
// 노드 설명이 있으면 노드 위에 표시
if (!string.IsNullOrEmpty(node.Description))
{
var descFont = new Font("Arial", 6);
var descTextSize = g.MeasureString(node.Description, descFont);
var descTextPos = new PointF(
node.Position.X - descTextSize.Width / 2,
node.Position.Y - NODE_RADIUS - descTextSize.Height - 2
);
g.DrawString(node.Description, descFont, Brushes.DarkBlue, descTextPos);
descFont.Dispose();
}
// 노드 ID 표시
var font = new Font("Arial", 8);
var textBrush = Brushes.Black;
var textSize = g.MeasureString(node.NodeId, font);
var textPos = new PointF(
node.Position.X - textSize.Width / 2,
node.Position.Y + NODE_RADIUS + 2
);
g.DrawString(node.NodeId, font, textBrush, textPos);
// RFID 값이 있으면 노드 이름 아래에 작게 표시
if (!string.IsNullOrEmpty(node.RfidId))
{
var rfidFont = new Font("Arial", 6);
var rfidTextSize = g.MeasureString(node.RfidId, rfidFont);
var rfidTextPos = new PointF(
node.Position.X - rfidTextSize.Width / 2,
node.Position.Y + NODE_RADIUS + 2 + textSize.Height
);
g.DrawString(node.RfidId, rfidFont, Brushes.Gray, rfidTextPos);
rfidFont.Dispose();
}
font.Dispose();
}
}
private void DrawArrow(Graphics g, Point start, Point end)
{
var angle = Math.Atan2(end.Y - start.Y, end.X - start.X);
var arrowLength = 8;
var arrowAngle = Math.PI / 6;
var arrowPoint1 = new PointF(
(float)(end.X - arrowLength * Math.Cos(angle - arrowAngle)),
(float)(end.Y - arrowLength * Math.Sin(angle - arrowAngle))
);
var arrowPoint2 = new PointF(
(float)(end.X - arrowLength * Math.Cos(angle + arrowAngle)),
(float)(end.Y - arrowLength * Math.Sin(angle + arrowAngle))
);
g.DrawLine(_connectionPen, end, arrowPoint1);
g.DrawLine(_connectionPen, end, arrowPoint2);
}
private void DrawUI(Graphics g)
{
// 현재 모드 표시
var modeText = $"모드: {GetModeText(_editMode)}";
var font = new Font("Arial", 10, FontStyle.Bold);
var textBrush = Brushes.Black;
var backgroundBrush = new SolidBrush(Color.FromArgb(200, Color.White));
var textSize = g.MeasureString(modeText, font);
var textRect = new RectangleF(10, 10, textSize.Width + 10, textSize.Height + 5);
g.FillRectangle(backgroundBrush, textRect);
g.DrawString(modeText, font, textBrush, 15, 12);
font.Dispose();
backgroundBrush.Dispose();
}
private string GetModeText(EditMode mode)
{
switch (mode)
{
case EditMode.Select: return "선택";
case EditMode.Move: return "이동";
case EditMode.AddNode: return "노드 추가";
case EditMode.Connect: return "노드 연결";
case EditMode.Delete: return "삭제";
case EditMode.AddLabel: return "라벨 추가";
case EditMode.AddImage: return "이미지 추가";
default: return "알 수 없음";
}
}
private void DrawPentagon(Graphics g, Brush fillBrush, Pen outlinePen, Point center)
{
var points = GetPentagonPoints(center, NODE_RADIUS);
g.FillPolygon(fillBrush, points);
g.DrawPolygon(outlinePen, points);
}
private void DrawPentagonOutline(Graphics g, Pen pen, Point center)
{
var points = GetPentagonPoints(center, NODE_RADIUS);
g.DrawPolygon(pen, points);
}
private PointF[] GetPentagonPoints(Point center, int radius)
{
var points = new PointF[5];
var angle = -Math.PI / 2; // 시작 각도 (위쪽부터)
for (int i = 0; i < 5; i++)
{
points[i] = new PointF(
center.X + (float)(radius * Math.Cos(angle)),
center.Y + (float)(radius * Math.Sin(angle))
);
angle += 2 * Math.PI / 5; // 72도씩 증가
}
return points;
}
private void DrawTriangle(Graphics g, Brush fillBrush, Pen outlinePen, Point center)
{
var points = GetTrianglePoints(center, NODE_RADIUS);
g.FillPolygon(fillBrush, points);
g.DrawPolygon(outlinePen, points);
}
private void DrawTriangleOutline(Graphics g, Pen pen, Point center)
{
var points = GetTrianglePoints(center, NODE_RADIUS);
g.DrawPolygon(pen, points);
}
private PointF[] GetTrianglePoints(Point center, int radius)
{
var points = new PointF[3];
var angle = -Math.PI / 2; // 시작 각도 (위쪽부터)
for (int i = 0; i < 3; i++)
{
points[i] = new PointF(
center.X + (float)(radius * Math.Cos(angle)),
center.Y + (float)(radius * Math.Sin(angle))
);
angle += 2 * Math.PI / 3; // 120도씩 증가
}
return points;
}
///
/// 라벨 노드 그리기
///
private void DrawLabelNode(Graphics g, MapNode node)
{
if (string.IsNullOrEmpty(node.LabelText)) return;
// 폰트 생성
var font = new Font(node.FontFamily, node.FontSize, node.FontStyle);
var textBrush = new SolidBrush(node.ForeColor);
// 배경 브러쉬 생성 (필요시)
Brush backgroundBrush = null;
if (node.ShowBackground)
{
backgroundBrush = new SolidBrush(node.BackColor);
}
// 텍스트 크기 측정
var textSize = g.MeasureString(node.LabelText, font);
var textRect = new RectangleF(
node.Position.X - textSize.Width / 2,
node.Position.Y - textSize.Height / 2,
textSize.Width,
textSize.Height
);
// 배경 그리기 (필요시)
if (backgroundBrush != null)
{
g.FillRectangle(backgroundBrush, textRect);
}
// 텍스트 그리기
g.DrawString(node.LabelText, font, textBrush, textRect.Location);
// 선택된 노드는 테두리 표시
if (node == _selectedNode)
{
var selectedPen = new Pen(Color.Red, 2);
g.DrawRectangle(selectedPen, Rectangle.Round(textRect));
selectedPen.Dispose();
}
// 리소스 정리
font.Dispose();
textBrush.Dispose();
backgroundBrush?.Dispose();
}
///
/// 이미지 노드 그리기
///
private void DrawImageNode(Graphics g, MapNode node)
{
// 이미지 로드 (필요시)
if (node.LoadedImage == null && !string.IsNullOrEmpty(node.ImagePath))
{
node.LoadImage();
}
if (node.LoadedImage == null) return;
// 실제 표시 크기 계산
var displaySize = node.GetDisplaySize();
if (displaySize.IsEmpty) return;
var imageRect = new Rectangle(
node.Position.X - displaySize.Width / 2,
node.Position.Y - displaySize.Height / 2,
displaySize.Width,
displaySize.Height
);
// 투명도 적용
var colorMatrix = new System.Drawing.Imaging.ColorMatrix();
colorMatrix.Matrix33 = node.Opacity; // 알파 값 설정
var imageAttributes = new System.Drawing.Imaging.ImageAttributes();
imageAttributes.SetColorMatrix(colorMatrix, System.Drawing.Imaging.ColorMatrixFlag.Default,
System.Drawing.Imaging.ColorAdjustType.Bitmap);
// 회전 변환 적용 (필요시)
var originalTransform = g.Transform;
if (node.Rotation != 0)
{
g.TranslateTransform(node.Position.X, node.Position.Y);
g.RotateTransform(node.Rotation);
g.TranslateTransform(-node.Position.X, -node.Position.Y);
}
// 이미지 그리기
g.DrawImage(node.LoadedImage, imageRect, 0, 0, node.LoadedImage.Width, node.LoadedImage.Height,
GraphicsUnit.Pixel, imageAttributes);
// 변환 복원
g.Transform = originalTransform;
// 선택된 노드는 테두리 표시
if (node == _selectedNode)
{
var selectedPen = new Pen(Color.Red, 2);
g.DrawRectangle(selectedPen, imageRect);
selectedPen.Dispose();
}
// 리소스 정리
imageAttributes.Dispose();
}
#endregion
#region Cleanup
protected override void Dispose(bool disposing)
{
if (disposing)
{
// 컴포넌트 정리
if (components != null)
{
components.Dispose();
}
// 브러쉬 정리
_normalNodeBrush?.Dispose();
_rotationNodeBrush?.Dispose();
_dockingNodeBrush?.Dispose();
_chargingNodeBrush?.Dispose();
_selectedNodeBrush?.Dispose();
_hoveredNodeBrush?.Dispose();
_gridBrush?.Dispose();
// 펜 정리
_connectionPen?.Dispose();
_gridPen?.Dispose();
_tempConnectionPen?.Dispose();
_selectedNodePen?.Dispose();
// 컨텍스트 메뉴 정리
_contextMenu?.Dispose();
}
base.Dispose(disposing);
}
#endregion
}
}