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 } }