Major improvements to AGV navigation system: • Consolidated RFID management into MapNode, removing duplicate RfidMapping class • Enhanced MapNode with RFID metadata fields (RfidStatus, RfidDescription) • Added automatic bidirectional connection generation in pathfinding algorithms • Updated all components to use unified MapNode-based RFID system • Added command line argument support for AGVMapEditor auto-loading files • Fixed pathfinding failures by ensuring proper node connectivity Technical changes: - Removed RfidMapping class and dependencies across all projects - Updated AStarPathfinder with EnsureBidirectionalConnections() method - Modified MapLoader to use AssignAutoRfidIds() for RFID automation - Enhanced UnifiedAGVCanvas, SimulatorForm, and MainForm for MapNode integration - Improved data consistency and reduced memory footprint 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1358 lines
44 KiB
C#
1358 lines
44 KiB
C#
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
|
|
{
|
|
/// <summary>
|
|
/// 대화형 맵 편집 캔버스 컨트롤
|
|
/// 마우스로 노드 추가, 드래그 이동, 연결 등의 기능 제공
|
|
/// </summary>
|
|
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
|
|
|
|
/// <summary>
|
|
/// 편집 모드 열거형
|
|
/// </summary>
|
|
public enum EditMode
|
|
{
|
|
Select, // 선택 모드
|
|
Move, // 이동 모드
|
|
AddNode, // 노드 추가 모드
|
|
Connect, // 연결 모드
|
|
Delete, // 삭제 모드
|
|
AddLabel, // 라벨 추가 모드
|
|
AddImage // 이미지 추가 모드
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Fields
|
|
|
|
private List<MapNode> _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
|
|
|
|
/// <summary>
|
|
/// 현재 편집 모드
|
|
/// </summary>
|
|
public EditMode CurrentEditMode
|
|
{
|
|
get => _editMode;
|
|
set
|
|
{
|
|
_editMode = value;
|
|
// 모드 변경시 연결 모드 해제
|
|
if (_editMode != EditMode.Connect)
|
|
{
|
|
CancelConnection();
|
|
}
|
|
Cursor = GetCursorForMode(_editMode);
|
|
Invalidate();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 그리드 표시 여부
|
|
/// </summary>
|
|
public bool ShowGrid
|
|
{
|
|
get => _showGrid;
|
|
set
|
|
{
|
|
_showGrid = value;
|
|
Invalidate();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 줌 팩터
|
|
/// </summary>
|
|
public float ZoomFactor
|
|
{
|
|
get => _zoomFactor;
|
|
set
|
|
{
|
|
_zoomFactor = Math.Max(0.1f, Math.Min(5.0f, value));
|
|
Invalidate();
|
|
}
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// 선택된 노드
|
|
/// </summary>
|
|
public MapNode SelectedNode => _selectedNode;
|
|
|
|
/// <summary>
|
|
/// 노드 목록
|
|
/// </summary>
|
|
public List<MapNode> Nodes
|
|
{
|
|
get => _nodes;
|
|
set
|
|
{
|
|
_nodes = value ?? new List<MapNode>();
|
|
UpdateNodeCounter();
|
|
Invalidate();
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Events
|
|
|
|
/// <summary>
|
|
/// 노드가 추가되었을 때 발생하는 이벤트
|
|
/// </summary>
|
|
public event EventHandler<MapNode> NodeAdded;
|
|
|
|
/// <summary>
|
|
/// 노드가 선택되었을 때 발생하는 이벤트
|
|
/// </summary>
|
|
public event EventHandler<MapNode> NodeSelected;
|
|
|
|
/// <summary>
|
|
/// 노드가 이동되었을 때 발생하는 이벤트
|
|
/// </summary>
|
|
public event EventHandler<MapNode> NodeMoved;
|
|
|
|
/// <summary>
|
|
/// 노드가 삭제되었을 때 발생하는 이벤트
|
|
/// </summary>
|
|
public event EventHandler<MapNode> NodeDeleted;
|
|
|
|
|
|
/// <summary>
|
|
/// 노드 연결이 생성되었을 때 발생하는 이벤트
|
|
/// </summary>
|
|
public event EventHandler<(MapNode From, MapNode To)> ConnectionCreated;
|
|
|
|
/// <summary>
|
|
/// 맵이 변경되었을 때 발생하는 이벤트
|
|
/// </summary>
|
|
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<MapNode>();
|
|
|
|
// 더블 버퍼링 및 기타 스타일 설정
|
|
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
|
|
|
|
/// <summary>
|
|
/// 편집 모드 설정
|
|
/// </summary>
|
|
public void SetEditMode(EditMode mode)
|
|
{
|
|
CurrentEditMode = mode;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 노드 추가
|
|
/// </summary>
|
|
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<string>(),
|
|
CanRotate = nodeType == NodeType.Rotation
|
|
};
|
|
|
|
_nodes.Add(newNode);
|
|
SelectNode(newNode);
|
|
|
|
NodeAdded?.Invoke(this, newNode);
|
|
MapChanged?.Invoke(this, EventArgs.Empty);
|
|
|
|
Invalidate();
|
|
return newNode;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 노드 삭제
|
|
/// </summary>
|
|
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();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 두 노드를 연결
|
|
/// </summary>
|
|
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();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 화면 좌표를 월드 좌표로 변환
|
|
/// </summary>
|
|
public Point ScreenToWorld(Point screenPoint)
|
|
{
|
|
return new Point(
|
|
(int)((screenPoint.X - _panOffset.X) / _zoomFactor),
|
|
(int)((screenPoint.Y - _panOffset.Y) / _zoomFactor)
|
|
);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 월드 좌표를 화면 좌표로 변환
|
|
/// </summary>
|
|
public Point WorldToScreen(Point worldPoint)
|
|
{
|
|
return new Point(
|
|
(int)(worldPoint.X * _zoomFactor + _panOffset.X),
|
|
(int)(worldPoint.Y * _zoomFactor + _panOffset.Y)
|
|
);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 맵 전체 맞춤
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// 라벨 노드 그리기
|
|
/// </summary>
|
|
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();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 이미지 노드 그리기
|
|
/// </summary>
|
|
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
|
|
}
|
|
} |