using System; using System.Collections.Generic; using System.Drawing; using System.IO; using System.Linq; using System.Windows.Forms; using AGVMapEditor.Models; using AGVNavigationCore.Controls; using AGVNavigationCore.Models; using MapImage = AGVNavigationCore.Models.MapImage; using MapLabel = AGVNavigationCore.Models.MapLabel; using Newtonsoft.Json; using System.ComponentModel; namespace AGVMapEditor.Forms { /// /// AGV 맵 에디터 메인 폼 /// public partial class MainForm : Form { #region Fields // private List this._mapCanvas.Nodes; private UnifiedAGVCanvas _mapCanvas; // 현재 선택된 노드 private NodeBase _selectedNode; // 파일 경로 private string _currentMapFile = string.Empty; private bool _hasChanges = false; private bool _hasCommandLineArgs = false; // 노드 연결 정보를 표현하는 클래스 public class NodeConnectionInfo { public string FromNodeId { get; set; } public ushort FromRfidId { get; set; } public string ToNodeId { get; set; } public ushort ToRfidId { get; set; } public string ConnectionType { get; set; } public override string ToString() { // RFID가 있으면 RFID(노드이름), 없으면 NodeID(노드이름) 형태로 표시 string fromDisplay = FromRfidId > 0 ? $"{FromRfidId:0000}(*{FromNodeId.PadLeft(4, '0')})" : $"(*{FromNodeId})"; string toDisplay = ToRfidId > 0 ? $"{ToRfidId:0000}(*{ToNodeId.PadLeft(4, '0')})" : $"(*{ToNodeId})"; // 양방향 연결은 ↔ 기호 사용 string arrow = ConnectionType == "양방향" ? "↔" : "→"; return $"{fromDisplay} {arrow} {toDisplay}"; } } #endregion #region Constructor public MainForm() : this(null) { } public MainForm(string[] args) { InitializeComponent(); InitializeData(); InitializeMapCanvas(); UpdateTitle(); // 명령줄 인수로 파일이 전달되었으면 자동으로 열기 if (args != null && args.Length > 0) { _hasCommandLineArgs = true; string filePath = args[0]; if (System.IO.File.Exists(filePath)) { LoadMapFromFile(filePath); } else { MessageBox.Show($"지정된 파일을 찾을 수 없습니다: {filePath}", "파일 오류", MessageBoxButtons.OK, MessageBoxIcon.Warning); } } // 명령줄 인수가 없는 경우는 Form_Load에서 마지막 맵 파일 자동 로드 확인 } #endregion #region Initialization private void InitializeData() { // this._mapCanvas.Nodes = new List(); } private void InitializeMapCanvas() { _mapCanvas = new UnifiedAGVCanvas(); _mapCanvas.Dock = DockStyle.Fill; _mapCanvas.Mode = UnifiedAGVCanvas.CanvasMode.Edit; // 이벤트 연결 _mapCanvas.NodeAdded += OnNodeAdded; //_mapCanvas.NodeSelected += OnNodeSelected; _mapCanvas.NodesSelected += OnNodesSelected; // 다중 선택 이벤트 _mapCanvas.NodeMoved += OnNodeMoved; _mapCanvas.NodeDeleted += OnNodeDeleted; _mapCanvas.ConnectionDeleted += OnConnectionDeleted; _mapCanvas.ImageDoubleClicked += OnImageDoubleClicked; _mapCanvas.MapChanged += OnMapChanged; // 스플리터 패널에 맵 캔버스 추가 panel1.Controls.Add(_mapCanvas); // ... // 툴바 버튼 이벤트 연결 WireToolbarButtonEvents(); } /// /// 툴바 버튼 이벤트 핸들러 연결 /// private void WireToolbarButtonEvents() { // 편집 모드 버튼들 btnSelect.Click += (s, e) => _mapCanvas.CurrentEditMode = UnifiedAGVCanvas.EditMode.Select; btnMove.Click += (s, e) => _mapCanvas.CurrentEditMode = UnifiedAGVCanvas.EditMode.Move; // btnAddNode는 이제 SplitButton이므로 ButtonClick 사용 btnAddNode.ButtonClick += (s, e) => _mapCanvas.CurrentEditMode = UnifiedAGVCanvas.EditMode.AddNode; // 드롭다운 메뉴 항목들 (btnAddLabel, btnAddImage) btnAddLabel.Click += (s, e) => _mapCanvas.CurrentEditMode = UnifiedAGVCanvas.EditMode.AddLabel; btnAddImage.Click += (s, e) => _mapCanvas.CurrentEditMode = UnifiedAGVCanvas.EditMode.AddImage; btnConnect.Click += (s, e) => _mapCanvas.CurrentEditMode = UnifiedAGVCanvas.EditMode.Connect; btnDelete.Click += (s, e) => _mapCanvas.CurrentEditMode = UnifiedAGVCanvas.EditMode.Delete; btnDeleteConnection.Click += (s, e) => _mapCanvas.CurrentEditMode = UnifiedAGVCanvas.EditMode.DeleteConnection; // 그리드 토글 버튼 btnToggleGrid.Click += (s, e) => _mapCanvas.ShowGrid = !_mapCanvas.ShowGrid; // 맵 맞춤 버튼 btnFitMap.Click += (s, e) => _mapCanvas.FitToNodes(); // 이미지 편집 버튼은 이미 Designer.cs에서 연결됨 (BtnToolbarEditImage_Click) } #endregion #region Event Handlers private void MainForm_Load(object sender, EventArgs e) { RefreshNodeList(); // 속성 변경 시 이벤트 연결 _propertyGrid.PropertyValueChanged += PropertyGrid_PropertyValueChanged; // 명령줄 인수가 없는 경우에만 마지막 맵 파일 자동 로드 확인 if (!_hasCommandLineArgs) { this.Show(); Application.DoEvents(); CheckAndLoadLastMapFile(); } } private void OnNodeAdded(object sender, NodeBase node) { _hasChanges = true; UpdateTitle(); RefreshNodeList(); // RFID 자동 할당 } //private void OnNodeSelected(object sender, MapNode node) //{ // _selectedNode = node; // if (node == null) // { // // 빈 공간 클릭 시 캔버스 속성 표시 // ShowCanvasProperties(); // } // else // { // // 노드 클릭 시 노드 속성 표시 // UpdateNodeProperties(); // UpdateImageEditButton(); // 이미지 노드 선택 시 이미지 편집 버튼 활성화 // } //} private void OnNodesSelected(object sender, List nodes) { // 다중 선택 시 처리 if (nodes == null || nodes.Count == 0) { ShowCanvasProperties(); return; } if (nodes.Count == 1) { // 단일 선택은 기존 방식 사용 _selectedNode = nodes[0]; UpdateNodeProperties(); UpdateImageEditButton(); } else { // 다중 선택: 상태바에 선택 개수 표시 toolStripStatusLabel1.Text = $"{nodes.Count}개 노드 선택됨 - PropertyGrid에서 공통 속성 일괄 변경 가능"; // 다중 선택 PropertyWrapper 표시 //var multiWrapper = new MultiNodePropertyWrapper(nodes); _propertyGrid.SelectedObjects = nodes.ToArray();// multiWrapper; _propertyGrid.Focus(); } } private void OnNodeMoved(object sender, NodeBase node) { _hasChanges = true; UpdateTitle(); RefreshNodeList(); } private void OnNodeDeleted(object sender, NodeBase node) { _hasChanges = true; UpdateTitle(); RefreshNodeList(); ClearNodeProperties(); // RFID 자동 할당 } private void OnConnectionCreated(object sender, (MapNode From, MapNode To) connection) { _hasChanges = true; UpdateTitle(); UpdateNodeProperties(); // 연결 정보 업데이트 } private void OnConnectionDeleted(object sender, (MapNode From, MapNode To) connection) { _hasChanges = true; UpdateTitle(); RefreshNodeConnectionList(); UpdateNodeProperties(); // 연결 정보 업데이트 } private void OnImageDoubleClicked(object sender, MapImage image) { // 이미지 노드 더블클릭 시 이미지 편집창 표시 using (var editor = new ImageEditorForm(image)) { if (editor.ShowDialog(this) == DialogResult.OK) { _hasChanges = true; UpdateTitle(); _mapCanvas.Invalidate(); // 캔버스 다시 그리기 UpdateNodeProperties(); // 속성 업데이트 } } } private void OnMapChanged(object sender, EventArgs e) { _hasChanges = true; UpdateTitle(); } private void OnBackgroundClicked(object sender, Point location) { _selectedNode = null; ClearNodeProperties(); } #endregion #region ToolStrip Button Event Handlers private void btnNew_Click(object sender, EventArgs e) { if (CheckSaveChanges()) { NewMap(); } } private void btnOpen_Click(object sender, EventArgs e) { if (CheckSaveChanges()) { OpenMap(); } } private void btnReopen_Click(object sender, EventArgs e) { if (string.IsNullOrEmpty(_currentMapFile)) { MessageBox.Show("다시 열 파일이 없습니다.", "알림", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } if (!File.Exists(_currentMapFile)) { MessageBox.Show($"파일을 찾을 수 없습니다: {_currentMapFile}", "오류", MessageBoxButtons.OK, MessageBoxIcon.Error); return; } if (CheckSaveChanges()) { LoadMapFromFile(_currentMapFile); UpdateStatusBar($"파일을 다시 열었습니다: {Path.GetFileName(_currentMapFile)}"); } } private void btnClose_Click(object sender, EventArgs e) { CloseMap(); } private void btnSave_Click(object sender, EventArgs e) { SaveMap(); } private void btnSaveAs_Click(object sender, EventArgs e) { SaveAsMap(); } private void btnExit_Click(object sender, EventArgs e) { this.Close(); } #endregion #region Keyboard Shortcuts protected override bool ProcessCmdKey(ref Message msg, Keys keyData) { switch (keyData) { case Keys.Control | Keys.N: btnNew_Click(null, null); return true; case Keys.Control | Keys.O: btnOpen_Click(null, null); return true; case Keys.Control | Keys.S: btnSave_Click(null, null); return true; } return base.ProcessCmdKey(ref msg, keyData); } #endregion #region Button Event Handlers private void btnAddNode_Click(object sender, EventArgs e) { AddNewNode(); } private void btnDeleteNode_Click(object sender, EventArgs e) { DeleteSelectedNode(); } private void btnAddConnection_Click(object sender, EventArgs e) { AddConnectionToSelectedNode(); } private void btnRemoveConnection_Click(object sender, EventArgs e) { RemoveConnectionFromSelectedNode(); } #endregion #region Node Management private void AddNewNode() { var nodeId = GenerateNodeId(); var position = new Point(100 + this._mapCanvas.Nodes.Count * 50, 100 + this._mapCanvas.Nodes.Count * 50); var node = new MapNode(nodeId, position, StationType.Normal); this._mapCanvas.Nodes.Add(node); _hasChanges = true; RefreshNodeList(); RefreshMapCanvas(); UpdateTitle(); } private void DeleteSelectedNode() { if (_selectedNode == null) { MessageBox.Show("삭제할 노드를 선택하세요.", "알림", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } var rfidDisplay = (_selectedNode as MapNode)?.RfidId ?? 0; var result = MessageBox.Show($"노드 {rfidDisplay}[{_selectedNode.Id}] 를 삭제하시겠습니까?\n연결된 RFID 매핑도 함께 삭제됩니다.", "삭제 확인", MessageBoxButtons.YesNo, MessageBoxIcon.Question); if (result == DialogResult.Yes) { // 노드 제거 _mapCanvas.RemoveItem(_selectedNode); _selectedNode = null; _hasChanges = true; RefreshNodeList(); RefreshMapCanvas(); ClearNodeProperties(); UpdateTitle(); } } private void AddConnectionToSelectedNode() { if (!(_selectedNode is MapNode selectedMapNode)) { MessageBox.Show("연결을 추가할 노드(MapNode)를 선택하세요.", "알림", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } // 다른 노드들 중에서 선택 var availableNodes = this._mapCanvas.Nodes.Where(n => n.Id != selectedMapNode.Id && !selectedMapNode.ConnectedNodes.Contains(n.Id)).ToList(); if (availableNodes.Count == 0) { MessageBox.Show("연결 가능한 노드가 없습니다.", "알림", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } // 간단한 선택 다이얼로그 (실제로는 별도 폼을 만들어야 함) var nodeNames = availableNodes.Select(n => $"{n.Id}: {n.RfidId}").ToArray(); var input = Microsoft.VisualBasic.Interaction.InputBox("연결할 노드를 선택하세요:", "노드 연결", nodeNames[0]); var targetNode = availableNodes.FirstOrDefault(n => input.StartsWith(n.Id)); if (targetNode != null) { selectedMapNode.AddConnection(targetNode.Id); _hasChanges = true; RefreshMapCanvas(); UpdateNodeProperties(); UpdateTitle(); } } private void RemoveConnectionFromSelectedNode() { if (!(_selectedNode is MapNode selectedMapNode) || selectedMapNode.ConnectedNodes.Count == 0) { MessageBox.Show("연결을 제거할 노드를 선택하거나 연결된 노드가 없습니다.", "알림", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } // 연결된 노드들 중에서 선택 var connectedNodeNames = selectedMapNode.ConnectedNodes.Select(connectedNodeId => { var node = this._mapCanvas.Nodes.FirstOrDefault(n => n.Id == connectedNodeId); return node != null ? $"{node.Id}: {node.RfidId}" : connectedNodeId; }).ToArray(); var input = Microsoft.VisualBasic.Interaction.InputBox("제거할 연결을 선택하세요:", "연결 제거", connectedNodeNames[0]); var targetNodeId = input.Split(':')[0]; if (selectedMapNode.ConnectedNodes.Contains(targetNodeId)) { selectedMapNode.RemoveConnection(targetNodeId); _hasChanges = true; RefreshMapCanvas(); UpdateNodeProperties(); UpdateTitle(); } } private string GenerateNodeId() { int counter = 1; string nodeId; do { nodeId = $"N{counter:D3}"; counter++; } while (this._mapCanvas.Nodes.Any(n => n.Id == nodeId)); return nodeId; } #endregion #region File Operations private void NewMap() { this._mapCanvas.Nodes.Clear(); _selectedNode = null; _currentMapFile = string.Empty; _hasChanges = false; RefreshAll(); UpdateTitle(); } private void CloseMap() { if (CheckSaveChanges()) { this._mapCanvas.Nodes.Clear(); _selectedNode = null; _currentMapFile = string.Empty; _hasChanges = false; RefreshAll(); UpdateTitle(); } } private void OpenMap() { var openFileDialog = new OpenFileDialog { Filter = "AGV Map Files (*.json)|*.json|All Files (*.*)|*.*", DefaultExt = "json", }; if (openFileDialog.ShowDialog() == DialogResult.OK) { try { LoadMapFromFile(openFileDialog.FileName); _currentMapFile = openFileDialog.FileName; _hasChanges = false; RefreshAll(); UpdateTitle(); } catch (Exception ex) { MessageBox.Show($"맵 로드 중 오류가 발생했습니다: {ex.Message}", "오류", MessageBoxButtons.OK, MessageBoxIcon.Error); } } } private void SaveMap() { if (string.IsNullOrEmpty(_currentMapFile)) { SaveAsMap(); } else { try { SaveMapToFile(_currentMapFile); _hasChanges = false; UpdateTitle(); MessageBox.Show("맵이 성공적으로 저장되었습니다.", "성공", MessageBoxButtons.OK, MessageBoxIcon.Information); } catch (Exception ex) { MessageBox.Show($"맵 저장 중 오류가 발생했습니다: {ex.Message}", "오류", MessageBoxButtons.OK, MessageBoxIcon.Error); } } } private void SaveAsMap() { var saveFileDialog = new SaveFileDialog { Filter = "AGV Map Files (*.json)|*.json", DefaultExt = "json", FileName = "NewMap.json" }; if (saveFileDialog.ShowDialog() == DialogResult.OK) { try { SaveMapToFile(saveFileDialog.FileName); _currentMapFile = saveFileDialog.FileName; _hasChanges = false; UpdateTitle(); MessageBox.Show("맵이 성공적으로 저장되었습니다.", "성공", MessageBoxButtons.OK, MessageBoxIcon.Information); } catch (Exception ex) { MessageBox.Show($"맵 저장 중 오류가 발생했습니다: {ex.Message}", "오류", MessageBoxButtons.OK, MessageBoxIcon.Error); } } } private void LoadMapFromFile(string filePath) { var result = MapLoader.LoadMapFromFile(filePath); if (result.Success) { // 맵 캔버스에 데이터 설정 _mapCanvas.SetMapLoadResult(result); // 현재 파일 경로 업데이트 _currentMapFile = filePath; _hasChanges = false; // 설정에 마지막 맵 파일 경로 저장 EditorSettings.Instance.UpdateLastMapFile(filePath); UpdateTitle(); UpdateNodeList(); RefreshNodeConnectionList(); UpdateStatusBar($"맵 파일을 성공적으로 로드했습니다: {Path.GetFileName(filePath)}"); } else { MessageBox.Show($"맵 파일 로딩 실패: {result.ErrorMessage}", "오류", MessageBoxButtons.OK, MessageBoxIcon.Error); } } private void SaveMapToFile(string filePath) { // 🔥 백업 파일 생성 (기존 파일이 있을 경우) if (File.Exists(filePath)) { try { // 날짜시간 포함 백업 파일명 생성 var directory = Path.GetDirectoryName(filePath); var fileNameWithoutExt = Path.GetFileNameWithoutExtension(filePath); var extension = Path.GetExtension(filePath); var timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss"); var backupFileName = $"{fileNameWithoutExt}_{timestamp}{extension}.bak"; var backupFilePath = Path.Combine(directory, backupFileName); // 기존 파일을 백업 파일로 복사 File.Copy(filePath, backupFilePath, true); } catch (Exception ex) { // 백업 파일 생성 실패 시 경고만 표시하고 계속 진행 MessageBox.Show($"백업 파일 생성 실패: {ex.Message}\n원본 파일은 계속 저장됩니다.", "백업 경고", MessageBoxButtons.OK, MessageBoxIcon.Warning); } } // 🔥 현재 캔버스 설정을 맵 파일에 저장 var settings = new MapLoader.MapSettings { BackgroundColorArgb = _mapCanvas.BackColor.ToArgb(), ShowGrid = _mapCanvas.ShowGrid }; if (MapLoader.SaveMapToFile(filePath, _mapCanvas.Nodes, _mapCanvas.Labels, _mapCanvas.Images, _mapCanvas.Marks, _mapCanvas.Magnets, settings)) { // 현재 파일 경로 업데이트 _currentMapFile = filePath; _hasChanges = false; // 설정에 마지막 맵 파일 경로 저장 EditorSettings.Instance.UpdateLastMapFile(filePath); UpdateTitle(); UpdateStatusBar($"맵 파일을 성공적으로 저장했습니다: {Path.GetFileName(filePath)}"); } else { MessageBox.Show("맵 파일 저장 실패", "오류", MessageBoxButtons.OK, MessageBoxIcon.Error); } } /// /// RFID 매핑 업데이트 (공용 MapLoader 사용) /// private void UpdateRfidMappings() { // RFID 자동 할당 제거 - 사용자가 직접 입력한 값 유지 // MapLoader.AssignAutoRfidIds(this._mapCanvas.Nodes); } private bool CheckSaveChanges() { if (_hasChanges) { var result = MessageBox.Show("변경사항이 있습니다. 저장하시겠습니까?", "변경사항 저장", MessageBoxButtons.YesNoCancel, MessageBoxIcon.Question); if (result == DialogResult.Yes) { SaveMap(); return !_hasChanges; // 저장이 성공했으면 true } else if (result == DialogResult.Cancel) { return false; } } return true; } /// /// 마지막 맵 파일이 있는지 확인하고 사용자에게 로드할지 물어봄 /// private void CheckAndLoadLastMapFile() { var settings = EditorSettings.Instance; if (settings.AutoLoadLastMapFile && settings.HasValidLastMapFile()) { string fileName = Path.GetFileName(settings.LastMapFilePath); var result = MessageBox.Show( $"마지막으로 사용한 맵 파일을 찾았습니다:\n\n{fileName}\n\n이 파일을 열까요?", "마지막 맵 파일 로드", MessageBoxButtons.YesNo, MessageBoxIcon.Question); if (result == DialogResult.Yes) { LoadMapFromFile(settings.LastMapFilePath); } } } #endregion #region UI Updates private void RefreshAll() { RefreshNodeList(); RefreshNodeConnectionList(); RefreshMapCanvas(); ClearNodeProperties(); } private void RefreshNodeList() { listBoxNodes.DataSource = null; listBoxNodes.DataSource = this._mapCanvas.Items; listBoxNodes.DisplayMember = "DisplayText"; listBoxNodes.ValueMember = "Id"; // 노드 목록 클릭 이벤트 연결 listBoxNodes.SelectedIndexChanged -= ListBoxNodes_SelectedIndexChanged; listBoxNodes.SelectedIndexChanged += ListBoxNodes_SelectedIndexChanged; // 노드 타입별 색상 적용 listBoxNodes.DrawMode = DrawMode.OwnerDrawFixed; listBoxNodes.DrawItem -= ListBoxNodes_DrawItem; listBoxNodes.DrawItem += ListBoxNodes_DrawItem; } private void ListBoxNodes_SelectedIndexChanged(object sender, EventArgs e) { if (listBoxNodes.SelectedItem is NodeBase selectedNode) { _selectedNode = selectedNode; UpdateNodeProperties(); // 맵 캔버스에서도 선택된 노드 표시 if (_mapCanvas != null) { _mapCanvas.Invalidate(); } } } private void ListBoxNodes_DrawItem(object sender, DrawItemEventArgs e) { e.DrawBackground(); if (e.Index >= 0 && e.Index < this._mapCanvas.Items.Count) { var node = this._mapCanvas.Items[e.Index]; // 노드 타입에 따른 색상 설정 Color foreColor = Color.Black; Color backColor = e.BackColor; if ((e.State & DrawItemState.Selected) == DrawItemState.Selected) { backColor = SystemColors.Highlight; foreColor = SystemColors.HighlightText; } else { backColor = Color.White; switch (node.Type) { case NodeType.Normal: var item = node as MapNode; if (item.StationType == StationType.Normal) foreColor = Color.DimGray; else if (item.StationType == StationType.Charger1 || item.StationType == StationType.Charger2) foreColor = Color.Red; else foreColor = Color.DarkGreen; break; case NodeType.Label: case NodeType.Mark: case NodeType.Image: foreColor = Color.DarkBlue; break; case NodeType.Magnet: foreColor = Color.DarkMagenta; break; default: foreColor = Color.DarkRed; break; } } // 배경 그리기 using (var brush = new SolidBrush(backColor)) { e.Graphics.FillRectangle(brush, e.Bounds); } // 텍스트 그리기 (노드ID - 노드명 - RFID 순서) string displayText; if (node.Type == NodeType.Normal) { var item = node as MapNode; if (item.HasRfid()) displayText = $"[{node.Id}-{item.RfidId}] {item.StationType}"; else displayText = $"[{node.Id}] {item.StationType}"; } else if (node.Type == NodeType.Label) { var item = node as MapLabel; displayText = $"{item.Type.ToString().ToUpper()} - {item.Text}"; } else displayText = $"{node.Type.ToString().ToUpper()}"; using (var brush = new SolidBrush(foreColor)) { e.Graphics.DrawString(displayText, e.Font, brush, e.Bounds.X + 2, e.Bounds.Y + 2); } } e.DrawFocusRectangle(); } private void RefreshNodeConnectionList() { var connections = new List(); var processedPairs = new HashSet(); // 모든 노드의 연결 정보를 수집 (중복 방지) foreach (var fromNode in this._mapCanvas.Nodes) { foreach (var toNodeId in fromNode.ConnectedNodes) { var toNode = this._mapCanvas.Nodes.FirstOrDefault(n => n.Id == toNodeId); if (toNode != null) { // 중복 체크 (단일 연결만 표시) string pairKey1 = $"{fromNode.Id}-{toNode.Id}"; string pairKey2 = $"{toNode.Id}-{fromNode.Id}"; if (!processedPairs.Contains(pairKey1) && !processedPairs.Contains(pairKey2)) { // 사전 순으로 정렬하여 일관성 있게 표시 var (firstNode, secondNode) = string.Compare(fromNode.Id, toNode.Id) < 0 ? (fromNode, toNode) : (toNode, fromNode); connections.Add(new NodeConnectionInfo { FromNodeId = firstNode.Id, FromRfidId = firstNode.RfidId, ToNodeId = secondNode.Id, ToRfidId = secondNode.RfidId, ConnectionType = "양방향" // 모든 연결이 양방향 }); processedPairs.Add(pairKey1); processedPairs.Add(pairKey2); } } } } // 리스트박스에 표시 lstNodeConnection.Font = new Font("돋움체", 10); lstNodeConnection.DataSource = null; lstNodeConnection.DataSource = connections; lstNodeConnection.DisplayMember = "ToString"; // 리스트박스 클릭 이벤트 연결 lstNodeConnection.SelectedIndexChanged -= LstNodeConnection_SelectedIndexChanged; lstNodeConnection.SelectedIndexChanged += LstNodeConnection_SelectedIndexChanged; // 더블클릭 이벤트 연결 (연결 삭제) lstNodeConnection.DoubleClick -= LstNodeConnection_DoubleClick; lstNodeConnection.DoubleClick += LstNodeConnection_DoubleClick; } private void LstNodeConnection_SelectedIndexChanged(object sender, EventArgs e) { if (lstNodeConnection.SelectedItem is NodeConnectionInfo connectionInfo) { // 캔버스에서 해당 연결선 강조 표시 _mapCanvas?.HighlightConnection(connectionInfo.FromNodeId, connectionInfo.ToNodeId); // 연결된 노드들을 맵에서 하이라이트 표시 (선택적) var fromNode = this._mapCanvas.Nodes.FirstOrDefault(n => n.Id == connectionInfo.FromNodeId); if (fromNode != null) { _selectedNode = fromNode; UpdateNodeProperties(); _mapCanvas?.Invalidate(); } } else { // 선택 해제 시 강조 표시 제거 _mapCanvas?.ClearHighlightedConnection(); } } private void LstNodeConnection_DoubleClick(object sender, EventArgs e) { // 더블클릭으로 연결 삭제 DeleteSelectedConnection(); } private void RefreshMapCanvas() { _mapCanvas?.Invalidate(); } private void UpdateNodeProperties() { if (_selectedNode == null) { ShowCanvasProperties(); return; } _propertyGrid.SelectedObject = _selectedNode; _propertyGrid.Focus(); // 이미지 노드인 경우 편집 버튼 활성화 UpdateImageEditButton(); // 마그넷 방향 리스트 업데이트 RefreshMagnetDirectionList(); } /// /// 캔버스 속성 표시 (배경색 등) /// private void ShowCanvasProperties() { _propertyGrid.SelectedObject = _mapCanvas; DisableImageEditButton(); } private void ClearNodeProperties() { _propertyGrid.SelectedObject = null; DisableImageEditButton(); lstMagnetDirection.DataSource = null; } /// /// 선택된 노드가 이미지 노드이면 편집 버튼 활성화 /// private void UpdateImageEditButton() { // ToolStripButton으로 변경됨 btnEditImage.Enabled = (_mapCanvas.SelectedImage != null); } /// /// 이미지 편집 버튼 비활성화 /// private void DisableImageEditButton() { // ToolStripButton으로 변경됨 btnEditImage.Enabled = false; } /// /// 상단 툴바의 이미지 편집 버튼 클릭 이벤트 /// private void BtnToolbarEditImage_Click(object sender, EventArgs e) { var selectedImage = _mapCanvas.SelectedImage; if (selectedImage != null) { using (var editor = new ImageEditorForm(selectedImage)) { if (editor.ShowDialog(this) == DialogResult.OK) { _hasChanges = true; UpdateTitle(); _mapCanvas.Invalidate(); // 캔버스 다시 그리기 UpdateNodeProperties(); // 속성 업데이트 } } } } private void UpdateTitle() { var title = "AGV Map Editor"; if (!string.IsNullOrEmpty(_currentMapFile)) { title += $" - {Path.GetFileName(_currentMapFile)}"; } if (_hasChanges) { title += " *"; } this.Text = title; } /// /// 노드 목록을 업데이트 /// private void UpdateNodeList() { if (listBoxNodes != null) { listBoxNodes.DataSource = null; listBoxNodes.DataSource = this._mapCanvas.Items; listBoxNodes.DisplayMember = "DisplayText"; } } /// /// 상태바에 메시지 표시 /// /// 표시할 메시지 private void UpdateStatusBar(string message) { if (toolStripStatusLabel1 != null) { toolStripStatusLabel1.Text = message; } } #endregion #region Form Events private void MainForm_FormClosing(object sender, FormClosingEventArgs e) { if (!CheckSaveChanges()) { e.Cancel = true; } } #endregion #region PropertyGrid private void PropertyGrid_PropertyValueChanged(object s, PropertyValueChangedEventArgs e) { // 변경된 속성명 디버그 출력 System.Diagnostics.Debug.WriteLine($"[PropertyGrid] 속성 변경됨: {e.ChangedItem.PropertyDescriptor.Name}"); // 🔥 MagnetDirectionInfo 변경 처리 if (_propertyGrid.SelectedObject is MagnetDirectionInfo magInfo) { ApplyDirectionChange(magInfo); return; } // RFID 값 변경시 중복 검사 if (e.ChangedItem.PropertyDescriptor.Name == "RFID") { var newRfidValue = ushort.Parse(e.ChangedItem.Value?.ToString()); if (newRfidValue != 0 && CheckRfidDuplicate(newRfidValue)) { // 중복된 RFID 값 발견 MessageBox.Show($"RFID 값 '{newRfidValue}'이(가) 이미 다른 노드에서 사용 중입니다.\n입력값을 되돌립니다.", "RFID 중복 오류", MessageBoxButtons.OK, MessageBoxIcon.Warning); // 원래 값으로 되돌리기 - PropertyGrid의 SelectedObject 사용 e.ChangedItem.PropertyDescriptor.SetValue(_propertyGrid.SelectedObject, e.OldValue); _propertyGrid.Refresh(); return; } } // 속성이 변경되었을 때 자동으로 변경사항 표시 _hasChanges = true; UpdateTitle(); // 🔥 다중 선택 여부 확인 및 선택된 노드들 저장 bool isMultiSelect = _propertyGrid.SelectedObjects is MapNode[]; // _propertyGrid.SelectedObject is MapNode[]; //var a = _propertyGrid.SelectedObject; List selectedNodes = null; if (isMultiSelect) { // 캔버스에서 현재 선택된 노드들 가져오기 selectedNodes = new List(_mapCanvas.SelectedNodes); System.Diagnostics.Debug.WriteLine($"[PropertyGrid] 다중 선택 노드 수: {selectedNodes.Count}"); } // 현재 선택된 노드를 기억 (단일 선택인 경우만) var currentSelectedNode = _selectedNode; RefreshNodeList(); RefreshMapCanvas(); // 🔥 캔버스 강제 갱신 (bool 타입 속성 변경 시 특히 필요) _mapCanvas.Invalidate(); _mapCanvas.Update(); // 🔥 다중 선택인 경우 MultiNodePropertyWrapper를 다시 생성하여 바인딩 if (isMultiSelect && selectedNodes != null && selectedNodes.Count > 0) { // System.Diagnostics.Debug.WriteLine($"[PropertyGrid] MultiNodePropertyWrapper 재생성: {selectedNodes.Count}개"); //var multiWrapper = new MultiNodePropertyWrapper(selectedNodes); _propertyGrid.SelectedObjects = selectedNodes.ToArray();// multiWrapper; } // PropertyGrid 새로고침 _propertyGrid.Refresh(); // 🔥 단일 선택인 경우에만 노드를 다시 선택 (다중 선택은 캔버스에서 유지) if (!isMultiSelect && currentSelectedNode is MapNode mapNode) { var nodeIndex = this._mapCanvas.Nodes.IndexOf(mapNode); if (nodeIndex >= 0) { listBoxNodes.SelectedIndex = nodeIndex; } } } /// /// RFID 값 중복 검사 /// /// 검사할 RFID 값 /// 중복되면 true, 아니면 false private bool CheckRfidDuplicate(ushort rfidValue) { if (rfidValue == 0 || this._mapCanvas.Nodes == null) return false; // 현재 편집 중인 노드 제외하고 중복 검사 string currentNodeId = null; var selectedObject = _propertyGrid.SelectedObject; int duplicateCount = 0; foreach (var node in this._mapCanvas.Nodes) { // 현재 편집 중인 노드는 제외 if (node.Id == currentNodeId) continue; // 같은 RFID 값을 가진 노드가 있는지 확인 if (node.RfidId != 0 && node.RfidId == rfidValue) { duplicateCount++; break; // 하나라도 발견되면 중복 } } return duplicateCount > 0; } #endregion #region Data Model for Serialization #endregion private void btNodeRemove_Click(object sender, EventArgs e) { DeleteSelectedConnection(); } private void DeleteSelectedConnection() { if (lstNodeConnection.SelectedItem is NodeConnectionInfo connectionInfo) { var result = MessageBox.Show( $"다음 연결을 삭제하시겠습니까?\n{connectionInfo}", "연결 삭제 확인", MessageBoxButtons.YesNo, MessageBoxIcon.Question); if (result == DialogResult.Yes) { // 단일 연결 삭제 var fromNode = this._mapCanvas.Nodes.FirstOrDefault(n => n.Id == connectionInfo.FromNodeId); var toNode = this._mapCanvas.Nodes.FirstOrDefault(n => n.Id == connectionInfo.ToNodeId); if (fromNode != null && toNode != null) { // 양방향 연결 삭제 (양쪽 방향 모두 제거) bool removed = false; if (fromNode.ConnectedNodes.Contains(toNode.Id)) { fromNode.RemoveConnection(toNode.Id); removed = true; } if (toNode.ConnectedNodes.Contains(fromNode.Id)) { toNode.RemoveConnection(fromNode.Id); removed = true; } if (!removed) { MessageBox.Show("연결을 찾을 수 없습니다.", "알림", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } _hasChanges = true; RefreshNodeConnectionList(); RefreshMapCanvas(); UpdateNodeProperties(); UpdateTitle(); toolStripStatusLabel1.Text = $"연결 삭제됨: {connectionInfo.FromNodeId} ↔ {connectionInfo.ToNodeId}"; } } } else { MessageBox.Show("삭제할 연결을 선택하세요.", "알림", MessageBoxButtons.OK, MessageBoxIcon.Information); } } private void allTurnLeftRightCrossOnToolStripMenuItem_Click(object sender, EventArgs e) { //모든노드의 trun left/right/ cross 속성을 true로 변경합니다 if (this._mapCanvas.Nodes == null || this._mapCanvas.Nodes.Count == 0) { MessageBox.Show("맵에 노드가 없습니다.", "알림", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } var result = MessageBox.Show( $"모든 노드({this._mapCanvas.Nodes.Count}개)의 회전/교차 속성을 활성화하시겠습니까?\n\n" + "• CanTurnLeft = true\n" + "• CanTurnRight = true\n" + "• DisableCross = false", "일괄 속성 변경", MessageBoxButtons.YesNo, MessageBoxIcon.Question); if (result == DialogResult.Yes) { foreach (var node in this._mapCanvas.Nodes) { node.CanTurnLeft = true; node.CanTurnRight = true; node.DisableCross = false; node.ModifiedDate = DateTime.Now; } _hasChanges = true; UpdateTitle(); RefreshMapCanvas(); MessageBox.Show( $"{this._mapCanvas.Nodes.Count}개 노드의 회전/교차 속성이 모두 활성화되었습니다.", "완료", MessageBoxButtons.OK, MessageBoxIcon.Information); UpdateStatusBar($"모든 노드의 회전/교차 속성 활성화 완료"); } } /// /// 마그넷 방향 정보를 표현하는 클래스 /// public class MagnetDirectionInfo { [JsonIgnore, Browsable(false)] public MapNode FromNode { get; set; } [JsonIgnore, Browsable(false)] public MapNode ToNode { get; set; } [Category("연결 정보")] [DisplayName("출발 노드")] [Description("출발 노드의 ID입니다.")] [ReadOnly(true)] public string FromNodeId => FromNode?.ID2 ?? "Unknown"; [Category("연결 정보")] [DisplayName("도착 노드")] [Description("도착 노드의 ID입니다.")] [ReadOnly(true)] public string ToNodeId => ToNode?.ID2 ?? "Unknown"; [Category("설정")] [DisplayName("방향")] [Description("이동할 마그넷 방향입니다.")] public MagnetPosition? Direction { get; set; } public override string ToString() { string dirStr = Direction.HasValue ? Direction.Value.ToString() : "None"; string fromStr = FromNode != null ? FromNode.ID2 : "Unknown"; string toStr = ToNode != null ? ToNode.ID2 : "Unknown"; return $"{fromStr} -> {toStr} : {dirStr}"; } } private void RefreshMagnetDirectionList() { // 현재 선택된 항목 기억 int selectedIndex = lstMagnetDirection.SelectedIndex; // 데이터 소스 초기화 (UI 갱신 강제) lstMagnetDirection.DataSource = null; lstMagnetDirection.Items.Clear(); if (this._mapCanvas.Nodes == null) return; var directions = new List(); // 모든 노드 검색 foreach (var nodeItem in this._mapCanvas.Nodes) { if (nodeItem is MapNode node) { if (node.MagnetDirections != null && node.MagnetDirections.Count > 0) { foreach (var kvp in node.MagnetDirections) { var neighborId = kvp.Key; var dir = kvp.Value; var neighbor = this._mapCanvas.Nodes.FirstOrDefault(t => t.Id == neighborId); directions.Add(new MagnetDirectionInfo { FromNode = node, ToNode = neighbor, Direction = dir }); } } } } // 보기 좋게 정렬 (FromNode ID 순) directions.Sort((a, b) => string.Compare(a.FromNode.Id, b.FromNode.Id)); if (directions.Count > 0) { lstMagnetDirection.DataSource = directions; } // 이벤트 연결 lstMagnetDirection.SelectedIndexChanged -= LstMagnetDirection_SelectedIndexChanged; lstMagnetDirection.SelectedIndexChanged += LstMagnetDirection_SelectedIndexChanged; lstMagnetDirection.DoubleClick -= LstMagnetDirection_DoubleClick; lstMagnetDirection.DoubleClick += LstMagnetDirection_DoubleClick; // 선택 항목 복원 (가능한 경우) -> 선택된 객체가 다르게 생성되므로 인덱스로 복원 시도 if (selectedIndex >= 0 && selectedIndex < lstMagnetDirection.Items.Count) { lstMagnetDirection.SelectedIndex = selectedIndex; } } private void LstMagnetDirection_SelectedIndexChanged(object sender, EventArgs e) { if (lstMagnetDirection.SelectedItem is MagnetDirectionInfo info) { // 버튼 상태 업데이트 UpdateDirectionButtons(info); } } private void LstMagnetDirection_DoubleClick(object sender, EventArgs e) { if (lstMagnetDirection.SelectedItem is MagnetDirectionInfo info) { var node = info.FromNode; if (node != null) { // 방향 순환 if (info.Direction == MagnetPosition.S) info.Direction = MagnetPosition.L; else if (info.Direction == MagnetPosition.L) info.Direction = MagnetPosition.R; else if (info.Direction == MagnetPosition.R) info.Direction = MagnetPosition.S; ApplyDirectionChange(info); } } } private void ApplyDirectionChange(MagnetDirectionInfo info) { var node = info.FromNode; if (node == null) return; // 딕셔너리 업데이트 if (node.MagnetDirections == null) node.MagnetDirections = new Dictionary(); if (info.ToNode != null) { if (info.Direction == null) { if (node.MagnetDirections.ContainsKey(info.ToNode.Id)) node.MagnetDirections.Remove(info.ToNode.Id); } else { node.MagnetDirections[info.ToNode.Id] = info.Direction.Value; } _hasChanges = true; UpdateTitle(); // 리스트 갱신 RefreshMagnetDirectionList(); // 캔버스 등 갱신 _mapCanvas.Invalidate(); // 속성창 갱신 (선택된 객체가 바뀌었을 수 있으므로 다시 설정은 RefreshMagnetDirectionList의 선택 복원에서 처리됨) _propertyGrid.Refresh(); } } private void btMakeDirdata_Click(object sender, EventArgs e) { } private void toolStripButton3_Click(object sender, EventArgs e) { if (this._mapCanvas.Nodes == null || this._mapCanvas.Nodes.Count == 0) return; // 기존 목록을 모두 지울지 물어보고 var result = MessageBox.Show( "마그넷방향을 자동 생성할까요? 없는 부분만 추가됩니다.", "마그넷 방향 자동 생성", MessageBoxButtons.YesNo, MessageBoxIcon.Question); if (result != DialogResult.Yes) return; bool clearAll = false;// (result == DialogResult.Yes); int updateCount = 0; foreach (var node in this._mapCanvas.Nodes) { // 연결 노드가 3개 이상인 노드들을 찾아서 if (node.Type == NodeType.Normal && node is MapNode mapNode) { if (clearAll) { if (mapNode.MagnetDirections != null) mapNode.MagnetDirections.Clear(); else mapNode.MagnetDirections = new Dictionary(); } if (mapNode.ConnectedNodes.Count >= 3) { // 마그넷 방향 딕셔너리가 없으면 생성 if (mapNode.MagnetDirections == null) mapNode.MagnetDirections = new Dictionary(); foreach (var connectedId in mapNode.ConnectedNodes) { // 이미 설정된 경우 건너뜀 (모두 초기화 안 한 경우) if (!clearAll && mapNode.MagnetDirections.ContainsKey(connectedId)) continue; // 모두 자동 생성을 해준다 (기본은 직진으로) mapNode.MagnetDirections[connectedId] = MagnetPosition.S; updateCount++; } } } } if (updateCount > 0) { _hasChanges = true; UpdateTitle(); // 현재 선택된 노드의 속성창 및 리스트 갱신 UpdateNodeProperties(); MessageBox.Show($"총 {updateCount}개의 연결에 대해 마그넷 방향(직진)이 설정되었습니다.", "완료", MessageBoxButtons.OK, MessageBoxIcon.Information); } else { MessageBox.Show("변경된 사항이 없습니다.", "알림", MessageBoxButtons.OK, MessageBoxIcon.Information); } } private void btDirDelete_Click(object sender, EventArgs e) { if (lstMagnetDirection.SelectedItem is MagnetDirectionInfo info) { // 선택된 방향정보를 삭제한다. var result = MessageBox.Show( $"선택한 마그넷 방향 정보를 삭제하시겠습니까?\n{info.FromNodeId} -> {info.ToNodeId} : {info.Direction}", "삭제 확인", MessageBoxButtons.YesNo, MessageBoxIcon.Question); if (result == DialogResult.Yes) { if (info.FromNode != null && info.FromNode.MagnetDirections != null) { if (info.ToNode != null && info.FromNode.MagnetDirections.ContainsKey(info.ToNode.Id)) { info.FromNode.MagnetDirections.Remove(info.ToNode.Id); _hasChanges = true; UpdateTitle(); // 리스트 및 UI 갱신 RefreshMagnetDirectionList(); // 캔버스 갱신 _mapCanvas.Invalidate(); } } } } else { MessageBox.Show("삭제할 항목을 선택해주세요.", "알림", MessageBoxButtons.OK, MessageBoxIcon.Information); } } private void button1_Click(object sender, EventArgs e) { //set left if (lstMagnetDirection.SelectedItem is MagnetDirectionInfo info) { info.Direction = MagnetPosition.L; ApplyDirectionChange(info); UpdateDirectionButtons(info); } } private void button2_Click(object sender, EventArgs e) { //set straight if (lstMagnetDirection.SelectedItem is MagnetDirectionInfo info) { info.Direction = MagnetPosition.S; ApplyDirectionChange(info); UpdateDirectionButtons(info); } } private void button3_Click(object sender, EventArgs e) { //set right if (lstMagnetDirection.SelectedItem is MagnetDirectionInfo info) { info.Direction = MagnetPosition.R; ApplyDirectionChange(info); UpdateDirectionButtons(info); } } private void UpdateDirectionButtons(MagnetDirectionInfo info) { button1.BackColor = SystemColors.Control; button2.BackColor = SystemColors.Control; button3.BackColor = SystemColors.Control; if (info.Direction == MagnetPosition.L) button1.BackColor = Color.Lime; else if (info.Direction == MagnetPosition.S) button2.BackColor = Color.Lime; else if (info.Direction == MagnetPosition.R) button3.BackColor = Color.Lime; } private void toolStripButton2_Click(object sender, EventArgs e) { var result = MessageBox.Show( "기존 설정된 마그넷 방향 정보를 모두 초기화하시겠습니까?", "마그넷 방향 일괄 삭제", MessageBoxButtons.YesNoCancel, MessageBoxIcon.Question); if (result == DialogResult.Cancel) return; bool clearAll = (result == DialogResult.Yes); int updateCount = 0; foreach (var node in this._mapCanvas.Nodes) { // 연결 노드가 3개 이상인 노드들을 찾아서 if (node.Type == NodeType.Normal && node is MapNode mapNode) { if (clearAll) { if (mapNode.MagnetDirections != null) mapNode.MagnetDirections.Clear(); else mapNode.MagnetDirections = new Dictionary(); } } } // 현재 선택된 노드의 속성창 및 리스트 갱신 UpdateNodeProperties(); } private void btAddMagnet_Click(object sender, EventArgs e) { // 마그넷 추가 var result = MessageBox.Show("곡선 마그넷(Bezier)을 추가하시겠습니까?\n(예: 베지어 곡선, 아니오: 직선, 취소: 중단)", "마그넷 타입 선택", MessageBoxButtons.YesNoCancel, MessageBoxIcon.Question); if (result == DialogResult.Cancel) return; bool isBezier = (result == DialogResult.Yes); // 화면 중앙 좌표 계산 (World Coordinate) float zoom = _mapCanvas.ZoomFactor; PointF pan = _mapCanvas.PanOffset; float worldCX = (_mapCanvas.Width / 2f) / zoom - pan.X; float worldCY = (_mapCanvas.Height / 2f) / zoom - pan.Y; // 고유 ID 생성 string id = _mapCanvas.GenerateUniqueNodeId(); var magnet = new MapMagnet { Id = id }; // 점 생성 시 정규화(Snap) 처리 int cx = (int)worldCX; int cy = (int)worldCY; magnet.StartPoint = new Point(cx - 50, cy); magnet.EndPoint = new Point(cx + 50, cy); if (isBezier) { magnet.ControlPoint = new MapMagnet.MagnetPoint { X = cx, Y = cy - 50 }; } // 캔버스에 추가 _mapCanvas.Magnets.Add(magnet); _hasChanges = true; UpdateTitle(); RefreshMapCanvas(); RefreshNodeList(); // 추가된 마그넷 선택 //_mapCanvas.SelectedNode = magnet; UpdateNodeProperties(); } } }