using System; using System.Drawing; using System.Linq; using System.Windows.Forms; using AGVNavigationCore.Models; namespace AGVNavigationCore.Controls { public partial class UnifiedAGVCanvas { #region Mouse Events private void UnifiedAGVCanvas_MouseClick(object sender, MouseEventArgs e) { Focus(); // 포커스 설정 if (_canvasMode == CanvasMode.ViewOnly) return; var worldPoint = ScreenToWorld(e.Location); var hitNode = GetNodeAt(worldPoint); switch (_editMode) { case EditMode.Select: HandleSelectClick(hitNode); break; case EditMode.AddNode: HandleAddNodeClick(worldPoint); break; case EditMode.Connect: HandleConnectClick(hitNode); break; case EditMode.Delete: HandleDeleteClick(hitNode); break; } } private void UnifiedAGVCanvas_MouseDoubleClick(object sender, MouseEventArgs e) { if (_canvasMode == CanvasMode.ViewOnly) return; var worldPoint = ScreenToWorld(e.Location); var hitNode = GetNodeAt(worldPoint); if (hitNode != null) { // 노드 속성 편집 (이벤트 발생) NodeSelected?.Invoke(this, hitNode); } } private void UnifiedAGVCanvas_MouseDown(object sender, MouseEventArgs e) { var worldPoint = ScreenToWorld(e.Location); if (e.Button == MouseButtons.Left) { if (_canvasMode == CanvasMode.Edit && _editMode == EditMode.Move) { var hitNode = GetNodeAt(worldPoint); if (hitNode != null) { _isDragging = true; _selectedNode = hitNode; _dragOffset = new Point( worldPoint.X - hitNode.Position.X, worldPoint.Y - hitNode.Position.Y ); Cursor = Cursors.SizeAll; Invalidate(); return; } } // 팬 시작 (우클릭 또는 중간버튼) _isPanning = true; _lastMousePosition = e.Location; Cursor = Cursors.SizeAll; } else if (e.Button == MouseButtons.Right) { // 컨텍스트 메뉴 (편집 모드에서만) if (_canvasMode == CanvasMode.Edit) { var hitNode = GetNodeAt(worldPoint); ShowContextMenu(e.Location, hitNode); } } } private void UnifiedAGVCanvas_MouseMove(object sender, MouseEventArgs e) { var worldPoint = ScreenToWorld(e.Location); // 호버 노드 업데이트 var newHoveredNode = GetNodeAt(worldPoint); if (newHoveredNode != _hoveredNode) { _hoveredNode = newHoveredNode; Invalidate(); } if (_isPanning) { // 팬 처리 var deltaX = e.X - _lastMousePosition.X; var deltaY = e.Y - _lastMousePosition.Y; _panOffset.X += deltaX; _panOffset.Y += deltaY; _lastMousePosition = e.Location; Invalidate(); } else if (_isDragging && _canvasMode == CanvasMode.Edit) { // 노드 드래그 if (_selectedNode != null) { var newPosition = new Point( worldPoint.X - _dragOffset.X, worldPoint.Y - _dragOffset.Y ); // 그리드 스냅 if (ModifierKeys.HasFlag(Keys.Control)) { newPosition.X = (newPosition.X / GRID_SIZE) * GRID_SIZE; newPosition.Y = (newPosition.Y / GRID_SIZE) * GRID_SIZE; } _selectedNode.Position = newPosition; NodeMoved?.Invoke(this, _selectedNode); MapChanged?.Invoke(this, EventArgs.Empty); Invalidate(); } } else if (_isConnectionMode && _canvasMode == CanvasMode.Edit) { // 임시 연결선 업데이트 _connectionEndPoint = worldPoint; Invalidate(); } // 툴팁 표시 (호버된 노드/AGV 정보) UpdateTooltip(worldPoint); } private void UnifiedAGVCanvas_MouseUp(object sender, MouseEventArgs e) { if (e.Button == MouseButtons.Left) { if (_isDragging && _canvasMode == CanvasMode.Edit) { _isDragging = false; Cursor = GetCursorForMode(_editMode); } if (_isPanning) { _isPanning = false; Cursor = Cursors.Default; } } } private void UnifiedAGVCanvas_MouseWheel(object sender, MouseEventArgs e) { // 줌 처리 var mouseWorldPoint = ScreenToWorld(e.Location); var oldZoom = _zoomFactor; if (e.Delta > 0) _zoomFactor = Math.Min(_zoomFactor * 1.2f, 5.0f); else _zoomFactor = Math.Max(_zoomFactor / 1.2f, 0.1f); // 마우스 위치를 중심으로 줌 var zoomRatio = _zoomFactor / oldZoom; _panOffset.X = (int)(e.X - (e.X - _panOffset.X) * zoomRatio); _panOffset.Y = (int)(e.Y - (e.Y - _panOffset.Y) * zoomRatio); Invalidate(); } #endregion #region Mouse Helper Methods private Point ScreenToWorld(Point screenPoint) { return new Point( (int)((screenPoint.X - _panOffset.X) / _zoomFactor), (int)((screenPoint.Y - _panOffset.Y) / _zoomFactor) ); } private Point WorldToScreen(Point worldPoint) { return new Point( (int)(worldPoint.X * _zoomFactor + _panOffset.X), (int)(worldPoint.Y * _zoomFactor + _panOffset.Y) ); } private MapNode GetNodeAt(Point worldPoint) { if (_nodes == null) return null; // 역순으로 검사하여 위에 그려진 노드부터 확인 for (int i = _nodes.Count - 1; i >= 0; i--) { var node = _nodes[i]; if (IsPointInNode(worldPoint, node)) return node; } return null; } private bool IsPointInNode(Point point, MapNode node) { switch (node.Type) { case NodeType.Label: return IsPointInLabelNode(point, node); case NodeType.Image: return IsPointInImageNode(point, node); default: return IsPointInCircularNode(point, node); } } private bool IsPointInCircularNode(Point point, MapNode node) { switch (node.Type) { case NodeType.Docking: return IsPointInPentagon(point, node); case NodeType.Charging: return IsPointInTriangle(point, node); default: return IsPointInCircle(point, node); } } private bool IsPointInCircle(Point point, MapNode node) { var hitRadius = Math.Max(NODE_RADIUS, 10 / _zoomFactor); var distance = Math.Sqrt( Math.Pow(node.Position.X - point.X, 2) + Math.Pow(node.Position.Y - point.Y, 2) ); return distance <= hitRadius; } private bool IsPointInPentagon(Point point, MapNode node) { var radius = NODE_RADIUS; var center = node.Position; // 5각형 꼭짓점 계산 var points = new Point[5]; for (int i = 0; i < 5; i++) { var angle = (Math.PI * 2 * i / 5) - Math.PI / 2; points[i] = new Point( (int)(center.X + radius * Math.Cos(angle)), (int)(center.Y + radius * Math.Sin(angle)) ); } return IsPointInPolygon(point, points); } private bool IsPointInTriangle(Point point, MapNode node) { var radius = NODE_RADIUS; var center = node.Position; // 삼각형 꼭짓점 계산 var points = new Point[3]; for (int i = 0; i < 3; i++) { var angle = (Math.PI * 2 * i / 3) - Math.PI / 2; points[i] = new Point( (int)(center.X + radius * Math.Cos(angle)), (int)(center.Y + radius * Math.Sin(angle)) ); } return IsPointInPolygon(point, points); } private bool IsPointInPolygon(Point point, Point[] polygon) { // Ray casting 알고리즘 사용 bool inside = false; int j = polygon.Length - 1; for (int i = 0; i < polygon.Length; i++) { var xi = polygon[i].X; var yi = polygon[i].Y; var xj = polygon[j].X; var yj = polygon[j].Y; if (((yi > point.Y) != (yj > point.Y)) && (point.X < (xj - xi) * (point.Y - yi) / (yj - yi) + xi)) { inside = !inside; } j = i; } return inside; } private bool IsPointInLabelNode(Point point, MapNode node) { var text = string.IsNullOrEmpty(node.LabelText) ? node.NodeId : node.LabelText; // 임시 Graphics로 텍스트 크기 측정 using (var tempBitmap = new Bitmap(1, 1)) using (var tempGraphics = Graphics.FromImage(tempBitmap)) { var font = new Font(node.FontFamily, node.FontSize, node.FontStyle); var textSize = tempGraphics.MeasureString(text, font); var textRect = new Rectangle( (int)(node.Position.X - textSize.Width / 2), (int)(node.Position.Y - textSize.Height / 2), (int)textSize.Width, (int)textSize.Height ); font.Dispose(); return textRect.Contains(point); } } private bool IsPointInImageNode(Point point, MapNode node) { var displaySize = node.GetDisplaySize(); if (displaySize.IsEmpty) displaySize = new Size(50, 50); // 기본 크기 var imageRect = new Rectangle( node.Position.X - displaySize.Width / 2, node.Position.Y - displaySize.Height / 2, displaySize.Width, displaySize.Height ); return imageRect.Contains(point); } private IAGV GetAGVAt(Point worldPoint) { if (_agvList == null) return null; var hitRadius = Math.Max(AGV_SIZE / 2, 15 / _zoomFactor); return _agvList.FirstOrDefault(agv => { if (!_agvPositions.ContainsKey(agv.AgvId)) return false; var agvPos = _agvPositions[agv.AgvId]; var distance = Math.Sqrt( Math.Pow(agvPos.X - worldPoint.X, 2) + Math.Pow(agvPos.Y - worldPoint.Y, 2) ); return distance <= hitRadius; }); } private void HandleSelectClick(MapNode hitNode) { if (hitNode != _selectedNode) { _selectedNode = hitNode; NodeSelected?.Invoke(this, hitNode); Invalidate(); } } private void HandleAddNodeClick(Point worldPoint) { // 그리드 스냅 if (ModifierKeys.HasFlag(Keys.Control)) { worldPoint.X = (worldPoint.X / GRID_SIZE) * GRID_SIZE; worldPoint.Y = (worldPoint.Y / GRID_SIZE) * GRID_SIZE; } var newNode = new MapNode { NodeId = $"N{_nodeCounter:D3}", Position = worldPoint, Type = NodeType.Normal }; _nodeCounter++; _nodes.Add(newNode); NodeAdded?.Invoke(this, newNode); MapChanged?.Invoke(this, EventArgs.Empty); Invalidate(); } private void HandleConnectClick(MapNode hitNode) { if (hitNode == null) return; if (!_isConnectionMode) { // 연결 시작 _isConnectionMode = true; _connectionStartNode = hitNode; _selectedNode = hitNode; } else { // 연결 완료 if (_connectionStartNode != null && _connectionStartNode != hitNode) { CreateConnection(_connectionStartNode, hitNode); } CancelConnection(); } Invalidate(); } private void HandleDeleteClick(MapNode hitNode) { if (hitNode == null) return; // 연결된 모든 연결선도 제거 foreach (var node in _nodes) { node.RemoveConnection(hitNode.NodeId); } _nodes.Remove(hitNode); if (_selectedNode == hitNode) _selectedNode = null; NodeDeleted?.Invoke(this, hitNode); MapChanged?.Invoke(this, EventArgs.Empty); Invalidate(); } private void CreateConnection(MapNode fromNode, MapNode toNode) { // 중복 연결 체크 if (fromNode.ConnectedNodes.Contains(toNode.NodeId)) return; fromNode.AddConnection(toNode.NodeId); MapChanged?.Invoke(this, EventArgs.Empty); } private float CalculateDistance(Point from, Point to) { var dx = to.X - from.X; var dy = to.Y - from.Y; return (float)Math.Sqrt(dx * dx + dy * dy); } private void ShowContextMenu(Point location, MapNode hitNode) { _contextMenu.Items.Clear(); if (hitNode != null) { _contextMenu.Items.Add("노드 속성...", null, (s, e) => NodeSelected?.Invoke(this, hitNode)); _contextMenu.Items.Add("노드 삭제", null, (s, e) => HandleDeleteClick(hitNode)); _contextMenu.Items.Add("-"); } _contextMenu.Items.Add("노드 추가", null, (s, e) => { var worldPoint = ScreenToWorld(location); HandleAddNodeClick(worldPoint); }); _contextMenu.Items.Add("전체 맞춤", null, (s, e) => FitToNodes()); _contextMenu.Items.Add("줌 리셋", null, (s, e) => ResetZoom()); _contextMenu.Show(this, location); } private void UpdateTooltip(Point worldPoint) { string tooltipText = ""; // 노드 툴팁 var hitNode = GetNodeAt(worldPoint); if (hitNode != null) { tooltipText = $"노드: {hitNode.NodeId}\n타입: {hitNode.Type}\n위치: ({hitNode.Position.X}, {hitNode.Position.Y})"; } else { // AGV 툴팁 var hitAGV = GetAGVAt(worldPoint); if (hitAGV != null) { var state = _agvStates.ContainsKey(hitAGV.AgvId) ? _agvStates[hitAGV.AgvId] : AGVState.Idle; tooltipText = $"AGV: {hitAGV.AgvId}\n상태: {state}\n배터리: {hitAGV.BatteryLevel:F1}%\n위치: ({hitAGV.CurrentPosition.X}, {hitAGV.CurrentPosition.Y})"; } } // 툴팁 업데이트 (기존 ToolTip 컨트롤 사용) if (!string.IsNullOrEmpty(tooltipText)) { // ToolTip 설정 (필요시 추가 구현) } } #endregion #region View Control Methods /// /// 모든 노드가 보이도록 뷰 조정 /// public void FitToNodes() { if (_nodes == null || _nodes.Count == 0) return; var minX = _nodes.Min(n => n.Position.X); var maxX = _nodes.Max(n => n.Position.X); var minY = _nodes.Min(n => n.Position.Y); var maxY = _nodes.Max(n => n.Position.Y); var margin = 50; var contentWidth = maxX - minX + margin * 2; var contentHeight = maxY - minY + margin * 2; var zoomX = (float)Width / contentWidth; var zoomY = (float)Height / contentHeight; _zoomFactor = Math.Min(zoomX, zoomY) * 0.9f; var centerX = (minX + maxX) / 2; var centerY = (minY + maxY) / 2; _panOffset.X = (int)(Width / 2 - centerX * _zoomFactor); _panOffset.Y = (int)(Height / 2 - centerY * _zoomFactor); Invalidate(); } /// /// 줌 리셋 /// public void ResetZoom() { _zoomFactor = 1.0f; _panOffset = Point.Empty; Invalidate(); } /// /// 특정 위치로 이동 /// public void PanTo(Point worldPoint) { _panOffset.X = (int)(Width / 2 - worldPoint.X * _zoomFactor); _panOffset.Y = (int)(Height / 2 - worldPoint.Y * _zoomFactor); Invalidate(); } /// /// 특정 노드로 이동 /// public void PanToNode(string nodeId) { var node = _nodes?.FirstOrDefault(n => n.NodeId == nodeId); if (node != null) { PanTo(node.Position); } } /// /// 특정 AGV로 이동 /// public void PanToAGV(string agvId) { if (_agvPositions.ContainsKey(agvId)) { PanTo(_agvPositions[agvId]); } } #endregion } }