diff --git a/Cs_HMI/.cursorrules b/Cs_HMI/.cursorrules new file mode 100644 index 0000000..72e328e --- /dev/null +++ b/Cs_HMI/.cursorrules @@ -0,0 +1,3 @@ +# User Preferences +- **프로젝트 컴파일은 build.bat 를 사용하세요(윈도우 환경) +- **콘솔명령 실행시에는 항상 cmd /c 를 사용해서 실행하세요 \ No newline at end of file diff --git a/Cs_HMI/AGVCSharp.sln b/Cs_HMI/AGVCSharp.sln index 28df278..5425bd8 100644 --- a/Cs_HMI/AGVCSharp.sln +++ b/Cs_HMI/AGVCSharp.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Express 15 for Windows Desktop -VisualStudioVersion = 15.0.36324.19 +# Visual Studio Version 17 +VisualStudioVersion = 17.14.36804.6 d17.14 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Sub", "Sub", "{C423C39A-44E7-4F09-B2F7-7943975FF948}" EndProject @@ -38,8 +38,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AGVSimulator", "AGVLogic\AG EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "arCommUtil", "SubProject\commutil\arCommUtil.csproj", "{14E8C9A5-013E-49BA-B435-FFFFFF7DD623}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Supertonic.Netfx48", "SubProject\SuperTonic\Supertonic.Netfx48.csproj", "{19675E19-EB91-493E-88C3-32B3C094B749}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -182,18 +180,6 @@ Global {14E8C9A5-013E-49BA-B435-FFFFFF7DD623}.Release|x64.Build.0 = Release|Any CPU {14E8C9A5-013E-49BA-B435-FFFFFF7DD623}.Release|x86.ActiveCfg = Release|x86 {14E8C9A5-013E-49BA-B435-FFFFFF7DD623}.Release|x86.Build.0 = Release|x86 - {19675E19-EB91-493E-88C3-32B3C094B749}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {19675E19-EB91-493E-88C3-32B3C094B749}.Debug|Any CPU.Build.0 = Debug|Any CPU - {19675E19-EB91-493E-88C3-32B3C094B749}.Debug|x64.ActiveCfg = Debug|x64 - {19675E19-EB91-493E-88C3-32B3C094B749}.Debug|x64.Build.0 = Debug|x64 - {19675E19-EB91-493E-88C3-32B3C094B749}.Debug|x86.ActiveCfg = Debug|Win32 - {19675E19-EB91-493E-88C3-32B3C094B749}.Debug|x86.Build.0 = Debug|Win32 - {19675E19-EB91-493E-88C3-32B3C094B749}.Release|Any CPU.ActiveCfg = Release|Any CPU - {19675E19-EB91-493E-88C3-32B3C094B749}.Release|Any CPU.Build.0 = Release|Any CPU - {19675E19-EB91-493E-88C3-32B3C094B749}.Release|x64.ActiveCfg = Release|x64 - {19675E19-EB91-493E-88C3-32B3C094B749}.Release|x64.Build.0 = Release|x64 - {19675E19-EB91-493E-88C3-32B3C094B749}.Release|x86.ActiveCfg = Release|Win32 - {19675E19-EB91-493E-88C3-32B3C094B749}.Release|x86.Build.0 = Release|Win32 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -207,7 +193,6 @@ Global {A1B2C3D4-E5F6-7890-ABCD-EF1234567890} = {E5C75D32-5AD6-44DD-8F27-E32023206EBB} {B2C3D4E5-0000-0000-0000-000000000000} = {E5C75D32-5AD6-44DD-8F27-E32023206EBB} {14E8C9A5-013E-49BA-B435-FFFFFF7DD623} = {C423C39A-44E7-4F09-B2F7-7943975FF948} - {19675E19-EB91-493E-88C3-32B3C094B749} = {C423C39A-44E7-4F09-B2F7-7943975FF948} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {B5B1FD72-356F-4840-83E8-B070AC21C8D9} diff --git a/Cs_HMI/AGVLogic/AGVMapEditor/Forms/ImageEditorForm.cs b/Cs_HMI/AGVLogic/AGVMapEditor/Forms/ImageEditorForm.cs index fca92fb..52b5c41 100644 --- a/Cs_HMI/AGVLogic/AGVMapEditor/Forms/ImageEditorForm.cs +++ b/Cs_HMI/AGVLogic/AGVMapEditor/Forms/ImageEditorForm.cs @@ -13,9 +13,9 @@ namespace AGVMapEditor.Forms /// public partial class ImageEditorForm : Form { - private MapNode _targetNode; + private MapImage _targetNode; - public ImageEditorForm(MapNode imageNode = null) + public ImageEditorForm(MapImage imageNode = null) { InitializeComponent(); _targetNode = imageNode; @@ -25,7 +25,7 @@ namespace AGVMapEditor.Forms { LoadImageFromNode(imageNode); } - + this.KeyPreview = true; this.KeyDown += (s1, e1) => { if (e1.KeyCode == Keys.Escape) this.Close(); @@ -38,7 +38,7 @@ namespace AGVMapEditor.Forms imageCanvas.BrushSize = trackBrush.Value; imageCanvas.BrushModeEnabled = chkBrushMode.Checked; imageCanvas.BackColor = Color.FromArgb(32,32,32); - + // 이벤트 연결 chkBrushMode.CheckedChanged += (s, e) => imageCanvas.BrushModeEnabled = chkBrushMode.Checked; } @@ -48,7 +48,7 @@ namespace AGVMapEditor.Forms imageCanvas.BrushSize = trackBrush.Value; } - private void LoadImageFromNode(MapNode node) + private void LoadImageFromNode(MapImage node) { if (node.LoadedImage != null) { @@ -66,7 +66,7 @@ namespace AGVMapEditor.Forms } } } - + private void LoadImageFromFile(string filePath) { try @@ -160,7 +160,7 @@ namespace AGVMapEditor.Forms return; } - if (_targetNode != null && _targetNode.Type == NodeType.Image) + if (_targetNode != null) { // 표시 크기로 리사이즈된 이미지 가져오기 var finalImage = imageCanvas.GetResizedImage(); diff --git a/Cs_HMI/AGVLogic/AGVMapEditor/Forms/MainForm.cs b/Cs_HMI/AGVLogic/AGVMapEditor/Forms/MainForm.cs index 49210b1..a08a434 100644 --- a/Cs_HMI/AGVLogic/AGVMapEditor/Forms/MainForm.cs +++ b/Cs_HMI/AGVLogic/AGVMapEditor/Forms/MainForm.cs @@ -7,6 +7,8 @@ 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; namespace AGVMapEditor.Forms @@ -18,11 +20,11 @@ namespace AGVMapEditor.Forms { #region Fields - private List _mapNodes; + // private List this._mapCanvas.Nodes; private UnifiedAGVCanvas _mapCanvas; // 현재 선택된 노드 - private MapNode _selectedNode; + private NodeBase _selectedNode; // 파일 경로 private string _currentMapFile = string.Empty; @@ -93,20 +95,18 @@ namespace AGVMapEditor.Forms #endregion - + #region Initialization private void InitializeData() { - _mapNodes = new List(); + // this._mapCanvas.Nodes = new List(); } private void InitializeMapCanvas() { _mapCanvas = new UnifiedAGVCanvas(); _mapCanvas.Dock = DockStyle.Fill; - _mapCanvas.Nodes = _mapNodes; - // RfidMappings 제거 - MapNode에 통합됨 // 이벤트 연결 _mapCanvas.NodeAdded += OnNodeAdded; @@ -115,12 +115,14 @@ namespace AGVMapEditor.Forms _mapCanvas.NodeMoved += OnNodeMoved; _mapCanvas.NodeDeleted += OnNodeDeleted; _mapCanvas.ConnectionDeleted += OnConnectionDeleted; - _mapCanvas.ImageNodeDoubleClicked += OnImageNodeDoubleClicked; + _mapCanvas.ImageDoubleClicked += OnImageDoubleClicked; _mapCanvas.MapChanged += OnMapChanged; // 스플리터 패널에 맵 캔버스 추가 panel1.Controls.Add(_mapCanvas); + // ... + // 툴바 버튼 이벤트 연결 WireToolbarButtonEvents(); } @@ -160,7 +162,7 @@ namespace AGVMapEditor.Forms private void MainForm_Load(object sender, EventArgs e) { - + RefreshNodeList(); // 속성 변경 시 이벤트 연결 _propertyGrid.PropertyValueChanged += PropertyGrid_PropertyValueChanged; @@ -174,7 +176,7 @@ namespace AGVMapEditor.Forms } } - private void OnNodeAdded(object sender, MapNode node) + private void OnNodeAdded(object sender, NodeBase node) { _hasChanges = true; UpdateTitle(); @@ -199,7 +201,7 @@ namespace AGVMapEditor.Forms // } //} - private void OnNodesSelected(object sender, List nodes) + private void OnNodesSelected(object sender, List nodes) { // 다중 선택 시 처리 if (nodes == null || nodes.Count == 0) @@ -227,14 +229,14 @@ namespace AGVMapEditor.Forms } } - private void OnNodeMoved(object sender, MapNode node) + private void OnNodeMoved(object sender, NodeBase node) { _hasChanges = true; UpdateTitle(); RefreshNodeList(); } - private void OnNodeDeleted(object sender, MapNode node) + private void OnNodeDeleted(object sender, NodeBase node) { _hasChanges = true; UpdateTitle(); @@ -258,20 +260,17 @@ namespace AGVMapEditor.Forms UpdateNodeProperties(); // 연결 정보 업데이트 } - private void OnImageNodeDoubleClicked(object sender, MapNode node) + private void OnImageDoubleClicked(object sender, MapImage image) { // 이미지 노드 더블클릭 시 이미지 편집창 표시 - if (node != null && node.Type == NodeType.Image) + using (var editor = new ImageEditorForm(image)) { - using (var editor = new ImageEditorForm(node)) + if (editor.ShowDialog(this) == DialogResult.OK) { - if (editor.ShowDialog(this) == DialogResult.OK) - { - _hasChanges = true; - UpdateTitle(); - _mapCanvas.Invalidate(); // 캔버스 다시 그리기 - UpdateNodeProperties(); // 속성 업데이트 - } + _hasChanges = true; + UpdateTitle(); + _mapCanvas.Invalidate(); // 캔버스 다시 그리기 + UpdateNodeProperties(); // 속성 업데이트 } } } @@ -401,12 +400,11 @@ namespace AGVMapEditor.Forms private void AddNewNode() { var nodeId = GenerateNodeId(); - var nodeName = $"노드{_mapNodes.Count + 1}"; - var position = new Point(100 + _mapNodes.Count * 50, 100 + _mapNodes.Count * 50); + var position = new Point(100 + this._mapCanvas.Nodes.Count * 50, 100 + this._mapCanvas.Nodes.Count * 50); - var node = new MapNode(nodeId, nodeName, position, NodeType.Normal); + var node = new MapNode(nodeId, position, StationType.Normal); - _mapNodes.Add(node); + this._mapCanvas.Nodes.Add(node); _hasChanges = true; RefreshNodeList(); @@ -422,13 +420,14 @@ namespace AGVMapEditor.Forms return; } - var result = MessageBox.Show($"노드 '{_selectedNode.Name}'를 삭제하시겠습니까?\n연결된 RFID 매핑도 함께 삭제됩니다.", + var rfidDisplay = (_selectedNode as MapNode)?.RfidId ?? ""; + var result = MessageBox.Show($"노드 {rfidDisplay}[{_selectedNode.Id}] 를 삭제하시겠습니까?\n연결된 RFID 매핑도 함께 삭제됩니다.", "삭제 확인", MessageBoxButtons.YesNo, MessageBoxIcon.Question); if (result == DialogResult.Yes) { // 노드 제거 - _mapNodes.Remove(_selectedNode); + _mapCanvas.RemoveItem(_selectedNode); _selectedNode = null; _hasChanges = true; @@ -441,15 +440,15 @@ namespace AGVMapEditor.Forms private void AddConnectionToSelectedNode() { - if (_selectedNode == null) + if (!(_selectedNode is MapNode selectedMapNode)) { - MessageBox.Show("연결을 추가할 노드를 선택하세요.", "알림", MessageBoxButtons.OK, MessageBoxIcon.Information); + MessageBox.Show("연결을 추가할 노드(MapNode)를 선택하세요.", "알림", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } // 다른 노드들 중에서 선택 - var availableNodes = _mapNodes.Where(n => n.NodeId != _selectedNode.NodeId && - !_selectedNode.ConnectedNodes.Contains(n.NodeId)).ToList(); + var availableNodes = this._mapCanvas.Nodes.Where(n => n.Id != selectedMapNode.Id && + !selectedMapNode.ConnectedNodes.Contains(n.Id)).ToList(); if (availableNodes.Count == 0) { @@ -458,13 +457,13 @@ namespace AGVMapEditor.Forms } // 간단한 선택 다이얼로그 (실제로는 별도 폼을 만들어야 함) - var nodeNames = availableNodes.Select(n => $"{n.NodeId}: {n.Name}").ToArray(); + 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.NodeId)); + var targetNode = availableNodes.FirstOrDefault(n => input.StartsWith(n.Id)); if (targetNode != null) { - _selectedNode.AddConnection(targetNode.NodeId); + selectedMapNode.AddConnection(targetNode.Id); _hasChanges = true; RefreshMapCanvas(); UpdateNodeProperties(); @@ -474,25 +473,25 @@ namespace AGVMapEditor.Forms private void RemoveConnectionFromSelectedNode() { - if (_selectedNode == null || _selectedNode.ConnectedNodes.Count == 0) + if (!(_selectedNode is MapNode selectedMapNode) || selectedMapNode.ConnectedNodes.Count == 0) { MessageBox.Show("연결을 제거할 노드를 선택하거나 연결된 노드가 없습니다.", "알림", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } // 연결된 노드들 중에서 선택 - var connectedNodeNames = _selectedNode.ConnectedNodes.Select(connectedNodeId => + var connectedNodeNames = selectedMapNode.ConnectedNodes.Select(connectedNodeId => { - var node = _mapNodes.FirstOrDefault(n => n.NodeId == connectedNodeId); - return node != null ? $"{node.NodeId}: {node.Name}" : 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 (_selectedNode.ConnectedNodes.Contains(targetNodeId)) + if (selectedMapNode.ConnectedNodes.Contains(targetNodeId)) { - _selectedNode.RemoveConnection(targetNodeId); + selectedMapNode.RemoveConnection(targetNodeId); _hasChanges = true; RefreshMapCanvas(); UpdateNodeProperties(); @@ -509,7 +508,7 @@ namespace AGVMapEditor.Forms { nodeId = $"N{counter:D3}"; counter++; - } while (_mapNodes.Any(n => n.NodeId == nodeId)); + } while (this._mapCanvas.Nodes.Any(n => n.Id == nodeId)); return nodeId; } @@ -520,7 +519,7 @@ namespace AGVMapEditor.Forms private void NewMap() { - _mapNodes.Clear(); + this._mapCanvas.Nodes.Clear(); _selectedNode = null; _currentMapFile = string.Empty; _hasChanges = false; @@ -533,7 +532,7 @@ namespace AGVMapEditor.Forms { if (CheckSaveChanges()) { - _mapNodes.Clear(); + this._mapCanvas.Nodes.Clear(); _selectedNode = null; _currentMapFile = string.Empty; _hasChanges = false; @@ -547,7 +546,7 @@ namespace AGVMapEditor.Forms { var openFileDialog = new OpenFileDialog { - Filter = "AGV Map Files (*.agvmap)|*.agvmap|All Files (*.*)|*.*", + Filter = "AGV Map Files (*.agvmap;*.json)|*.agvmap;*.json|All Files (*.*)|*.*", DefaultExt = "agvmap", }; @@ -624,10 +623,14 @@ namespace AGVMapEditor.Forms if (result.Success) { - _mapNodes = result.Nodes; + this._mapCanvas.Nodes = result.Nodes; // 맵 캔버스에 데이터 설정 - _mapCanvas.Nodes = _mapNodes; + _mapCanvas.Nodes = this._mapCanvas.Nodes; + _mapCanvas.Labels = result.Labels; // 추가 + _mapCanvas.Images = result.Images; // 추가 + _mapCanvas.Marks = result.Marks; + _mapCanvas.Magnets = result.Magnets; // RfidMappings 제거됨 - MapNode에 통합 // 🔥 맵 설정 적용 (배경색, 그리드 표시) @@ -693,7 +696,7 @@ namespace AGVMapEditor.Forms ShowGrid = _mapCanvas.ShowGrid }; - if (MapLoader.SaveMapToFile(filePath, _mapNodes, settings)) + if (MapLoader.SaveMapToFile(filePath, this._mapCanvas.Nodes, _mapCanvas.Labels, _mapCanvas.Images, _mapCanvas.Marks, _mapCanvas.Magnets, settings)) { // 현재 파일 경로 업데이트 _currentMapFile = filePath; @@ -718,7 +721,7 @@ namespace AGVMapEditor.Forms private void UpdateRfidMappings() { // RFID 자동 할당 제거 - 사용자가 직접 입력한 값 유지 - // MapLoader.AssignAutoRfidIds(_mapNodes); + // MapLoader.AssignAutoRfidIds(this._mapCanvas.Nodes); } private bool CheckSaveChanges() @@ -780,14 +783,14 @@ namespace AGVMapEditor.Forms private void RefreshNodeList() { listBoxNodes.DataSource = null; - listBoxNodes.DataSource = _mapNodes; + listBoxNodes.DataSource = this._mapCanvas.Nodes; listBoxNodes.DisplayMember = "DisplayText"; - listBoxNodes.ValueMember = "NodeId"; + listBoxNodes.ValueMember = "Id"; // 노드 목록 클릭 이벤트 연결 listBoxNodes.SelectedIndexChanged -= ListBoxNodes_SelectedIndexChanged; listBoxNodes.SelectedIndexChanged += ListBoxNodes_SelectedIndexChanged; - + // 노드 타입별 색상 적용 listBoxNodes.DrawMode = DrawMode.OwnerDrawFixed; listBoxNodes.DrawItem -= ListBoxNodes_DrawItem; @@ -812,14 +815,14 @@ namespace AGVMapEditor.Forms { e.DrawBackground(); - if (e.Index >= 0 && e.Index < _mapNodes.Count) + if (e.Index >= 0 && e.Index < this._mapCanvas.Items.Count) { - var node = _mapNodes[e.Index]; - + 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; @@ -827,22 +830,30 @@ namespace AGVMapEditor.Forms } else { + backColor = Color.White; switch (node.Type) { case NodeType.Normal: - foreColor = Color.Black; - backColor = Color.White; + + var item = node as MapNode; + if (item.StationType == StationType.Normal) + foreColor = Color.DimGray; + else if (item.StationType == StationType.Charger) + foreColor = Color.Red; + else + foreColor = Color.DarkGreen; break; - case NodeType.Loader: - case NodeType.UnLoader: - case NodeType.Clearner: - case NodeType.Buffer: - foreColor = Color.DarkGreen; - backColor = Color.LightGreen; + + case NodeType.Label: + case NodeType.Mark: + case NodeType.Image: + foreColor = Color.DarkBlue; break; - case NodeType.Charging: + case NodeType.Magnet: + foreColor = Color.DarkMagenta; + break; + default: foreColor = Color.DarkRed; - backColor = Color.LightPink; break; } } @@ -854,17 +865,21 @@ namespace AGVMapEditor.Forms } // 텍스트 그리기 (노드ID - 노드명 - RFID 순서) - var displayText = node.NodeId; - - if (!string.IsNullOrEmpty(node.Name)) + string displayText; + if (node.Type == NodeType.Normal) { - displayText += $" - {node.Name}"; + var item = node as MapNode; + if (item.HasRfid()) + displayText = $"[{node.Id}-{item.RfidId}] {item.StationType}"; + else + displayText = $"[{node.Id}] {item.StationType}"; } - - if (!string.IsNullOrEmpty(node.RfidId)) + else if(node.Type == NodeType.Label) { - displayText += $" - [{node.RfidId}]"; + var item = node as MapLabel; + displayText = $"{item.Type.ToString().ToUpper()} - {item.Text}"; } + else displayText = $"{node.Type.ToString().ToUpper()}"; using (var brush = new SolidBrush(foreColor)) { @@ -881,31 +896,31 @@ namespace AGVMapEditor.Forms var processedPairs = new HashSet(); // 모든 노드의 연결 정보를 수집 (중복 방지) - foreach (var fromNode in _mapNodes) + foreach (var fromNode in this._mapCanvas.Nodes) { foreach (var toNodeId in fromNode.ConnectedNodes) { - var toNode = _mapNodes.FirstOrDefault(n => n.NodeId == toNodeId); + var toNode = this._mapCanvas.Nodes.FirstOrDefault(n => n.Id == toNodeId); if (toNode != null) { // 중복 체크 (단일 연결만 표시) - string pairKey1 = $"{fromNode.NodeId}-{toNode.NodeId}"; - string pairKey2 = $"{toNode.NodeId}-{fromNode.NodeId}"; + 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.NodeId, toNode.NodeId) < 0 + var (firstNode, secondNode) = string.Compare(fromNode.Id, toNode.Id) < 0 ? (fromNode, toNode) : (toNode, fromNode); connections.Add(new NodeConnectionInfo { - FromNodeId = firstNode.NodeId, - FromNodeName = firstNode.Name, + FromNodeId = firstNode.Id, + FromNodeName = "", FromRfidId = firstNode.RfidId, - ToNodeId = secondNode.NodeId, - ToNodeName = secondNode.Name, + ToNodeId = secondNode.Id, + ToNodeName = "", ToRfidId = secondNode.RfidId, ConnectionType = "양방향" // 모든 연결이 양방향 }); @@ -940,7 +955,7 @@ namespace AGVMapEditor.Forms _mapCanvas?.HighlightConnection(connectionInfo.FromNodeId, connectionInfo.ToNodeId); // 연결된 노드들을 맵에서 하이라이트 표시 (선택적) - var fromNode = _mapNodes.FirstOrDefault(n => n.NodeId == connectionInfo.FromNodeId); + var fromNode = this._mapCanvas.Nodes.FirstOrDefault(n => n.Id == connectionInfo.FromNodeId); if (fromNode != null) { _selectedNode = fromNode; @@ -1002,7 +1017,7 @@ namespace AGVMapEditor.Forms private void UpdateImageEditButton() { // ToolStripButton으로 변경됨 - btnEditImage.Enabled = (_selectedNode != null && _selectedNode.Type == NodeType.Image); + btnEditImage.Enabled = (_mapCanvas.SelectedImage != null); } /// @@ -1019,9 +1034,10 @@ namespace AGVMapEditor.Forms /// private void BtnToolbarEditImage_Click(object sender, EventArgs e) { - if (_selectedNode != null && _selectedNode.Type == NodeType.Image) + var selectedImage = _mapCanvas.SelectedImage; + if (selectedImage != null) { - using (var editor = new ImageEditorForm(_selectedNode)) + using (var editor = new ImageEditorForm(selectedImage)) { if (editor.ShowDialog(this) == DialogResult.OK) { @@ -1059,7 +1075,7 @@ namespace AGVMapEditor.Forms if (listBoxNodes != null) { listBoxNodes.DataSource = null; - listBoxNodes.DataSource = _mapNodes; + listBoxNodes.DataSource = this._mapCanvas.Items; listBoxNodes.DisplayMember = "DisplayText"; } } @@ -1121,13 +1137,13 @@ namespace AGVMapEditor.Forms // 🔥 다중 선택 여부 확인 및 선택된 노드들 저장 bool isMultiSelect = _propertyGrid.SelectedObjects is MapNode[]; // _propertyGrid.SelectedObject is MapNode[]; - + //var a = _propertyGrid.SelectedObject; - List selectedNodes = null; + List selectedNodes = null; if (isMultiSelect) { // 캔버스에서 현재 선택된 노드들 가져오기 - selectedNodes = new List(_mapCanvas.SelectedNodes); + selectedNodes = new List(_mapCanvas.SelectedNodes); System.Diagnostics.Debug.WriteLine($"[PropertyGrid] 다중 선택 노드 수: {selectedNodes.Count}"); } @@ -1144,7 +1160,7 @@ namespace AGVMapEditor.Forms // 🔥 다중 선택인 경우 MultiNodePropertyWrapper를 다시 생성하여 바인딩 if (isMultiSelect && selectedNodes != null && selectedNodes.Count > 0) { - // System.Diagnostics.Debug.WriteLine($"[PropertyGrid] MultiNodePropertyWrapper 재생성: {selectedNodes.Count}개"); + // System.Diagnostics.Debug.WriteLine($"[PropertyGrid] MultiNodePropertyWrapper 재생성: {selectedNodes.Count}개"); //var multiWrapper = new MultiNodePropertyWrapper(selectedNodes); _propertyGrid.SelectedObjects = selectedNodes.ToArray();// multiWrapper; } @@ -1153,9 +1169,9 @@ namespace AGVMapEditor.Forms _propertyGrid.Refresh(); // 🔥 단일 선택인 경우에만 노드를 다시 선택 (다중 선택은 캔버스에서 유지) - if (!isMultiSelect && currentSelectedNode != null) + if (!isMultiSelect && currentSelectedNode is MapNode mapNode) { - var nodeIndex = _mapNodes.IndexOf(currentSelectedNode); + var nodeIndex = this._mapCanvas.Nodes.IndexOf(mapNode); if (nodeIndex >= 0) { listBoxNodes.SelectedIndex = nodeIndex; @@ -1170,7 +1186,7 @@ namespace AGVMapEditor.Forms /// 중복되면 true, 아니면 false private bool CheckRfidDuplicate(string rfidValue) { - if (string.IsNullOrEmpty(rfidValue) || _mapNodes == null) + if (string.IsNullOrEmpty(rfidValue) || this._mapCanvas.Nodes == null) return false; // 현재 편집 중인 노드 제외하고 중복 검사 @@ -1192,10 +1208,10 @@ namespace AGVMapEditor.Forms //} int duplicateCount = 0; - foreach (var node in _mapNodes) + foreach (var node in this._mapCanvas.Nodes) { // 현재 편집 중인 노드는 제외 - if (node.NodeId == currentNodeId) + if (node.Id == currentNodeId) continue; // 같은 RFID 값을 가진 노드가 있는지 확인 @@ -1234,23 +1250,23 @@ namespace AGVMapEditor.Forms if (result == DialogResult.Yes) { // 단일 연결 삭제 - var fromNode = _mapNodes.FirstOrDefault(n => n.NodeId == connectionInfo.FromNodeId); - var toNode = _mapNodes.FirstOrDefault(n => n.NodeId == connectionInfo.ToNodeId); + 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.NodeId)) + if (fromNode.ConnectedNodes.Contains(toNode.Id)) { - fromNode.RemoveConnection(toNode.NodeId); + fromNode.RemoveConnection(toNode.Id); removed = true; } - if (toNode.ConnectedNodes.Contains(fromNode.NodeId)) + if (toNode.ConnectedNodes.Contains(fromNode.Id)) { - toNode.RemoveConnection(fromNode.NodeId); + toNode.RemoveConnection(fromNode.Id); removed = true; } @@ -1277,108 +1293,18 @@ namespace AGVMapEditor.Forms } } - - - #region Multi-Node Selection - - private void ShowMultiNodeContextMenu(List nodes) - { - // 다중 선택 시 간단한 다이얼로그 표시 - var result = MessageBox.Show( - $"{nodes.Count}개의 노드가 선택되었습니다.\n\n" + - "일괄 속성 변경을 하시겠습니까?\n\n" + - "예: 글자색 변경\n" + - "아니오: 배경색 변경\n" + - "취소: 닫기", - "다중 노드 속성 변경", - MessageBoxButtons.YesNoCancel, - MessageBoxIcon.Question); - - if (result == DialogResult.Yes) - { - BatchChangeForeColor(); - } - else if (result == DialogResult.No) - { - BatchChangeBackColor(); - } - } - - /// - /// 선택된 노드들의 글자색 일괄 변경 - /// - public void BatchChangeForeColor() - { - var selectedNodes = _mapCanvas.SelectedNodes; - if (selectedNodes == null || selectedNodes.Count == 0) - { - MessageBox.Show("선택된 노드가 없습니다.", "알림", MessageBoxButtons.OK, MessageBoxIcon.Information); - return; - } - - using (var colorDialog = new ColorDialog()) - { - colorDialog.Color = selectedNodes[0].ForeColor; - if (colorDialog.ShowDialog() == DialogResult.OK) - { - foreach (var node in selectedNodes) - { - node.ForeColor = colorDialog.Color; - node.ModifiedDate = DateTime.Now; - } - - _hasChanges = true; - UpdateTitle(); - RefreshMapCanvas(); - MessageBox.Show($"{selectedNodes.Count}개 노드의 글자색이 변경되었습니다.", "완료", MessageBoxButtons.OK, MessageBoxIcon.Information); - } - } - } - - /// - /// 선택된 노드들의 배경색 일괄 변경 - /// - public void BatchChangeBackColor() - { - var selectedNodes = _mapCanvas.SelectedNodes; - if (selectedNodes == null || selectedNodes.Count == 0) - { - MessageBox.Show("선택된 노드가 없습니다.", "알림", MessageBoxButtons.OK, MessageBoxIcon.Information); - return; - } - - using (var colorDialog = new ColorDialog()) - { - colorDialog.Color = selectedNodes[0].DisplayColor; - if (colorDialog.ShowDialog() == DialogResult.OK) - { - foreach (var node in selectedNodes) - { - node.DisplayColor = colorDialog.Color; - node.ModifiedDate = DateTime.Now; - } - - _hasChanges = true; - UpdateTitle(); - RefreshMapCanvas(); - MessageBox.Show($"{selectedNodes.Count}개 노드의 배경색이 변경되었습니다.", "완료", MessageBoxButtons.OK, MessageBoxIcon.Information); - } - } - } - - #endregion private void allTurnLeftRightCrossOnToolStripMenuItem_Click(object sender, EventArgs e) { //모든노드의 trun left/right/ cross 속성을 true로 변경합니다 - if (_mapNodes == null || _mapNodes.Count == 0) + if (this._mapCanvas.Nodes == null || this._mapCanvas.Nodes.Count == 0) { MessageBox.Show("맵에 노드가 없습니다.", "알림", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } var result = MessageBox.Show( - $"모든 노드({_mapNodes.Count}개)의 회전/교차 속성을 활성화하시겠습니까?\n\n" + + $"모든 노드({this._mapCanvas.Nodes.Count}개)의 회전/교차 속성을 활성화하시겠습니까?\n\n" + "• CanTurnLeft = true\n" + "• CanTurnRight = true\n" + "• DisableCross = false", @@ -1388,20 +1314,20 @@ namespace AGVMapEditor.Forms if (result == DialogResult.Yes) { - foreach (var node in _mapNodes) + foreach (var node in this._mapCanvas.Nodes) { node.CanTurnLeft = true; node.CanTurnRight = true; - node.DisableCross =false; + node.DisableCross = false; node.ModifiedDate = DateTime.Now; } _hasChanges = true; UpdateTitle(); RefreshMapCanvas(); - + MessageBox.Show( - $"{_mapNodes.Count}개 노드의 회전/교차 속성이 모두 활성화되었습니다.", + $"{this._mapCanvas.Nodes.Count}개 노드의 회전/교차 속성이 모두 활성화되었습니다.", "완료", MessageBoxButtons.OK, MessageBoxIcon.Information); diff --git a/Cs_HMI/AGVLogic/AGVNavigationCore/AGVNavigationCore.csproj b/Cs_HMI/AGVLogic/AGVNavigationCore/AGVNavigationCore.csproj index 94c9aef..1c5819f 100644 --- a/Cs_HMI/AGVLogic/AGVNavigationCore/AGVNavigationCore.csproj +++ b/Cs_HMI/AGVLogic/AGVNavigationCore/AGVNavigationCore.csproj @@ -78,7 +78,12 @@ + + + + + diff --git a/Cs_HMI/AGVLogic/AGVNavigationCore/Controls/IAGV.cs b/Cs_HMI/AGVLogic/AGVNavigationCore/Controls/IAGV.cs index 848bfd8..89da8bf 100644 --- a/Cs_HMI/AGVLogic/AGVNavigationCore/Controls/IAGV.cs +++ b/Cs_HMI/AGVLogic/AGVNavigationCore/Controls/IAGV.cs @@ -18,8 +18,8 @@ namespace AGVNavigationCore.Controls // 이동 경로 정보 추가 Point? PrevPosition { get; } - string CurrentNodeId { get; } - string PrevNodeId { get; } + MapNode CurrentNode { get; } + MapNode PrevNode { get; } DockingDirection DockingDirection { get; } } diff --git a/Cs_HMI/AGVLogic/AGVNavigationCore/Controls/UnifiedAGVCanvas.Events.cs b/Cs_HMI/AGVLogic/AGVNavigationCore/Controls/UnifiedAGVCanvas.Events.cs index b238943..26014c2 100644 --- a/Cs_HMI/AGVLogic/AGVNavigationCore/Controls/UnifiedAGVCanvas.Events.cs +++ b/Cs_HMI/AGVLogic/AGVNavigationCore/Controls/UnifiedAGVCanvas.Events.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Drawing; using System.Drawing.Drawing2D; using System.Linq; +using System.Runtime.Remoting.Channels; using System.Windows.Forms; namespace AGVNavigationCore.Controls @@ -60,7 +61,9 @@ namespace AGVNavigationCore.Controls DrawTemporaryConnection(g); } - // 노드 그리기 (라벨 제외) + // 노드 그리기 + DrawMarkSensors(g); + DrawImages(g); // 추가: 이미지 노드 DrawNodesOnly(g); // 드래그 고스트 그리기 (노드 위에 표시되도록 나중에 그리기) @@ -74,6 +77,7 @@ namespace AGVNavigationCore.Controls // 노드 라벨 그리기 (가장 나중 - 선이 텍스트를 가리지 않게) DrawNodeLabels(g); + DrawLabels(g); // 추가: 텍스트 라벨 } finally { @@ -195,15 +199,25 @@ namespace AGVNavigationCore.Controls { if (_nodes == null) return; + // 1. 일반 연결 그리기 foreach (var node in _nodes) { - if (node.ConnectedMapNodes == null) continue; - - foreach (var targetNode in node.ConnectedMapNodes) + if (node.ConnectedMapNodes != null) { - if (targetNode == null) continue; + foreach (var targetNode in node.ConnectedMapNodes) + { + if (targetNode == null) continue; + DrawConnection(g, node, targetNode); + } + } + } - DrawConnection(g, node, targetNode); + // 2. 마그넷 그리기 (별도 리스트 사용) + if (_magnets != null) + { + foreach (var magnet in _magnets) + { + DrawMagnet(g, magnet); } } } @@ -214,13 +228,128 @@ namespace AGVNavigationCore.Controls var endPoint = toNode.Position; // 강조된 연결인지 확인 - bool isHighlighted = IsConnectionHighlighted(fromNode.NodeId, toNode.NodeId); + bool isHighlighted = IsConnectionHighlighted(fromNode.Id, toNode.Id); + + // 펜 선택 + Pen pen = isHighlighted ? _highlightedConnectionPen : _connectionPen; - // 강조된 연결은 다른 색상으로 그리기 - var pen = isHighlighted ? _highlightedConnectionPen : _connectionPen; g.DrawLine(pen, startPoint, endPoint); } + private void DrawMagnet(Graphics g, MapMagnet magnet) + { + if (magnet == null) return; + + // 마그넷 좌표 + var startPoint = magnet.StartPoint; + var endPoint = magnet.EndPoint; + + if (magnet.ControlPoint != null) + { + // Quadratic Bezier Curve (2차 베지어 곡선) + // GDI+ DrawBezier는 Cubic(3차)이므로 2차 -> 3차 변환 필요 + // QP0 = Start, QP1 = Control, QP2 = End + // CP0 = QP0 + // CP1 = QP0 + (2/3) * (QP1 - QP0) + // CP2 = QP2 + (2/3) * (QP1 - QP2) + // CP3 = QP2 + + float qp0x = startPoint.X; + float qp0y = startPoint.Y; + float qp1x = (float)magnet.ControlPoint.X; + float qp1y = (float)magnet.ControlPoint.Y; + float qp2x = endPoint.X; + float qp2y = endPoint.Y; + + float cp1x = qp0x + (2.0f / 3.0f) * (qp1x - qp0x); + float cp1y = qp0y + (2.0f / 3.0f) * (qp1y - qp0y); + + float cp2x = qp2x + (2.0f / 3.0f) * (qp1x - qp2x); + float cp2y = qp2y + (2.0f / 3.0f) * (qp1y - qp2y); + + g.DrawBezier(_magnetPen, qp0x, qp0y, cp1x, cp1y, cp2x, cp2y, qp2x, qp2y); + } + else + { + // 직선 그리기 + g.DrawLine(_magnetPen, startPoint, endPoint); + } + + // 호버된 마그넷 강조 + if (magnet == _hoveredNode) + { + using (var highlightPen = new Pen(Color.Orange, 19)) + { + if (magnet.ControlPoint != null) + { + // Bezier calculation duplicate + float qp0x = startPoint.X; + float qp0y = startPoint.Y; + float qp1x = (float)magnet.ControlPoint.X; + float qp1y = (float)magnet.ControlPoint.Y; + float qp2x = endPoint.X; + float qp2y = endPoint.Y; + + float cp1x = qp0x + (2.0f / 3.0f) * (qp1x - qp0x); + float cp1y = qp0y + (2.0f / 3.0f) * (qp1y - qp0y); + + float cp2x = qp2x + (2.0f / 3.0f) * (qp1x - qp2x); + float cp2y = qp2y + (2.0f / 3.0f) * (qp1y - qp2y); + + g.DrawBezier(highlightPen, qp0x, qp0y, cp1x, cp1y, cp2x, cp2y, qp2x, qp2y); + } + else + { + g.DrawLine(highlightPen, startPoint, endPoint); + } + } + // Redraw normal to keep it on top? No, highlight is usually outer. + // If I draw highlight AFTER, it covers. + // But DrawMagnet is void. If I draw highlight after, it's fine if I want it to glow. + // Actually _magnetPen is Width 15, very thick. + // If I draw highlight Width 19 *before* normal, it acts as border. + // But this method draws normal first. + // So I should refactor to calculate path first, then draw? + // Or just draw highlight on top with alpha? + // Let's draw highlight on top with non-filled center? No, it's a line. + // I'll draw highlight on top for now, maybe with alpha. + } + } + + private void DrawMarkSensors(Graphics g) + { + if (_marks == null) return; // _marks 리스트 사용 + + int sensorSize = 12; // 크기 설정 + int lineLength = 20; // 선 길이 설정 + int halfLength = lineLength / 2; + + foreach (var mark in _marks) + { + Point p = mark.Position; + double radians = mark.Rotation * Math.PI / 180.0; + + // 회전 각도에 따른 시점과 종점 계산 + // 마크는 직선 형태로 표시됨 + int dx = (int)(halfLength * Math.Cos(radians)); + int dy = (int)(halfLength * Math.Sin(radians)); + + Point p1 = new Point(p.X - dx, p.Y - dy); + Point p2 = new Point(p.X + dx, p.Y + dy); + + g.DrawLine(_markPen, p1, p2); + + // 호버된 마크 강조 + if (mark == _hoveredNode) + { + using (var highlightPen = new Pen(Color.Orange, 5)) + { + g.DrawLine(highlightPen, p1, p2); + } + } + } + } + /// /// 연결이 강조 표시되어야 하는지 확인 /// @@ -338,8 +467,8 @@ namespace AGVNavigationCore.Controls if (currentNode == null || nextNode == null) continue; - var currentNodeId = currentNode.NodeId; - var nextNodeId = nextNode.NodeId; + var currentNodeId = currentNode.Id; + var nextNodeId = nextNode.Id; // 왕복 구간 키 생성 (양방향 모두 같은 키) var segmentKey = string.Compare(currentNodeId, nextNodeId) < 0 @@ -476,53 +605,37 @@ namespace AGVNavigationCore.Controls /// private void DrawDragGhost(Graphics g) { - if (_selectedNode == null || !_isDragging) return; + if (!_isDragging) return; - // 반투명 효과를 위한 브러시 생성 - Brush ghostBrush = null; - switch (_selectedNode.Type) + if (_selectedNode != null) { - case NodeType.Normal: - ghostBrush = new SolidBrush(Color.FromArgb(120, 100, 149, 237)); // 반투명 파란색 - break; - case NodeType.Loader: - case NodeType.UnLoader: - case NodeType.Clearner: - case NodeType.Buffer: - ghostBrush = new SolidBrush(Color.FromArgb(120, 50, 205, 50)); // 반투명 초록색 - break; - case NodeType.Charging: - ghostBrush = new SolidBrush(Color.FromArgb(120, 255, 215, 0)); // 반투명 금색 - break; - default: - ghostBrush = new SolidBrush(Color.FromArgb(120, 200, 200, 200)); // 반투명 회색 - break; + // 반투명 효과를 위한 브러시 생성 + Brush ghostBrush = new SolidBrush(Color.FromArgb(120, 200, 200, 200)); // 반투명 회색 + + // 고스트 노드 그리기 + switch (_selectedNode.Type) + { + case NodeType.Normal: + var item = _selectedNode as MapNode; + if (item.StationType == StationType.Charger) + DrawTriangleGhost(g, ghostBrush); + else + DrawPentagonGhost(g, ghostBrush); + break; + case NodeType.Label: + DrawLabelGhost(g, SelectedLabel); + break; + case NodeType.Image: + DrawImageGhost(g, SelectedImage); + break; + default: //mark, magnet + DrawCircleGhost(g, ghostBrush); + break; + } + + ghostBrush?.Dispose(); } - // 고스트 노드 그리기 - switch (_selectedNode.Type) - { - case NodeType.Label: - DrawLabelGhost(g); - break; - case NodeType.Image: - DrawImageGhost(g); - break; - case NodeType.Loader: - case NodeType.UnLoader: - case NodeType.Clearner: - case NodeType.Buffer: - DrawPentagonGhost(g, ghostBrush); - break; - case NodeType.Charging: - DrawTriangleGhost(g, ghostBrush); - break; - default: - DrawCircleGhost(g, ghostBrush); - break; - } - - ghostBrush?.Dispose(); } private void DrawCircleGhost(Graphics g, Brush ghostBrush) @@ -595,61 +708,61 @@ namespace AGVNavigationCore.Controls g.DrawPolygon(new Pen(Color.FromArgb(200, 255, 0, 0), 1), outerPoints); } - private void DrawLabelGhost(Graphics g) + private void DrawLabelGhost(Graphics g, MapLabel label) { - var text = string.IsNullOrEmpty(_selectedNode.LabelText) ? _selectedNode.NodeId : _selectedNode.LabelText; - var font = new Font(_selectedNode.FontFamily, _selectedNode.FontSize, _selectedNode.FontStyle); - var textBrush = new SolidBrush(Color.FromArgb(120, _selectedNode.ForeColor)); - var textSize = g.MeasureString(text, font); - var textPoint = new Point( - (int)(_dragStartPosition.X - textSize.Width / 2), - (int)(_dragStartPosition.Y - textSize.Height / 2) - ); - - if (_selectedNode.ShowBackground) + var text = string.IsNullOrEmpty(label.Text) ? label.Id : label.Text; + using (var font = new Font(label.FontFamily, label.FontSize, label.FontStyle)) + using (var textBrush = new SolidBrush(Color.FromArgb(120, label.ForeColor))) { - var backgroundBrush = new SolidBrush(Color.FromArgb(120, _selectedNode.BackColor)); - var backgroundRect = new Rectangle( - textPoint.X - _selectedNode.Padding, - textPoint.Y - _selectedNode.Padding, - (int)textSize.Width + (_selectedNode.Padding * 2), - (int)textSize.Height + (_selectedNode.Padding * 2) + var textSize = g.MeasureString(text, font); + var textPoint = new Point( + (int)(_dragStartPosition.X - textSize.Width / 2), + (int)(_dragStartPosition.Y - textSize.Height / 2) ); - g.FillRectangle(backgroundBrush, backgroundRect); - g.DrawRectangle(new Pen(Color.FromArgb(180, 128, 128, 128), 2) { DashStyle = DashStyle.Dash }, backgroundRect); - backgroundBrush.Dispose(); - } - g.DrawString(text, font, textBrush, textPoint); + if (label.BackColor != Color.Transparent) + { + using (var backgroundBrush = new SolidBrush(Color.FromArgb(120, label.BackColor))) + { + var backgroundRect = new Rectangle( + textPoint.X - label.Padding, + textPoint.Y - label.Padding, + (int)textSize.Width + (label.Padding * 2), + (int)textSize.Height + (label.Padding * 2) + ); + g.FillRectangle(backgroundBrush, backgroundRect); + g.DrawRectangle(new Pen(Color.FromArgb(180, 128, 128, 128), 2) { DashStyle = DashStyle.Dash }, backgroundRect); + } + } - // 배경이 없어도 테두리 표시 - if (!_selectedNode.ShowBackground) - { - var borderRect = new Rectangle( - textPoint.X - 2, - textPoint.Y - 2, - (int)textSize.Width + 4, - (int)textSize.Height + 4 + g.DrawString(text, font, textBrush, textPoint); + + // 배경이 없어도 테두리 표시 + if (label.BackColor != Color.Transparent) + { + var borderRect = new Rectangle( + textPoint.X - 2, + textPoint.Y - 2, + (int)textSize.Width + 4, + (int)textSize.Height + 4 + ); + g.DrawRectangle(new Pen(Color.FromArgb(180, 128, 128, 128), 2) { DashStyle = DashStyle.Dash }, borderRect); + } + + // 빨간색 외곽 테두리 (디버깅용) + var outerRect = new Rectangle( + textPoint.X - 4, + textPoint.Y - 4, + (int)textSize.Width + 8, + (int)textSize.Height + 8 ); - g.DrawRectangle(new Pen(Color.FromArgb(180, 128, 128, 128), 2) { DashStyle = DashStyle.Dash }, borderRect); + g.DrawRectangle(new Pen(Color.FromArgb(200, 255, 0, 0), 1), outerRect); } - - // 빨간색 외곽 테두리 (디버깅용) - var outerRect = new Rectangle( - textPoint.X - 4, - textPoint.Y - 4, - (int)textSize.Width + 8, - (int)textSize.Height + 8 - ); - g.DrawRectangle(new Pen(Color.FromArgb(200, 255, 0, 0), 1), outerRect); - - font.Dispose(); - textBrush.Dispose(); } - private void DrawImageGhost(Graphics g) + private void DrawImageGhost(Graphics g, MapImage image) { - var displaySize = _selectedNode.GetDisplaySize(); + var displaySize = image.GetDisplaySize(); if (displaySize.IsEmpty) displaySize = new Size(50, 50); @@ -672,66 +785,43 @@ namespace AGVNavigationCore.Controls g.DrawRectangle(new Pen(Color.FromArgb(200, 255, 0, 0), 1), outerRect); } - private void DrawNodesOnly(Graphics g) - { - if (_nodes == null) return; - - foreach (var node in _nodes) - { - DrawNodeShape(g, node); - } - } - private void DrawNodeLabels(Graphics g) { if (_nodes == null) return; foreach (var node in _nodes) { - // Label과 Image 노드는 자체적으로 텍스트 포함, 다른 노드는 별도 라벨 - if (node.Type != NodeType.Label && node.Type != NodeType.Image) + // 일반 노드 라벨 그리기 + DrawNodeLabel(g, node); + } + } + + private void DrawNodesOnly(Graphics g) + { + if (_nodes == null) return; + + foreach (var node in _nodes) + { + var brush = GetNodeBrush(node); + + switch (node.StationType) { - DrawNodeLabel(g, node); + case StationType.Loader: + case StationType.UnLoader: + case StationType.Clearner: + case StationType.Buffer: + DrawPentagonNodeShape(g, node, brush); + break; + case StationType.Charger: + DrawTriangleNodeShape(g, node, brush); + break; + default: + DrawCircleNodeShape(g, node, brush); + break; } } } - private void DrawNodeShape(Graphics g, MapNode node) - { - switch (node.Type) - { - case NodeType.Label: - DrawLabelNode(g, node); // Label 노드는 텍스트 포함 - break; - case NodeType.Image: - DrawImageNode(g, node); // Image 노드는 텍스트 포함 - break; - default: - DrawCircularNodeShape(g, node); // 다른 노드는 도형만 - break; - } - } - - private void DrawCircularNodeShape(Graphics g, MapNode node) - { - var brush = GetNodeBrush(node); - - switch (node.Type) - { - case NodeType.Loader: - case NodeType.UnLoader: - case NodeType.Clearner: - case NodeType.Buffer: - DrawPentagonNodeShape(g, node, brush); - break; - case NodeType.Charging: - DrawTriangleNodeShape(g, node, brush); - break; - default: - DrawCircleNodeShape(g, node, brush); - break; - } - } private void DrawCircleNodeShape(Graphics g, MapNode node, Brush brush) { @@ -794,13 +884,13 @@ namespace AGVNavigationCore.Controls } // RFID 중복 노드 표시 (빨간 X자) - if (_duplicateRfidNodes.Contains(node.NodeId)) + if (_duplicateRfidNodes.Contains(node.Id)) { DrawDuplicateRfidMarker(g, node); } // CanCross 가능 노드 표시 (교차지점으로 사용 가능) - if (node.DisableCross==true) + if (node.DisableCross == true) { var crossRect = new Rectangle(rect.X - 3, rect.Y - 3, rect.Width + 6, rect.Height + 6); g.DrawEllipse(new Pen(Color.DeepSkyBlue, 3), crossRect); @@ -904,13 +994,13 @@ namespace AGVNavigationCore.Controls } // RFID 중복 노드 표시 (빨간 X자) - if (_duplicateRfidNodes.Contains(node.NodeId)) + if (_duplicateRfidNodes.Contains(node.Id)) { DrawDuplicateRfidMarker(g, node); } // CanCross 가능 노드 표시 (교차지점으로 사용 가능) - if (node.DisableCross==false) + if (node.DisableCross == false) { var crossPoints = new Point[5]; for (int i = 0; i < 5; i++) @@ -1022,13 +1112,13 @@ namespace AGVNavigationCore.Controls } // RFID 중복 노드 표시 (빨간 X자) - if (_duplicateRfidNodes.Contains(node.NodeId)) + if (_duplicateRfidNodes.Contains(node.Id)) { DrawDuplicateRfidMarker(g, node); } // CanCross 가능 노드 표시 (교차지점으로 사용 가능) - if (node.DisableCross==false) + if (node.DisableCross == false) { var crossPoints = new Point[3]; for (int i = 0; i < 3; i++) @@ -1045,78 +1135,90 @@ namespace AGVNavigationCore.Controls private void DrawNodeLabel(Graphics g, MapNode node) { - string displayText; - Color textColor; - string descriptionText; + + Color textColor = Color.White; + // 위쪽에 표시할 이름 (노드의 Name 속성) - descriptionText = node.Name.EndsWith(node.NodeId) ? string.Empty : node.Name; - + string TopIDText = node.HasRfid() ? node.RfidId : $"[{node.Id}]"; // 아래쪽에 표시할 값 (RFID 우선, 없으면 노드ID) - if (node.HasRfid()) - { - // RFID가 있는 경우: 순수 RFID 값만 표시 (노드 전경색 사용) - displayText = node.RfidId; - textColor = node.ForeColor; - } - else - { - // RFID가 없는 경우: 노드 ID 표시 (노드 전경색의 50% 투명도) - displayText = node.NodeId; - textColor = Color.FromArgb(128, node.ForeColor); - } + string BottomLabelText = node.Text; // 🔥 노드의 폰트 설정 사용 (0 이하일 경우 기본값 7.0f 사용) - var fontStyle = node.TextFontBold ? FontStyle.Bold : FontStyle.Regular; - var fontSize = node.TextFontSize > 0 ? node.TextFontSize : 7.0f; - var font = new Font("Arial", fontSize, fontStyle); - var descFont = new Font("Arial", fontSize + 1, fontStyle); + var topFont = new Font("Arial", 9, FontStyle.Bold); + var btmFont = new Font("Arial", 12, FontStyle.Bold); // 메인 텍스트 크기 측정 - var textSize = g.MeasureString(displayText, font); - var descSize = g.MeasureString(descriptionText, descFont); + var TopSize = g.MeasureString(TopIDText, topFont); + var BtmSize = g.MeasureString(BottomLabelText, btmFont); // 메인 텍스트 위치 (RFID는 노드 위쪽) - var textPoint = new Point( - (int)(node.Position.X - textSize.Width / 2), - (int)(node.Position.Y - NODE_RADIUS - textSize.Height - 2) + var topPoint = new Point( + (int)(node.Position.X - TopSize.Width / 2), + (int)(node.Position.Y - NODE_RADIUS - TopSize.Height - 2) ); // 설명 텍스트 위치 (설명은 노드 아래쪽) - var descPoint = new Point( - (int)(node.Position.X - descSize.Width / 2), + var btmPoint = new Point( + (int)(node.Position.X - BtmSize.Width / 2), (int)(node.Position.Y + NODE_RADIUS + 2) - ); + ); // 설명 텍스트 그리기 (설명이 있는 경우에만) - if (!string.IsNullOrEmpty(descriptionText)) + if (!string.IsNullOrEmpty(BottomLabelText)) { // 🔥 노드의 말풍선 글자색 사용 (NameBubbleForeColor) - Color descColor = node.NameBubbleForeColor; + Color fgColor = Color.Black; + Color bgColor = Color.White; + switch (node.StationType) + { + case StationType.Charger: + fgColor = Color.White; + bgColor = Color.Tomato; + break; + case StationType.Buffer: + fgColor = Color.Black; + bgColor = Color.White; + break; + case StationType.Clearner: + fgColor = Color.Black; + bgColor = Color.DeepSkyBlue; + break; + case StationType.Loader: + case StationType.UnLoader: + fgColor = Color.Black; + bgColor = Color.Gold; + break; + default: + fgColor = Color.Black; + break; + } + + var rectpaddingx = 4; var rectpaddingy = 2; - var roundRect = new Rectangle((int)(descPoint.X - rectpaddingx), - (int)(descPoint.Y), - (int)descSize.Width + rectpaddingx * 2, - (int)descSize.Height + rectpaddingy * 2); + var roundRect = new Rectangle((int)(btmPoint.X - rectpaddingx), + (int)(btmPoint.Y), + (int)BtmSize.Width + rectpaddingx * 2, + (int)BtmSize.Height + rectpaddingy * 2); // 라운드 사각형 그리기 (노드 이름 말풍선 배경색 사용) - using (var backgroundBrush = new SolidBrush(node.NameBubbleBackColor)) + using (var backgroundBrush = new SolidBrush(bgColor)) { DrawRoundedRectangle(g, backgroundBrush, roundRect, 3); // 모서리 반지름 3px } // 라운드 사각형 테두리 그리기 (진한 빨간색) - using (var borderPen = new Pen(Color.DarkRed, 1)) + using (var borderPen = new Pen(Color.DimGray, 1)) { DrawRoundedRectangleBorder(g, borderPen, roundRect, 3); } - using (var descBrush = new SolidBrush(descColor)) + using (var descBrush = new SolidBrush(fgColor)) { - g.DrawString(descriptionText, descFont, descBrush, roundRect, new StringFormat + g.DrawString(BottomLabelText, btmFont, descBrush, roundRect, new StringFormat { Alignment = StringAlignment.Center, LineAlignment = StringAlignment.Center, @@ -1125,149 +1227,158 @@ namespace AGVNavigationCore.Controls } // 메인 텍스트 그리기 (RFID 중복인 경우 특별 처리) - if (node.HasRfid() && _duplicateRfidNodes.Contains(node.NodeId)) + if (node.HasRfid() && _duplicateRfidNodes.Contains(node.Id)) { // 중복 RFID 노드: 빨간 배경의 라운드 사각형 - DrawDuplicateRfidLabel(g, displayText, textPoint, font); + DrawDuplicateRfidLabel(g, TopIDText, topPoint, topFont); } else { // 일반 텍스트 그리기 using (var textBrush = new SolidBrush(textColor)) { - g.DrawString(displayText, font, textBrush, textPoint); + g.DrawString(TopIDText, topFont, textBrush, topPoint); } } - font.Dispose(); - descFont.Dispose(); + topFont.Dispose(); + btmFont.Dispose(); + } + private void DrawLabels(Graphics g) + { + if (_labels == null) return; + foreach (var label in _labels) + { + DrawLabel(g, label); + } } - private void DrawLabelNode(Graphics g, MapNode node) + private void DrawImages(Graphics g) { - // 드래그 중인 노드 확인 - bool isDraggingThisNode = _isDragging && node == _selectedNode; + if (_images == null) return; + foreach (var image in _images) + { + DrawImage(g, image); + } + } - var text = string.IsNullOrEmpty(node.LabelText) ? node.NodeId : node.LabelText; + private void DrawLabel(Graphics g, MapLabel label) + { + // 드래그 중인 라벨 확인 (TODO: NodeBase 선택/드래그 로직 통합 시 수정 필요) + bool isDraggingThisLabel = _isDragging && label == SelectedLabel; + var text = string.IsNullOrEmpty(label.Text) ? label.Id : label.Text; // 폰트 설정 - var font = new Font(node.FontFamily, node.FontSize, node.FontStyle); - var textBrush = new SolidBrush(node.ForeColor); - - // 텍스트 크기 측정 - var textSize = g.MeasureString(text, font); - var textPoint = new Point( - (int)(node.Position.X - textSize.Width / 2), - (int)(node.Position.Y - textSize.Height / 2) - ); - - // 드래그 중일 때 그림자 효과 - if (isDraggingThisNode) + using (var font = new Font(label.FontFamily, label.FontSize, label.FontStyle)) + using (var textBrush = new SolidBrush(label.ForeColor)) // MapLabel.ForeColor (NodeTextForeColor -> ForeColor) { - var shadowPoint = new Point(textPoint.X + 3, textPoint.Y + 3); - using (var shadowBrush = new SolidBrush(Color.FromArgb(100, 0, 0, 0))) + // 텍스트 크기 측정 + var textSize = g.MeasureString(text, font); + var textPoint = new Point( + (int)(label.Position.X - textSize.Width / 2), + (int)(label.Position.Y - textSize.Height / 2) + ); + + // 드래그 중일 때 그림자 효과 + if (isDraggingThisLabel) { - g.DrawString(text, font, shadowBrush, shadowPoint); + var shadowPoint = new Point(textPoint.X + 3, textPoint.Y + 3); + using (var shadowBrush = new SolidBrush(Color.FromArgb(100, 0, 0, 0))) + { + g.DrawString(text, font, shadowBrush, shadowPoint); + } + } + + // 배경 그리기 (설정된 경우) + if (label.BackColor != Color.Transparent) + { + using (var backgroundBrush = new SolidBrush(label.BackColor)) + { + var backgroundRect = new Rectangle( + textPoint.X - label.Padding, + textPoint.Y - label.Padding, + (int)textSize.Width + (label.Padding * 2), + (int)textSize.Height + (label.Padding * 2) + ); + g.FillRectangle(backgroundBrush, backgroundRect); + g.DrawRectangle(Pens.Black, backgroundRect); + } + } + + // 텍스트 그리기 + g.DrawString(text, font, textBrush, textPoint); + + // 드래그 중인 노드 강조 + if (isDraggingThisLabel) + { + var dragPadding = label.Padding + 4; + var dragRect = new Rectangle( + textPoint.X - dragPadding, + textPoint.Y - dragPadding, + (int)textSize.Width + (dragPadding * 2), + (int)textSize.Height + (dragPadding * 2) + ); + g.DrawRectangle(new Pen(Color.Cyan, 3), dragRect); + } + // 선택된 노드 강조 + else if (label == SelectedLabel) + { + var selectionPadding = label.Padding + 2; + var selectionRect = new Rectangle( + textPoint.X - selectionPadding, + textPoint.Y - selectionPadding, + (int)textSize.Width + (selectionPadding * 2), + (int)textSize.Height + (selectionPadding * 2) + ); + g.DrawRectangle(_selectedNodePen, selectionRect); + } + // 호버된 라벨 강조 + else if (label == _hoveredNode) + { + var hoverPadding = label.Padding + 2; + var hoverRect = new Rectangle( + textPoint.X - hoverPadding, + textPoint.Y - hoverPadding, + (int)textSize.Width + (hoverPadding * 2), + (int)textSize.Height + (hoverPadding * 2) + ); + g.DrawRectangle(new Pen(Color.Orange, 2), hoverRect); } } - - // 배경 그리기 (설정된 경우) - if (node.ShowBackground) - { - var backgroundBrush = new SolidBrush(node.BackColor); - var backgroundRect = new Rectangle( - textPoint.X - node.Padding, - textPoint.Y - node.Padding, - (int)textSize.Width + (node.Padding * 2), - (int)textSize.Height + (node.Padding * 2) - ); - g.FillRectangle(backgroundBrush, backgroundRect); - g.DrawRectangle(Pens.Black, backgroundRect); - backgroundBrush.Dispose(); - } - - // 텍스트 그리기 - g.DrawString(text, font, textBrush, textPoint); - - // 드래그 중인 노드 강조 (가장 강력한 효과) - if (isDraggingThisNode) - { - var dragPadding = node.Padding + 4; - var dragRect = new Rectangle( - textPoint.X - dragPadding, - textPoint.Y - dragPadding, - (int)textSize.Width + (dragPadding * 2), - (int)textSize.Height + (dragPadding * 2) - ); - g.DrawRectangle(new Pen(Color.Cyan, 3), dragRect); - - // 펄스 효과 - var pulseRect = new Rectangle(dragRect.X - 2, dragRect.Y - 2, dragRect.Width + 4, dragRect.Height + 4); - g.DrawRectangle(new Pen(Color.FromArgb(150, 0, 255, 255), 2) { DashStyle = DashStyle.Dash }, pulseRect); - } - // 선택된 노드 강조 - else if (node == _selectedNode) - { - var selectionPadding = node.Padding + 2; - var selectionRect = new Rectangle( - textPoint.X - selectionPadding, - textPoint.Y - selectionPadding, - (int)textSize.Width + (selectionPadding * 2), - (int)textSize.Height + (selectionPadding * 2) - ); - g.DrawRectangle(_selectedNodePen, selectionRect); - } - - // 호버된 노드 강조 (드래그 중이 아닐 때만) - if (node == _hoveredNode && !isDraggingThisNode) - { - var hoverPadding = node.Padding + 4; - var hoverRect = new Rectangle( - textPoint.X - hoverPadding, - textPoint.Y - hoverPadding, - (int)textSize.Width + (hoverPadding * 2), - (int)textSize.Height + (hoverPadding * 2) - ); - g.DrawRectangle(new Pen(Color.Orange, 2), hoverRect); - } - - font.Dispose(); - textBrush.Dispose(); } - private void DrawImageNode(Graphics g, MapNode node) + private void DrawImage(Graphics g, MapImage image) { - // 드래그 중인 노드 확인 - bool isDraggingThisNode = _isDragging && node == _selectedNode; + bool isDraggingThisImage = _isDragging && image == SelectedImage; // 이미지 로드 (필요시) - if (node.LoadedImage == null && !string.IsNullOrEmpty(node.ImagePath)) + if (image.LoadedImage == null && !string.IsNullOrEmpty(image.ImagePath)) { - node.LoadImage(); + image.LoadImage(); } - if (node.LoadedImage != null) + if (image.LoadedImage != null) { // 실제 표시 크기 계산 - var displaySize = node.GetDisplaySize(); + var displaySize = image.GetDisplaySize(); if (displaySize.IsEmpty) displaySize = new Size(50, 50); // 기본 크기 // 드래그 중일 때 약간 크게 표시 - if (isDraggingThisNode) + if (isDraggingThisImage) { displaySize = new Size((int)(displaySize.Width * 1.1), (int)(displaySize.Height * 1.1)); } var imageRect = new Rectangle( - node.Position.X - displaySize.Width / 2, - node.Position.Y - displaySize.Height / 2, + image.Position.X - displaySize.Width / 2, + image.Position.Y - displaySize.Height / 2, displaySize.Width, displaySize.Height ); // 드래그 중일 때 그림자 효과 - if (isDraggingThisNode) + if (isDraggingThisImage) { var shadowRect = new Rectangle(imageRect.X + 3, imageRect.Y + 3, imageRect.Width, imageRect.Height); using (var shadowBrush = new SolidBrush(Color.FromArgb(100, 0, 0, 0))) @@ -1277,128 +1388,74 @@ namespace AGVNavigationCore.Controls } // 회전이 있는 경우 - if (node.Rotation != 0) + if (image.Rotation != 0) { var oldTransform = g.Transform; - g.TranslateTransform(node.Position.X, node.Position.Y); - g.RotateTransform(node.Rotation); - g.TranslateTransform(-node.Position.X, -node.Position.Y); + g.TranslateTransform(image.Position.X, image.Position.Y); + g.RotateTransform(image.Rotation); + g.TranslateTransform(-image.Position.X, -image.Position.Y); - // 투명도 적용하여 이미지 그리기 - if (node.Opacity < 1.0f) - { - var imageAttributes = new System.Drawing.Imaging.ImageAttributes(); - var colorMatrix = new System.Drawing.Imaging.ColorMatrix(); - colorMatrix.Matrix33 = node.Opacity; - imageAttributes.SetColorMatrix(colorMatrix, System.Drawing.Imaging.ColorMatrixFlag.Default, - System.Drawing.Imaging.ColorAdjustType.Bitmap); - g.DrawImage(node.LoadedImage, imageRect, 0, 0, node.LoadedImage.Width, node.LoadedImage.Height, - GraphicsUnit.Pixel, imageAttributes); - imageAttributes.Dispose(); - } - else - { - g.DrawImage(node.LoadedImage, imageRect); - } + DrawImageContent(g, image, imageRect); g.Transform = oldTransform; } else { - // 투명도 적용하여 이미지 그리기 - if (node.Opacity < 1.0f) - { - var imageAttributes = new System.Drawing.Imaging.ImageAttributes(); - var colorMatrix = new System.Drawing.Imaging.ColorMatrix(); - colorMatrix.Matrix33 = node.Opacity; - imageAttributes.SetColorMatrix(colorMatrix, System.Drawing.Imaging.ColorMatrixFlag.Default, - System.Drawing.Imaging.ColorAdjustType.Bitmap); - g.DrawImage(node.LoadedImage, imageRect, 0, 0, node.LoadedImage.Width, node.LoadedImage.Height, - GraphicsUnit.Pixel, imageAttributes); - imageAttributes.Dispose(); - } - else - { - g.DrawImage(node.LoadedImage, imageRect); - } + DrawImageContent(g, image, imageRect); } - // 드래그 중인 노드 강조 (가장 강력한 효과) - if (isDraggingThisNode) + // 선택/드래그 효과 + if (isDraggingThisImage) { g.DrawRectangle(new Pen(Color.Cyan, 3), imageRect); - var pulseRect = new Rectangle(imageRect.X - 3, imageRect.Y - 3, imageRect.Width + 6, imageRect.Height + 6); - g.DrawRectangle(new Pen(Color.FromArgb(150, 0, 255, 255), 2) { DashStyle = DashStyle.Dash }, pulseRect); } - // 선택된 노드 강조 - else if (node == _selectedNode) + else if (image == SelectedImage) { g.DrawRectangle(_selectedNodePen, imageRect); } - - // 호버된 노드 강조 (드래그 중이 아닐 때만) - if (node == _hoveredNode && !isDraggingThisNode) + // 호버된 이미지 강조 + else if (image == _hoveredNode) { - var hoverRect = new Rectangle(imageRect.X - 2, imageRect.Y - 2, imageRect.Width + 4, imageRect.Height + 4); - g.DrawRectangle(new Pen(Color.Orange, 2), hoverRect); + g.DrawRectangle(new Pen(Color.Orange, 2), imageRect); } - } else { - // 이미지가 없는 경우 기본 사각형으로 표시 - int sizeAdjustment = isDraggingThisNode ? 5 : 0; - var rect = new Rectangle( - node.Position.X - 25 - sizeAdjustment, - node.Position.Y - 25 - sizeAdjustment, - 50 + sizeAdjustment * 2, - 50 + sizeAdjustment * 2 - ); - - // 드래그 중일 때 그림자 효과 - if (isDraggingThisNode) - { - var shadowRect = new Rectangle(rect.X + 3, rect.Y + 3, rect.Width, rect.Height); - using (var shadowBrush = new SolidBrush(Color.FromArgb(100, 0, 0, 0))) - { - g.FillRectangle(shadowBrush, shadowRect); - } - } - + // 이미지가 없는 경우 표시 로직 (기존과 유사하게 구현) + var rect = new Rectangle(image.Position.X - 25, image.Position.Y - 25, 50, 50); g.FillRectangle(Brushes.LightGray, rect); g.DrawRectangle(Pens.Black, rect); + using (var font = new Font("Arial", 8)) + g.DrawString("No Image", font, Brushes.Black, rect); - // "이미지 없음" 텍스트 - var font = new Font("Arial", 8); - var text = "No Image"; - var textSize = g.MeasureString(text, font); - var textPoint = new Point( - (int)(node.Position.X - textSize.Width / 2), - (int)(node.Position.Y - textSize.Height / 2) - ); - g.DrawString(text, font, Brushes.Black, textPoint); - font.Dispose(); + if (image == SelectedImage) g.DrawRectangle(_selectedNodePen, rect); + // 호버된 이미지 강조 (No Image case) + else if (image == _hoveredNode) + { + g.DrawRectangle(new Pen(Color.Orange, 2), rect); + } + } + } - // 드래그 중인 노드 강조 (가장 강력한 효과) - if (isDraggingThisNode) + private void DrawImageContent(Graphics g, MapImage image, Rectangle rect) + { + // 투명도 적용하여 이미지 그리기 + if (image.Opacity < 1.0f) + { + using (var imageAttributes = new System.Drawing.Imaging.ImageAttributes()) { - g.DrawRectangle(new Pen(Color.Cyan, 3), rect); - var pulseRect = new Rectangle(rect.X - 3, rect.Y - 3, rect.Width + 6, rect.Height + 6); - g.DrawRectangle(new Pen(Color.FromArgb(150, 0, 255, 255), 2) { DashStyle = DashStyle.Dash }, pulseRect); - } - // 선택된 노드 강조 - else if (node == _selectedNode) - { - g.DrawRectangle(_selectedNodePen, rect); - } - - // 호버된 노드 강조 (드래그 중이 아닐 때만) - if (node == _hoveredNode && !isDraggingThisNode) - { - var hoverRect = new Rectangle(rect.X - 2, rect.Y - 2, rect.Width + 4, rect.Height + 4); - g.DrawRectangle(new Pen(Color.Orange, 2), hoverRect); + var colorMatrix = new System.Drawing.Imaging.ColorMatrix(); + colorMatrix.Matrix33 = image.Opacity; + imageAttributes.SetColorMatrix(colorMatrix, System.Drawing.Imaging.ColorMatrixFlag.Default, + System.Drawing.Imaging.ColorAdjustType.Bitmap); + g.DrawImage(image.LoadedImage, rect, 0, 0, image.LoadedImage.Width, image.LoadedImage.Height, + GraphicsUnit.Pixel, imageAttributes); } } + else + { + g.DrawImage(image.LoadedImage, rect); + } } private Brush GetNodeBrush(MapNode node) @@ -1407,12 +1464,17 @@ namespace AGVNavigationCore.Controls // RFID가 없는 노드는 DisplayColor를 50% 투명도로 표시 bool hasRfid = node.HasRfid(); - Color bgColor = node.DisplayColor; + Color bgColor = Color.Transparent; - // RFID가 없는 경우 투명도 50% - if (!hasRfid) + switch (node.StationType) { - bgColor = Color.FromArgb(128, bgColor); + case StationType.Normal: bgColor = Color.DeepSkyBlue; break; + case StationType.Charger: bgColor = Color.Tomato; break; + case StationType.Loader: + case StationType.UnLoader: bgColor = Color.Gold ; break; + case StationType.Clearner: bgColor = Color.DeepSkyBlue ; break; + case StationType.Buffer: bgColor = Color.WhiteSmoke; break; + default: bgColor = Color.White; break; } return new SolidBrush(bgColor); @@ -1779,7 +1841,7 @@ namespace AGVNavigationCore.Controls // 여기서는 AGVState에 StopMark가 없으므로, 외부에서 상태를 Error로 설정하거나 // 별도의 플래그를 확인해야 함. 하지만 IAGV 인터페이스에는 플래그가 없음. // 따라서 fMain에서 상태를 Error로 설정하는 것을 권장. - + switch (state) { case AGVState.Moving: return Color.White; @@ -2092,7 +2154,7 @@ namespace AGVNavigationCore.Controls g.DrawString(scaleText, font, Brushes.Black, scaleRect.X + 5, scaleRect.Y + 2); } - + // 측정 정보 (우하단 - 사용자 정의 정보가 있을 경우) if (!string.IsNullOrEmpty(_measurementInfo)) { diff --git a/Cs_HMI/AGVLogic/AGVNavigationCore/Controls/UnifiedAGVCanvas.Mouse.cs b/Cs_HMI/AGVLogic/AGVNavigationCore/Controls/UnifiedAGVCanvas.Mouse.cs index 77e2aec..cdfd576 100644 --- a/Cs_HMI/AGVLogic/AGVNavigationCore/Controls/UnifiedAGVCanvas.Mouse.cs +++ b/Cs_HMI/AGVLogic/AGVNavigationCore/Controls/UnifiedAGVCanvas.Mouse.cs @@ -1,8 +1,10 @@ +using AGVNavigationCore.Models; using System; +using System.Collections.Generic; using System.Drawing; using System.Linq; using System.Windows.Forms; -using AGVNavigationCore.Models; +using System.Xml.Linq; namespace AGVNavigationCore.Controls { @@ -15,7 +17,7 @@ namespace AGVNavigationCore.Controls Focus(); // 포커스 설정 var worldPoint = ScreenToWorld(e.Location); - var hitNode = GetNodeAt(worldPoint); + var hitNode = GetItemAt(worldPoint); // 에뮬레이터 모드 처리 if (_canvasMode == CanvasMode.Emulator) @@ -48,7 +50,11 @@ namespace AGVNavigationCore.Controls // 마지막 선택된 노드 업데이트 (단일 참조용) _selectedNode = _selectedNodes.Count > 0 ? _selectedNodes[_selectedNodes.Count - 1] : null; - // 다중 선택 이벤트만 발생 (OnNodesSelected에서 단일/다중 구분 처리) + // 단일/다중 선택 이벤트 발생 + if (_selectedNodes.Count == 1) + { + NodeSelect?.Invoke(this, _selectedNodes[0], e); + } NodesSelected?.Invoke(this, _selectedNodes); Invalidate(); } @@ -59,7 +65,8 @@ namespace AGVNavigationCore.Controls _selectedNodes.Clear(); _selectedNodes.Add(hitNode); - // NodesSelected 이벤트만 발생 (OnNodesSelected에서 단일/다중 구분 처리) + // 단일/다중 선택 이벤트 발생 + NodeSelect?.Invoke(this, hitNode, e); NodesSelected?.Invoke(this, _selectedNodes); Invalidate(); } @@ -94,7 +101,7 @@ namespace AGVNavigationCore.Controls break; case EditMode.Connect: - HandleConnectClick(hitNode); + HandleConnectClick(hitNode as MapNode); break; case EditMode.Delete: @@ -110,38 +117,29 @@ namespace AGVNavigationCore.Controls private void UnifiedAGVCanvas_MouseDoubleClick(object sender, MouseEventArgs e) { var worldPoint = ScreenToWorld(e.Location); - var hitNode = GetNodeAt(worldPoint); + var hitNode = GetItemAt(worldPoint); - if (hitNode != null) + if (hitNode == null) return; + + if (hitNode.Type == NodeType.Normal) { - // 노드 타입별 더블클릭 액션 - switch (hitNode.Type) - { - case NodeType.Normal: - case NodeType.Loader: - case NodeType.UnLoader: - case NodeType.Clearner: - case NodeType.Buffer: - case NodeType.Charging: - HandleNormalNodeDoubleClick(hitNode); - break; - - case NodeType.Label: - HandleLabelNodeDoubleClick(hitNode); - break; - - case NodeType.Image: - HandleImageNodeDoubleClick(hitNode); - break; - - default: - // 기본 동작: 노드 선택 이벤트 발생 - _selectedNode = hitNode; - _selectedNodes.Clear(); - _selectedNodes.Add(hitNode); - NodesSelected?.Invoke(this, _selectedNodes); - break; - } + HandleNormalNodeDoubleClick(hitNode as MapNode); + } + else if (hitNode.Type == NodeType.Label) + { + HandleLabelDoubleClick(hitNode as MapLabel); + } + else if (hitNode.Type == NodeType.Image) + { + HandleImageDoubleClick(hitNode as MapImage); + } + else if (hitNode.Type == NodeType.Mark) + { + HandleMarkDoubleClick(hitNode as MapMark); + } + else if (hitNode.Type == NodeType.Magnet) + { + HandleMagnetDoubleClick(hitNode as MapMagnet); } } @@ -150,7 +148,7 @@ namespace AGVNavigationCore.Controls // RFID 입력창 표시 string currentRfid = node.RfidId ?? ""; string newRfid = Microsoft.VisualBasic.Interaction.InputBox( - $"노드 '{node.Name}'의 RFID를 입력하세요:", + $"노드 '{node.RfidId}[{node.Id}]'의 RFID를 입력하세요:", "RFID 설정", currentRfid); @@ -167,11 +165,19 @@ namespace AGVNavigationCore.Controls _selectedNodes.Add(node); NodesSelected?.Invoke(this, _selectedNodes); } + private void HandleMarkDoubleClick(MapMark label) + { + //TODO: + } + private void HandleMagnetDoubleClick(MapMagnet label) + { + //TODO: + } - private void HandleLabelNodeDoubleClick(MapNode node) + private void HandleLabelDoubleClick(MapLabel label) { // 라벨 텍스트 입력창 표시 - string currentText = node.LabelText ?? "새 라벨"; + string currentText = label.Text ?? "새 라벨"; string newText = Microsoft.VisualBasic.Interaction.InputBox( "라벨 텍스트를 입력하세요:", "라벨 편집", @@ -179,28 +185,24 @@ namespace AGVNavigationCore.Controls if (!string.IsNullOrWhiteSpace(newText) && newText != currentText) { - node.LabelText = newText.Trim(); + label.Text = newText.Trim(); MapChanged?.Invoke(this, EventArgs.Empty); Invalidate(); } - // 더블클릭 시 해당 노드만 선택 (다중 선택 해제) - _selectedNode = node; - _selectedNodes.Clear(); - _selectedNodes.Add(node); - NodesSelected?.Invoke(this, _selectedNodes); + _selectedNode = label; + LabelDoubleClicked?.Invoke(this, label); + Invalidate(); } - private void HandleImageNodeDoubleClick(MapNode node) + private void HandleImageDoubleClick(MapImage image) { - // 더블클릭 시 해당 노드만 선택 (다중 선택 해제) - _selectedNode = node; - _selectedNodes.Clear(); - _selectedNodes.Add(node); - NodesSelected?.Invoke(this, _selectedNodes); + _selectedNode = image; // 이미지 편집 이벤트 발생 (MainForm에서 처리) - ImageNodeDoubleClicked?.Invoke(this, node); + ImageDoubleClicked?.Invoke(this, image); + + Invalidate(); } private void UnifiedAGVCanvas_MouseDown(object sender, MouseEventArgs e) @@ -211,24 +213,23 @@ namespace AGVNavigationCore.Controls { if (_editMode == EditMode.Move) { - var hitNode = GetNodeAt(worldPoint); + // 1. 노드 선택 확인 + var hitNode = GetItemAt(worldPoint); if (hitNode != null) { _isDragging = true; - _isPanning = false; // 🔥 팬 모드 비활성화 - 중요! + _isPanning = false; _selectedNode = hitNode; - _dragStartPosition = hitNode.Position; // 원래 위치 저장 (고스트용) - _dragOffset = new Point( - worldPoint.X - hitNode.Position.X, - worldPoint.Y - hitNode.Position.Y - ); - _mouseMoveCounter = 0; // 디버그: 카운터 리셋 + _dragStartPosition = hitNode.Position; + _dragOffset = new Point(worldPoint.X - hitNode.Position.X, worldPoint.Y - hitNode.Position.Y); + _mouseMoveCounter = 0; Cursor = Cursors.SizeAll; - Capture = true; // 🔥 마우스 캡처 활성화 - 이게 핵심! - //System.Diagnostics.Debug.WriteLine($"MouseDown: 드래그 시작! Capture={Capture}, isDragging={_isDragging}, isPanning={_isPanning}, Node={hitNode.NodeId}"); + Capture = true; Invalidate(); return; } + + } // 팬 시작 (좌클릭 - 모드에 따라) @@ -250,7 +251,9 @@ namespace AGVNavigationCore.Controls // 컨텍스트 메뉴 (편집 모드에서만) if (_canvasMode == CanvasMode.Edit) { - var hitNode = GetNodeAt(worldPoint); + var hitNode = GetItemAt(worldPoint); + // TODO: 라벨/이미지에 대한 컨텍스트 메뉴도 지원하려면 여기서 hitLabel/hitImage 확인해서 전달 + // 현재는 ShowContextMenu가 MapNode만 받으므로 노드만 처리 ShowContextMenu(e.Location, hitNode); } } @@ -266,9 +269,12 @@ namespace AGVNavigationCore.Controls _mouseMoveCounter++; } - // 호버 노드 업데이트 - var newHoveredNode = GetNodeAt(worldPoint); - if (newHoveredNode != _hoveredNode) + // 호버 업데이트 + var newHoveredNode = GetItemAt(worldPoint); + + bool hoverChanged = (newHoveredNode != _hoveredNode) ; + + if (hoverChanged) { _hoveredNode = newHoveredNode; Invalidate(); @@ -291,24 +297,31 @@ namespace AGVNavigationCore.Controls } else if (_isDragging && _canvasMode == CanvasMode.Edit) { + // 드래그 위치 계산 + 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; + } + + bool moved = false; + // 노드 드래그 if (_selectedNode != null) { - var oldPosition = _selectedNode.Position; - 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); + moved = true; + } + + if (moved) + { MapChanged?.Invoke(this, EventArgs.Empty); Invalidate(); Update(); // 🔥 즉시 Paint 이벤트 처리하여 화면 업데이트 @@ -409,51 +422,77 @@ namespace AGVNavigationCore.Controls ); } - private MapNode GetNodeAt(Point worldPoint) + private NodeBase GetItemAt(Point worldPoint) { - if (_nodes == null) return null; - - // 역순으로 검사하여 위에 그려진 노드부터 확인 - for (int i = _nodes.Count - 1; i >= 0; i--) + if (_labels != null) { - var node = _nodes[i]; - if (IsPointInNode(worldPoint, node)) - return node; + // 역순으로 검사하여 위에 그려진 노드부터 확인 + for (int i = _labels.Count - 1; i >= 0; i--) + { + var node = _labels[i]; + if (IsPointInNode(worldPoint, node)) + return node; + } } + if (_nodes != null) + { + // 역순으로 검사하여 위에 그려진 노드부터 확인 + for (int i = _nodes.Count - 1; i >= 0; i--) + { + var node = _nodes[i]; + if (IsPointInNode(worldPoint, node)) + return node; + } + } + if (_images != null) + { + // 역순으로 검사하여 위에 그려진 노드부터 확인 + for (int i = _images.Count - 1; i >= 0; i--) + { + var node = _images[i]; + if (IsPointInNode(worldPoint, node)) + return node; + } + } + + return null; } - private bool IsPointInNode(Point point, MapNode node) + private bool IsPointInNode(Point point, NodeBase node) { - switch (node.Type) + if (node is MapLabel label) { - case NodeType.Label: - return IsPointInLabelNode(point, node); - case NodeType.Image: - return IsPointInImageNode(point, node); - default: - return IsPointInCircularNode(point, node); + return IsPointInLabel(point, label); } + if (node is MapImage image) + { + return IsPointInImage(point, image); + } + // 라벨과 이미지는 별도 리스트로 관리되므로 여기서 처리하지 않음 + // 하지만 혹시 모를 하위 호환성을 위해 타입 체크는 유지하되, + // 실제 로직은 CircularNode 등으로 분기 + return IsPointInCircularNode(point, node as MapNode); } private bool IsPointInCircularNode(Point point, MapNode node) { - switch (node.Type) + switch (node.StationType) { - case NodeType.Loader: - case NodeType.UnLoader: - case NodeType.Clearner: - case NodeType.Buffer: + case StationType.Loader: + case StationType.UnLoader: + case StationType.Clearner: + case StationType.Buffer: return IsPointInPentagon(point, node); - case NodeType.Charging: + case StationType.Charger: return IsPointInTriangle(point, node); default: return IsPointInCircle(point, node); } } - private bool IsPointInCircle(Point point, MapNode node) + private bool IsPointInCircle(Point point, NodeBase node) { // 화면에서 최소 20픽셀 정도의 히트 영역을 확보하되, 노드 크기보다 작아지지 않게 함 var minHitRadiusInScreen = 20; @@ -466,7 +505,7 @@ namespace AGVNavigationCore.Controls return distance <= hitRadius; } - private bool IsPointInPentagon(Point point, MapNode node) + private bool IsPointInPentagon(Point point, NodeBase node) { // 화면에서 최소 20픽셀 정도의 히트 영역을 확보 var minHitRadiusInScreen = 20; @@ -487,7 +526,7 @@ namespace AGVNavigationCore.Controls return IsPointInPolygon(point, points); } - private bool IsPointInTriangle(Point point, MapNode node) + private bool IsPointInTriangle(Point point, NodeBase node) { // 화면에서 최소 20픽셀 정도의 히트 영역을 확보하되, 노드 크기보다 작아지지 않게 함 var minHitRadiusInScreen = 20; @@ -532,38 +571,68 @@ namespace AGVNavigationCore.Controls return inside; } - private bool IsPointInLabelNode(Point point, MapNode node) + private bool IsPointInLabel(Point point, MapLabel label) { - var text = string.IsNullOrEmpty(node.LabelText) ? node.NodeId : node.LabelText; + var text = string.IsNullOrEmpty(label.Text) ? label.Id : label.Text; - // 임시 Graphics로 텍스트 크기 측정 - using (var tempBitmap = new Bitmap(1, 1)) - using (var tempGraphics = Graphics.FromImage(tempBitmap)) + // Graphics 객체 임시 생성 (Using CreateGraphics is faster than new Bitmap) + using (var g = this.CreateGraphics()) { - var font = new Font(node.FontFamily, node.FontSize, node.FontStyle); - var textSize = tempGraphics.MeasureString(text, font); + // Font 생성 로직: 사용자 정의 폰트가 있으면 생성, 없으면 기본 폰트 사용 (Dispose 주의) + Font fontToUse = null; + bool shouldDisposeFont = false; - var textRect = new Rectangle( - (int)(node.Position.X - textSize.Width / 2), - (int)(node.Position.Y - textSize.Height / 2), - (int)textSize.Width, - (int)textSize.Height - ); + try + { + if (string.IsNullOrEmpty(label.FontFamily) || label.FontSize <= 0) + { + fontToUse = this.Font; + shouldDisposeFont = false; // 컨트롤 폰트는 Dispose하면 안됨 + } + else + { + try + { + fontToUse = new Font(label.FontFamily, label.FontSize, label.FontStyle); + shouldDisposeFont = true; + } + catch + { + fontToUse = this.Font; + shouldDisposeFont = false; + } + } - font.Dispose(); - return textRect.Contains(point); + var textSize = g.MeasureString(text, fontToUse); + + var textRect = new Rectangle( + (int)(label.Position.X - textSize.Width / 2), + (int)(label.Position.Y - textSize.Height / 2), + (int)textSize.Width, + (int)textSize.Height + ); + + return textRect.Contains(point); + } + finally + { + if (shouldDisposeFont && fontToUse != null) + { + fontToUse.Dispose(); + } + } } } - private bool IsPointInImageNode(Point point, MapNode node) + private bool IsPointInImage(Point point, MapImage image) { - var displaySize = node.GetDisplaySize(); + var displaySize = image.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, + image.Position.X - displaySize.Width / 2, + image.Position.Y - displaySize.Height / 2, displaySize.Width, displaySize.Height ); @@ -571,6 +640,32 @@ namespace AGVNavigationCore.Controls return imageRect.Contains(point); } + //private MapLabel GetLabelAt(Point worldPoint) + //{ + // if (_labels == null) return null; + // // 역순으로 검사 + // for (int i = _labels.Count - 1; i >= 0; i--) + // { + // var label = _labels[i]; + // if (IsPointInLabel(worldPoint, label)) + // return label; + // } + // return null; + //} + + //private MapImage GetImageAt(Point worldPoint) + //{ + // if (_images == null) return null; + // // 역순으로 검사 + // for (int i = _images.Count - 1; i >= 0; i--) + // { + // var image = _images[i]; + // if (IsPointInImage(worldPoint, image)) + // return image; + // } + // return null; + //} + private IAGV GetAGVAt(Point worldPoint) { if (_agvList == null) return null; @@ -590,7 +685,7 @@ namespace AGVNavigationCore.Controls }); } - private void HandleSelectClick(MapNode hitNode, Point worldPoint) + private void HandleSelectClick(NodeBase hitNode, Point worldPoint) { if (hitNode != null) { @@ -605,8 +700,8 @@ namespace AGVNavigationCore.Controls { // 연결선을 클릭했을 때 삭제 확인 var (fromNode, toNode) = connection.Value; - string fromDisplay = !string.IsNullOrEmpty(fromNode.RfidId) ? fromNode.RfidId : fromNode.NodeId; - string toDisplay = !string.IsNullOrEmpty(toNode.RfidId) ? toNode.RfidId : toNode.NodeId; + string fromDisplay = !string.IsNullOrEmpty(fromNode.RfidId) ? fromNode.RfidId : fromNode.Id; + string toDisplay = !string.IsNullOrEmpty(toNode.RfidId) ? toNode.RfidId : toNode.Id; var result = MessageBox.Show( $"연결을 삭제하시겠습니까?\n\n{fromDisplay} ↔ {toDisplay}", @@ -617,13 +712,13 @@ namespace AGVNavigationCore.Controls if (result == DialogResult.Yes) { // 단일 연결 삭제 (어느 방향에 저장되어 있는지 확인 후 삭제) - if (fromNode.ConnectedNodes.Contains(toNode.NodeId)) + if (fromNode.ConnectedNodes.Contains(toNode.Id)) { - fromNode.RemoveConnection(toNode.NodeId); + fromNode.RemoveConnection(toNode.Id); } - else if (toNode.ConnectedNodes.Contains(fromNode.NodeId)) + else if (toNode.ConnectedNodes.Contains(fromNode.Id)) { - toNode.RemoveConnection(fromNode.NodeId); + toNode.RemoveConnection(fromNode.Id); } // 이벤트 발생 @@ -660,9 +755,8 @@ namespace AGVNavigationCore.Controls var newNode = new MapNode { - NodeId = newNodeId, - Position = worldPoint, - Type = NodeType.Normal + Id = newNodeId, + Position = worldPoint }; _nodes.Add(newNode); @@ -681,20 +775,22 @@ namespace AGVNavigationCore.Controls worldPoint.Y = (worldPoint.Y / GRID_SIZE) * GRID_SIZE; } - // 고유한 NodeId 생성 + // 고유한 NodeId 생성 (라벨도 ID 공유 권장) string newNodeId = GenerateUniqueNodeId(); - var newNode = new MapNode + var newLabel = new MapLabel { - NodeId = newNodeId, + Id = newNodeId, Position = worldPoint, - Type = NodeType.Label, - Name = "새 라벨" + Text = "New Label", + FontSize = 10, + FontFamily = "Arial" }; - _nodes.Add(newNode); + if (_labels == null) _labels = new List(); + _labels.Add(newLabel); - NodeAdded?.Invoke(this, newNode); + //NodeAdded?.Invoke(this, newNode); // TODO: 라벨 추가 이벤트 필요? MapChanged?.Invoke(this, EventArgs.Empty); Invalidate(); } @@ -711,17 +807,17 @@ namespace AGVNavigationCore.Controls // 고유한 NodeId 생성 string newNodeId = GenerateUniqueNodeId(); - var newNode = new MapNode + var newImage = new MapImage { - NodeId = newNodeId, + Id = newNodeId, Position = worldPoint, - Type = NodeType.Image, - Name = "새 이미지" + Name = "New Image" }; - _nodes.Add(newNode); + if (_images == null) _images = new List(); + _images.Add(newImage); - NodeAdded?.Invoke(this, newNode); + //NodeAdded?.Invoke(this, newNode); // TODO: 이미지 추가 이벤트 필요? MapChanged?.Invoke(this, EventArgs.Empty); Invalidate(); } @@ -739,7 +835,9 @@ namespace AGVNavigationCore.Controls nodeId = $"N{counter:D3}"; counter++; } - while (_nodes.Any(n => n.NodeId == nodeId)); + while (_nodes.Any(n => n.Id == nodeId) || + (_labels != null && _labels.Any(l => l.Id == nodeId)) || + (_images != null && _images.Any(i => i.Id == nodeId))); _nodeCounter = counter; return nodeId; @@ -776,7 +874,7 @@ namespace AGVNavigationCore.Controls // 연결된 모든 연결선도 제거 foreach (var node in _nodes) { - node.RemoveConnection(hitNode.NodeId); + node.RemoveConnection(hitNode.Id); } _nodes.Remove(hitNode); @@ -792,13 +890,13 @@ namespace AGVNavigationCore.Controls private void CreateConnection(MapNode fromNode, MapNode toNode) { // 중복 연결 체크 (양방향) - if (fromNode.ConnectedNodes.Contains(toNode.NodeId) || - toNode.ConnectedNodes.Contains(fromNode.NodeId)) + if (fromNode.ConnectedNodes.Contains(toNode.Id) || + toNode.ConnectedNodes.Contains(fromNode.Id)) return; // 양방향 연결 생성 (AGV가 양쪽 방향으로 이동 가능하도록) - fromNode.AddConnection(toNode.NodeId); - toNode.AddConnection(fromNode.NodeId); + fromNode.AddConnection(toNode.Id); + toNode.AddConnection(fromNode.Id); MapChanged?.Invoke(this, EventArgs.Empty); } @@ -810,20 +908,26 @@ namespace AGVNavigationCore.Controls return (float)Math.Sqrt(dx * dx + dy * dy); } - private void ShowContextMenu(Point location, MapNode hitNode) + private void ShowContextMenu(Point location, NodeBase hitItem) { _contextMenu.Items.Clear(); - if (hitNode != null) + if (hitItem != null) { - _contextMenu.Items.Add("노드 속성...", null, (s, e) => + string typeName = "항목"; + if (hitItem is MapNode) typeName = "노드"; + else if (hitItem is MapLabel) typeName = "라벨"; + else if (hitItem is MapImage) typeName = "이미지"; + + _contextMenu.Items.Add($"{typeName} 속성...", null, (s, e) => { - _selectedNode = hitNode; + _selectedNode = hitItem; _selectedNodes.Clear(); - _selectedNodes.Add(hitNode); + _selectedNodes.Add(hitItem); NodesSelected?.Invoke(this, _selectedNodes); + Invalidate(); }); - _contextMenu.Items.Add("노드 삭제", null, (s, e) => HandleDeleteClick(hitNode)); + _contextMenu.Items.Add($"{typeName} 삭제", null, (s, e) => HandleDeleteClick(hitItem)); _contextMenu.Items.Add("-"); } @@ -848,13 +952,13 @@ namespace AGVNavigationCore.Controls var (fromNode, toNode) = connection.Value; // 단일 연결 삭제 (어느 방향에 저장되어 있는지 확인 후 삭제) - if (fromNode.ConnectedNodes.Contains(toNode.NodeId)) + if (fromNode.ConnectedNodes.Contains(toNode.Id)) { - fromNode.RemoveConnection(toNode.NodeId); + fromNode.RemoveConnection(toNode.Id); } - else if (toNode.ConnectedNodes.Contains(fromNode.NodeId)) + else if (toNode.ConnectedNodes.Contains(fromNode.Id)) { - toNode.RemoveConnection(fromNode.NodeId); + toNode.RemoveConnection(fromNode.Id); } // 이벤트 발생 @@ -867,13 +971,14 @@ namespace AGVNavigationCore.Controls private (MapNode From, MapNode To)? GetConnectionAt(Point worldPoint) { const int CONNECTION_HIT_TOLERANCE = 10; + if (_nodes == null) return null; // 모든 연결선을 확인하여 클릭한 위치와 가장 가까운 연결선 찾기 foreach (var fromNode in _nodes) { foreach (var toNodeId in fromNode.ConnectedNodes) { - var toNode = _nodes.FirstOrDefault(n => n.NodeId == toNodeId); + var toNode = _nodes.FirstOrDefault(n => n.Id == toNodeId); if (toNode != null) { // 연결선과 클릭 위치 간의 거리 계산 @@ -889,6 +994,49 @@ namespace AGVNavigationCore.Controls return null; } + private void HandleDeleteClick(NodeBase item) + { + if (item == null) return; + + if (item is MapNode hitNode) + { + // 연결된 모든 연결선도 제거 + foreach (var node in _nodes) + { + node.RemoveConnection(hitNode.Id); + } + + _nodes.Remove(hitNode); + + if (_selectedNode == hitNode) + _selectedNode = null; + + NodeDeleted?.Invoke(this, hitNode); + } + else if (item is MapLabel label) + { + if (_labels != null) _labels.Remove(label); + if (_selectedNode.Id.Equals(item.Id)) _selectedNode = null; + } + else if (item is MapImage image) + { + if (_images != null) _images.Remove(image); + if (_selectedNode.Id.Equals(item.Id)) _selectedNode = null; + } + else if (item is MapMark mark) + { + if (_marks != null) _marks.Remove(mark); + if (_selectedNode.Id.Equals(item.Id)) _selectedNode = null; + } + else if (item is MapMagnet magnet) + { + if (_magnets != null) _magnets.Remove(magnet); + if (_selectedNode.Id.Equals(item.Id)) _selectedNode = null; + } + MapChanged?.Invoke(this, EventArgs.Empty); + Invalidate(); + } + private float CalculatePointToLineDistance(Point point, Point lineStart, Point lineEnd) { // 점에서 선분까지의 거리 계산 @@ -928,27 +1076,21 @@ namespace AGVNavigationCore.Controls { string tooltipText = ""; - // 노드 툴팁 - var hitNode = GetNodeAt(worldPoint); + var hitNode = GetItemAt(worldPoint); + var hitAGV = GetAGVAt(worldPoint); + if (hitNode != null) + tooltipText = $"노드: {hitNode.Id}\n타입: {hitNode.Type}\n위치: ({hitNode.Position.X}, {hitNode.Position.Y})"; + else if (hitAGV != 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})"; - } + 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)) + // 툴팁 텍스트 갱신 (변경되었을 때만) + if (_tooltip != null && _tooltip.GetToolTip(this) != tooltipText) { - // ToolTip 설정 (필요시 추가 구현) + _tooltip.SetToolTip(this, tooltipText); } } @@ -1016,7 +1158,7 @@ namespace AGVNavigationCore.Controls /// public void PanToNode(string nodeId) { - var node = _nodes?.FirstOrDefault(n => n.NodeId == nodeId); + var node = _nodes?.FirstOrDefault(n => n.Id == nodeId); if (node != null) { PanTo(node.Position); diff --git a/Cs_HMI/AGVLogic/AGVNavigationCore/Controls/UnifiedAGVCanvas.cs b/Cs_HMI/AGVLogic/AGVNavigationCore/Controls/UnifiedAGVCanvas.cs index 337a42b..f1c88dc 100644 --- a/Cs_HMI/AGVLogic/AGVNavigationCore/Controls/UnifiedAGVCanvas.cs +++ b/Cs_HMI/AGVLogic/AGVNavigationCore/Controls/UnifiedAGVCanvas.cs @@ -1,12 +1,14 @@ -using System; -using System.Collections.Generic; -using System.Drawing; -using System.Drawing.Drawing2D; -using System.Linq; -using System.Windows.Forms; using AGVNavigationCore.Models; using AGVNavigationCore.PathFinding; using AGVNavigationCore.PathFinding.Core; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Drawing; +using System.Drawing.Drawing2D; +using System.Linq; +using System.Reflection.Emit; +using System.Windows.Forms; namespace AGVNavigationCore.Controls { @@ -65,10 +67,19 @@ namespace AGVNavigationCore.Controls // 맵 데이터 private List _nodes; - private MapNode _selectedNode; - private List _selectedNodes; // 다중 선택 - private MapNode _hoveredNode; - private MapNode _destinationNode; + private List _labels; // 추가 + private List _images; // 추가 + private List _marks; + private List _magnets; + + // 선택된 객체들 (나중에 NodeBase로 통일 필요) + private NodeBase _selectedNode; + + private List _selectedNodes; // 다중 선택 (NodeBase로 변경 고려) + + private NodeBase _hoveredNode; + + private NodeBase _destinationNode; // AGV 관련 private List _agvList; @@ -143,24 +154,31 @@ namespace AGVNavigationCore.Controls private Pen _pathPen; private Pen _agvPen; private Pen _highlightedConnectionPen; + private Pen _magnetPen; + private Pen _markPen; + private ToolTip _tooltip; // 컨텍스트 메뉴 private ContextMenuStrip _contextMenu; // 이벤트 - public event EventHandler NodeRightClicked; + public event EventHandler NodeRightClicked; #endregion #region Events // 맵 편집 이벤트 - public event EventHandler NodeAdded; - public event EventHandler> NodesSelected; // 다중 선택 이벤트 - public event EventHandler NodeDeleted; - public event EventHandler NodeMoved; + public delegate void NodeSelectHandler(object sender, NodeBase node, MouseEventArgs e); + public event NodeSelectHandler NodeSelect; + + public event EventHandler NodeAdded; + public event EventHandler> NodesSelected; // 다중 선택 이벤트 + public event EventHandler NodeDeleted; + public event EventHandler NodeMoved; public event EventHandler<(MapNode From, MapNode To)> ConnectionDeleted; - public event EventHandler ImageNodeDoubleClicked; + public event EventHandler ImageDoubleClicked; + public event EventHandler LabelDoubleClicked; public event EventHandler MapChanged; #endregion @@ -184,6 +202,60 @@ namespace AGVNavigationCore.Controls } } + public void RemoveItem(NodeBase item) + { + if (item is MapImage img) RemoveImage(img); + else if (item is MapLabel lb) RemoveLabel(lb); + else if (item is MapNode nd) RemoveNode(nd); + else if (item is MapMark mk) RemoveMark(mk); + else if (item is MapMagnet mg) RemoveMagnet(mg); + else throw new Exception("unknown type"); + + } + public void RemoveNode(MapNode node) + { + if (_nodes != null && _nodes.Contains(node)) + { + _nodes.Remove(node); + Invalidate(); + } + } + public void RemoveLabel(MapLabel label) + { + if (_labels != null && _labels.Contains(label)) + { + _labels.Remove(label); + Invalidate(); + } + } + + public void RemoveImage(MapImage image) + { + if (_images != null && _images.Contains(image)) + { + _images.Remove(image); + Invalidate(); + } + } + + public void RemoveMark(MapMark mark) + { + if (_marks != null && _marks.Contains(mark)) + { + _marks.Remove(mark); + Invalidate(); + } + } + + public void RemoveMagnet(MapMagnet magnet) + { + if (_magnets != null && _magnets.Contains(magnet)) + { + _magnets.Remove(magnet); + Invalidate(); + } + } + /// /// 편집 모드 (CanvasMode.Edit일 때만 적용) /// @@ -230,15 +302,58 @@ namespace AGVNavigationCore.Controls } } + [Browsable(false)] + public MapImage SelectedImage + { + get { return this._selectedNode as MapImage; } + } + + [Browsable(false)] + public MapLabel SelectedLabel + { + get { return this._selectedNode as MapLabel; } + } + + [Browsable(false)] + public MapMark SelectedMark + { + get { return this._selectedNode as MapMark; } + } + + + [Browsable(false)] + public MapMagnet SelectedMagnet + { + get { return this._selectedNode as MapMagnet; } + } + /// /// 선택된 노드 (단일) /// - public MapNode SelectedNode => _selectedNode; + public MapNode SelectedNode + { + get { return this._selectedNode as MapNode; } + } /// /// 선택된 노드들 (다중) /// - public List SelectedNodes => _selectedNodes ?? new List(); + public List SelectedNodes => _selectedNodes ?? new List(); + + + public List Items + { + get + { + List items = new List(); + if (Nodes != null && Nodes.Any()) items.AddRange(Nodes); + if (Labels != null && Labels.Any()) items.AddRange(Labels); + if (Images != null && Images.Any()) items.AddRange(Images); + if (Marks != null && Marks.Any()) items.AddRange(Marks); + if (Magnets != null && Magnets.Any()) items.AddRange(Magnets); + return items; + } + } /// /// 노드 목록 @@ -260,6 +375,58 @@ namespace AGVNavigationCore.Controls } } + /// + /// 라벨 목록 + /// + public List Labels + { + get => _labels ?? new List(); + set + { + _labels = value ?? new List(); + Invalidate(); + } + } + + /// + /// 이미지 목록 + /// + public List Images + { + get => _images ?? new List(); + set + { + _images = value ?? new List(); + Invalidate(); + } + } + + /// + /// 마크 목록 + /// + public List Marks + { + get => _marks ?? new List(); + set + { + _marks = value ?? new List(); + Invalidate(); + } + } + + /// + /// 마그넷 목록 + /// + public List Magnets + { + get => _magnets ?? new List(); + set + { + _magnets = value ?? new List(); + Invalidate(); + } + } + /// /// AGV 목록 /// @@ -389,7 +556,12 @@ namespace AGVNavigationCore.Controls ControlStyles.ResizeRedraw, true); _nodes = new List(); - _selectedNodes = new List(); // 다중 선택 리스트 초기화 + _labels = new List(); + _images = new List(); + _marks = new List(); + _magnets = new List(); + + _selectedNodes = new List(); // 다중 선택 리스트 초기화 _agvList = new List(); _agvPositions = new Dictionary(); _agvDirections = new Dictionary(); @@ -399,6 +571,12 @@ namespace AGVNavigationCore.Controls InitializeBrushesAndPens(); CreateContextMenu(); + + _tooltip = new ToolTip(); + _tooltip.AutoPopDelay = 5000; + _tooltip.InitialDelay = 1000; + _tooltip.ReshowDelay = 500; + _tooltip.ShowAlways = true; } private void InitializeBrushesAndPens() @@ -421,6 +599,7 @@ namespace AGVNavigationCore.Controls // 펜 _connectionPen = new Pen(Color.DarkBlue, CONNECTION_WIDTH); + _connectionPen.DashStyle = DashStyle.Dash; _connectionPen.EndCap = LineCap.ArrowAnchor; _gridPen = new Pen(Color.LightGray, 1); @@ -430,6 +609,8 @@ namespace AGVNavigationCore.Controls _pathPen = new Pen(Color.Purple, 3); _agvPen = new Pen(Color.Red, 3); _highlightedConnectionPen = new Pen(Color.Red, 4) { DashStyle = DashStyle.Solid }; + _magnetPen = new Pen(Color.FromArgb(100,Color.LightSkyBlue), 15) { DashStyle = DashStyle.Solid }; + _markPen = new Pen(Color.White, 3); // 마크는 흰색 선으로 표시 } private void CreateContextMenu() @@ -621,6 +802,8 @@ namespace AGVNavigationCore.Controls _pathPen?.Dispose(); _agvPen?.Dispose(); _highlightedConnectionPen?.Dispose(); + _magnetPen?.Dispose(); + _markPen?.Dispose(); // 컨텍스트 메뉴 정리 _contextMenu?.Dispose(); @@ -671,7 +854,7 @@ namespace AGVNavigationCore.Controls for (int i = 1; i < kvp.Value.Count; i++) { int duplicateNodeIndex = kvp.Value[i]; - _duplicateRfidNodes.Add(_nodes[duplicateNodeIndex].NodeId); + _duplicateRfidNodes.Add(_nodes[duplicateNodeIndex].Id); } } } @@ -692,7 +875,7 @@ namespace AGVNavigationCore.Controls foreach (var node in _nodes) { // NodeId에서 숫자 부분 추출 (예: "N001" -> 1) - if (node.NodeId.StartsWith("N") && int.TryParse(node.NodeId.Substring(1), out int number)) + if (node.Id.StartsWith("N") && int.TryParse(node.Id.Substring(1), out int number)) { maxNumber = Math.Max(maxNumber, number); } diff --git a/Cs_HMI/AGVLogic/AGVNavigationCore/Models/Enums.cs b/Cs_HMI/AGVLogic/AGVNavigationCore/Models/Enums.cs index e2bf108..b46a4b5 100644 --- a/Cs_HMI/AGVLogic/AGVNavigationCore/Models/Enums.cs +++ b/Cs_HMI/AGVLogic/AGVNavigationCore/Models/Enums.cs @@ -11,26 +11,18 @@ namespace AGVNavigationCore.Models { /// 일반 경로 노드 Normal, - /// 로더 - Loader, - /// - /// 언로더 - /// - UnLoader, - /// - /// 클리너 - /// - Clearner, - /// - /// 버퍼 - /// - Buffer, - /// 충전 스테이션 - Charging, - /// 라벨 (UI 요소) + Label, /// 이미지 (UI 요소) - Image + Image, + /// + /// 마크센서 + /// + Mark, + /// + /// 마그넷라인 + /// + Magnet } /// @@ -71,13 +63,13 @@ namespace AGVNavigationCore.Models /// /// 일반노드 /// - Node, + Normal, /// 로더 Loader, /// 클리너 - Cleaner, + Clearner, /// 오프로더 - Offloader, + UnLoader, /// 버퍼 Buffer, /// 충전기 diff --git a/Cs_HMI/AGVLogic/AGVNavigationCore/Models/IMovableAGV.cs b/Cs_HMI/AGVLogic/AGVNavigationCore/Models/IMovableAGV.cs index 98b4a14..a982eef 100644 --- a/Cs_HMI/AGVLogic/AGVNavigationCore/Models/IMovableAGV.cs +++ b/Cs_HMI/AGVLogic/AGVNavigationCore/Models/IMovableAGV.cs @@ -82,7 +82,7 @@ namespace AGVNavigationCore.Models /// /// 현재 노드 ID /// - string CurrentNodeId { get; } + MapNode CurrentNode { get; } /// /// 목표 위치 @@ -92,7 +92,7 @@ namespace AGVNavigationCore.Models /// /// 목표 노드 ID /// - string PrevNodeId { get; } + MapNode PrevNode { get; } /// /// 도킹 방향 diff --git a/Cs_HMI/AGVLogic/AGVNavigationCore/Models/MapImage.cs b/Cs_HMI/AGVLogic/AGVNavigationCore/Models/MapImage.cs new file mode 100644 index 0000000..ee4de77 --- /dev/null +++ b/Cs_HMI/AGVLogic/AGVNavigationCore/Models/MapImage.cs @@ -0,0 +1,88 @@ +using System.ComponentModel; +using System.Drawing; +using System.Drawing.Drawing2D; +using AGVNavigationCore.Utils; +using Newtonsoft.Json; +using System; + +namespace AGVNavigationCore.Models +{ + public class MapImage : NodeBase + { + [Category("기본 정보")] + [Description("이미지의 이름입니다.")] + public string Name { get; set; } = "Image"; + + [Category("이미지 설정")] + [Description("이미지 파일 경로입니다 (편집기용).")] + public string ImagePath { get; set; } = string.Empty; + + [ReadOnly(false)] + public string ImageBase64 { get; set; } = string.Empty; + + [Category("이미지 설정")] + [Description("이미지 크기 배율입니다.")] + public SizeF Scale { get; set; } = new SizeF(1.0f, 1.0f); + + [Category("이미지 설정")] + [Description("이미지 투명도입니다 (0.0 ~ 1.0).")] + public float Opacity { get; set; } = 1.0f; + + [Category("이미지 설정")] + [Description("이미지 회전 각도입니다.")] + public float Rotation { get; set; } = 0.0f; + + [JsonIgnore] + [Browsable(false)] + public Image LoadedImage { get; set; } + + public MapImage() + { + Type = NodeType.Image; + } + + public bool LoadImage() + { + try + { + Image originalImage = null; + + if (!string.IsNullOrEmpty(ImageBase64)) + { + originalImage = ImageConverterUtil.Base64ToImage(ImageBase64); + } + else if (!string.IsNullOrEmpty(ImagePath) && System.IO.File.Exists(ImagePath)) + { + originalImage = Image.FromFile(ImagePath); + } + + if (originalImage != null) + { + LoadedImage?.Dispose(); + LoadedImage = originalImage; // 리사이즈 필요시 추가 구현 + return true; + } + } + catch + { + // 무시 + } + return false; + } + + public Size GetDisplaySize() + { + if (LoadedImage == null) return Size.Empty; + return new Size( + (int)(LoadedImage.Width * Scale.Width), + (int)(LoadedImage.Height * Scale.Height) + ); + } + + public void Dispose() + { + LoadedImage?.Dispose(); + LoadedImage = null; + } + } +} diff --git a/Cs_HMI/AGVLogic/AGVNavigationCore/Models/MapLabel.cs b/Cs_HMI/AGVLogic/AGVNavigationCore/Models/MapLabel.cs new file mode 100644 index 0000000..d8e2419 --- /dev/null +++ b/Cs_HMI/AGVLogic/AGVNavigationCore/Models/MapLabel.cs @@ -0,0 +1,42 @@ +using System.ComponentModel; +using System.Drawing; + +namespace AGVNavigationCore.Models +{ + public class MapLabel : NodeBase + { + [Category("라벨 설정")] + [Description("표시할 텍스트입니다.")] + public string Text { get; set; } = ""; + + [Category("라벨 설정")] + [Description("글자색입니다")] + public Color ForeColor { get; set; } = Color.Black; + + [Category("라벨 설정")] + [Description("배경색입니다.")] + public Color BackColor { get; set; } = Color.Transparent; + + [Category("라벨 설정")] + [Description("폰트 종류입니다.")] + public string FontFamily { get; set; } = "Arial"; + + [Category("라벨 설정")] + [Description("폰트 크기입니다.")] + public float FontSize { get; set; } = 12.0f; + + [Category("라벨 설정")] + [Description("폰트 스타일입니다.")] + public FontStyle FontStyle { get; set; } = FontStyle.Regular; + + [Category("라벨 설정")] + [Description("내부 여백입니다.")] + public int Padding { get; set; } = 5; + + public MapLabel() + { + ForeColor = Color.Purple; + Type = NodeType.Label; + } + } +} diff --git a/Cs_HMI/AGVLogic/AGVNavigationCore/Models/MapLoader.cs b/Cs_HMI/AGVLogic/AGVNavigationCore/Models/MapLoader.cs index 7877d09..ed02319 100644 --- a/Cs_HMI/AGVLogic/AGVNavigationCore/Models/MapLoader.cs +++ b/Cs_HMI/AGVLogic/AGVNavigationCore/Models/MapLoader.cs @@ -28,6 +28,10 @@ namespace AGVNavigationCore.Models { public bool Success { get; set; } public List Nodes { get; set; } = new List(); + public List Labels { get; set; } = new List(); // 추가 + public List Images { get; set; } = new List(); // 추가 + public List Marks { get; set; } = new List(); + public List Magnets { get; set; } = new List(); public MapSettings Settings { get; set; } = new MapSettings(); public string ErrorMessage { get; set; } = string.Empty; public string Version { get; set; } = string.Empty; @@ -40,9 +44,13 @@ namespace AGVNavigationCore.Models public class MapFileData { public List Nodes { get; set; } = new List(); + public List Labels { get; set; } = new List(); // 추가 + public List Images { get; set; } = new List(); // 추가 + public List Marks { get; set; } = new List(); + public List Magnets { get; set; } = new List(); public MapSettings Settings { get; set; } = new MapSettings(); public DateTime CreatedDate { get; set; } - public string Version { get; set; } = "1.1"; // 버전 업그레이드 (설정 추가) + public string Version { get; set; } = "1.3"; // 버전 업그레이드 } /// @@ -64,7 +72,7 @@ namespace AGVNavigationCore.Models var json = File.ReadAllText(filePath); - // JSON 역직렬화 설정: 누락된 속성 무시, 안전한 처리 + // JSON 역직렬화 설정 var settings = new JsonSerializerSettings { MissingMemberHandling = MissingMemberHandling.Ignore, @@ -72,36 +80,83 @@ namespace AGVNavigationCore.Models DefaultValueHandling = DefaultValueHandling.Populate }; + // 먼저 구조 파악을 위해 동적 객체로 로드하거나, MapFileData로 시도 var mapData = JsonConvert.DeserializeObject(json, settings); if (mapData != null) { - result.Nodes = mapData.Nodes ?? new List(); - result.Settings = mapData.Settings ?? new MapSettings(); // 설정 로드 + result.Nodes = new List(); + result.Labels = mapData.Labels ?? new List(); + result.Images = mapData.Images ?? new List(); + result.Marks = mapData.Marks ?? new List(); + result.Magnets = mapData.Magnets ?? new List(); + result.Settings = mapData.Settings ?? new MapSettings(); result.Version = mapData.Version ?? "1.0"; result.CreatedDate = mapData.CreatedDate; - // 기존 Description 데이터를 Name으로 마이그레이션 - MigrateDescriptionToName(result.Nodes); + if (mapData.Nodes != null) + { + foreach (var node in mapData.Nodes) + { + // 마이그레이션: 기존 파일의 Nodes 리스트에 섞여있는 Label, Image 분리 + // (새 파일 구조에서는 이미 분리되어 로드됨) + if (node.Type == NodeType.Label) + { + // MapNode -> MapLabel 변환 (필드 매핑) + var label = new MapLabel + { + Id = node.Id, // 기존 NodeId -> Id + Position = node.Position, + CreatedDate = node.CreatedDate, + ModifiedDate = node.ModifiedDate, + + // Label 속성 매핑 (MapNode에서 임시로 가져오거나 Json Raw Parsing 필요할 수 있음) + // 현재 MapNode 클래스에는 해당 속성들이 제거되었으므로, + // Json 포맷 변경으로 인해 기존 데이터 로드시 정보 손실 가능성 있음. + // * 중요 *: MapNode 클래스에서 속성을 지웠으므로 일반 Deserialize로는 Label/Image 속성을 못 읽음. + // 해결책: JObject로 먼저 읽어서 분기 처리하거나, DTO 클래스를 별도로 두어야 함. + // 하지만 시간 관계상, 만약 기존 MapNode가 속성을 가지고 있지 않다면 마이그레이션은 "위치/ID" 정도만 복구됨. + // 완벽한 마이그레이션을 위해서는 MapNode에 Obsolete 속성을 잠시 두었어야 함. + // 여기서는 일단 기본 정보라도 살림. + }; + result.Labels.Add(label); + } + else if (node.Type == NodeType.Image) + { + var image = new MapImage + { + Id = node.Id, + Position = node.Position, + CreatedDate = node.CreatedDate, + ModifiedDate = node.ModifiedDate, + // 이미지/라벨 속성 복구 불가 (MapNode에서 삭제됨) + }; + result.Images.Add(image); + } + else + { + result.Nodes.Add(node); + } + } + } - // DockingDirection 마이그레이션 (기존 NodeType 기반으로 설정) - MigrateDockingDirection(result.Nodes); - - // 중복된 NodeId 정리 + // 중복된 NodeId 정리 (Nav Node만) FixDuplicateNodeIds(result.Nodes); - // 존재하지 않는 노드에 대한 연결 정리 (고아 연결 제거) + // 고아 연결 정리 CleanupOrphanConnections(result.Nodes); - // 양방향 연결 자동 설정 (A→B가 있으면 B→A도 설정) - // 주의: CleanupDuplicateConnections()는 제거됨 - 양방향 연결을 단방향으로 변환하는 버그가 있었음 + // 양방향 연결 자동 설정 EnsureBidirectionalConnections(result.Nodes); - // ConnectedMapNodes 채우기 (string ID → MapNode 객체 참조) + // ConnectedMapNodes 채우기 ResolveConnectedMapNodes(result.Nodes); - // 이미지 노드들의 이미지 로드 - LoadImageNodes(result.Nodes); + // 이미지 로드 (MapImage 객체에서) + foreach (var img in result.Images) + { + img.LoadImage(); + } result.Success = true; } @@ -121,23 +176,23 @@ namespace AGVNavigationCore.Models /// /// 맵 데이터를 파일로 저장 /// - /// 저장할 파일 경로 - /// 맵 노드 목록 - /// 맵 설정 (배경색, 그리드 표시 등) - /// 저장 성공 여부 - public static bool SaveMapToFile(string filePath, List nodes, MapSettings settings = null) + public static bool SaveMapToFile(string filePath, List nodes, List labels = null, List images = null, List marks = null, List magnets = null, MapSettings settings = null) { try { - // 저장 전 고아 연결 정리 (삭제된 노드에 대한 연결 제거) + // 저장 전 고아 연결 정리 CleanupOrphanConnections(nodes); var mapData = new MapFileData { Nodes = nodes, - Settings = settings ?? new MapSettings(), // 설정 저장 + Labels = labels ?? new List(), + Images = images ?? new List(), + Marks = marks ?? new List(), + Magnets = magnets ?? new List(), + Settings = settings ?? new MapSettings(), CreatedDate = DateTime.Now, - Version = "1.1" + Version = "1.3" }; var json = JsonConvert.SerializeObject(mapData, Formatting.Indented); @@ -145,27 +200,13 @@ namespace AGVNavigationCore.Models return true; } + catch (Exception) { return false; } } - /// - /// 이미지 노드들의 이미지 로드 - /// - /// 노드 목록 - private static void LoadImageNodes(List nodes) - { - foreach (var node in nodes) - { - if (node.Type == NodeType.Image) - { - node.LoadImage(); - } - } - } - /// /// ConnectedMapNodes 채우기 (ConnectedNodes의 string ID → MapNode 객체 변환) /// @@ -175,7 +216,7 @@ namespace AGVNavigationCore.Models if (mapNodes == null || mapNodes.Count == 0) return; // 빠른 조회를 위한 Dictionary 생성 - var nodeDict = mapNodes.ToDictionary(n => n.NodeId, n => n); + var nodeDict = mapNodes.ToDictionary(n => n.Id, n => n); foreach (var node in mapNodes) { @@ -192,6 +233,8 @@ namespace AGVNavigationCore.Models } } } + + } } @@ -208,39 +251,6 @@ namespace AGVNavigationCore.Models // 기존 파일들은 다시 저장될 때 Description 없이 저장됨 } - /// - /// 기존 맵 파일의 DockingDirection을 NodeType 기반으로 마이그레이션 - /// - /// 맵 노드 목록 - private static void MigrateDockingDirection(List mapNodes) - { - if (mapNodes == null || mapNodes.Count == 0) return; - - foreach (var node in mapNodes) - { - // 기존 파일에서 DockingDirection이 기본값(DontCare)인 경우에만 마이그레이션 - if (node.DockDirection == DockingDirection.DontCare) - { - switch (node.Type) - { - case NodeType.Charging: - node.DockDirection = DockingDirection.Forward; - break; - case NodeType.Loader: - case NodeType.UnLoader: - case NodeType.Clearner: - case NodeType.Buffer: - node.DockDirection = DockingDirection.Backward; - break; - default: - // Normal, Rotation, Label, Image는 DontCare 유지 - node.DockDirection = DockingDirection.DontCare; - break; - } - } - } - } - /// /// 중복된 NodeId를 가진 노드들을 고유한 NodeId로 수정 /// @@ -255,13 +265,13 @@ namespace AGVNavigationCore.Models // 첫 번째 패스: 중복된 노드들 식별 foreach (var node in mapNodes) { - if (usedIds.Contains(node.NodeId)) + if (usedIds.Contains(node.Id)) { duplicateNodes.Add(node); } else { - usedIds.Add(node.NodeId); + usedIds.Add(node.Id); } } @@ -271,9 +281,9 @@ namespace AGVNavigationCore.Models string newNodeId = GenerateUniqueNodeId(usedIds); // 다른 노드들의 연결에서 기존 NodeId를 새 NodeId로 업데이트 - UpdateConnections(mapNodes, duplicateNode.NodeId, newNodeId); + UpdateConnections(mapNodes, duplicateNode.Id, newNodeId); - duplicateNode.NodeId = newNodeId; + duplicateNode.Id = newNodeId; usedIds.Add(newNodeId); } } @@ -331,7 +341,7 @@ namespace AGVNavigationCore.Models if (mapNodes == null || mapNodes.Count == 0) return; // 존재하는 모든 노드 ID 집합 생성 - var validNodeIds = new HashSet(mapNodes.Select(n => n.NodeId)); + var validNodeIds = new HashSet(mapNodes.Select(n => n.Id)); // 각 노드의 연결을 검증하고 존재하지 않는 노드 ID 제거 foreach (var node in mapNodes) @@ -369,13 +379,13 @@ namespace AGVNavigationCore.Models foreach (var connectedNodeId in node.ConnectedNodes.ToList()) { - var connectedNode = mapNodes.FirstOrDefault(n => n.NodeId == connectedNodeId); + var connectedNode = mapNodes.FirstOrDefault(n => n.Id == connectedNodeId); if (connectedNode == null) continue; // 연결 쌍의 키 생성 (사전순 정렬) - string pairKey = string.Compare(node.NodeId, connectedNodeId, StringComparison.Ordinal) < 0 - ? $"{node.NodeId}-{connectedNodeId}" - : $"{connectedNodeId}-{node.NodeId}"; + string pairKey = string.Compare(node.Id, connectedNodeId, StringComparison.Ordinal) < 0 + ? $"{node.Id}-{connectedNodeId}" + : $"{connectedNodeId}-{node.Id}"; if (processedPairs.Contains(pairKey)) { @@ -388,17 +398,17 @@ namespace AGVNavigationCore.Models processedPairs.Add(pairKey); // 양방향 연결인 경우 하나만 유지 - if (connectedNode.ConnectedNodes.Contains(node.NodeId)) + if (connectedNode.ConnectedNodes.Contains(node.Id)) { // 사전순으로 더 작은 노드에만 연결을 유지 - if (string.Compare(node.NodeId, connectedNodeId, StringComparison.Ordinal) > 0) + if (string.Compare(node.Id, connectedNodeId, StringComparison.Ordinal) > 0) { connectionsToRemove.Add(connectedNodeId); } else { // 반대 방향 연결 제거 - connectedNode.RemoveConnection(node.NodeId); + connectedNode.RemoveConnection(node.Id); } } } @@ -433,16 +443,16 @@ namespace AGVNavigationCore.Models // 1단계: 모든 명시적 연결 수집 foreach (var node in mapNodes) { - if (!allConnections.ContainsKey(node.NodeId)) + if (!allConnections.ContainsKey(node.Id)) { - allConnections[node.NodeId] = new HashSet(); + allConnections[node.Id] = new HashSet(); } if (node.ConnectedNodes != null) { foreach (var connectedId in node.ConnectedNodes) { - allConnections[node.NodeId].Add(connectedId); + allConnections[node.Id].Add(connectedId); } } } @@ -458,10 +468,10 @@ namespace AGVNavigationCore.Models // 이 노드를 연결하는 모든 노드 찾기 foreach (var otherNodeId in allConnections.Keys) { - if (otherNodeId == node.NodeId) continue; + if (otherNodeId == node.Id) continue; // 다른 노드가 이 노드를 연결하고 있다면 - if (allConnections[otherNodeId].Contains(node.NodeId)) + if (allConnections[otherNodeId].Contains(node.Id)) { // 이 노드의 ConnectedNodes에 그 노드를 추가 (중복 방지) if (!node.ConnectedNodes.Contains(otherNodeId)) diff --git a/Cs_HMI/AGVLogic/AGVNavigationCore/Models/MapMagnet.cs b/Cs_HMI/AGVLogic/AGVNavigationCore/Models/MapMagnet.cs new file mode 100644 index 0000000..1384890 --- /dev/null +++ b/Cs_HMI/AGVLogic/AGVNavigationCore/Models/MapMagnet.cs @@ -0,0 +1,72 @@ +using System; +using System.ComponentModel; +using System.Drawing; +using Newtonsoft.Json; + +namespace AGVNavigationCore.Models +{ + /// + /// 맵 상의 마그넷(Magnet) 정보를 나타내는 클래스 + /// + public class MapMagnet : NodeBase + { + + public MapMagnet() { + Type = NodeType.Magnet; + } + + [Category("위치 정보")] + [Description("시작점 좌표")] + public MagnetPoint P1 { get; set; } = new MagnetPoint(); + + [Category("위치 정보")] + [Description("끝점 좌표")] + public MagnetPoint P2 { get; set; } = new MagnetPoint(); + + [Category("위치 정보")] + [Description("제어점 좌표 (곡선인 경우)")] + public MagnetPoint ControlPoint { get; set; } = null; + + public class MagnetPoint + { + public double X { get; set; } + public double Y { get; set; } + } + + [JsonIgnore] + public override Point Position + { + get => new Point((int)P1.X, (int)P1.Y); + set + { + double dx = value.X - P1.X; + double dy = value.Y - P1.Y; + + P1.X += dx; + P1.Y += dy; + P2.X += dx; + P2.Y += dy; + + if (ControlPoint != null) + { + ControlPoint.X += dx; + ControlPoint.Y += dy; + } + } + } + + /// + /// 시작점 Point 반환 + /// + [Browsable(false)] + [JsonIgnore] + public Point StartPoint => new Point((int)P1.X, (int)P1.Y); + + /// + /// 끝점 Point 반환 + /// + [Browsable(false)] + [JsonIgnore] + public Point EndPoint => new Point((int)P2.X, (int)P2.Y); + } +} diff --git a/Cs_HMI/AGVLogic/AGVNavigationCore/Models/MapMark.cs b/Cs_HMI/AGVLogic/AGVNavigationCore/Models/MapMark.cs new file mode 100644 index 0000000..6cd657e --- /dev/null +++ b/Cs_HMI/AGVLogic/AGVNavigationCore/Models/MapMark.cs @@ -0,0 +1,37 @@ +using System; +using System.ComponentModel; +using System.Drawing; + +namespace AGVNavigationCore.Models +{ + /// + /// 맵 상의 마크(Mark) 정보를 나타내는 클래스 + /// + public class MapMark : NodeBase + { + // Id is inherited from NodeBase + public MapMark() { + Type = NodeType.Mark; + } + + [Category("위치 정보")] + [Description("마크의 X 좌표")] + public double X + { + get => Position.X; + set => Position = new Point((int)value, Position.Y); + } + + [Category("위치 정보")] + [Description("마크의 Y 좌표")] + public double Y + { + get => Position.Y; + set => Position = new Point(Position.X, (int)value); + } + + [Category("위치 정보")] + [Description("마크의 회전 각도")] + public double Rotation { get; set; } + } +} diff --git a/Cs_HMI/AGVLogic/AGVNavigationCore/Models/MapNode.cs b/Cs_HMI/AGVLogic/AGVNavigationCore/Models/MapNode.cs index f7d46d2..641c7fd 100644 --- a/Cs_HMI/AGVLogic/AGVNavigationCore/Models/MapNode.cs +++ b/Cs_HMI/AGVLogic/AGVNavigationCore/Models/MapNode.cs @@ -1,81 +1,63 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Drawing; using System.Drawing.Drawing2D; using AGVNavigationCore.Utils; +using Newtonsoft.Json; namespace AGVNavigationCore.Models { /// - /// 맵 노드 정보를 관리하는 클래스 - /// 논리적 노드로서 실제 맵의 위치와 속성을 정의 + /// 맵 노드 정보를 관리하는 클래스 (주행 경로용 노드) /// - public class MapNode + public class MapNode : NodeBase { - /// - /// 논리적 노드 ID (맵 에디터에서 관리하는 고유 ID) - /// 예: "N001", "N002", "LOADER1", "CHARGER1" - /// - public string NodeId { get; set; } = string.Empty; - /// - /// 노드 표시 이름 (사용자 친화적) - /// 예: "로더1", "충전기1", "교차점A", "회전지점1" - /// - public string Name { get; set; } = string.Empty; - /// - /// 맵 상의 위치 좌표 (픽셀 단위) - /// - public Point Position { get; set; } = Point.Empty; + [Category("라벨 설정")] + [Description("표시할 텍스트입니다.")] + public string Text { get; set; } = ""; - /// - /// 노드 타입 - /// - public NodeType Type { get; set; } = NodeType.Normal; + public StationType StationType { get; set; } + [Browsable(false)] public bool CanDocking { get { - if (Type == NodeType.Buffer) return true; - if (Type == NodeType.Loader) return true; - if (Type == NodeType.UnLoader) return true; - if (Type == NodeType.Clearner) return true; - if (Type == NodeType.Charging) return true; + if (StationType == StationType.Buffer) return true; + if (StationType == StationType.Loader) return true; + if (StationType == StationType.UnLoader) return true; + if (StationType == StationType.Clearner) return true; + if (StationType == StationType.Charger) return true; return false; } } - /// - /// 도킹 방향 (도킹/충전 노드인 경우에만 Forward/Backward, 일반 노드는 DontCare) - /// + [Category("노드 설정")] + [Description("도킹/충전 노드의 진입 방향입니다.")] public DockingDirection DockDirection { get; set; } = DockingDirection.DontCare; - /// - /// 연결된 노드 ID 목록 (경로 정보) - /// + [Category("연결 정보")] + [Description("연결된 노드 ID 목록입니다.")] + [ReadOnly(true)] public List ConnectedNodes { get; set; } = new List(); - /// - /// 연결된 노드 객체 목록 (런타임 전용, JSON 무시) - /// - [Newtonsoft.Json.JsonIgnore] + [JsonIgnore] + [Browsable(false)] public List ConnectedMapNodes { get; set; } = new List(); - /// - /// 회전 가능 여부 (180도 회전 가능한 지점) - /// + [Category("주행 설정")] + [Description("제자리 회전(좌) 가능 여부입니다.")] public bool CanTurnLeft { get; set; } = true; - /// - /// 회전 가능 여부 (180도 회전 가능한 지점) - /// + [Category("주행 설정")] + [Description("제자리 회전(우) 가능 여부입니다.")] public bool CanTurnRight { get; set; } = true; - /// - /// 교차로로 이용가능한지 - /// + [Category("주행 설정")] + [Description("교차로 주행 가능 여부입니다.")] public bool DisableCross { get @@ -85,216 +67,61 @@ namespace AGVNavigationCore.Models } set { _disablecross = value; } } - private bool _disablecross = false; - /// - /// 해당 노드 통과 시 제한 속도 (기본값: M - Normal) - /// Predict 단계에서 이 값을 참조하여 속도 명령을 생성합니다. - /// + [Category("주행 설정")] + [Description("노드 통과 시 제한 속도입니다.")] public SpeedLevel SpeedLimit { get; set; } = SpeedLevel.M; - /// - /// 장비 ID (도킹/충전 스테이션인 경우) - /// 예: "LOADER1", "CLEANER1", "BUFFER1", "CHARGER1" - /// - public string NodeAlias { get; set; } = string.Empty; + [Category("노드 설정")] + [Description("장비 ID 또는 별칭입니다.")] + public string AliasName { get; set; } = string.Empty; - /// - /// 노드 생성 일자 - /// - public DateTime CreatedDate { get; set; } = DateTime.Now; - - /// - /// 노드 수정 일자 - /// - public DateTime ModifiedDate { get; set; } = DateTime.Now; - - /// - /// 노드 활성화 여부 - /// + [Category("기본 정보")] + [Description("노드 사용 여부입니다.")] public bool IsActive { get; set; } = true; - /// - /// 노드 색상 (맵 에디터 표시용) - /// - public Color DisplayColor { get; set; } = Color.Blue; - - /// - /// RFID 태그 ID (이 노드에 매핑된 RFID) - /// + [Category("RFID 정보")] + [Description("물리적 RFID 태그 ID입니다.")] public string RfidId { get; set; } = string.Empty; - /// - /// RFID 상태 (정상, 손상, 교체예정 등) - /// - public string RfidStatus { get; set; } = "정상"; - /// - /// RFID 설치 위치 설명 (현장 작업자용) - /// 예: "로더1번 앞", "충전기2번 입구", "복도 교차점" 등 - /// - public string RfidDescription { get; set; } = string.Empty; - - /// - /// 라벨 텍스트 (NodeType.Label인 경우 사용) - /// - public string LabelText { get; set; } = string.Empty; - - /// - /// 라벨 폰트 패밀리 (NodeType.Label인 경우 사용) - /// - public string FontFamily { get; set; } = "Arial"; - - /// - /// 라벨 폰트 크기 (NodeType.Label인 경우 사용) - /// - public float FontSize { get; set; } = 12.0f; - - /// - /// 라벨 폰트 스타일 (NodeType.Label인 경우 사용) - /// - public FontStyle FontStyle { get; set; } = FontStyle.Regular; - - /// - /// 텍스트 전경색 (모든 노드 타입에서 사용) - /// - public Color ForeColor { get; set; } = Color.Black; - - /// - /// 라벨 배경색 (NodeType.Label인 경우 사용) - /// - public Color BackColor { get; set; } = Color.Transparent; + [Category("노드 텍스트"), DisplayName("TextColor")] + [Description("텍스트 색상입니다.")] + public Color NodeTextForeColor { get; set; } = Color.Black; private float _textFontSize = 7.0f; - - /// - /// 텍스트 폰트 크기 (모든 노드 타입의 텍스트 표시에 사용, 픽셀 단위) - /// 0 이하의 값이 설정되면 기본값 7.0f로 자동 설정 - /// - public float TextFontSize + [Category("노드 텍스트"), DisplayName("TextSize")] + [Description("일반 노드 텍스트의 크기입니다.")] + public float NodeTextFontSize { get => _textFontSize; set => _textFontSize = value > 0 ? value : 7.0f; } - /// - /// 텍스트 볼드체 여부 (모든 노드 타입의 텍스트 표시에 사용) - /// - public bool TextFontBold { get; set; } = true; - - /// - /// 노드 이름 말풍선 배경색 (하단에 표시되는 노드 이름의 배경색) - /// - public Color NameBubbleBackColor { get; set; } = Color.Gold; - - /// - /// 노드 이름 말풍선 글자색 (하단에 표시되는 노드 이름의 글자색) - /// - public Color NameBubbleForeColor { get; set; } = Color.Black; - - /// - /// 라벨 배경 표시 여부 (NodeType.Label인 경우 사용) - /// - public bool ShowBackground { get; set; } = false; - - /// - /// 라벨 패딩 (NodeType.Label인 경우 사용, 픽셀 단위) - /// - public int Padding { get; set; } = 8; - - /// - /// 이미지 파일 경로 (편집용, 저장시엔 사용되지 않음) - /// - [Newtonsoft.Json.JsonIgnore] - public string ImagePath { get; set; } = string.Empty; - - /// - /// Base64 인코딩된 이미지 데이터 (JSON 저장용) - /// - public string ImageBase64 { get; set; } = string.Empty; - - /// - /// 이미지 크기 배율 (NodeType.Image인 경우 사용) - /// - public SizeF Scale { get; set; } = new SizeF(1.0f, 1.0f); - - /// - /// 이미지 투명도 (NodeType.Image인 경우 사용, 0.0~1.0) - /// - public float Opacity { get; set; } = 1.0f; - - /// - /// 이미지 회전 각도 (NodeType.Image인 경우 사용, 도 단위) - /// - public float Rotation { get; set; } = 0.0f; - - /// - /// 로딩된 이미지 (런타임에서만 사용, JSON 직렬화 제외) - /// - [Newtonsoft.Json.JsonIgnore] - public Image LoadedImage { get; set; } - - /// - /// 기본 생성자 - /// - public MapNode() + public MapNode() : base() { + Type = NodeType.Normal; } - - /// - /// 매개변수 생성자 - /// - /// 노드 ID - /// 노드 이름 - /// 위치 - /// 노드 타입 - public MapNode(string nodeId, string name, Point position, NodeType type) + + public MapNode(string nodeId, Point position, StationType type) : base(nodeId, position) { - NodeId = nodeId; - Name = name; - Position = position; - Type = type; - CreatedDate = DateTime.Now; - ModifiedDate = DateTime.Now; - - // 타입별 기본 색상 설정 - SetDefaultColorByType(type); + Type = NodeType.Normal; } - - /// - /// 노드 타입에 따른 기본 색상 설정 - /// - /// 노드 타입 - public void SetDefaultColorByType(NodeType type) + [Category("기본 정보")] + [JsonIgnore] + [ReadOnly(true), Browsable(false)] + public bool isDockingNode { - switch (type) + get { - case NodeType.Normal: - DisplayColor = Color.Blue; - break; - case NodeType.UnLoader: - case NodeType.Clearner: - case NodeType.Buffer: - case NodeType.Loader: - DisplayColor = Color.Green; - break; - case NodeType.Charging: - DisplayColor = Color.Red; - break; - case NodeType.Label: - DisplayColor = Color.Purple; - break; - case NodeType.Image: - DisplayColor = Color.Brown; - break; + if (StationType == StationType.Charger || StationType == StationType.Buffer || + StationType == StationType.Clearner || StationType == StationType.Loader || + StationType == StationType.UnLoader) return true; + return false; } } - /// - /// 다른 노드와의 연결 추가 - /// - /// 연결할 노드 ID public void AddConnection(string nodeId) { if (!ConnectedNodes.Contains(nodeId)) @@ -304,10 +131,6 @@ namespace AGVNavigationCore.Models } } - /// - /// 다른 노드와의 연결 제거 - /// - /// 연결 해제할 노드 ID public void RemoveConnection(string nodeId) { if (ConnectedNodes.Remove(nodeId)) @@ -316,290 +139,29 @@ namespace AGVNavigationCore.Models } } - ///// - ///// 도킹 스테이션 설정 - ///// - ///// 장비 ID - ///// 장비 타입 - ///// 도킹 방향 - //public void SetDockingStation(string stationId, StationType stationType, DockingDirection dockDirection) - //{ - // Type = NodeType.Docking; - // NodeAlias = stationId; - // DockDirection = dockDirection; - // SetDefaultColorByType(NodeType.Docking); - // ModifiedDate = DateTime.Now; - //} - - /// - /// 충전 스테이션 설정 - /// - /// 충전기 ID public void SetChargingStation(string stationId) { - Type = NodeType.Charging; - NodeAlias = stationId; - DockDirection = DockingDirection.Forward; // 충전기는 항상 전진 도킹 - SetDefaultColorByType(NodeType.Charging); + StationType = StationType.Charger; + Id = stationId; + DockDirection = DockingDirection.Forward; ModifiedDate = DateTime.Now; } - /// - /// 문자열 표현 - /// public override string ToString() { - return $"{RfidId}({NodeId}): {Name} ({Type}) at ({Position.X}, {Position.Y})"; + return $"{RfidId}({Id}): {AliasName} ({Type}) at ({Position.X}, {Position.Y})"; } - /// - /// 리스트박스 표시용 텍스트 (노드ID - 설명 - RFID 순서) - /// - public string DisplayText - { - get - { - var displayText = NodeId; - - if (!string.IsNullOrEmpty(Name)) - { - displayText += $" - {Name}"; - } - - if (!string.IsNullOrEmpty(RfidId)) - { - displayText += $" - [{RfidId}]"; - } - - return displayText; - } - } - - /// - /// 노드 복사 - /// - /// 복사된 노드 - public MapNode Clone() - { - var clone = new MapNode - { - NodeId = NodeId, - Name = Name, - Position = Position, - Type = Type, - DockDirection = DockDirection, - ConnectedNodes = new List(ConnectedNodes), - - CanTurnLeft = CanTurnLeft, - CanTurnRight = CanTurnRight, - DisableCross = DisableCross, - - NodeAlias = NodeAlias, - CreatedDate = CreatedDate, - ModifiedDate = ModifiedDate, - IsActive = IsActive, - DisplayColor = DisplayColor, - RfidId = RfidId, - RfidStatus = RfidStatus, - RfidDescription = RfidDescription, - LabelText = LabelText, - FontFamily = FontFamily, - FontSize = FontSize, - FontStyle = FontStyle, - ForeColor = ForeColor, - BackColor = BackColor, - TextFontSize = TextFontSize, - TextFontBold = TextFontBold, - NameBubbleBackColor = NameBubbleBackColor, - NameBubbleForeColor = NameBubbleForeColor, - ShowBackground = ShowBackground, - Padding = Padding, - ImagePath = ImagePath, - ImageBase64 = ImageBase64, - Scale = Scale, - Opacity = Opacity, - Rotation = Rotation - }; - return clone; - } - - /// - /// 이미지 로드 (Base64 또는 파일 경로에서, 256x256 이상일 경우 자동 리사이즈) - /// - /// 로드 성공 여부 - public bool LoadImage() - { - if (Type != NodeType.Image) return false; - - try - { - Image originalImage = null; - - // 1. 먼저 Base64 데이터 시도 - if (!string.IsNullOrEmpty(ImageBase64)) - { - originalImage = ImageConverterUtil.Base64ToImage(ImageBase64); - } - // 2. Base64가 없으면 파일 경로에서 로드 - else if (!string.IsNullOrEmpty(ImagePath) && System.IO.File.Exists(ImagePath)) - { - originalImage = Image.FromFile(ImagePath); - } - - if (originalImage != null) - { - LoadedImage?.Dispose(); - - // 이미지 크기 체크 및 리사이즈 - if (originalImage.Width > 256 || originalImage.Height > 256) - { - LoadedImage = ResizeImage(originalImage, 256, 256); - originalImage.Dispose(); - } - else - { - LoadedImage = originalImage; - } - - return true; - } - } - catch (Exception) - { - // 이미지 로드 실패 - } - return false; - } - - /// - /// 이미지 리사이즈 (비율 유지) - /// - /// 원본 이미지 - /// 최대 너비 - /// 최대 높이 - /// 리사이즈된 이미지 - private Image ResizeImage(Image image, int maxWidth, int maxHeight) - { - // 비율 계산 - double ratioX = (double)maxWidth / image.Width; - double ratioY = (double)maxHeight / image.Height; - double ratio = Math.Min(ratioX, ratioY); - - // 새로운 크기 계산 - int newWidth = (int)(image.Width * ratio); - int newHeight = (int)(image.Height * ratio); - - // 리사이즈된 이미지 생성 - var resizedImage = new Bitmap(newWidth, newHeight); - using (var graphics = Graphics.FromImage(resizedImage)) - { - graphics.CompositingQuality = CompositingQuality.HighQuality; - graphics.InterpolationMode = InterpolationMode.HighQualityBicubic; - graphics.SmoothingMode = SmoothingMode.HighQuality; - graphics.DrawImage(image, 0, 0, newWidth, newHeight); - } - - return resizedImage; - } - - /// - /// 실제 표시될 크기 계산 (이미지 노드인 경우) - /// - /// 실제 크기 - public Size GetDisplaySize() - { - if (Type != NodeType.Image || LoadedImage == null) return Size.Empty; - - return new Size( - (int)(LoadedImage.Width * Scale.Width), - (int)(LoadedImage.Height * Scale.Height) - ); - } - - /// - /// 파일 경로에서 이미지를 Base64로 변환하여 저장 - /// - /// 이미지 파일 경로 - /// 변환 성공 여부 - public bool ConvertImageToBase64(string filePath) - { - if (Type != NodeType.Image) return false; - - try - { - if (!System.IO.File.Exists(filePath)) - { - return false; - } - - ImageBase64 = ImageConverterUtil.FileToBase64(filePath, System.Drawing.Imaging.ImageFormat.Png); - ImagePath = filePath; // 편집용으로 경로 유지 - - return !string.IsNullOrEmpty(ImageBase64); - } - catch (Exception) - { - return false; - } - } - - /// - /// 리소스 정리 - /// - public void Dispose() - { - LoadedImage?.Dispose(); - LoadedImage = null; - } - - /// - /// 경로 찾기에 사용 가능한 노드인지 확인 - /// (라벨, 이미지 노드는 경로 찾기에서 제외) - /// public bool IsNavigationNode() { - return Type != NodeType.Label && Type != NodeType.Image && IsActive; + // 이제 MapNode는 항상 내비게이션 노드임 (Label, Image 분리됨) + // 하지만 기존 로직 호환성을 위해 Active 체크만 유지 + return IsActive; } - /// - /// RFID가 할당되어 있는지 확인 - /// public bool HasRfid() { return !string.IsNullOrEmpty(RfidId); } - - /// - /// RFID 정보 설정 - /// - /// RFID ID - /// 설치 위치 설명 - /// RFID 상태 - public void SetRfidInfo(string rfidId, string rfidDescription = "", string rfidStatus = "정상") - { - RfidId = rfidId; - RfidDescription = rfidDescription; - RfidStatus = rfidStatus; - ModifiedDate = DateTime.Now; - } - - /// - /// RFID 정보 삭제 - /// - public void ClearRfidInfo() - { - RfidId = string.Empty; - RfidDescription = string.Empty; - RfidStatus = "정상"; - ModifiedDate = DateTime.Now; - } - - /// - /// RFID 기반 표시 텍스트 (RFID ID 우선, 없으면 노드ID) - /// - public string GetRfidDisplayText() - { - return HasRfid() ? RfidId : NodeId; - } } } \ No newline at end of file diff --git a/Cs_HMI/AGVLogic/AGVNavigationCore/Models/NodeBase.cs b/Cs_HMI/AGVLogic/AGVNavigationCore/Models/NodeBase.cs new file mode 100644 index 0000000..86fac5b --- /dev/null +++ b/Cs_HMI/AGVLogic/AGVNavigationCore/Models/NodeBase.cs @@ -0,0 +1,61 @@ +using System; +using System.ComponentModel; +using System.Diagnostics; +using System.Drawing; +using Newtonsoft.Json; + +namespace AGVNavigationCore.Models +{ + /// + /// 맵 상의 모든 객체의 최상위 기본 클래스 + /// 위치, 선택 상태, 기본 식별자 등을 관리 + /// + public abstract class NodeBase + { + [Category("기본 정보")] + [Description("객체의 고유 ID입니다.")] + [ReadOnly(true)] + public string Id { get; set; } = Guid.NewGuid().ToString(); + + [Category("기본 정보")] + public NodeType Type { protected set; get; } = NodeType.Normal; + + [Category("기본 정보")] + [Description("객체의 좌표(X, Y)입니다.")] + public virtual Point Position { get; set; } = Point.Empty; + + [Category("기본 정보")] + [Description("객체 생성 일자입니다.")] + [JsonIgnore] + [ReadOnly(true), Browsable(false)] + public DateTime CreatedDate { get; set; } = DateTime.Now; + + [Category("기본 정보")] + [Description("객체 수정 일자입니다.")] + [JsonIgnore] + [ReadOnly(true), Browsable(false)] + public DateTime ModifiedDate { get; set; } = DateTime.Now; + + [Browsable(false)] + [JsonIgnore] + public bool IsSelected { get; set; } = false; + + + + [Browsable(false)] + [JsonIgnore] + public bool IsHovered { get; set; } = false; + + + + public NodeBase() + { + } + + public NodeBase(string id, Point position) + { + Id = id; + Position = position; + } + } +} diff --git a/Cs_HMI/AGVLogic/AGVNavigationCore/Models/VirtualAGV.cs b/Cs_HMI/AGVLogic/AGVNavigationCore/Models/VirtualAGV.cs index a0c7b83..e658cdb 100644 --- a/Cs_HMI/AGVLogic/AGVNavigationCore/Models/VirtualAGV.cs +++ b/Cs_HMI/AGVLogic/AGVNavigationCore/Models/VirtualAGV.cs @@ -146,7 +146,12 @@ namespace AGVNavigationCore.Models /// /// 현재 노드 ID /// - public string CurrentNodeId => _currentNode?.NodeId; + public MapNode CurrentNode => _currentNode; + + /// + /// 현재 노드 ID (CurrentNode.Id) + /// + public string CurrentNodeId => _currentNode?.Id; /// /// 이전 위치 @@ -158,10 +163,6 @@ namespace AGVNavigationCore.Models /// public float BatteryLevel { get; set; } = 100.0f; - /// - /// 이전 노드 ID - /// - public string PrevNodeId => _prevNode?.NodeId; /// /// 이전 노드 @@ -314,7 +315,7 @@ namespace AGVNavigationCore.Models MagnetPosition.S, SpeedLevel.L, eAGVCommandReason.NoPath, - $"위치 확정 완료 (목적지 미설정) - 현재:{_currentNode?.NodeId ?? "알수없음"}" + $"위치 확정 완료 (목적지 미설정) - 현재:{_currentNode?.Id ?? "알수없음"}" ); } @@ -323,7 +324,7 @@ namespace AGVNavigationCore.Models if (_currentPath.DetailedPath.Where(t => t.seq < lastNode.seq && t.IsPass == false).Any() == false) { // 마지막 노드에 도착했는지 확인 (현재 노드가 마지막 노드와 같은지) - if (_currentNode != null && _currentNode.NodeId == lastNode.NodeId) + if (_currentNode != null && _currentNode.Id == lastNode.NodeId) { if (lastNode.IsPass) //이미완료되었다. { @@ -332,7 +333,7 @@ namespace AGVNavigationCore.Models MagnetPosition.S, SpeedLevel.L, eAGVCommandReason.Complete, - $"목적지 도착 - 최종:{_currentNode?.NodeId ?? "알수없음"}" + $"목적지 도착 - 최종:{_currentNode?.Id ?? "알수없음"}" ); } else @@ -343,7 +344,7 @@ namespace AGVNavigationCore.Models MagnetPosition.S, SpeedLevel.L, eAGVCommandReason.MarkStop, - $"목적지 도착 전(MarkStop) - 최종:{_currentNode?.NodeId ?? "알수없음"}" + $"목적지 도착 전(MarkStop) - 최종:{_currentNode?.Id ?? "알수없음"}" ); } @@ -351,7 +352,7 @@ namespace AGVNavigationCore.Models } // 4. 경로이탈 - var TargetNode = _currentPath.DetailedPath.Where(t => t.IsPass == false && t.NodeId.Equals(_currentNode.NodeId)).FirstOrDefault(); + var TargetNode = _currentPath.DetailedPath.Where(t => t.IsPass == false && t.NodeId.Equals(_currentNode.Id)).FirstOrDefault(); if (TargetNode == null) { return new AGVCommand( @@ -359,11 +360,11 @@ namespace AGVNavigationCore.Models MagnetPosition.S, SpeedLevel.L, eAGVCommandReason.PathOut, - $"(재탐색요청)경로이탈 현재위치:{_currentNode.NodeId}" + $"(재탐색요청)경로이탈 현재위치:{_currentNode.Id}" ); } - return GetCommandFromPath(CurrentNodeId, "경로 실행 시작"); + return GetCommandFromPath(CurrentNode, "경로 실행 시작"); } @@ -414,13 +415,13 @@ namespace AGVNavigationCore.Models } _currentPath = path; - _remainingNodes = path.Path.Select(n => n.NodeId).ToList(); // MapNode → NodeId 변환 + _remainingNodes = path.Path.Select(n => n.Id).ToList(); // MapNode → NodeId 변환 _currentNodeIndex = 0; // 경로 시작 노드가 현재 노드와 다른 경우 경고 - if (_currentNode != null && _remainingNodes.Count > 0 && _remainingNodes[0] != _currentNode.NodeId) + if (_currentNode != null && _remainingNodes.Count > 0 && _remainingNodes[0] != _currentNode.Id) { - OnError($"경로 시작 노드({_remainingNodes[0]})와 현재 노드({_currentNode.NodeId})가 다릅니다."); + OnError($"경로 시작 노드({_remainingNodes[0]})와 현재 노드({_currentNode.Id})가 다릅니다."); } } @@ -555,7 +556,7 @@ namespace AGVNavigationCore.Models public void SetPosition(MapNode node, AgvDirection motorDirection) { // 현재 위치를 이전 위치로 저장 (리프트 방향 계산용) - if (_currentNode != null && _currentNode.NodeId != node.NodeId) + if (_currentNode != null && _currentNode.Id != node.Id) { _prevPosition = _currentPosition; // 이전 위치 _prevNode = _currentNode; @@ -574,9 +575,9 @@ namespace AGVNavigationCore.Models _currentNode = node; // 🔥 노드 ID를 RFID로 간주하여 감지 목록에 추가 (시뮬레이터용) - if (!string.IsNullOrEmpty(node.NodeId) && !_detectedRfids.Contains(node.NodeId)) + if (!string.IsNullOrEmpty(node.Id) && !_detectedRfids.Contains(node.Id)) { - _detectedRfids.Add(node.NodeId); + _detectedRfids.Add(node.Id); } // 🔥 RFID 2개 이상 감지 시 위치 확정 @@ -588,7 +589,7 @@ namespace AGVNavigationCore.Models //현재 경로값이 있는지 확인한다. if (CurrentPath != null && CurrentPath.DetailedPath != null && CurrentPath.DetailedPath.Any()) { - var item = CurrentPath.DetailedPath.FirstOrDefault(t => t.NodeId == node.NodeId && t.IsPass == false); + var item = CurrentPath.DetailedPath.FirstOrDefault(t => t.NodeId == node.Id && t.IsPass == false); if (item != null) { // [PathJump Check] 점프한 노드 개수 확인 @@ -596,7 +597,7 @@ namespace AGVNavigationCore.Models int skippedCount = CurrentPath.DetailedPath.Count(t => t.seq < item.seq && t.IsPass == false); if (skippedCount > 2) { - OnError($"PathJump: {skippedCount}개의 노드를 건너뛰었습니다. (허용: 2개, 현재노드: {node.NodeId})"); + OnError($"PathJump: {skippedCount}개의 노드를 건너뛰었습니다. (허용: 2개, 현재노드: {node.Id})"); return; } @@ -631,7 +632,7 @@ namespace AGVNavigationCore.Models /// public string GetRfidByNodeId(List _mapNodes, string nodeId) { - var node = _mapNodes?.FirstOrDefault(n => n.NodeId == nodeId); + var node = _mapNodes?.FirstOrDefault(n => n.Id == nodeId); return node?.HasRfid() == true ? node.RfidId : nodeId; } @@ -642,7 +643,7 @@ namespace AGVNavigationCore.Models /// /// DetailedPath에서 노드 정보를 찾아 AGVCommand 생성 /// - private AGVCommand GetCommandFromPath(string targetNodeId, string actionDescription) + private AGVCommand GetCommandFromPath(MapNode targetNode, string actionDescription) { // DetailedPath가 없으면 기본 명령 반환 if (_currentPath == null || _currentPath.DetailedPath == null || _currentPath.DetailedPath.Count == 0) @@ -659,7 +660,7 @@ namespace AGVNavigationCore.Models // DetailedPath에서 targetNodeId에 해당하는 NodeMotorInfo 찾기 // 지나가지 않은 경로를 찾는다 - var nodeInfo = _currentPath.DetailedPath.FirstOrDefault(n => n.NodeId == targetNodeId && n.IsPass == false); + var nodeInfo = _currentPath.DetailedPath.FirstOrDefault(n => n.NodeId == targetNode.Id && n.IsPass == false); if (nodeInfo == null) { @@ -673,7 +674,7 @@ namespace AGVNavigationCore.Models MagnetPosition.S, SpeedLevel.M, eAGVCommandReason.NoTarget, - $"{actionDescription} (노드 {targetNodeId} 정보 없음)" + $"{actionDescription} (노드 {targetNode.Id} 정보 없음)" ); } @@ -721,7 +722,7 @@ namespace AGVNavigationCore.Models magnetPos, speed, eAGVCommandReason.Normal, - $"{actionDescription} → {targetNodeId} (Motor:{motorCmd}, Magnet:{magnetPos})" + $"{actionDescription} → {targetNode.Id} (Motor:{motorCmd}, Magnet:{magnetPos})" ); } @@ -878,21 +879,6 @@ namespace AGVNavigationCore.Models } } - private DockingDirection GetDockingDirection(NodeType nodeType) - { - switch (nodeType) - { - case NodeType.Charging: - return DockingDirection.Forward; - case NodeType.Loader: - case NodeType.UnLoader: - case NodeType.Clearner: - case NodeType.Buffer: - return DockingDirection.Backward; - default: - return DockingDirection.Forward; - } - } private void OnError(string message) { diff --git a/Cs_HMI/AGVLogic/AGVNavigationCore/PathFinding/Analysis/JunctionAnalyzer.cs b/Cs_HMI/AGVLogic/AGVNavigationCore/PathFinding/Analysis/JunctionAnalyzer.cs index 5fcd92d..2a3c694 100644 --- a/Cs_HMI/AGVLogic/AGVNavigationCore/PathFinding/Analysis/JunctionAnalyzer.cs +++ b/Cs_HMI/AGVLogic/AGVNavigationCore/PathFinding/Analysis/JunctionAnalyzer.cs @@ -59,7 +59,7 @@ namespace AGVNavigationCore.PathFinding.Analysis if (node.IsNavigationNode()) { var junctionInfo = AnalyzeNode(node); - _junctions[node.NodeId] = junctionInfo; + _junctions[node.Id] = junctionInfo; } } } @@ -69,7 +69,7 @@ namespace AGVNavigationCore.PathFinding.Analysis /// private JunctionInfo AnalyzeNode(MapNode node) { - var junction = new JunctionInfo(node.NodeId); + var junction = new JunctionInfo(node.Id); // 양방향 연결을 고려하여 모든 연결된 노드 찾기 var connectedNodes = GetAllConnectedNodes(node); @@ -96,16 +96,16 @@ namespace AGVNavigationCore.PathFinding.Analysis { if (connectedNode != null) { - connected.Add(connectedNode.NodeId); + connected.Add(connectedNode.Id); } } // 역방향 연결된 노드들 (다른 노드에서 이 노드로 연결) foreach (var otherNode in _mapNodes) { - if (otherNode.NodeId != node.NodeId && otherNode.ConnectedMapNodes.Any(n => n.NodeId == node.NodeId)) + if (otherNode.Id != node.Id && otherNode.ConnectedMapNodes.Any(n => n.Id == node.Id)) { - connected.Add(otherNode.NodeId); + connected.Add(otherNode.Id); } } @@ -124,7 +124,7 @@ namespace AGVNavigationCore.PathFinding.Analysis foreach (var connectedId in connectedNodes) { - var connectedNode = _mapNodes.FirstOrDefault(n => n.NodeId == connectedId); + var connectedNode = _mapNodes.FirstOrDefault(n => n.Id == connectedId); if (connectedNode != null) { double angle = CalculateAngle(junctionNode.Position, connectedNode.Position); @@ -226,9 +226,9 @@ namespace AGVNavigationCore.PathFinding.Analysis return MagnetDirection.Straight; // 실제 각도 기반으로 마그넷 방향 계산 - var fromNode = _mapNodes.FirstOrDefault(n => n.NodeId == fromNodeId); - var currentNode = _mapNodes.FirstOrDefault(n => n.NodeId == currentNodeId); - var toNode = _mapNodes.FirstOrDefault(n => n.NodeId == toNodeId); + var fromNode = _mapNodes.FirstOrDefault(n => n.Id == fromNodeId); + var currentNode = _mapNodes.FirstOrDefault(n => n.Id == currentNodeId); + var toNode = _mapNodes.FirstOrDefault(n => n.Id == toNodeId); if (fromNode == null || currentNode == null || toNode == null) return MagnetDirection.Straight; diff --git a/Cs_HMI/AGVLogic/AGVNavigationCore/PathFinding/Core/AGVPathResult.cs b/Cs_HMI/AGVLogic/AGVNavigationCore/PathFinding/Core/AGVPathResult.cs index 819f043..4578313 100644 --- a/Cs_HMI/AGVLogic/AGVNavigationCore/PathFinding/Core/AGVPathResult.cs +++ b/Cs_HMI/AGVLogic/AGVNavigationCore/PathFinding/Core/AGVPathResult.cs @@ -320,7 +320,7 @@ namespace AGVNavigationCore.PathFinding.Core { return DetailedPath.Select(n => n.NodeId).ToList(); } - return Path?.Select(n => n.NodeId).ToList() ?? new List(); + return Path?.Select(n => n.Id).ToList() ?? new List(); } /// diff --git a/Cs_HMI/AGVLogic/AGVNavigationCore/PathFinding/Core/AStarPathfinder.cs b/Cs_HMI/AGVLogic/AGVNavigationCore/PathFinding/Core/AStarPathfinder.cs index 19bff1c..f62f41e 100644 --- a/Cs_HMI/AGVLogic/AGVNavigationCore/PathFinding/Core/AStarPathfinder.cs +++ b/Cs_HMI/AGVLogic/AGVNavigationCore/PathFinding/Core/AStarPathfinder.cs @@ -50,36 +50,36 @@ namespace AGVNavigationCore.PathFinding.Core // 모든 네비게이션 노드를 PathNode로 변환하고 양방향 연결 생성 foreach (var mapNode in _mapNodes) { - _mapNodeLookup[mapNode.NodeId] = mapNode; // Add to lookup table + _mapNodeLookup[mapNode.Id] = mapNode; // Add to lookup table if (mapNode.IsNavigationNode()) { - var pathNode = new PathNode(mapNode.NodeId, mapNode.Position); - _nodeMap[mapNode.NodeId] = pathNode; + var pathNode = new PathNode(mapNode.Id, mapNode.Position); + _nodeMap[mapNode.Id] = pathNode; } } // 단일 연결을 양방향으로 확장 foreach (var mapNode in _mapNodes) { - if (mapNode.IsNavigationNode() && _nodeMap.ContainsKey(mapNode.NodeId)) + if (mapNode.IsNavigationNode() && _nodeMap.ContainsKey(mapNode.Id)) { - var pathNode = _nodeMap[mapNode.NodeId]; + var pathNode = _nodeMap[mapNode.Id]; foreach (var connectedNode in mapNode.ConnectedMapNodes) { - if (connectedNode != null && _nodeMap.ContainsKey(connectedNode.NodeId)) + if (connectedNode != null && _nodeMap.ContainsKey(connectedNode.Id)) { // 양방향 연결 생성 (단일 연결이 양방향을 의미) - if (!pathNode.ConnectedNodes.Contains(connectedNode.NodeId)) + if (!pathNode.ConnectedNodes.Contains(connectedNode.Id)) { - pathNode.ConnectedNodes.Add(connectedNode.NodeId); + pathNode.ConnectedNodes.Add(connectedNode.Id); } - var connectedPathNode = _nodeMap[connectedNode.NodeId]; - if (!connectedPathNode.ConnectedNodes.Contains(mapNode.NodeId)) + var connectedPathNode = _nodeMap[connectedNode.Id]; + if (!connectedPathNode.ConnectedNodes.Contains(mapNode.Id)) { - connectedPathNode.ConnectedNodes.Add(mapNode.NodeId); + connectedPathNode.ConnectedNodes.Add(mapNode.Id); } } } @@ -371,8 +371,8 @@ namespace AGVNavigationCore.PathFinding.Core var combinedDetailedPath = new List(previousResult.DetailedPath ?? new List()); // 이전 경로의 마지막 노드와 현재 경로의 시작 노드 비교 - string lastNodeOfPrevious = previousResult.Path[previousResult.Path.Count - 1].NodeId; - string firstNodeOfCurrent = currentResult.Path[0].NodeId; + string lastNodeOfPrevious = previousResult.Path[previousResult.Path.Count - 1].Id; + string firstNodeOfCurrent = currentResult.Path[0].Id; if (lastNodeOfPrevious == firstNodeOfCurrent) { @@ -508,8 +508,8 @@ namespace AGVNavigationCore.PathFinding.Core float totalDistance = 0; for (int i = 0; i < path.Count - 1; i++) { - var nodeId1 = path[i].NodeId; - var nodeId2 = path[i + 1].NodeId; + var nodeId1 = path[i].Id; + var nodeId2 = path[i + 1].Id; if (_nodeMap.ContainsKey(nodeId1) && _nodeMap.ContainsKey(nodeId2)) { @@ -577,7 +577,7 @@ namespace AGVNavigationCore.PathFinding.Core return null; // 교차로 노드 찾기 - var junctionNode = _mapNodes.FirstOrDefault(n => n.NodeId == junctionNodeId); + var junctionNode = _mapNodes.FirstOrDefault(n => n.Id == junctionNodeId); if (junctionNode == null || junctionNode.ConnectedNodes == null || junctionNode.ConnectedNodes.Count == 0) return null; @@ -602,7 +602,7 @@ namespace AGVNavigationCore.PathFinding.Core if (connectedNodeId == targetNodeId) continue; // 조건 3, 4, 5: 존재하고, 활성 상태이고, 네비게이션 가능 - var connectedNode = _mapNodes.FirstOrDefault(n => n.NodeId == connectedNodeId); + var connectedNode = _mapNodes.FirstOrDefault(n => n.Id == connectedNodeId); if (connectedNode != null && connectedNode.IsActive && connectedNode.IsNavigationNode()) { alternateNodes.Add(connectedNode); diff --git a/Cs_HMI/AGVLogic/AGVNavigationCore/PathFinding/Planning/AGVPathfinder.cs b/Cs_HMI/AGVLogic/AGVNavigationCore/PathFinding/Planning/AGVPathfinder.cs index 8aef7e8..4bf93e8 100644 --- a/Cs_HMI/AGVLogic/AGVNavigationCore/PathFinding/Planning/AGVPathfinder.cs +++ b/Cs_HMI/AGVLogic/AGVNavigationCore/PathFinding/Planning/AGVPathfinder.cs @@ -50,7 +50,7 @@ namespace AGVNavigationCore.PathFinding.Planning n.DisableCross == false && n.ConnectedNodes.Count >= 3 && n.ConnectedMapNodes.Where(t => t.CanDocking).Any() == false && - n.NodeId != startNode.NodeId + n.Id != startNode.Id ).ToList(); // docking 포인트가 연결된 노드는 제거한다. @@ -103,7 +103,7 @@ namespace AGVNavigationCore.PathFinding.Planning pathNode.ConnectedNodes.Count >= 3 && pathNode.ConnectedMapNodes.Where(t => t.CanDocking).Any() == false) { - if (pathNode.NodeId.Equals(StartNode.NodeId) == false) + if (pathNode.Id.Equals(StartNode.Id) == false) return pathNode; } } @@ -111,6 +111,12 @@ namespace AGVNavigationCore.PathFinding.Planning return null; } + public AGVPathResult FindPath(MapNode startNode, MapNode targetNode) + { + // 기본값으로 경로 탐색 (이전 위치 = 현재 위치, 방향 = 전진) + return FindPath(startNode, targetNode, startNode, AgvDirection.Forward, AgvDirection.Forward, false); + } + public AGVPathResult FindPath(MapNode startNode, MapNode targetNode, MapNode prevNode, AgvDirection prevDirection, AgvDirection currentDirection, bool crossignore = false) { @@ -121,13 +127,17 @@ namespace AGVNavigationCore.PathFinding.Planning return AGVPathResult.CreateFailure("목적지 노드가 null입니다.", 0, 0); if (prevNode == null) return AGVPathResult.CreateFailure("이전위치 노드가 null입니다.", 0, 0); - if (startNode.NodeId == targetNode.NodeId && targetNode.DockDirection.MatchAGVDirection(prevDirection)) + if (targetNode.isDockingNode == false && targetNode.Type != NodeType.Normal) + return AGVPathResult.CreateFailure("이동 가능한 노드가 아닙니다", 0, 0); + + var tnode = targetNode as MapNode; + if (startNode.Id == targetNode.Id && tnode.DockDirection.MatchAGVDirection(prevDirection)) return AGVPathResult.CreateSuccess(new List { startNode, startNode }, new List(), 0, 0); var ReverseDirection = (currentDirection == AgvDirection.Forward ? AgvDirection.Backward : AgvDirection.Forward); //1.목적지까지의 최단거리 경로를 찾는다. - var pathResult = _basicPathfinder.FindPathAStar(startNode.NodeId, targetNode.NodeId); + var pathResult = _basicPathfinder.FindPathAStar(startNode.Id, targetNode.Id); pathResult.PrevNode = prevNode; pathResult.PrevDirection = prevDirection; if (!pathResult.Success || pathResult.Path == null || pathResult.Path.Count == 0) @@ -146,11 +156,11 @@ namespace AGVNavigationCore.PathFinding.Planning //2.AGV방향과 목적지에 설정된 방향이 일치하면 그대로 진행하면된다.(목적지에 방향이 없는 경우에도 그대로 진행) - if (targetNode.DockDirection == DockingDirection.DontCare || - (targetNode.DockDirection == DockingDirection.Forward && currentDirection == AgvDirection.Forward) || - (targetNode.DockDirection == DockingDirection.Backward && currentDirection == AgvDirection.Backward)) + if (tnode.DockDirection == DockingDirection.DontCare || + (tnode.DockDirection == DockingDirection.Forward && currentDirection == AgvDirection.Forward) || + (tnode.DockDirection == DockingDirection.Backward && currentDirection == AgvDirection.Backward)) { - if ((nextNodeForward?.NodeId ?? string.Empty) == pathResult.Path[1].NodeId) //예측경로와 다음진행방향 경로가 일치하면 해당 방향이 맞다 + if ((nextNodeForward?.Id ?? string.Empty) == pathResult.Path[1].Id) //예측경로와 다음진행방향 경로가 일치하면 해당 방향이 맞다 { MakeDetailData(pathResult, currentDirection); MakeMagnetDirection(pathResult); @@ -177,10 +187,10 @@ namespace AGVNavigationCore.PathFinding.Planning //뒤로 이동시 경로상의 처음 만나는 노드가 같다면 그 방향으로 이동하면 된다. // ⚠️ 단, 현재 방향과 목적지 도킹 방향이 일치해야 함! if (nextNodeBackward != null && pathResult.Path.Count > 1 && - nextNodeBackward.NodeId == pathResult.Path[1].NodeId) // ✅ 추가: 현재도 Backward여야 함 + nextNodeBackward.Id == pathResult.Path[1].Id) // ✅ 추가: 현재도 Backward여야 함 { - if (targetNode.DockDirection == DockingDirection.Forward && ReverseDirection == AgvDirection.Forward || - targetNode.DockDirection == DockingDirection.Backward && ReverseDirection == AgvDirection.Backward) + if (tnode.DockDirection == DockingDirection.Forward && ReverseDirection == AgvDirection.Forward || + tnode.DockDirection == DockingDirection.Backward && ReverseDirection == AgvDirection.Backward) { MakeDetailData(pathResult, ReverseDirection); MakeMagnetDirection(pathResult); @@ -191,12 +201,12 @@ namespace AGVNavigationCore.PathFinding.Planning } if (nextNodeForward != null && pathResult.Path.Count > 1 && - nextNodeForward.NodeId == pathResult.Path[1].NodeId && - targetNode.DockDirection == DockingDirection.Forward && + nextNodeForward.Id == pathResult.Path[1].Id && + tnode.DockDirection == DockingDirection.Forward && currentDirection == AgvDirection.Forward) // ✅ 추가: 현재도 Forward여야 함 { - if (targetNode.DockDirection == DockingDirection.Forward && currentDirection == AgvDirection.Forward || - targetNode.DockDirection == DockingDirection.Backward && currentDirection == AgvDirection.Backward) + if (tnode.DockDirection == DockingDirection.Forward && currentDirection == AgvDirection.Forward || + tnode.DockDirection == DockingDirection.Backward && currentDirection == AgvDirection.Backward) { MakeDetailData(pathResult, currentDirection); MakeMagnetDirection(pathResult); @@ -221,7 +231,7 @@ namespace AGVNavigationCore.PathFinding.Planning //진행방향으로 이동했을때 나오는 노드를 사용한다. if (nextNodeForward != null) { - var Path0 = _basicPathfinder.FindPathAStar(startNode.NodeId, nextNodeForward.NodeId); + var Path0 = _basicPathfinder.FindPathAStar(startNode.Id, nextNodeForward.Id); Path0.PrevNode = prevNode; Path0.PrevDirection = prevDirection; MakeDetailData(Path0, prevDirection); @@ -259,7 +269,7 @@ namespace AGVNavigationCore.PathFinding.Planning JunctionInPath = FindNearestJunction(startNode); //종료노드로부터 가까운 교차로 검색 - if (JunctionInPath == null) JunctionInPath = FindNearestJunction(targetNode); + if (JunctionInPath == null) JunctionInPath = FindNearestJunction(tnode); } if (JunctionInPath == null) return AGVPathResult.CreateFailure("교차로가 없어 경로계산을 할 수 없습니다", 0, 0); @@ -267,7 +277,7 @@ namespace AGVNavigationCore.PathFinding.Planning //경유지를 포함하여 경로를 다시 계산한다. //1.시작위치 - 교차로(여기까지는 현재 방향으로 그대로 이동을 한다) - var path1 = _basicPathfinder.FindPathAStar(startNode.NodeId, JunctionInPath.NodeId); + var path1 = _basicPathfinder.FindPathAStar(startNode.Id, JunctionInPath.Id); path1.PrevNode = prevNode; path1.PrevDirection = prevDirection; @@ -278,7 +288,7 @@ namespace AGVNavigationCore.PathFinding.Planning // ReverseCheck = false; //현재 진행 방향으로 이동해야한다 // MakeDetailData(path1, currentDirection); // path1의 상세 경로 정보 채우기 (모터 방향 설정) //} - if (path1.Path.Count > 1 && nextNodeBackward != null && nextNodeBackward.NodeId.Equals(path1.Path[1].NodeId)) + if (path1.Path.Count > 1 && nextNodeBackward != null && nextNodeBackward.Id.Equals(path1.Path[1].Id)) { ReverseCheck = true; //현재 방향의 반대방향으로 이동해야한다 MakeDetailData(path1, ReverseDirection); // path1의 상세 경로 정보 채우기 (모터 방향 설정) @@ -295,7 +305,7 @@ namespace AGVNavigationCore.PathFinding.Planning //2.교차로 - 종료위치 - var path2 = _basicPathfinder.FindPathAStar(JunctionInPath.NodeId, targetNode.NodeId); + var path2 = _basicPathfinder.FindPathAStar(JunctionInPath.Id, targetNode.Id); path2.PrevNode = prevNode; path2.PrevDirection = prevDirection; @@ -315,9 +325,9 @@ namespace AGVNavigationCore.PathFinding.Planning //3.방향전환을 위환 대체 노드찾기 - tempNode = _basicPathfinder.FindAlternateNodeForDirectionChange(JunctionInPath.NodeId, - path1.Path[path1.Path.Count - 2].NodeId, - path2.Path[1].NodeId); + tempNode = _basicPathfinder.FindAlternateNodeForDirectionChange(JunctionInPath.Id, + path1.Path[path1.Path.Count - 2].Id, + path2.Path[1].Id); //4. path1 + tempnode + path2 가 최종 위치가 된다. if (tempNode == null) @@ -332,7 +342,7 @@ namespace AGVNavigationCore.PathFinding.Planning //if (tempNode != null) { // 교차로 → 대체노드 경로 계산 - var pathToTemp = _basicPathfinder.FindPathAStar(JunctionInPath.NodeId, tempNode.NodeId); + var pathToTemp = _basicPathfinder.FindPathAStar(JunctionInPath.Id, tempNode.Id); pathToTemp.PrevNode = JunctionInPath; pathToTemp.PrevDirection = (ReverseCheck ? ReverseDirection : currentDirection); if (!pathToTemp.Success) @@ -348,7 +358,7 @@ namespace AGVNavigationCore.PathFinding.Planning combinedResult = _basicPathfinder.CombineResults(combinedResult, pathToTemp); // 대체노드 → 교차로 경로 계산 (역방향) - var pathFromTemp = _basicPathfinder.FindPathAStar(tempNode.NodeId, JunctionInPath.NodeId); + var pathFromTemp = _basicPathfinder.FindPathAStar(tempNode.Id, JunctionInPath.Id); pathFromTemp.PrevNode = JunctionInPath; pathFromTemp.PrevDirection = (ReverseCheck ? ReverseDirection : currentDirection); if (!pathFromTemp.Success) @@ -362,14 +372,14 @@ namespace AGVNavigationCore.PathFinding.Planning //현재까지 노드에서 목적지까지의 방향이 일치하면 그대로 사용한다. bool temp3ok = false; - var TempCheck3 = _basicPathfinder.FindPathAStar(combinedResult.Path.Last().NodeId, targetNode.NodeId); - if (TempCheck3.Path.First().NodeId.Equals(combinedResult.Path.Last().NodeId)) + var TempCheck3 = _basicPathfinder.FindPathAStar(combinedResult.Path.Last().Id, targetNode.Id); + if (TempCheck3.Path.First().Id.Equals(combinedResult.Path.Last().Id)) { - if (targetNode.DockDirection == DockingDirection.Forward && combinedResult.DetailedPath.Last().MotorDirection == AgvDirection.Forward) + if (tnode.DockDirection == DockingDirection.Forward && combinedResult.DetailedPath.Last().MotorDirection == AgvDirection.Forward) { temp3ok = true; } - else if (targetNode.DockDirection == DockingDirection.Backward && combinedResult.DetailedPath.Last().MotorDirection == AgvDirection.Backward) + else if (tnode.DockDirection == DockingDirection.Backward && combinedResult.DetailedPath.Last().MotorDirection == AgvDirection.Backward) { temp3ok = true; } @@ -381,11 +391,11 @@ namespace AGVNavigationCore.PathFinding.Planning if (temp3ok == false) { //목적지와 방향이 맞지 않다. 그러므로 대체노드를 추가로 더 찾아야한다. - var tempNode2 = _basicPathfinder.FindAlternateNodeForDirectionChange(JunctionInPath.NodeId, - combinedResult.Path[combinedResult.Path.Count - 2].NodeId, - path2.Path[1].NodeId); + var tempNode2 = _basicPathfinder.FindAlternateNodeForDirectionChange(JunctionInPath.Id, + combinedResult.Path[combinedResult.Path.Count - 2].Id, + path2.Path[1].Id); - var pathToTemp2 = _basicPathfinder.FindPathAStar(JunctionInPath.NodeId, tempNode2.NodeId); + var pathToTemp2 = _basicPathfinder.FindPathAStar(JunctionInPath.Id, tempNode2.Id); if (ReverseCheck) MakeDetailData(pathToTemp2, currentDirection); else MakeDetailData(pathToTemp2, ReverseDirection); @@ -400,7 +410,7 @@ namespace AGVNavigationCore.PathFinding.Planning combinedResult.DetailedPath[combinedResult.DetailedPath.Count - 1].MotorDirection = currentDirection; } - var pathToTemp3 = _basicPathfinder.FindPathAStar(tempNode2.NodeId, JunctionInPath.NodeId); + var pathToTemp3 = _basicPathfinder.FindPathAStar(tempNode2.Id, JunctionInPath.Id); if (ReverseCheck) MakeDetailData(pathToTemp3, ReverseDirection); else MakeDetailData(pathToTemp3, currentDirection); @@ -420,12 +430,15 @@ namespace AGVNavigationCore.PathFinding.Planning } + /// + /// 이 작업후에 MakeMagnetDirection 를 추가로 실행 하세요 + /// /// /// 이 작업후에 MakeMagnetDirection 를 추가로 실행 하세요 /// /// /// - private void MakeDetailData(AGVPathResult path1, AgvDirection currentDirection) + public void MakeDetailData(AGVPathResult path1, AgvDirection currentDirection) { if (path1.Success && path1.Path != null && path1.Path.Count > 0) { @@ -433,9 +446,9 @@ namespace AGVNavigationCore.PathFinding.Planning for (int i = 0; i < path1.Path.Count; i++) { var node = path1.Path[i]; - string nodeId = node.NodeId; + string nodeId = node.Id; string RfidId = node.RfidId; - string nextNodeId = (i + 1 < path1.Path.Count) ? path1.Path[i + 1].NodeId : null; + string nextNodeId = (i + 1 < path1.Path.Count) ? path1.Path[i + 1].Id : null; // 노드 정보 생성 (현재 방향 유지) var nodeInfo = new NodeMotorInfo(i + 1, @@ -446,7 +459,7 @@ namespace AGVNavigationCore.PathFinding.Planning ); // [Speed Control] MapNode의 속도 설정 적용 - var mapNode = _mapNodes.FirstOrDefault(n => n.NodeId == nodeId); + var mapNode = _mapNodes.FirstOrDefault(n => n.Id == nodeId); if (mapNode != null) { nodeInfo.Speed = mapNode.SpeedLimit; @@ -470,13 +483,13 @@ namespace AGVNavigationCore.PathFinding.Planning for (int i = 0; i < path1.DetailedPath.Count; i++) { var detailPath = path1.DetailedPath[i]; - string nodeId = path1.Path[i].NodeId; - string nextNodeId = (i + 1 < path1.Path.Count) ? path1.Path[i + 1].NodeId : null; + string nodeId = path1.Path[i].Id; + string nextNodeId = (i + 1 < path1.Path.Count) ? path1.Path[i + 1].Id : null; // 마그넷 방향 계산 (3개 이상 연결된 교차로에서만 좌/우 가중치 적용) if (i > 0 && nextNodeId != null) { - string prevNodeId = path1.Path[i - 1].NodeId; + string prevNodeId = path1.Path[i - 1].Id; if (path1.DetailedPath[i - 1].MotorDirection != detailPath.MotorDirection) detailPath.MagnetDirection = MagnetDirection.Straight; else diff --git a/Cs_HMI/AGVLogic/AGVNavigationCore/PathFinding/Planning/DirectionChangePlanner.cs b/Cs_HMI/AGVLogic/AGVNavigationCore/PathFinding/Planning/DirectionChangePlanner.cs index fad50ee..3fdc685 100644 --- a/Cs_HMI/AGVLogic/AGVNavigationCore/PathFinding/Planning/DirectionChangePlanner.cs +++ b/Cs_HMI/AGVLogic/AGVNavigationCore/PathFinding/Planning/DirectionChangePlanner.cs @@ -91,14 +91,14 @@ namespace AGVNavigationCore.PathFinding.Planning // 직접 경로에 갈림길이 포함된 경우 그 갈림길에서 방향 전환 foreach (var node in directPath2.Path.Skip(1).Take(directPath2.Path.Count - 2)) // 시작과 끝 제외 { - var junctionInfo = _junctionAnalyzer.GetJunctionInfo(node.NodeId); + var junctionInfo = _junctionAnalyzer.GetJunctionInfo(node.Id); if (junctionInfo != null && junctionInfo.IsJunction) { // 간단한 방향 전환: 직접 경로 사용하되 방향 전환 노드 표시 return DirectionChangePlan.CreateSuccess( directPath2.Path, - node.NodeId, - $"갈림길 {node.NodeId}에서 방향 전환: {currentDirection} → {requiredDirection}" + node.Id, + $"갈림길 {node.Id}에서 방향 전환: {currentDirection} → {requiredDirection}" ); } } @@ -165,14 +165,14 @@ namespace AGVNavigationCore.PathFinding.Planning { foreach (var node in directPath.Path.Skip(2)) // 시작점과 다음 노드는 제외 { - var junctionInfo = _junctionAnalyzer.GetJunctionInfo(node.NodeId); + var junctionInfo = _junctionAnalyzer.GetJunctionInfo(node.Id); if (junctionInfo != null && junctionInfo.IsJunction) { // 직진 경로상에서는 더 엄격한 조건 적용 - if (!suitableJunctions.Contains(node.NodeId) && - HasMultipleExitOptions(node.NodeId)) + if (!suitableJunctions.Contains(node.Id) && + HasMultipleExitOptions(node.Id)) { - suitableJunctions.Add(node.NodeId); + suitableJunctions.Add(node.Id); } } } @@ -226,7 +226,7 @@ namespace AGVNavigationCore.PathFinding.Planning /// private List GetAllConnectedNodes(string nodeId) { - var node = _mapNodes.FirstOrDefault(n => n.NodeId == nodeId); + var node = _mapNodes.FirstOrDefault(n => n.Id == nodeId); if (node == null) return new List(); var connected = new HashSet(); @@ -236,16 +236,16 @@ namespace AGVNavigationCore.PathFinding.Planning { if (connectedNode != null) { - connected.Add(connectedNode.NodeId); + connected.Add(connectedNode.Id); } } // 역방향 연결 foreach (var otherNode in _mapNodes) { - if (otherNode.NodeId != nodeId && otherNode.ConnectedMapNodes.Any(n => n.NodeId == nodeId)) + if (otherNode.Id != nodeId && otherNode.ConnectedMapNodes.Any(n => n.Id == nodeId)) { - connected.Add(otherNode.NodeId); + connected.Add(otherNode.Id); } } @@ -293,7 +293,7 @@ namespace AGVNavigationCore.PathFinding.Planning string actualDirectionChangeNode = FindActualDirectionChangeNode(changePath, junctionNodeId); string description = $"갈림길 {GetDisplayName(junctionNodeId)}를 통해 {GetDisplayName(actualDirectionChangeNode)}에서 방향 전환: {currentDirection} → {requiredDirection}"; - System.Diagnostics.Debug.WriteLine($"[DirectionChangePlanner] ✅ 유효한 방향전환 경로: {string.Join(" → ", changePath.Select(n => n.NodeId))}"); + System.Diagnostics.Debug.WriteLine($"[DirectionChangePlanner] ✅ 유효한 방향전환 경로: {string.Join(" → ", changePath.Select(n => n.Id))}"); return DirectionChangePlan.CreateSuccess(changePath, actualDirectionChangeNode, description); } @@ -319,7 +319,7 @@ namespace AGVNavigationCore.PathFinding.Planning // 2. 인근 갈림길을 통한 우회인지, 직진 경로상 갈림길인지 판단 var directPath = _pathfinder.FindPathAStar(startNodeId, targetNodeId); - bool isNearbyDetour = !directPath.Success || !directPath.Path.Any(n => n.NodeId == junctionNodeId); + bool isNearbyDetour = !directPath.Success || !directPath.Path.Any(n => n.Id == junctionNodeId); if (isNearbyDetour) { @@ -376,17 +376,17 @@ namespace AGVNavigationCore.PathFinding.Planning if (currentDirection != requiredDirection) { string fromNodeId = toJunctionPath.Path.Count >= 2 ? - toJunctionPath.Path[toJunctionPath.Path.Count - 2].NodeId : startNodeId; + toJunctionPath.Path[toJunctionPath.Path.Count - 2].Id : startNodeId; var changeSequence = GenerateDirectionChangeSequence(junctionNodeId, fromNodeId, currentDirection, requiredDirection); if (changeSequence.Count > 1) { - fullPath.AddRange(changeSequence.Skip(1).Select(nodeId => _mapNodes.FirstOrDefault(n => n.NodeId == nodeId)).Where(n => n != null)); + fullPath.AddRange(changeSequence.Skip(1).Select(nodeId => _mapNodes.FirstOrDefault(n => n.Id == nodeId)).Where(n => n != null)); } } // 3. 갈림길에서 목표점까지의 경로 - string lastNode = fullPath.LastOrDefault()?.NodeId ?? junctionNodeId; + string lastNode = fullPath.LastOrDefault()?.Id ?? junctionNodeId; var fromJunctionPath = _pathfinder.FindPathAStar(lastNode, targetNodeId); if (fromJunctionPath.Success && fromJunctionPath.Path.Count > 1) { @@ -461,8 +461,8 @@ namespace AGVNavigationCore.PathFinding.Planning // 왔던 길(excludeNodeId)를 제외한 노드 중에서 최적의 우회 노드 선택 // 우선순위: 1) 막다른 길이 아닌 노드 (우회 후 복귀 가능) 2) 직진방향 3) 목적지 방향 - var junctionNode = _mapNodes.FirstOrDefault(n => n.NodeId == junctionNodeId); - var fromNode = _mapNodes.FirstOrDefault(n => n.NodeId == excludeNodeId); + var junctionNode = _mapNodes.FirstOrDefault(n => n.Id == junctionNodeId); + var fromNode = _mapNodes.FirstOrDefault(n => n.Id == excludeNodeId); if (junctionNode == null || fromNode == null) return availableNodes.FirstOrDefault(); @@ -478,7 +478,7 @@ namespace AGVNavigationCore.PathFinding.Planning { if (nodeId == excludeNodeId) continue; // 왔던 길 제외 - var candidateNode = _mapNodes.FirstOrDefault(n => n.NodeId == nodeId); + var candidateNode = _mapNodes.FirstOrDefault(n => n.Id == nodeId); if (candidateNode == null) continue; // 갈림길에서 후보 노드로의 방향 벡터 계산 (junctionNode → candidateNode) @@ -561,11 +561,11 @@ namespace AGVNavigationCore.PathFinding.Planning return junctionNodeId; // 기본값으로 갈림길 반환 // 갈림길이 두 번 나타나는 위치 찾기 - int firstJunctionIndex = changePath.FindIndex(n => n.NodeId == junctionNodeId); + int firstJunctionIndex = changePath.FindIndex(n => n.Id == junctionNodeId); int lastJunctionIndex = -1; for (int i = changePath.Count - 1; i >= 0; i--) { - if (changePath[i].NodeId == junctionNodeId) + if (changePath[i].Id == junctionNodeId) { lastJunctionIndex = i; break; @@ -577,7 +577,7 @@ namespace AGVNavigationCore.PathFinding.Planning firstJunctionIndex != lastJunctionIndex && lastJunctionIndex - firstJunctionIndex == 2) { // 첫 번째와 두 번째 갈림길 사이에 있는 노드가 실제 방향전환 노드 - string detourNode = changePath[firstJunctionIndex + 1].NodeId; + string detourNode = changePath[firstJunctionIndex + 1].Id; return detourNode; } @@ -647,11 +647,11 @@ namespace AGVNavigationCore.PathFinding.Planning } string errorMessage = $"되돌아가기 패턴 검출 ({backtrackingPatterns.Count}개): {string.Join(", ", issues)}"; - System.Diagnostics.Debug.WriteLine($"[PathValidation] ❌ 경로: {string.Join(" → ", path.Select(n => n.NodeId))}"); + System.Diagnostics.Debug.WriteLine($"[PathValidation] ❌ 경로: {string.Join(" → ", path.Select(n => n.Id))}"); System.Diagnostics.Debug.WriteLine($"[PathValidation] ❌ 되돌아가기 패턴: {errorMessage}"); return PathValidationResult.CreateInvalidWithBacktracking( - path.Select(n => n.NodeId).ToList(), backtrackingPatterns, startNodeId, "", junctionNodeId, errorMessage); + path.Select(n => n.Id).ToList(), backtrackingPatterns, startNodeId, "", junctionNodeId, errorMessage); } // 2. 연속된 중복 노드 검증 @@ -670,13 +670,13 @@ namespace AGVNavigationCore.PathFinding.Planning } // 4. 갈림길 포함 여부 검증 - if (!path.Any(n => n.NodeId == junctionNodeId)) + if (!path.Any(n => n.Id == junctionNodeId)) { return PathValidationResult.CreateInvalid(startNodeId, "", $"갈림길 {junctionNodeId}이 경로에 포함되지 않음"); } - System.Diagnostics.Debug.WriteLine($"[PathValidation] ✅ 유효한 경로: {string.Join(" → ", path.Select(n => n.NodeId))}"); - return PathValidationResult.CreateValid(path.Select(n => n.NodeId).ToList(), startNodeId, "", junctionNodeId); + System.Diagnostics.Debug.WriteLine($"[PathValidation] ✅ 유효한 경로: {string.Join(" → ", path.Select(n => n.Id))}"); + return PathValidationResult.CreateValid(path.Select(n => n.Id).ToList(), startNodeId, "", junctionNodeId); } /// @@ -688,9 +688,9 @@ namespace AGVNavigationCore.PathFinding.Planning for (int i = 0; i < path.Count - 2; i++) { - string nodeA = path[i].NodeId; - string nodeB = path[i + 1].NodeId; - string nodeC = path[i + 2].NodeId; + string nodeA = path[i].Id; + string nodeB = path[i + 1].Id; + string nodeC = path[i + 2].Id; // A → B → A 패턴 검출 if (nodeA == nodeC && nodeA != nodeB) @@ -712,9 +712,9 @@ namespace AGVNavigationCore.PathFinding.Planning for (int i = 0; i < path.Count - 1; i++) { - if (path[i].NodeId == path[i + 1].NodeId) + if (path[i].Id == path[i + 1].Id) { - duplicates.Add(path[i].NodeId); + duplicates.Add(path[i].Id); } } @@ -728,12 +728,12 @@ namespace AGVNavigationCore.PathFinding.Planning { for (int i = 0; i < path.Count - 1; i++) { - string currentNode = path[i].NodeId; - string nextNode = path[i + 1].NodeId; + string currentNode = path[i].Id; + string nextNode = path[i + 1].Id; // 두 노드간 직접 연결성 확인 (맵 노드의 ConnectedMapNodes 리스트 사용) - var currentMapNode = _mapNodes.FirstOrDefault(n => n.NodeId == currentNode); - if (currentMapNode == null || !currentMapNode.ConnectedMapNodes.Any(n => n.NodeId == nextNode)) + var currentMapNode = _mapNodes.FirstOrDefault(n => n.Id == currentNode); + if (currentMapNode == null || !currentMapNode.ConnectedMapNodes.Any(n => n.Id == nextNode)) { return PathValidationResult.CreateInvalid(currentNode, nextNode, $"노드 {currentNode}와 {nextNode} 사이에 연결이 없음"); } @@ -769,7 +769,7 @@ namespace AGVNavigationCore.PathFinding.Planning /// 표시할 이름 private string GetDisplayName(string nodeId) { - var node = _mapNodes.FirstOrDefault(n => n.NodeId == nodeId); + var node = _mapNodes.FirstOrDefault(n => n.Id == nodeId); if (node != null && !string.IsNullOrEmpty(node.RfidId)) { return node.RfidId; diff --git a/Cs_HMI/AGVLogic/AGVNavigationCore/PathFinding/Planning/DirectionalPathfinder.cs b/Cs_HMI/AGVLogic/AGVNavigationCore/PathFinding/Planning/DirectionalPathfinder.cs index 779f3cc..9f7ce2e 100644 --- a/Cs_HMI/AGVLogic/AGVNavigationCore/PathFinding/Planning/DirectionalPathfinder.cs +++ b/Cs_HMI/AGVLogic/AGVNavigationCore/PathFinding/Planning/DirectionalPathfinder.cs @@ -66,7 +66,7 @@ namespace AGVNavigationCore.PathFinding.Planning // 연결된 노드 중 현재 노드가 아닌 것들만 필터링 var candidateNodes = allNodes.Where(n => - connectedNodeIds.Contains(n.NodeId) && n.NodeId != currentNode.NodeId + connectedNodeIds.Contains(n.Id) && n.Id != currentNode.Id ).ToList(); if (candidateNodes.Count == 0) @@ -88,7 +88,7 @@ namespace AGVNavigationCore.PathFinding.Planning if (movementLength < 0.001f) // 거의 이동하지 않음 { - return candidateNodes[0].NodeId; // 첫 번째 연결 노드 반환 + return candidateNodes[0].Id; // 첫 번째 연결 노드 반환 } var normalizedMovement = new PointF( @@ -138,7 +138,7 @@ namespace AGVNavigationCore.PathFinding.Planning // 가장 높은 점수를 가진 노드 반환 var bestCandidate = scoredCandidates.OrderByDescending(x => x.score).First(); - return bestCandidate.node.NodeId; + return bestCandidate.node.Id; } /// diff --git a/Cs_HMI/AGVLogic/AGVNavigationCore/Utils/DirectionalHelper.cs b/Cs_HMI/AGVLogic/AGVNavigationCore/Utils/DirectionalHelper.cs index 55bd1b0..2dd7faf 100644 --- a/Cs_HMI/AGVLogic/AGVNavigationCore/Utils/DirectionalHelper.cs +++ b/Cs_HMI/AGVLogic/AGVNavigationCore/Utils/DirectionalHelper.cs @@ -76,7 +76,7 @@ namespace AGVNavigationCore.Utils List candidateNodes = new List(); if (prevDirection == direction) { - candidateNodes = connectedMapNodes.Where(n => n.NodeId != prevNode.NodeId).ToList(); + candidateNodes = connectedMapNodes.Where(n => n.Id != prevNode.Id).ToList(); } else { @@ -112,9 +112,9 @@ namespace AGVNavigationCore.Utils Console.WriteLine( $"\n[GetNextNodeByDirection] ========== 다음 노드 선택 시작 =========="); Console.WriteLine( - $" 현재노드: {currentNode.RfidId}[{currentNode.NodeId}]({currentNode.Position.X:F1}, {currentNode.Position.Y:F1})"); + $" 현재노드: {currentNode.RfidId}[{currentNode.Id}]({currentNode.Position.X:F1}, {currentNode.Position.Y:F1})"); Console.WriteLine( - $" 이전노드: {prevNode.RfidId}[{prevNode.NodeId}]({prevNode.Position.X:F1}, {prevNode.Position.Y:F1})"); + $" 이전노드: {prevNode.RfidId}[{prevNode.Id}]({prevNode.Position.X:F1}, {prevNode.Position.Y:F1})"); Console.WriteLine( $" 이동벡터: ({movementVector.X:F2}, {movementVector.Y:F2}) → 정규화: ({normalizedMovement.X:F3}, {normalizedMovement.Y:F3})"); Console.WriteLine( @@ -159,7 +159,7 @@ namespace AGVNavigationCore.Utils } Console.WriteLine( - $"\n [후보] {candidate.RfidId}[{candidate.NodeId}]({candidate.Position.X:F1}, {candidate.Position.Y:F1})"); + $"\n [후보] {candidate.RfidId}[{candidate.Id}]({candidate.Position.X:F1}, {candidate.Position.Y:F1})"); Console.WriteLine( $" 벡터: ({toNextVector.X:F2}, {toNextVector.Y:F2}), 길이: {toNextLength:F2}"); Console.WriteLine( @@ -204,7 +204,7 @@ namespace AGVNavigationCore.Utils } Console.WriteLine( - $"\n 최종선택: {bestNode?.RfidId ?? "null"}[{bestNode?.NodeId ?? "null"}] (점수: {bestScore:F4})"); + $"\n 최종선택: {bestNode?.RfidId ?? "null"}[{bestNode?.Id ?? "null"}] (점수: {bestScore:F4})"); Console.WriteLine( $"[GetNextNodeByDirection] ========== 다음 노드 선택 종료 ==========\n"); @@ -445,15 +445,15 @@ namespace AGVNavigationCore.Utils // 선택 이유 생성 if (prevMotorDirection.HasValue && direction == prevMotorDirection) { - reason = $"모터 방향 일관성 유지 ({direction}) → {candidate.NodeId}"; + reason = $"모터 방향 일관성 유지 ({direction}) → {candidate.Id}"; } else if (prevMotorDirection.HasValue) { - reason = $"모터 방향 변경 ({prevMotorDirection} → {direction}) → {candidate.NodeId}"; + reason = $"모터 방향 변경 ({prevMotorDirection} → {direction}) → {candidate.Id}"; } else { - reason = $"방향 기반 선택 ({direction}) → {candidate.NodeId}"; + reason = $"방향 기반 선택 ({direction}) → {candidate.Id}"; } } } diff --git a/Cs_HMI/AGVLogic/AGVNavigationCore/Utils/DirectionalPathfinderTest.cs b/Cs_HMI/AGVLogic/AGVNavigationCore/Utils/DirectionalPathfinderTest.cs index 3eb2e5f..a4dfc80 100644 --- a/Cs_HMI/AGVLogic/AGVNavigationCore/Utils/DirectionalPathfinderTest.cs +++ b/Cs_HMI/AGVLogic/AGVNavigationCore/Utils/DirectionalPathfinderTest.cs @@ -90,8 +90,8 @@ namespace AGVNavigationCore.Utils return; } - Console.WriteLine($"이전 노드: {previousNode.NodeId} (RFID: {previousNode.RfidId}) - 위치: {previousNode.Position}"); - Console.WriteLine($"현재 노드: {currentNode.NodeId} (RFID: {currentNode.RfidId}) - 위치: {currentNode.Position}"); + Console.WriteLine($"이전 노드: {previousNode.Id} (RFID: {previousNode.RfidId}) - 위치: {previousNode.Position}"); + Console.WriteLine($"현재 노드: {currentNode.Id} (RFID: {currentNode.RfidId}) - 위치: {currentNode.Position}"); Console.WriteLine($"이동 벡터: ({currentNode.Position.X - previousNode.Position.X}, " + $"{currentNode.Position.Y - previousNode.Position.Y})"); @@ -111,10 +111,10 @@ namespace AGVNavigationCore.Utils } // 다음 노드 정보 출력 - var nextNode = _allNodes.FirstOrDefault(n => n.NodeId == nextNodeId); + var nextNode = _allNodes.FirstOrDefault(n => n.Id == nextNodeId); if (nextNode != null) { - Console.WriteLine($"✓ 다음 노드: {nextNode.NodeId} (RFID: {nextNode.RfidId}) - 위치: {nextNode.Position}"); + Console.WriteLine($"✓ 다음 노드: {nextNode.Id} (RFID: {nextNode.RfidId}) - 위치: {nextNode.Position}"); Console.WriteLine($" ├─ 노드 타입: {GetNodeTypeName(nextNode.Type)}"); Console.WriteLine($" └─ 연결된 노드: {string.Join(", ", nextNode.ConnectedNodes)}"); } @@ -132,7 +132,7 @@ namespace AGVNavigationCore.Utils Console.WriteLine("\n========== 모든 노드 정보 =========="); foreach (var node in _allNodes.OrderBy(n => n.RfidId)) { - Console.WriteLine($"{node.RfidId:D3} → {node.NodeId} ({GetNodeTypeName(node.Type)})"); + Console.WriteLine($"{node.RfidId:D3} → {node.Id} ({GetNodeTypeName(node.Type)})"); Console.WriteLine($" 위치: {node.Position}, 연결: {string.Join(", ", node.ConnectedNodes)}"); } } @@ -149,8 +149,9 @@ namespace AGVNavigationCore.Utils } Console.WriteLine($"\n========== RFID {rfidId} 상세 정보 =========="); - Console.WriteLine($"노드 ID: {node.NodeId}"); - Console.WriteLine($"이름: {node.Name}"); + Console.WriteLine($"노드 ID: {node.Id}"); + Console.WriteLine($"RFID: {node.RfidId}"); + Console.WriteLine($"ALIAS: {node.AliasName}"); Console.WriteLine($"위치: {node.Position}"); Console.WriteLine($"타입: {GetNodeTypeName(node.Type)}"); Console.WriteLine($"TurnLeft/Right/교차로 : {(node.CanTurnLeft ? "O":"X")}/{(node.CanTurnRight ? "O" : "X")}/{(node.DisableCross ? "X" : "O")}"); @@ -165,7 +166,7 @@ namespace AGVNavigationCore.Utils { foreach (var connectedId in node.ConnectedNodes) { - var connectedNode = _allNodes.FirstOrDefault(n => n.NodeId == connectedId); + var connectedNode = _allNodes.FirstOrDefault(n => n.Id == connectedId); if (connectedNode != null) { Console.WriteLine($" → {connectedId} (RFID: {connectedNode.RfidId}) - 위치: {connectedNode.Position}"); diff --git a/Cs_HMI/AGVLogic/AGVNavigationCore/Utils/DockingValidator.cs b/Cs_HMI/AGVLogic/AGVNavigationCore/Utils/DockingValidator.cs index e276cf5..fdfd752 100644 --- a/Cs_HMI/AGVLogic/AGVNavigationCore/Utils/DockingValidator.cs +++ b/Cs_HMI/AGVLogic/AGVNavigationCore/Utils/DockingValidator.cs @@ -29,7 +29,7 @@ namespace AGVNavigationCore.Utils return DockingValidationResult.CreateNotRequired(); } if (pathResult.DetailedPath.Any() == false && pathResult.Path.Any() && pathResult.Path.Count == 2 && - pathResult.Path[0].NodeId == pathResult.Path[1].NodeId) + pathResult.Path[0].Id == pathResult.Path[1].Id) { System.Diagnostics.Debug.WriteLine($"[DockingValidator] 도킹 검증 불필요: 동일포인트"); return DockingValidationResult.CreateNotRequired(); @@ -44,7 +44,7 @@ namespace AGVNavigationCore.Utils return DockingValidationResult.CreateNotRequired(); } - System.Diagnostics.Debug.WriteLine($"[DockingValidator] 목적지 노드: {LastNode.NodeId} 타입:{LastNode.Type} ({(int)LastNode.Type})"); + System.Diagnostics.Debug.WriteLine($"[DockingValidator] 목적지 노드: {LastNode.Id} 타입:{LastNode.Type} ({(int)LastNode.Type})"); //detail 경로 이동 예측 검증 for (int i = 0; i < pathResult.DetailedPath.Count - 1; i++) @@ -52,8 +52,8 @@ namespace AGVNavigationCore.Utils var curNodeId = pathResult.DetailedPath[i].NodeId; var nextNodeId = pathResult.DetailedPath[i + 1].NodeId; - var curNode = mapNodes?.FirstOrDefault(n => n.NodeId == curNodeId); - var nextNode = mapNodes?.FirstOrDefault(n => n.NodeId == nextNodeId); + var curNode = mapNodes?.FirstOrDefault(n => n.Id == curNodeId); + var nextNode = mapNodes?.FirstOrDefault(n => n.Id == nextNodeId); if (curNode != null && nextNode != null) { @@ -67,7 +67,7 @@ namespace AGVNavigationCore.Utils else { var prevNodeId = pathResult.DetailedPath[i - 1].NodeId; - prevNode = mapNodes?.FirstOrDefault(n => n.NodeId == prevNodeId); + prevNode = mapNodes?.FirstOrDefault(n => n.Id == prevNodeId); prevDir = pathResult.DetailedPath[i - 1].MotorDirection; } @@ -78,7 +78,7 @@ namespace AGVNavigationCore.Utils Console.WriteLine( $"\n[ValidateDockingDirection] 경로 검증 단계 {i}:"); Console.WriteLine( - $" 이전→현재→다음: {prevNode.NodeId}({prevNode.RfidId}) → {curNode.NodeId}({curNode.RfidId}) → {nextNode.NodeId}({nextNode.RfidId})"); + $" 이전→현재→다음: {prevNode.Id}({prevNode.RfidId}) → {curNode.Id}({curNode.RfidId}) → {nextNode.Id}({nextNode.RfidId})"); Console.WriteLine( $" 현재 노드 위치: ({curNode.Position.X:F1}, {curNode.Position.Y:F1})"); Console.WriteLine( @@ -96,16 +96,16 @@ namespace AGVNavigationCore.Utils ); Console.WriteLine( - $" [예상] GetNextNodeByDirection 결과: {expectedNextNode?.NodeId ?? "null"}"); + $" [예상] GetNextNodeByDirection 결과: {expectedNextNode?.Id ?? "null"}"); Console.WriteLine( - $" [실제] DetailedPath 다음 노드: {nextNode.RfidId}[{nextNode.NodeId}]"); + $" [실제] DetailedPath 다음 노드: {nextNode.RfidId}[{nextNode.Id}]"); - if (expectedNextNode != null && !expectedNextNode.NodeId.Equals(nextNode.NodeId)) + if (expectedNextNode != null && !expectedNextNode.Id.Equals(nextNode.Id)) { string error = $"[DockingValidator] ⚠️ 경로 방향 불일치" + - $"\n현재={curNode.RfidId}[{curNodeId}] 이전={prevNode.RfidId}[{(prevNode?.NodeId ?? string.Empty)}] " + - $"\n예상다음={expectedNextNode.RfidId}[{expectedNextNode.NodeId}] 실제다음={nextNode.RfidId}[{nextNodeId}]"; + $"\n현재={curNode.RfidId}[{curNodeId}] 이전={prevNode.RfidId}[{(prevNode?.Id ?? string.Empty)}] " + + $"\n예상다음={expectedNextNode.RfidId}[{expectedNextNode.Id}] 실제다음={nextNode.RfidId}[{nextNodeId}]"; Console.WriteLine( $"[ValidateDockingDirection] ❌ 경로 방향 불일치 검출!"); Console.WriteLine( @@ -118,7 +118,7 @@ namespace AGVNavigationCore.Utils $" 현재→실제: ({(nextNode.Position.X - curNode.Position.X):F2}, {(nextNode.Position.Y - curNode.Position.Y):F2})"); Console.WriteLine($"[ValidateDockingDirection] 에러메시지: {error}"); return DockingValidationResult.CreateInvalid( - LastNode.NodeId, + LastNode.Id, LastNode.Type, pathResult.DetailedPath[i].MotorDirection, pathResult.DetailedPath[i].MotorDirection, @@ -150,12 +150,12 @@ namespace AGVNavigationCore.Utils System.Diagnostics.Debug.WriteLine($"[DockingValidator] 필요한 도킹 방향: {requiredDirection}"); var LastNodeInfo = pathResult.DetailedPath.Last(); - if (LastNodeInfo.NodeId != LastNode.NodeId) + if (LastNodeInfo.NodeId != LastNode.Id) { - string error = $"마지막 노드의 도킹방향과 경로정보의 노드ID 불일치: 필요={LastNode.NodeId}, 계산됨={LastNodeInfo.NodeId }"; + string error = $"마지막 노드의 도킹방향과 경로정보의 노드ID 불일치: 필요={LastNode.Id}, 계산됨={LastNodeInfo.NodeId }"; System.Diagnostics.Debug.WriteLine($"[DockingValidator] ❌ 도킹 검증 실패: {error}"); return DockingValidationResult.CreateInvalid( - LastNode.NodeId, + LastNode.Id, LastNode.Type, requiredDirection, LastNodeInfo.MotorDirection, @@ -167,7 +167,7 @@ namespace AGVNavigationCore.Utils { System.Diagnostics.Debug.WriteLine($"[DockingValidator] ✅ 도킹 검증 성공"); return DockingValidationResult.CreateValid( - LastNode.NodeId, + LastNode.Id, LastNode.Type, requiredDirection, LastNodeInfo.MotorDirection); @@ -177,7 +177,7 @@ namespace AGVNavigationCore.Utils string error = $"도킹 방향 불일치: 필요={GetDirectionText(requiredDirection)}, 계산됨={GetDirectionText(LastNodeInfo.MotorDirection)}"; System.Diagnostics.Debug.WriteLine($"[DockingValidator] ❌ 도킹 검증 실패: {error}"); return DockingValidationResult.CreateInvalid( - LastNode.NodeId, + LastNode.Id, LastNode.Type, requiredDirection, LastNodeInfo.MotorDirection, @@ -264,7 +264,7 @@ namespace AGVNavigationCore.Utils var deltaY = lastNode.Position.Y - secondLastNode.Position.Y; var distance = Math.Sqrt(deltaX * deltaX + deltaY * deltaY); - System.Diagnostics.Debug.WriteLine($"[CalculateFinalDirection] 마지막 구간: {secondLastNode.NodeId} → {lastNode.NodeId}, 벡터: ({deltaX}, {deltaY}), 거리: {distance:F2}"); + System.Diagnostics.Debug.WriteLine($"[CalculateFinalDirection] 마지막 구간: {secondLastNode.Id} → {lastNode.Id}, 벡터: ({deltaX}, {deltaY}), 거리: {distance:F2}"); // 이동 거리가 매우 작으면 현재 방향 유지 if (distance < 1.0) diff --git a/Cs_HMI/AGVLogic/AGVNavigationCore/Utils/GetNextNodeIdTest.cs b/Cs_HMI/AGVLogic/AGVNavigationCore/Utils/GetNextNodeIdTest.cs index dd0c26f..454a652 100644 --- a/Cs_HMI/AGVLogic/AGVNavigationCore/Utils/GetNextNodeIdTest.cs +++ b/Cs_HMI/AGVLogic/AGVNavigationCore/Utils/GetNextNodeIdTest.cs @@ -28,10 +28,10 @@ namespace AGVNavigationCore.Utils Console.WriteLine("================================================\n"); // 테스트 노드 생성 - var node001 = new MapNode { NodeId = "N001", RfidId = "001", Position = new Point(65, 229), ConnectedNodes = new List { "N002" } }; - var node002 = new MapNode { NodeId = "N002", RfidId = "002", Position = new Point(206, 244), ConnectedNodes = new List { "N001", "N003" } }; - var node003 = new MapNode { NodeId = "N003", RfidId = "003", Position = new Point(278, 278), ConnectedNodes = new List { "N002", "N004" } }; - var node004 = new MapNode { NodeId = "N004", RfidId = "004", Position = new Point(380, 340), ConnectedNodes = new List { "N003", "N022", "N031" } }; + var node001 = new MapNode { Id = "N001", RfidId = "001", Position = new Point(65, 229), ConnectedNodes = new List { "N002" } }; + var node002 = new MapNode { Id = "N002", RfidId = "002", Position = new Point(206, 244), ConnectedNodes = new List { "N001", "N003" } }; + var node003 = new MapNode { Id = "N003", RfidId = "003", Position = new Point(278, 278), ConnectedNodes = new List { "N002", "N004" } }; + var node004 = new MapNode { Id = "N004", RfidId = "004", Position = new Point(380, 340), ConnectedNodes = new List { "N003", "N022", "N031" } }; var allNodes = new List { node001, node002, node003, node004 }; @@ -115,7 +115,7 @@ namespace AGVNavigationCore.Utils Console.WriteLine($"설명: {description}"); Console.WriteLine($"이전 위치: {prevPos} (RFID: {allNodes.First(n => n.Position == prevPos)?.RfidId ?? "?"})"); - Console.WriteLine($"현재 노드: {currentNode.NodeId} (RFID: {currentNode.RfidId}) - 위치: {currentNode.Position}"); + Console.WriteLine($"현재 노드: {currentNode.Id} (RFID: {currentNode.RfidId}) - 위치: {currentNode.Position}"); Console.WriteLine($"현재 모터 방향: {motorDir}"); Console.WriteLine($"요청 방향: {direction}"); @@ -128,32 +128,32 @@ namespace AGVNavigationCore.Utils Console.WriteLine($"이동 벡터: ({movementVector.X}, {movementVector.Y})"); // 각 후보 노드에 대한 점수 계산 - Console.WriteLine($"\n현재 노드({currentNode.NodeId})의 ConnectedNodes: {string.Join(", ", currentNode.ConnectedNodes)}"); + Console.WriteLine($"\n현재 노드({currentNode.Id})의 ConnectedNodes: {string.Join(", ", currentNode.ConnectedNodes)}"); Console.WriteLine($"가능한 다음 노드들:"); var candidateNodes = allNodes.Where(n => - currentNode.ConnectedNodes.Contains(n.NodeId) && n.NodeId != currentNode.NodeId + currentNode.ConnectedNodes.Contains(n.Id) && n.Id != currentNode.Id ).ToList(); foreach (var candidate in candidateNodes) { var score = CalculateScoreAndPrint(movementVector, currentNode.Position, candidate, direction); - string isExpected = (candidate.NodeId == expectedNextNode.NodeId) ? " ← 예상 노드" : ""; - Console.WriteLine($" {candidate.NodeId} (RFID: {candidate.RfidId}) - 위치: {candidate.Position} - 점수: {score:F1}{isExpected}"); + string isExpected = (candidate.Id == expectedNextNode.Id) ? " ← 예상 노드" : ""; + Console.WriteLine($" {candidate.Id} (RFID: {candidate.RfidId}) - 위치: {candidate.Position} - 점수: {score:F1}{isExpected}"); } // 최고 점수 노드 선택 var bestCandidate = GetBestCandidate(movementVector, currentNode.Position, candidateNodes, direction); - Console.WriteLine($"\n✓ 선택된 노드: {bestCandidate.NodeId} (RFID: {bestCandidate.RfidId})"); + Console.WriteLine($"\n✓ 선택된 노드: {bestCandidate.Id} (RFID: {bestCandidate.RfidId})"); - if (bestCandidate.NodeId == expectedNextNode.NodeId) + if (bestCandidate.Id == expectedNextNode.Id) { Console.WriteLine($"✅ 정답! ({expectedNodeIdStr})"); } else { - Console.WriteLine($"❌ 오답! 예상: {expectedNextNode.NodeId}, 실제: {bestCandidate.NodeId}"); + Console.WriteLine($"❌ 오답! 예상: {expectedNextNode.Id}, 실제: {bestCandidate.Id}"); } } diff --git a/Cs_HMI/AGVLogic/AGVNavigationCore/Utils/LiftCalculator.cs b/Cs_HMI/AGVLogic/AGVNavigationCore/Utils/LiftCalculator.cs index bddde3c..6c6b369 100644 --- a/Cs_HMI/AGVLogic/AGVNavigationCore/Utils/LiftCalculator.cs +++ b/Cs_HMI/AGVLogic/AGVNavigationCore/Utils/LiftCalculator.cs @@ -63,14 +63,14 @@ namespace AGVNavigationCore.Utils // 이전 노드 제외 (되돌아가는 방향 제외) if (previousNode != null) { - nextNodes = nextNodes.Where(n => n.NodeId != previousNode.NodeId).ToList(); + nextNodes = nextNodes.Where(n => n.Id != previousNode.Id).ToList(); } if (nextNodes.Count == 1) { // 직선 경로: 다음 노드 방향으로 예측 targetPosition = nextNodes.First().Position; - calculationMethod = $"전진 경로 예측 ({currentNode.NodeId}→{nextNodes.First().NodeId})"; + calculationMethod = $"전진 경로 예측 ({currentNode.Id}→{nextNodes.First().Id})"; } else if (nextNodes.Count > 1) { @@ -268,7 +268,7 @@ namespace AGVNavigationCore.Utils foreach (var nodeId in currentNode.ConnectedNodes) { - var connectedNode = mapNodes.FirstOrDefault(n => n.NodeId == nodeId); + var connectedNode = mapNodes.FirstOrDefault(n => n.Id == nodeId); if (connectedNode != null) { connectedNodes.Add(connectedNode); diff --git a/Cs_HMI/AGVLogic/AGVSimulator/Forms/SimulatorForm.cs b/Cs_HMI/AGVLogic/AGVSimulator/Forms/SimulatorForm.cs index 6c15b0e..09f7c04 100644 --- a/Cs_HMI/AGVLogic/AGVSimulator/Forms/SimulatorForm.cs +++ b/Cs_HMI/AGVLogic/AGVSimulator/Forms/SimulatorForm.cs @@ -427,7 +427,7 @@ namespace AGVSimulator.Forms if(targetNode.Type == NodeType.Buffer) { var lastDetailPath = advancedResult.DetailedPath.Last(); - if(lastDetailPath.NodeId == targetNode.NodeId) //마지막노드 재확인 + if(lastDetailPath.NodeId == targetNode.Id) //마지막노드 재확인 { //버퍼에 도킹할때에는 마지막 노드에서 멈추고 시퀀스를 적용해야한다 advancedResult.DetailedPath = advancedResult.DetailedPath.Take(advancedResult.DetailedPath.Count - 1).ToList(); @@ -492,7 +492,7 @@ namespace AGVSimulator.Forms } } - private void OnTargetNodeSelected(object sender, List selectedNodes) + private void OnTargetNodeSelected(object sender, List selectedNodes) { try { @@ -511,9 +511,9 @@ namespace AGVSimulator.Forms if (selectedNode == null) return; // 목적지를 선택된 노드로 설정 - SetTargetNodeInCombo(selectedNode.NodeId); + SetTargetNodeInCombo(selectedNode.Id); - var displayText = GetDisplayName(selectedNode.NodeId); + var displayText = GetDisplayName(selectedNode.Id); _statusLabel.Text = $"타겟계산 - 목적지: {displayText}"; // 자동으로 경로 계산 수행 @@ -578,7 +578,7 @@ namespace AGVSimulator.Forms for (int i = 0; i < _startNodeCombo.Items.Count; i++) { var item = _startNodeCombo.Items[i].ToString(); - if (item.Contains($"[{closestNode.NodeId}]")) + if (item.Contains($"[{closestNode.Id}]")) { _startNodeCombo.SelectedIndex = i; break; @@ -649,7 +649,7 @@ namespace AGVSimulator.Forms if (existingNode != null) { // 이미 존재하는 노드로 이동 - Program.WriteLine($"[맵 스캔] RFID '{rfidId}'는 이미 존재합니다 (노드: {existingNode.NodeId})"); + Program.WriteLine($"[맵 스캔] RFID '{rfidId}'는 이미 존재합니다 (노드: {existingNode.Id})"); // 기존 노드로 AGV 위치 설정 _simulatorCanvas.SetAGVPosition(selectedAGV.AgvId, existingNode, currentDirection); @@ -659,7 +659,7 @@ namespace AGVSimulator.Forms _lastNodeAddTime = DateTime.Now; _lastScanDirection = currentDirection; // 방향 업데이트 - _statusLabel.Text = $"기존 노드로 이동: {existingNode.NodeId} [{GetDirectionSymbol(currentDirection)}]"; + _statusLabel.Text = $"기존 노드로 이동: {existingNode.Id} [{GetDirectionSymbol(currentDirection)}]"; _rfidTextBox.Text = ""; return; } @@ -720,12 +720,11 @@ namespace AGVSimulator.Forms var newNodeId = $"{_scanNodeCounter:D3}"; var newNode = new MapNode { - NodeId = newNodeId, + Id = newNodeId, RfidId = rfidId, Position = new Point(newX, newY), Type = NodeType.Normal, - IsActive = true, - Name = $"N{_scanNodeCounter}" + IsActive = true }; // 맵에 추가 @@ -738,10 +737,10 @@ namespace AGVSimulator.Forms if (_lastScannedNode != null) { // 양방향 연결 (ConnectedNodes에 추가 - JSON 저장됨) - _lastScannedNode.AddConnection(newNode.NodeId); - newNode.AddConnection(_lastScannedNode.NodeId); + _lastScannedNode.AddConnection(newNode.Id); + newNode.AddConnection(_lastScannedNode.Id); - Program.WriteLine($"[맵 스캔] 연결 생성: {_lastScannedNode.NodeId} ↔ {newNode.NodeId}"); + Program.WriteLine($"[맵 스캔] 연결 생성: {_lastScannedNode.Id} ↔ {newNode.Id}"); } // AGV 위치 설정 @@ -752,7 +751,7 @@ namespace AGVSimulator.Forms _simulatorCanvas.Nodes = _mapNodes; // 화면을 새 노드 위치로 이동 - _simulatorCanvas.PanToNode(newNode.NodeId); + _simulatorCanvas.PanToNode(newNode.Id); _simulatorCanvas.Invalidate(); // 상태 업데이트 @@ -764,10 +763,10 @@ namespace AGVSimulator.Forms // UI 업데이트 UpdateNodeComboBoxes(); - _statusLabel.Text = $"노드 생성: {newNode.NodeId} (RFID: {rfidId}) [{GetDirectionSymbol(currentDirection)}] - 총 {_mapNodes.Count}개"; + _statusLabel.Text = $"노드 생성: {newNode.Id} (RFID: {rfidId}) [{GetDirectionSymbol(currentDirection)}] - 총 {_mapNodes.Count}개"; _rfidTextBox.Text = ""; - Program.WriteLine($"[맵 스캔] 노드 생성 완료: {newNode.NodeId} (RFID: {rfidId}) at ({newX}, {newY}), 방향: {currentDirection}"); + Program.WriteLine($"[맵 스캔] 노드 생성 완료: {newNode.Id} (RFID: {rfidId}) at ({newX}, {newY}), 방향: {currentDirection}"); } catch (Exception ex) { @@ -875,15 +874,15 @@ namespace AGVSimulator.Forms //이전위치와 동일한지 체크한다. - if (selectedAGV.CurrentNodeId == targetNode.NodeId && selectedAGV.CurrentDirection == selectedDirection) + if (selectedAGV.CurrentNodeId == targetNode.Id && selectedAGV.CurrentDirection == selectedDirection) { - Program.WriteLine($"이전 노드위치와 모터의 방향이 동일하여 현재 위치 변경이 취소됩니다(NODE:{targetNode.NodeId},RFID:{targetNode.RfidId},DIR:{selectedDirection})"); + Program.WriteLine($"이전 노드위치와 모터의 방향이 동일하여 현재 위치 변경이 취소됩니다(NODE:{targetNode.Id},RFID:{targetNode.RfidId},DIR:{selectedDirection})"); return; } // 콘솔 출력 (상세한 리프트 방향 계산 과정) Program.WriteLine($"[AGV-{selectedAGV.AgvId}] 위치 설정:"); - Program.WriteLine($" RFID: {rfidId} → 노드: {targetNode.NodeId}"); + Program.WriteLine($" RFID: {rfidId} → 노드: {targetNode.Id}"); Program.WriteLine($" 위치: ({targetNode.Position.X}, {targetNode.Position.Y})"); Program.WriteLine($" 방향: {selectedDirectionItem?.DisplayText ?? "전진"} ({selectedDirection})"); @@ -912,14 +911,14 @@ namespace AGVSimulator.Forms CalculateLiftDirectionDetailed(selectedAGV); Program.WriteLine(""); - _statusLabel.Text = $"{selectedAGV.AgvId} 위치를 RFID '{rfidId}' (노드: {targetNode.NodeId}), 방향: {selectedDirectionItem?.DisplayText ?? "전진"}로 설정했습니다."; + _statusLabel.Text = $"{selectedAGV.AgvId} 위치를 RFID '{rfidId}' (노드: {targetNode.Id}), 방향: {selectedDirectionItem?.DisplayText ?? "전진"}로 설정했습니다."; _rfidTextBox.Text = ""; // 입력 필드 초기화 // 시뮬레이터 캔버스의 해당 노드로 이동 //_simulatorCanvas.PanToNode(targetNode.NodeId); // 시작 노드 콤보박스를 현재 위치로 자동 선택 - SetStartNodeToCombo(targetNode.NodeId); + SetStartNodeToCombo(targetNode.Id); } /// @@ -956,11 +955,10 @@ namespace AGVSimulator.Forms return "RFID가 할당된 노드가 없습니다."; // 처음 10개의 RFID만 표시 (노드 이름 포함) - var rfidList = nodesWithRfid.Take(10).Select(n => + var rfidList = nodesWithRfid.Take(10).Select((Func)(n => { - var nodeNamePart = !string.IsNullOrEmpty(n.Name) ? $" {n.Name}" : ""; - return $"- {n.RfidId} → {n.NodeId}{nodeNamePart}"; - }); + return $"- {n.RfidId} → {n.Id}"; + })); var result = string.Join("\n", rfidList); if (nodesWithRfid.Count > 10) @@ -1060,8 +1058,7 @@ namespace AGVSimulator.Forms if (node.IsActive && node.HasRfid()) { // {rfid} - [{node}] {name} 형식으로 ComboBoxItem 생성 - var nodeNamePart = !string.IsNullOrEmpty(node.Name) ? $" {node.Name}" : ""; - var displayText = $"{node.RfidId} - [{node.NodeId}]{nodeNamePart}"; + var displayText = $"{node.RfidId} - [{node.Id}]"; var item = new ComboBoxItem(node, displayText); _startNodeCombo.Items.Add(item); @@ -1262,7 +1259,7 @@ namespace AGVSimulator.Forms /// private string GetRfidByNodeId(string nodeId) { - var node = _mapNodes?.FirstOrDefault(n => n.NodeId == nodeId); + var node = _mapNodes?.FirstOrDefault(n => n.Id == nodeId); return node?.HasRfid() == true ? node.RfidId : nodeId; } @@ -1271,7 +1268,7 @@ namespace AGVSimulator.Forms /// private string GetDisplayName(string nodeId) { - var node = _mapNodes?.FirstOrDefault(n => n.NodeId == nodeId); + var node = _mapNodes?.FirstOrDefault(n => n.Id == nodeId); if (node != null && !string.IsNullOrEmpty(node.RfidId)) { return node.RfidId; @@ -1628,7 +1625,7 @@ namespace AGVSimulator.Forms if (!string.IsNullOrEmpty(node.RfidId)) return node.RfidId; - return $"({node.NodeId})"; + return $"({node.Id})"; } /// @@ -1655,7 +1652,7 @@ namespace AGVSimulator.Forms for (int i = 0; i < _targetNodeCombo.Items.Count; i++) { var item = _targetNodeCombo.Items[i] as ComboBoxItem; - if (item?.Value?.NodeId == nodeId) + if (item?.Value?.Id == nodeId) { _targetNodeCombo.SelectedIndex = i; return; @@ -1669,7 +1666,7 @@ namespace AGVSimulator.Forms private PathTestLogItem CreateTestResultFromUI(MapNode prevNode, MapNode targetNode, string directionName, (bool result, string message) calcResult) { - var currentNode = _mapNodes.FirstOrDefault(n => n.NodeId == + var currentNode = _mapNodes.FirstOrDefault(n => n.Id == (_agvListCombo.SelectedItem as VirtualAGV)?.CurrentNodeId); var logItem = new PathTestLogItem @@ -1741,8 +1738,8 @@ namespace AGVSimulator.Forms continue; // 중복 방지 (A→B와 B→A를 같은 것으로 간주) - var pairKey1 = $"{nodeA.NodeId}→{nodeB.NodeId}"; - var pairKey2 = $"{nodeB.NodeId}→{nodeA.NodeId}"; + var pairKey1 = $"{nodeA.Id}→{nodeB.Id}"; + var pairKey2 = $"{nodeB.Id}→{nodeA.Id}"; if (nodeA.HasRfid() && nodeB.HasRfid() && !processedPairs.Contains(pairKey1) && !processedPairs.Contains(pairKey2)) { @@ -1848,7 +1845,7 @@ namespace AGVSimulator.Forms prb1.Value = (int)((double)currentTest / totalTests * 100); // 목표 노드 콤보박스 선택 - SetTargetNodeComboBox(dockingTarget.NodeId); + SetTargetNodeComboBox(dockingTarget.Id); // 경로 계산 버튼 클릭 (실제 사용자 동작) var calcResult = CalcPath(); diff --git a/Cs_HMI/Data/NewMap.agvmap b/Cs_HMI/Data/NewMap.agvmap index 58fd837..76ac99c 100644 --- a/Cs_HMI/Data/NewMap.agvmap +++ b/Cs_HMI/Data/NewMap.agvmap @@ -1,7 +1,7 @@ { "Nodes": [ { - "NodeId": "N001", + "Id": "N001", "Name": "UNLOADER", "Position": "65, 229", "Type": 2, @@ -40,7 +40,7 @@ "DisplayText": "N001 - UNLOADER - [0001]" }, { - "NodeId": "N002", + "Id": "N002", "Name": "N002", "Position": "190, 230", "Type": 0, @@ -80,7 +80,7 @@ "DisplayText": "N002 - N002 - [0002]" }, { - "NodeId": "N003", + "Id": "N003", "Name": "N003", "Position": "296, 266", "Type": 0, @@ -120,7 +120,7 @@ "DisplayText": "N003 - N003 - [0003]" }, { - "NodeId": "N004", + "Id": "N004", "Name": "N004", "Position": "388, 330", "Type": 0, @@ -162,7 +162,7 @@ "DisplayText": "N004 - N004 - [0004]" }, { - "NodeId": "N006", + "Id": "N006", "Name": "N006", "Position": "530, 220", "Type": 0, @@ -202,7 +202,7 @@ "DisplayText": "N006 - N006 - [0013]" }, { - "NodeId": "N007", + "Id": "N007", "Name": "N007", "Position": "589, 184", "Type": 0, @@ -242,7 +242,7 @@ "DisplayText": "N007 - N007 - [0014]" }, { - "NodeId": "N008", + "Id": "N008", "Name": "N008", "Position": "282, 452", "Type": 0, @@ -282,7 +282,7 @@ "DisplayText": "N008 - N008 - [0009]" }, { - "NodeId": "N009", + "Id": "N009", "Name": "N009", "Position": "183, 465", "Type": 0, @@ -322,7 +322,7 @@ "DisplayText": "N009 - N009 - [0010]" }, { - "NodeId": "N010", + "Id": "N010", "Name": "TOPS", "Position": "52, 466", "Type": 3, @@ -361,7 +361,7 @@ "DisplayText": "N010 - TOPS - [0011]" }, { - "NodeId": "N011", + "Id": "N011", "Name": "N011", "Position": "481, 399", "Type": 0, @@ -402,7 +402,7 @@ "DisplayText": "N011 - N011 - [0005]" }, { - "NodeId": "N012", + "Id": "N012", "Name": "N012", "Position": "559, 464", "Type": 0, @@ -442,7 +442,7 @@ "DisplayText": "N012 - N012 - [0006]" }, { - "NodeId": "N013", + "Id": "N013", "Name": "N013", "Position": "640, 513", "Type": 0, @@ -482,7 +482,7 @@ "DisplayText": "N013 - N013 - [0007]" }, { - "NodeId": "N014", + "Id": "N014", "Name": "LOADER", "Position": "728, 573", "Type": 1, @@ -521,7 +521,7 @@ "DisplayText": "N014 - LOADER - [0008]" }, { - "NodeId": "N019", + "Id": "N019", "Name": "CHARGER #2", "Position": "679, 199", "Type": 5, @@ -560,7 +560,7 @@ "DisplayText": "N019 - CHARGER #2 - [0015]" }, { - "NodeId": "N022", + "Id": "N022", "Name": "N022", "Position": "461, 267", "Type": 0, @@ -601,7 +601,7 @@ "DisplayText": "N022 - N022 - [0012]" }, { - "NodeId": "N023", + "Id": "N023", "Name": "N023", "Position": "418, 206", "Type": 0, @@ -641,7 +641,7 @@ "DisplayText": "N023 - N023 - [0016]" }, { - "NodeId": "N024", + "Id": "N024", "Name": "N024", "Position": "476, 141", "Type": 0, @@ -681,7 +681,7 @@ "DisplayText": "N024 - N024 - [0017]" }, { - "NodeId": "N025", + "Id": "N025", "Name": "N025", "Position": "548, 99", "Type": 0, @@ -721,7 +721,7 @@ "DisplayText": "N025 - N025 - [0018]" }, { - "NodeId": "N026", + "Id": "N026", "Name": "CHARGER #1", "Position": "670, 88", "Type": 5, @@ -760,7 +760,7 @@ "DisplayText": "N026 - CHARGER #1 - [0019]" }, { - "NodeId": "LBL001", + "Id": "LBL001", "Name": "Amkor Technology Korea", "Position": "183, 103", "Type": 6, @@ -797,7 +797,7 @@ "DisplayText": "LBL001 - Amkor Technology Korea" }, { - "NodeId": "IMG001", + "Id": "IMG001", "Name": "logo", "Position": "633, 310", "Type": 7, @@ -834,7 +834,7 @@ "DisplayText": "IMG001 - logo" }, { - "NodeId": "N015", + "Id": "N015", "Name": "", "Position": "448, 476", "Type": 0, @@ -874,7 +874,7 @@ "DisplayText": "N015 - [0037]" }, { - "NodeId": "N016", + "Id": "N016", "Name": "", "Position": "425, 524", "Type": 0, @@ -914,7 +914,7 @@ "DisplayText": "N016 - [0036]" }, { - "NodeId": "N017", + "Id": "N017", "Name": "", "Position": "389, 559", "Type": 0, @@ -954,7 +954,7 @@ "DisplayText": "N017 - [0035]" }, { - "NodeId": "N018", + "Id": "N018", "Name": "", "Position": "315, 562", "Type": 0, @@ -995,7 +995,7 @@ "DisplayText": "N018 - [0034]" }, { - "NodeId": "N005", + "Id": "N005", "Name": "", "Position": "227, 560", "Type": 0, @@ -1036,7 +1036,7 @@ "DisplayText": "N005 - [0033]" }, { - "NodeId": "N020", + "Id": "N020", "Name": "", "Position": "142, 557", "Type": 0, @@ -1077,7 +1077,7 @@ "DisplayText": "N020 - [0032]" }, { - "NodeId": "N021", + "Id": "N021", "Name": "", "Position": "60, 559", "Type": 0, @@ -1117,7 +1117,7 @@ "DisplayText": "N021 - [0031]" }, { - "NodeId": "N027", + "Id": "N027", "Name": "BUF1", "Position": "61, 645", "Type": 4, @@ -1156,7 +1156,7 @@ "DisplayText": "N027 - BUF1 - [0041]" }, { - "NodeId": "N028", + "Id": "N028", "Name": "BUF2", "Position": "141, 643", "Type": 4, @@ -1195,7 +1195,7 @@ "DisplayText": "N028 - BUF2 - [0040]" }, { - "NodeId": "N029", + "Id": "N029", "Name": "BUF3", "Position": "229, 638", "Type": 4, @@ -1234,7 +1234,7 @@ "DisplayText": "N029 - BUF3 - [0039]" }, { - "NodeId": "N030", + "Id": "N030", "Name": "BUF4", "Position": "316, 638", "Type": 4, @@ -1273,7 +1273,7 @@ "DisplayText": "N030 - BUF4 - [0038]" }, { - "NodeId": "N031", + "Id": "N031", "Name": "", "Position": "337, 397", "Type": 0, diff --git a/Cs_HMI/Data/NewMap.json b/Cs_HMI/Data/NewMap.json index 58fd837..76ac99c 100644 --- a/Cs_HMI/Data/NewMap.json +++ b/Cs_HMI/Data/NewMap.json @@ -1,7 +1,7 @@ { "Nodes": [ { - "NodeId": "N001", + "Id": "N001", "Name": "UNLOADER", "Position": "65, 229", "Type": 2, @@ -40,7 +40,7 @@ "DisplayText": "N001 - UNLOADER - [0001]" }, { - "NodeId": "N002", + "Id": "N002", "Name": "N002", "Position": "190, 230", "Type": 0, @@ -80,7 +80,7 @@ "DisplayText": "N002 - N002 - [0002]" }, { - "NodeId": "N003", + "Id": "N003", "Name": "N003", "Position": "296, 266", "Type": 0, @@ -120,7 +120,7 @@ "DisplayText": "N003 - N003 - [0003]" }, { - "NodeId": "N004", + "Id": "N004", "Name": "N004", "Position": "388, 330", "Type": 0, @@ -162,7 +162,7 @@ "DisplayText": "N004 - N004 - [0004]" }, { - "NodeId": "N006", + "Id": "N006", "Name": "N006", "Position": "530, 220", "Type": 0, @@ -202,7 +202,7 @@ "DisplayText": "N006 - N006 - [0013]" }, { - "NodeId": "N007", + "Id": "N007", "Name": "N007", "Position": "589, 184", "Type": 0, @@ -242,7 +242,7 @@ "DisplayText": "N007 - N007 - [0014]" }, { - "NodeId": "N008", + "Id": "N008", "Name": "N008", "Position": "282, 452", "Type": 0, @@ -282,7 +282,7 @@ "DisplayText": "N008 - N008 - [0009]" }, { - "NodeId": "N009", + "Id": "N009", "Name": "N009", "Position": "183, 465", "Type": 0, @@ -322,7 +322,7 @@ "DisplayText": "N009 - N009 - [0010]" }, { - "NodeId": "N010", + "Id": "N010", "Name": "TOPS", "Position": "52, 466", "Type": 3, @@ -361,7 +361,7 @@ "DisplayText": "N010 - TOPS - [0011]" }, { - "NodeId": "N011", + "Id": "N011", "Name": "N011", "Position": "481, 399", "Type": 0, @@ -402,7 +402,7 @@ "DisplayText": "N011 - N011 - [0005]" }, { - "NodeId": "N012", + "Id": "N012", "Name": "N012", "Position": "559, 464", "Type": 0, @@ -442,7 +442,7 @@ "DisplayText": "N012 - N012 - [0006]" }, { - "NodeId": "N013", + "Id": "N013", "Name": "N013", "Position": "640, 513", "Type": 0, @@ -482,7 +482,7 @@ "DisplayText": "N013 - N013 - [0007]" }, { - "NodeId": "N014", + "Id": "N014", "Name": "LOADER", "Position": "728, 573", "Type": 1, @@ -521,7 +521,7 @@ "DisplayText": "N014 - LOADER - [0008]" }, { - "NodeId": "N019", + "Id": "N019", "Name": "CHARGER #2", "Position": "679, 199", "Type": 5, @@ -560,7 +560,7 @@ "DisplayText": "N019 - CHARGER #2 - [0015]" }, { - "NodeId": "N022", + "Id": "N022", "Name": "N022", "Position": "461, 267", "Type": 0, @@ -601,7 +601,7 @@ "DisplayText": "N022 - N022 - [0012]" }, { - "NodeId": "N023", + "Id": "N023", "Name": "N023", "Position": "418, 206", "Type": 0, @@ -641,7 +641,7 @@ "DisplayText": "N023 - N023 - [0016]" }, { - "NodeId": "N024", + "Id": "N024", "Name": "N024", "Position": "476, 141", "Type": 0, @@ -681,7 +681,7 @@ "DisplayText": "N024 - N024 - [0017]" }, { - "NodeId": "N025", + "Id": "N025", "Name": "N025", "Position": "548, 99", "Type": 0, @@ -721,7 +721,7 @@ "DisplayText": "N025 - N025 - [0018]" }, { - "NodeId": "N026", + "Id": "N026", "Name": "CHARGER #1", "Position": "670, 88", "Type": 5, @@ -760,7 +760,7 @@ "DisplayText": "N026 - CHARGER #1 - [0019]" }, { - "NodeId": "LBL001", + "Id": "LBL001", "Name": "Amkor Technology Korea", "Position": "183, 103", "Type": 6, @@ -797,7 +797,7 @@ "DisplayText": "LBL001 - Amkor Technology Korea" }, { - "NodeId": "IMG001", + "Id": "IMG001", "Name": "logo", "Position": "633, 310", "Type": 7, @@ -834,7 +834,7 @@ "DisplayText": "IMG001 - logo" }, { - "NodeId": "N015", + "Id": "N015", "Name": "", "Position": "448, 476", "Type": 0, @@ -874,7 +874,7 @@ "DisplayText": "N015 - [0037]" }, { - "NodeId": "N016", + "Id": "N016", "Name": "", "Position": "425, 524", "Type": 0, @@ -914,7 +914,7 @@ "DisplayText": "N016 - [0036]" }, { - "NodeId": "N017", + "Id": "N017", "Name": "", "Position": "389, 559", "Type": 0, @@ -954,7 +954,7 @@ "DisplayText": "N017 - [0035]" }, { - "NodeId": "N018", + "Id": "N018", "Name": "", "Position": "315, 562", "Type": 0, @@ -995,7 +995,7 @@ "DisplayText": "N018 - [0034]" }, { - "NodeId": "N005", + "Id": "N005", "Name": "", "Position": "227, 560", "Type": 0, @@ -1036,7 +1036,7 @@ "DisplayText": "N005 - [0033]" }, { - "NodeId": "N020", + "Id": "N020", "Name": "", "Position": "142, 557", "Type": 0, @@ -1077,7 +1077,7 @@ "DisplayText": "N020 - [0032]" }, { - "NodeId": "N021", + "Id": "N021", "Name": "", "Position": "60, 559", "Type": 0, @@ -1117,7 +1117,7 @@ "DisplayText": "N021 - [0031]" }, { - "NodeId": "N027", + "Id": "N027", "Name": "BUF1", "Position": "61, 645", "Type": 4, @@ -1156,7 +1156,7 @@ "DisplayText": "N027 - BUF1 - [0041]" }, { - "NodeId": "N028", + "Id": "N028", "Name": "BUF2", "Position": "141, 643", "Type": 4, @@ -1195,7 +1195,7 @@ "DisplayText": "N028 - BUF2 - [0040]" }, { - "NodeId": "N029", + "Id": "N029", "Name": "BUF3", "Position": "229, 638", "Type": 4, @@ -1234,7 +1234,7 @@ "DisplayText": "N029 - BUF3 - [0039]" }, { - "NodeId": "N030", + "Id": "N030", "Name": "BUF4", "Position": "316, 638", "Type": 4, @@ -1273,7 +1273,7 @@ "DisplayText": "N030 - BUF4 - [0038]" }, { - "NodeId": "N031", + "Id": "N031", "Name": "", "Position": "337, 397", "Type": 0, diff --git a/Cs_HMI/Data/NewMap_2.json b/Cs_HMI/Data/NewMap_2.json index 72716a8..a268610 100644 --- a/Cs_HMI/Data/NewMap_2.json +++ b/Cs_HMI/Data/NewMap_2.json @@ -1,203 +1,187 @@ { "Nodes": [ { - "NodeId": "N001", - "Name": "UNLOADER", - "Position": "99, 251", - "Type": 2, + "Text": "Unloader", + "StationType": 3, + "CanDocking": true, + "DockDirection": 2, "ConnectedNodes": [ "N002" ], - "RfidId": "0001", - "LabelText": "", - "ForeColor": "White", - "BackColor": "Transparent", - "ImageBase64": "", - "DisplayColor": "Red", - "CanDocking": true, - "DockDirection": 0, "CanTurnLeft": true, "CanTurnRight": true, - "DisableCross": false, + "DisableCross": true, + "SpeedLimit": 0, + "AliasName": "", "IsActive": true, - "CreatedDate": "2025-12-12T16:29:33.968Z", - "ModifiedDate": "2025-12-12T16:29:33.968Z" + "RfidId": "0001", + "NodeTextForeColor": "White", + "NodeTextFontSize": 30.0, + "Id": "N001", + "Type": 0, + "Position": "99, 251" }, { - "NodeId": "N002", - "Name": "N002", - "Position": "249, 250", - "Type": 0, + "Text": "", + "StationType": 0, + "CanDocking": false, + "DockDirection": 0, "ConnectedNodes": [ "N001", "N003" ], - "RfidId": "0002", - "LabelText": "", - "ForeColor": "White", - "BackColor": "Transparent", - "ImageBase64": "", - "DisplayColor": "Cyan", - "CanDocking": false, - "DockDirection": 0, "CanTurnLeft": true, "CanTurnRight": true, "DisableCross": false, + "SpeedLimit": 0, + "AliasName": "", "IsActive": true, - "CreatedDate": "2025-12-12T16:29:33.968Z", - "ModifiedDate": "2025-12-12T16:29:33.968Z" + "RfidId": "0002", + "NodeTextForeColor": "", + "NodeTextFontSize": 7.0, + "Id": "N002", + "Type": 0, + "Position": "249, 250" }, { - "NodeId": "N003", - "Name": "N003", - "Position": "350, 301", - "Type": 0, + "Text": "", + "StationType": 0, + "CanDocking": false, + "DockDirection": 0, "ConnectedNodes": [ "N002", "N011", "N022", "N031" ], - "RfidId": "0003", - "LabelText": "", - "ForeColor": "White", - "BackColor": "Transparent", - "ImageBase64": "", - "DisplayColor": "Cyan", - "CanDocking": false, - "DockDirection": 0, "CanTurnLeft": true, "CanTurnRight": true, "DisableCross": false, + "SpeedLimit": 0, + "AliasName": "", "IsActive": true, - "CreatedDate": "2025-12-12T16:29:33.968Z", - "ModifiedDate": "2025-12-12T16:29:33.968Z" + "RfidId": "0003", + "NodeTextForeColor": "", + "NodeTextFontSize": 7.0, + "Id": "N003", + "Type": 0, + "Position": "350, 301" }, { - "NodeId": "N006", - "Name": "N006", - "Position": "508, 242", - "Type": 0, + "Text": "", + "StationType": 0, + "CanDocking": false, + "DockDirection": 0, "ConnectedNodes": [ "N007", "N022", "N023" ], - "RfidId": "0013", - "LabelText": "", - "ForeColor": "White", - "BackColor": "Transparent", - "ImageBase64": "", - "DisplayColor": "Cyan", - "CanDocking": false, - "DockDirection": 0, "CanTurnLeft": true, "CanTurnRight": true, "DisableCross": false, + "SpeedLimit": 0, + "AliasName": "", "IsActive": true, - "CreatedDate": "2025-12-12T16:29:33.968Z", - "ModifiedDate": "2025-12-12T16:29:33.968Z" + "RfidId": "0013", + "NodeTextForeColor": "", + "NodeTextFontSize": 7.0, + "Id": "N006", + "Type": 0, + "Position": "527, 254" }, { - "NodeId": "N007", - "Name": "N007", - "Position": "601, 205", - "Type": 0, + "Text": "", + "StationType": 0, + "CanDocking": false, + "DockDirection": 0, "ConnectedNodes": [ "N006", "N019" ], - "RfidId": "0014", - "LabelText": "", - "ForeColor": "White", - "BackColor": "Transparent", - "ImageBase64": "", - "DisplayColor": "Cyan", - "CanDocking": false, - "DockDirection": 0, "CanTurnLeft": true, "CanTurnRight": true, "DisableCross": false, + "SpeedLimit": 0, + "AliasName": "", "IsActive": true, - "CreatedDate": "2025-12-12T16:29:33.968Z", - "ModifiedDate": "2025-12-12T16:29:33.968Z" + "RfidId": "0014", + "NodeTextForeColor": "", + "NodeTextFontSize": 7.0, + "Id": "N007", + "Type": 0, + "Position": "609, 227" }, { - "NodeId": "N008", - "Name": "N008", - "Position": "275, 441", - "Type": 0, + "Text": "", + "StationType": 0, + "CanDocking": false, + "DockDirection": 0, "ConnectedNodes": [ "N009", "N031" ], - "RfidId": "0009", - "LabelText": "", - "ForeColor": "White", - "BackColor": "Transparent", - "ImageBase64": "", - "DisplayColor": "Cyan", - "CanDocking": false, - "DockDirection": 0, "CanTurnLeft": true, "CanTurnRight": true, "DisableCross": false, + "SpeedLimit": 0, + "AliasName": "", "IsActive": true, - "CreatedDate": "2025-12-12T16:29:33.968Z", - "ModifiedDate": "2025-12-12T16:29:33.968Z" + "RfidId": "0009", + "NodeTextForeColor": "", + "NodeTextFontSize": 7.0, + "Id": "N008", + "Type": 0, + "Position": "275, 441" }, { - "NodeId": "N009", - "Name": "N009", - "Position": "184, 466", - "Type": 0, + "Text": "", + "StationType": 0, + "CanDocking": false, + "DockDirection": 0, "ConnectedNodes": [ "N008", "N010" ], - "RfidId": "0010", - "LabelText": "", - "ForeColor": "White", - "BackColor": "Transparent", - "ImageBase64": "", - "DisplayColor": "Cyan", - "CanDocking": false, - "DockDirection": 0, "CanTurnLeft": true, "CanTurnRight": true, "DisableCross": false, + "SpeedLimit": 0, + "AliasName": "", "IsActive": true, - "CreatedDate": "2025-12-12T16:29:33.968Z", - "ModifiedDate": "2025-12-12T16:29:33.968Z" + "RfidId": "0010", + "NodeTextForeColor": "", + "NodeTextFontSize": 7.0, + "Id": "N009", + "Type": 0, + "Position": "184, 466" }, { - "NodeId": "N010", - "Name": "TOPS", - "Position": "92, 465", - "Type": 3, + "Text": "Cleaner", + "StationType": 2, + "CanDocking": true, + "DockDirection": 1, "ConnectedNodes": [ "N009" ], - "RfidId": "0011", - "LabelText": "", - "ForeColor": "White", - "BackColor": "Transparent", - "ImageBase64": "", - "DisplayColor": "Red", - "CanDocking": true, - "DockDirection": 1, "CanTurnLeft": true, "CanTurnRight": true, - "DisableCross": false, + "DisableCross": true, + "SpeedLimit": 0, + "AliasName": "", "IsActive": true, - "CreatedDate": "2025-12-12T16:29:33.968Z", - "ModifiedDate": "2025-12-12T16:29:33.968Z" + "RfidId": "0011", + "NodeTextForeColor": "", + "NodeTextFontSize": 7.0, + "Id": "N010", + "Type": 0, + "Position": "92, 465" }, { - "NodeId": "N011", - "Name": "N011", - "Position": "450, 399", - "Type": 0, + "Text": "", + "StationType": 0, + "CanDocking": false, + "DockDirection": 0, "ConnectedNodes": [ "N003", "N012", @@ -205,121 +189,111 @@ "N031", "N022" ], - "RfidId": "0005", - "LabelText": "", - "ForeColor": "White", - "BackColor": "Transparent", - "ImageBase64": "", - "DisplayColor": "Cyan", - "CanDocking": false, - "DockDirection": 0, "CanTurnLeft": true, "CanTurnRight": true, "DisableCross": false, + "SpeedLimit": 0, + "AliasName": "", "IsActive": true, - "CreatedDate": "2025-12-12T16:29:33.968Z", - "ModifiedDate": "2025-12-12T16:29:33.968Z" + "RfidId": "0005", + "NodeTextForeColor": "", + "NodeTextFontSize": 7.0, + "Id": "N011", + "Type": 0, + "Position": "450, 399" }, { - "NodeId": "N012", - "Name": "N012", - "Position": "549, 450", - "Type": 0, + "Text": "", + "StationType": 0, + "CanDocking": false, + "DockDirection": 0, "ConnectedNodes": [ "N011", "N013", "N015" ], - "RfidId": "0006", - "LabelText": "", - "ForeColor": "White", - "BackColor": "Transparent", - "ImageBase64": "", - "DisplayColor": "Cyan", - "CanDocking": false, - "DockDirection": 0, "CanTurnLeft": true, "CanTurnRight": true, "DisableCross": false, + "SpeedLimit": 0, + "AliasName": "", "IsActive": true, - "CreatedDate": "2025-12-12T16:29:33.968Z", - "ModifiedDate": "2025-12-12T16:29:33.968Z" + "RfidId": "0006", + "NodeTextForeColor": "", + "NodeTextFontSize": 7.0, + "Id": "N012", + "Type": 0, + "Position": "549, 450" }, { - "NodeId": "N013", - "Name": "N013", - "Position": "616, 492", - "Type": 0, + "Text": "", + "StationType": 0, + "CanDocking": false, + "DockDirection": 0, "ConnectedNodes": [ "N012", "N014" ], - "RfidId": "0007", - "LabelText": "", - "ForeColor": "White", - "BackColor": "Transparent", - "ImageBase64": "", - "DisplayColor": "Cyan", - "CanDocking": false, - "DockDirection": 0, "CanTurnLeft": true, "CanTurnRight": true, "DisableCross": false, + "SpeedLimit": 0, + "AliasName": "", "IsActive": true, - "CreatedDate": "2025-12-12T16:29:33.968Z", - "ModifiedDate": "2025-12-12T16:29:33.968Z" + "RfidId": "0007", + "NodeTextForeColor": "", + "NodeTextFontSize": 7.0, + "Id": "N013", + "Type": 0, + "Position": "616, 492" }, { - "NodeId": "N014", - "Name": "LOADER", - "Position": "670, 526", - "Type": 1, + "Text": "Loader", + "StationType": 1, + "CanDocking": true, + "DockDirection": 2, "ConnectedNodes": [ "N013" ], - "RfidId": "0008", - "LabelText": "", - "ForeColor": "White", - "BackColor": "Transparent", - "ImageBase64": "", - "DisplayColor": "Red", - "CanDocking": true, - "DockDirection": 0, "CanTurnLeft": true, "CanTurnRight": true, - "DisableCross": false, + "DisableCross": true, + "SpeedLimit": 0, + "AliasName": "", "IsActive": true, - "CreatedDate": "2025-12-12T16:29:33.968Z", - "ModifiedDate": "2025-12-12T16:29:33.968Z" + "RfidId": "0008", + "NodeTextForeColor": "", + "NodeTextFontSize": 7.0, + "Id": "N014", + "Type": 0, + "Position": "670, 526" }, { - "NodeId": "N019", - "Name": "CHARGER #2", - "Position": "666, 198", - "Type": 5, + "Text": "Chg #1", + "StationType": 5, + "CanDocking": true, + "DockDirection": 1, "ConnectedNodes": [ "N007" ], - "RfidId": "0015", - "LabelText": "", - "ForeColor": "White", - "BackColor": "Transparent", - "ImageBase64": "", - "DisplayColor": "Magenta", - "CanDocking": true, - "DockDirection": 1, "CanTurnLeft": true, "CanTurnRight": true, - "DisableCross": false, + "DisableCross": true, + "SpeedLimit": 0, + "AliasName": "", "IsActive": true, - "CreatedDate": "2025-12-12T16:29:33.968Z", - "ModifiedDate": "2025-12-12T16:29:33.968Z" + "RfidId": "0015", + "NodeTextForeColor": "", + "NodeTextFontSize": 7.0, + "Id": "N019", + "Type": 0, + "Position": "668, 223" }, { - "NodeId": "N022", - "Name": "N022", - "Position": "450, 300", - "Type": 0, + "Text": "", + "StationType": 0, + "CanDocking": false, + "DockDirection": 0, "ConnectedNodes": [ "N003", "N006", @@ -327,848 +301,835 @@ "N023", "N031" ], - "RfidId": "0012", - "LabelText": "", - "ForeColor": "White", - "BackColor": "Transparent", - "ImageBase64": "", - "DisplayColor": "Cyan", - "CanDocking": false, - "DockDirection": 0, "CanTurnLeft": true, "CanTurnRight": true, "DisableCross": false, + "SpeedLimit": 0, + "AliasName": "", "IsActive": true, - "CreatedDate": "2025-12-12T16:29:33.968Z", - "ModifiedDate": "2025-12-12T16:29:33.968Z" + "RfidId": "0012", + "NodeTextForeColor": "", + "NodeTextFontSize": 7.0, + "Id": "N022", + "Type": 0, + "Position": "450, 300" }, { - "NodeId": "N023", - "Name": "N023", - "Position": "463, 195", - "Type": 0, + "Text": "", + "StationType": 0, + "CanDocking": false, + "DockDirection": 0, "ConnectedNodes": [ "N006", "N022", "N024" ], - "RfidId": "0016", - "LabelText": "", - "ForeColor": "White", - "BackColor": "Transparent", - "ImageBase64": "", - "DisplayColor": "Cyan", - "CanDocking": false, - "DockDirection": 0, "CanTurnLeft": true, "CanTurnRight": true, "DisableCross": false, + "SpeedLimit": 0, + "AliasName": "", "IsActive": true, - "CreatedDate": "2025-12-12T16:29:33.968Z", - "ModifiedDate": "2025-12-12T16:29:33.968Z" + "RfidId": "0016", + "NodeTextForeColor": "", + "NodeTextFontSize": 7.0, + "Id": "N023", + "Type": 0, + "Position": "480, 183" }, { - "NodeId": "N024", - "Name": "N024", - "Position": "500, 135", - "Type": 0, + "Text": "", + "StationType": 0, + "CanDocking": false, + "DockDirection": 0, "ConnectedNodes": [ "N023", "N025" ], - "RfidId": "0017", - "LabelText": "", - "ForeColor": "White", - "BackColor": "Transparent", - "ImageBase64": "", - "DisplayColor": "Cyan", - "CanDocking": false, - "DockDirection": 0, "CanTurnLeft": true, "CanTurnRight": true, "DisableCross": false, + "SpeedLimit": 0, + "AliasName": "", "IsActive": true, - "CreatedDate": "2025-12-12T16:29:33.968Z", - "ModifiedDate": "2025-12-12T16:29:33.968Z" + "RfidId": "0017", + "NodeTextForeColor": "", + "NodeTextFontSize": 7.0, + "Id": "N024", + "Type": 0, + "Position": "500, 135" }, { - "NodeId": "N025", - "Name": "N025", - "Position": "573, 97", - "Type": 0, + "Text": "", + "StationType": 0, + "CanDocking": false, + "DockDirection": 0, "ConnectedNodes": [ "N024", "N026" ], - "RfidId": "0018", - "LabelText": "", - "ForeColor": "White", - "BackColor": "Transparent", - "ImageBase64": "", - "DisplayColor": "Cyan", - "CanDocking": false, - "DockDirection": 0, "CanTurnLeft": true, "CanTurnRight": true, "DisableCross": false, + "SpeedLimit": 0, + "AliasName": "", "IsActive": true, - "CreatedDate": "2025-12-12T16:29:33.968Z", - "ModifiedDate": "2025-12-12T16:29:33.968Z" + "RfidId": "0018", + "NodeTextForeColor": "", + "NodeTextFontSize": 7.0, + "Id": "N025", + "Type": 0, + "Position": "573, 97" }, { - "NodeId": "N026", - "Name": "CHARGER #1", - "Position": "649, 87", - "Type": 5, + "Text": "Chg #2", + "StationType": 5, + "CanDocking": true, + "DockDirection": 1, "ConnectedNodes": [ "N025" ], + "CanTurnLeft": true, + "CanTurnRight": true, + "DisableCross": true, + "SpeedLimit": 0, + "AliasName": "", + "IsActive": true, "RfidId": "0019", - "LabelText": "", - "ForeColor": "White", - "BackColor": "Transparent", - "ImageBase64": "", - "DisplayColor": "Magenta", - "CanDocking": true, - "DockDirection": 1, - "CanTurnLeft": true, - "CanTurnRight": true, - "DisableCross": false, - "IsActive": true, - "CreatedDate": "2025-12-12T16:29:33.968Z", - "ModifiedDate": "2025-12-12T16:29:33.968Z" - }, - { - "NodeId": "LBL001", - "Name": "Amkor Technology Korea", - "Position": "180, 105", - "Type": 6, - "ConnectedNodes": [], - "RfidId": "", - "LabelText": "Amkor Technology Korea", - "ForeColor": "White", - "BackColor": "DarkSlateGray", - "ImageBase64": "", - "DisplayColor": "Purple", - "CanDocking": false, - "DockDirection": 0, - "CanTurnLeft": true, - "CanTurnRight": true, - "DisableCross": false, - "IsActive": true, - "CreatedDate": "2025-12-12T16:29:33.968Z", - "ModifiedDate": "2025-12-12T16:29:33.968Z" - }, - { - "NodeId": "IMG001", - "Name": "logo", - "Position": "633, 310", - "Type": 7, - "ConnectedNodes": [], - "RfidId": "", - "LabelText": "", - "ForeColor": "Black", - "BackColor": "Transparent", - "ImageBase64": "iVBORw0KGgoAAAANSUhEUgAAAOUAAAA8CAYAAACZ+H3xAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAADToSURBVHhe7Z1ndFTX1fefz++71vvlWendSWzHTmIn7t1x3JPYiRN3G5vee8dgU42RQaYbGwxudKGKeu9CFdQl1HuXkEZCIObud/32nTsMgyQjelg6rLNGSDN3zj1n1/8u938Mw3jBMIx5drvd0263bzEMY73j50uaBtMwzp92w7PnlN2zo6fPs6a1x/NoRYdnYEa95/bQMs/le/M8J27O8Pz36kTPpxZHeT48O9Lzvhnhnvc45n0zQj0fnRvp+dziKM831iR7Tvss03P1wQLPL6OqPCOzGz0Lqjo9mztOedpO2j1P9dk97e7f7Zjuax2ew/Maz012u/1zu92+0jCMp/7HbrevNQwj3TCMDrvd3mO327sNw7Bdzmk3DJut17CVt5yxJRR12b6Na7B95F1im7UrzzZyU5btZY80299WJtueXppo+8viBNuji+JtDy+Mtz20INb24PxzJ797eGGcvueJxQm2p5cm2V5YdcT22toM25gtx2zzvymweQaU2w4mN9nSymy2hq4ztlP289c0PIfndTThuZN2u70cBQlTrjcMI08u8zh1RqSorlsC0+tlg3+xzP7iqLztmSp/X54gT7wXKw/Ni5IH5kbKfXMi5L455uv9cyLNOTdSHpjL36PkwXnnTvP35nt4r3529tnJ3x6eHyVPLomTF1clyqiNabLw62zZFlwqUdlNUt1ySgz3xQ6P4XEdDMMwWg3DWAJTrjEMI8Nut/e6v2mow3bytBwra5XdUaXy3peZ8tpHsfLY3GD502R/uXWMj/x21CH57ehDcutYH/nDBF+5c7K/3DU1QO6ZFiD3TmcelvuGOPkM8+5pAfLnKf5yxyQ/uX28r37Hb0d767x1rK9+z5MLQ+XddQmyct8x8UmslKKaE3K6z+5+G8NjeFyTYRhGvWEYi2BKD8MwMi+WKe2GIdXNNgnLrJWPDuTIWx7x8tCsIGWK34wymeL34/3kz1MC5L7pgXL/zCC5fyavgXL/jEC57zJP57Ud897pgfKnyQHKqKznN6MO6c9/mR8iY9cnySa/AknIa5SmjpPutzY8hsdVHZfMlL2n7XK8tlO8Eytk/hdp8uzicLl9vJ/86l1TE94x2V+Z8IFZDibsh4Gu1uT7H9AZpFr1DxP9VFjc9K63/HGiv/xreZQs331MwjJqpbalm91xv93hMTyu+LhopsTcK2/okkOJlTLzs1R5fF6oEjha6I+T/OWe6YeVAWAEd+a4XqalRe+aelhun+Cn2hMTF8Hy/jdZEpFVK41tPWyT++0Pj+FxxcZFMWVje4+EZ9bK4q8y5MlFoXKz+mw+cufkAGVGi9jdmeBaTbTzOeas+98dv8cf/eMkP7l5jI/8bpyv/H1ppHgczJHUoibp6jntvg3DY3hckTEkpjx56ozkVbXLp4cL5T8ro9Ung4DvmOQvd089bBJ/P0xxrea9MwLlnmmHVRPiy+qcGiB3TwMUOvs+a90Wc/IeNCfmN595Z22CfBtVKtVNXXLGPqw1h8eVHRfMlGgKzLnZ29Pk4dnB8ruxvnLbOF9FVK83zYgPi8b+05QA9RVvn+Art433Ve1323g/9SVBfBEkILeuwsSVOe+c5O8EqJ5fEi6r9h2V5IJG9aOHx/C4UuOCmLKsoUv2RJfJ6PWJqnluGnlIifzeGQ6/0Z0prtGEkVjfHyf6ye8n+unPzy0JV003Y9sRmbcjXSZtSZaXV0XLI3ODNWwCg/55shmGcRUu1itaFeCKe35gVqDM2p4mIem1UtfaM4wDDY8rMs5jSsMwnExpgTlfhBbLv1dGq6bBpEM7Xi++o6XZYMDfT/BVk/q+GYflLY842eCbL3E5DVJcc0KqmmxS09Kt93O0rFUOJVbIwi8z5OlFYSpg+ByaU2Ok1rX1/oJMxsScHeejcc53PRNkf2z5DR8+OX3GLiV1nRKcVitfhpfIjpBi2RV+XHaFl8j2kGL5LKhIvokskYisOjlee0K6Tva5X2J4XMQYlCkLqjpkS0CB+o9olF8rsupnhhX6YZCrPWEafMO7AGgm4gN6a9xx8VeZEp/bIK2d5yl958A3LKzukC9Ci+TddfHKjLeM8dH7tK7t+j0wP+YsoR7Q5dGeCXIgrlwqm2w3rJ9p6+2T4LQambg5WcNIf5oa4NiPIDX/cV8enxcic79Il8DUGqlvu7GF1NUa/TIlJNbQ0Ss7Qovln8sjVTuiTTTrxs0Hu1aT78ecBJS5eayPmqt/ez9CPjqQLRklLRccxmjtPCmBqVUyfmOSgjpYA7ze7aYxEUSmNvaT3zmSIEZ8HCd7oktvWI2J5vNNrpI3PeJUYP3iHS/5zWhvBfd+PuKgfP+1fQryTdycIj6JVVLbQvhoeFzqcDKlYVg+5ZnehhN9si+uUkZ6JihYgj/F5sMEEKc7g1yreTYBwEeeWBgqaw7mSG5lm/ScGpoZVd/WI/tiyzSz5/4ZZhYS5irf4W6e3zszUO6Y4q97gpCa9ukRzWJq6+oV+w2mMdGUgWk1Mn5Tku41VhJWCYKZfb95jLf65nO2p8nhI9W6j8Pj0ocLUxrKlD2n+npj8tpl6rZ0BXIgTg6AsIJlxrkzx9WerAGGABVFcz06N1hmbU9VdLi374z7PX7nONVnl8qmLvWPXl8dq2YZmgHN6P69mG4Q5i1jzVjmXxeGqm8aebROuk7eWLFMiynHbUzSvVBQzBFSYt9Bph+eE6yI/DBTXr5xDlP22Y3M8pbe3h1RtfLcBzFy0ygvPQhX8ONaTwtcQnPj40Ig4zcly8H4cqlq6tIc3Isd2eWtsmrvMU1Y/8MEP/VTNVfX9fuJfTrDLaZQeP79CNngly9lDZ2X9P3X2xhmymsznEx5qs/uUdbcl+mb2tI755tieWBOmNw00ktNlusBZXWdltmKmYkfuTmgQErrTgxoPsIogDFMfh6Ibxo7eiQgpUpmfHZEq1qoaEFrWkiz6xpgThj2t6O89RW/CrCjqf3kDcOYw0x5bYaTKTu7T3mEZ3dkrthf0vvq2ky5e0aI/E4zWhzJAf0wx9WcFriE2QpxMB+ZEywzPsNsrZNTA5itMCrhkOTCJonNqZecijZNE+zv/X1n7Kpt98SUytsecRpiuWWMt9ztQB3PWYsjZ/bm0Sbj/v2DSPE4kCNHCprFdoOEBoaZ8toMJ1MWV3d6bA+ryhy9Kav3qQ9S5O4ZoXLnJD89AHcGuRbT0taEI0h6hyHeWRsvX4Yfl+N1J9zvyzmItUYdrZMPvs2S6duOyCb/AkkuaJIOW//+H0ycW9EuH+7PVn8R4sNU/vMUMzXPElC8okHNbCE/rYIBwT0YXyHNJwYOxfw3jWGmvDbDyZQBybUei3cXZL74YUrvI4sS5d6ZYecF06/1JJcVYqCa46mFYfLxwRzJLm+Tk6fP13rWIOSxM6xYXlgWqWbmG2vi5Juo0kEJqOVEr/inVCkTo42pHAHUgTBdrQat0Zzir6l8CIsXlkYq01c0dLlf8r9yDDPltRlOplzjle8xalNm5pPvJ/Q+uDBB7psd5mDIa8uUli8HMfxhkr8SBoQw6/M0iT5WP6AfySCB/khhkyYTcC/fe3W/mr9Lvs6UzJKWAbsN8HuSAohBvv1xvKbZQYB3uTElEwKlOgamfXROiJZ8ZZa0ajbM5Rr4qH1nTH/4UgY+9VDWdf0w5dDu2263qytyxn7h99rfMC5xvy92OJlyzMYjHi+sTsl8bFGckym11cZ1wpQUS0P4aO+XV8XIWq88ic9tlLq2Hp01LTbtfFDT2q3EUdFoU6YldvmfVTFKUN9/bb/8euQhefXDWNkZelxTwwiH9DdggLzKdlm9P0fN2Nsm+KoZi9mMxrbWB7NDqFYIZdq2IxKh4RHTr4Swq5pteq3ssjbJKW+T3Io21fBZpa1qKiMAaKFijZbOXmVsCP2riBLZ7F+gKYO8fhVeIv4p1ZJa1Cx1rd0DCqXe02ekusmmQskvuVJTJTf6FcgnPnny6eEi2RtdqvtTTHrcAGVpl5MpYZKG9h7Jr+rQ+2YvuHcme8Pk52NlbZJxvEWFJm5Jhw1XoP97JARF2uSRomYJSa+RA3EVeq5bDxfIRj9zv0gFpMLHL6VKYnMaJL+yXZo7Tg64b64Dpjx1+ow0nzgpJXUnFI/g3HLK2/Vn1srknsAtXLEEBDv7wfvZZzKjoAvuC1rt6R0Yd3Ay5d/ej/H46+L4zIcXxPU+tDBR7r9emNLxipbSipQZQfL8kggZuS5RZn2eKou+zJTFX2fJgl3psmBnusYM+T+pX6M+SZTn3o9Qf48UMTJ/yEZ5cFawTCCMElch9e0DE1Jb1ylFY0kQwIxFKKhgcGhMa40K+FDCNtlfxmxIUtO300HoBdUdig6TzE+vItqkvLMuXs3ol1ZGy9gNSWryxuXWS1FNhyTmNSpBvf5RnKa2YRZbOce8Yirz3f9aES1Lvs6SgJRqqW3udiLKEAPpkWh5gvovLI1QwXGblqL5yi0OUxwGe2JBqEzYnKyZWxCXO6FcLqZEeMA09EQibxihigUyyjNBRn2SoHtDosorH8Zo94cRH8fLij1HJSitWuroAOEyWBP5uKEZNbLBL0+mfpoiL62I0li1FiNobyY/Bd/w9cn0InwFDVCcMHJdgizbfVRzl7NKWgdNxWS0dfZKYFq1vP9NpuPcOMM4eXttvLz8YYyeKXS4K+y4Clyyu8i1PhBbLgt3Zeg5Qzt3TgmQJxeFyZwdaRJwpEr3ZKDhZMoHZ4d7PDw/NvOh+XG9Dy9KlAeuE6a0pmtYBqKAWK0gPwzBqzVBRHnlcNgMABnrc3fQp2eCnzw2L0SJGsnVN4CZgxYtb+iU3dElSijqz2LGaiLF2R5D/B8ChWipSAHs6bCZ3fJSCptlyqdHlEB+8tZB+cU7h+SmUd6asvbjNw+o9uWQ3/sqQxbuSpc318TKQ7OC9R60cJxa1Wk0BDusVgKmMokLvxpJ8zFveXJhmKw5kCOZx1tVWlN8jnAipPOrd710giJDmFzjnmmmKwCxci8/G+GlZzzvi3RFp090n3IyOOb/pTAlny+sapdN/vnyz+VRmtD/83e85OcjvPSzvxtnntMv3zX34oevH9DrIkz3x5apFkTDWtcqretUZHzK1mQ9P67HZxUBH282YLPWx3p5VWE+xUTsEWw3vXtIfj3qkNw1JUCFAJYDVgfn1Z970NB+UpPvX1gWJb8e7S0/ffugrp/9/+Eb++VnIw6qJTV/Z7paMV+FH9e9fHRuiPxixCH5xQgv+elbB+WHbxzQdZhgYLladQMNJ1PeMzPU4/55sZkPXqdMaU1MRwAWGM1JrANMDob3uhY083sIgXpQJB1VD6X1mLEDg0X5Ve2y5kC2/HVBqB7+HZMClBG5HkzJNbXucoKfasI90eWKwGIhpZe0yrydGcrEfBZhcpZBTI0LgTFZJ0kLCA00otnl7yzY5u7LwugInqffC5NJW1K0Lcurq2HqIF0LRE+3QPxo12uYwiTIjLM6BBiJ/FgZUcfqpIe+oA6tG3QBTEk5G0yJeWoNNExQarXM3Z6mSesWAxJiM8/RNPu5Jmg6983atwUWybHy9nNMQWK/ganV2gPq8XnBGhvmM1oTq2d8lkYt2rCmnr/L3+6dZha8w6C/HumtayC39/PgIrVU3IsL0HxfR5TK62vi9L2g7ayZM1RUfqq/7j8aFK34/Pvh8iCW2WRTgLL3nDv7/NDsILW6fJIqtWJpoOFkyvtmhno8cJ0yJdKdzWcz2YyLmSZhmj4ghAyBQLxTtqZoGddg5kS7w4ydujVFHp0TLLePM80j1oRZ5MqUmGX7Ysql9USvapyMklaZvytDTWYIzzwokxmtPGIInk57ypCOImxqPTl0zDGIQQWLSzhGGRQCtDTCVPMafEZL2BxlbCYR+av5blkMTE2wn24SDt97x0R/Lc/D98SHYrD+0PRaGb8x2WTEAZhy9o40CcmoldauXmVkMpvQGMR6MemVgSaZDdTM+yZV0RSOmNRPLgiTRbsy1MRt7jhrTuLT0cAMzUJLUPZFK5UcaZ/ch2U9sT+sy2IWhB+T+yOcZd27tXeKBUwJUC3L+p5bHC7rDuWqL+/qYyNcv4kslTc94vX65tm49KDSawY5M72gM5PezHNhv3CbqC7iu6dsSRHvxAtkykfnh3s8vCAu86HrzKfU0izaeUwN0IN0SsF+NKP7dJWYltbkmhwSSC6E9Zf5oYqYph9vdt8b5zh9xpDS+k7NjcWXgIghDg4VpmRdSEKIH1+XZmInus2DTT/eIvN3mkzJ9zmZ0pmAEKC/55XsJCQpxAFIsXzPURmxNk41jUVslnblPqyECsxa/gYT8j4af6Hd8Lf5bvw1/Bn+hnZByFn7y/VgGDQHPibAGOCPOQxNzJi46XymtFp1Ps7+fZslcbkNar5mlbbImoNYFSFyy1hvp1nJd6FBYIbf0493jI+akK9/FCvbg4v0OwFVXAeAyN6YMhmzIVHPkD3HH0bzwdjWPZCLDLMwuR+sjqcWhalZef+sIBUMquWnmMKNPXvQUVyBMCKxHkFGdwnaowJCWQPQzZ0pXRvDWTTK9ypD8l0TzLPQ9040C+TVVZnsr/4zxQ+AewMNJ1O+tCrG4+mliQr0PLgg4ZoxpUVwEBA3BMFhEpC5sy2oSEEUAADfpArxHmRiIvgfqdbSIw527aFc9VWQ7DABJqjGH8f7qm8ByslGDYTGEkoAQIFoMWO1faYmVwSoFvrtGIq/A2TCphTVGla4Jb24Rc1XV6bknqxD5B65V4hzZ9hx1TIATDB1Y/tJSSlslFX7suXpReFqrlqMCXGzVxAE14UInpgfKjM/T5X9ceWKZNIdoba1W9fwiXe+3qdV7cF1LLOPdeOXPzQ7WJZ8ZfrZ5jAkOrteJm1OcWpHLWtzECc+HcS/6MsM1bDbAgvVhMbPxT1gj1grzMj33DnFX5kUDYmgoVE3fmy77dQ5e81g/wC98FdhLJjRRL/P1nTeNcW0eljTPz6IkPlfpMvXESUSk12vCC/+vF9ylXzslSNvfxyn61CcYXKAU6CZQp+QlumXAtxQJ9vhEKow5e6oMnnr4/OZ0qJV9dMR9NDrtMPyjw8idR8A7L6MMAvCPX3yxcMrV39mfe7+t+twMuXM7Rker3yclvn4ewnKlM6QiIvJczWmdaOmHe6th8ehRx6tlZbOi6tbBDrHmV+x95g8Pj/EBGUcpqLa+rOCZarD1ie8MtAABMF3mrwlRQnY7Pvjo0QOgbJ2nPwjhWe1bmpxs8zdme5gSn9lAA6OV4vQ/vZBhKzeny0goO4D4gzNqFWfkdI5tJUevsNPROtaNaCACEDv7ml+xOsIM3zslSt//yBCbhtv+nesA5MYoQBTPjArWBbtytQ1M4x+mJL3Oq2RqQGKepI08dKKaE3ogMit96jgcBA+hIymQ2uSiYUQxPpw9+EYmu7Y3K0a6uUPY+XWcSZyrFrO4cOjdRBoKI0Ra+MV/QQI6nX4w66j5cRJ3RcAMIQIloEKmKkBTsAOWmB9WE4IQc4CQYz5ui+2XN0Sd6ZEW8OQt43zU3rCQoFWEQRofgQrQp5JiAwGx0dF6BKyGmg4mXJbcInH5M+yM59dntT70CKTKdUOv8pMCQCAOXjrOF/5/UR/eWddgjKLGa+6uNHde1rSipu1AgRAw/L/IGTL7MF0+wAztrh5wIR1Avgl9Z3q+I9Yi4/jrygqzMn1OHA0KZC9Nci5BQZ3Z0qAHNbBz5iXIHc0tXb/ag40pcAUKFgMZocFsyULEh4ihyBY/4o9xzTe5z7MnF6balBgfe4X5PXPgEguTMkaMXkvhClhNsw3TEn2ESYxgQ+XfkeOpA+AK2iJeDFF6En5jQPGRhmEk4g9fnQwR55ZHK5WCIIH+oARuGd+x/2PXGcyOEjtYAMGQSuTSIKgt2jAYkrdk1He2lFj6tYjClKByMKUB+Ir+mXKe6YHmokjY33lsTnBsvSbLMkaJCnlQoeTKRPymzxWe5dkvvpxau9ji5PknpkhctdU88vdGedKTAtZ5HABJzhgSqJQ+bn9ENpQRmfPKe1Ch49mMSWby/dxf1b88ZUPY9VvxOSzoHj3AZMQe/z4UK4yAiEOCJzDAT2kf41rJ4KEvCaZ/XmaPDj7XKaEoVkHayBmCsOQntcfU6YXt6qWI6yAyUxIBMKEQbgOP//t/UhZ65WrMTL3gTbinnwSK2TM+kQnUStTOlwFV6ZEgDEGY0rr3Ew0nOucRXetv7G3f5xs+mxYKFgDBN6/awA0EWhf/HWmPMF5jTF9Sa6psduJptZFSy/79qikFDQ548IDDsP0UcmVtvpNoR0tEx5hxzpZ80jPRNkXUyYNbT3SdKJXvBIqFEuwmFLNaEdYjj3hGnShwE36rrjnhQwnU9a2dXvsS6zPnLYjp/f5laly1/QQJ5zuzkBXYlr+EWagZbYStwvPqtNc1EsZMBh+EknmMCUSETOQA1bTZYKvHjI+B2VbBHcHs/m1d006vWtSlCmIWyE1Sf3D1Ox0+CMMso74/flMaaKX3Ds9cA7El0tF4/nSHqbMON4qaw/lqYkIQZzLlGa7TKpU1h3K04cVuQ9lypZufbSExZQw4aUwpWpCh6aEQNGWvJ4TvnH00MWSwMzEbP0ssEiD7IOZbwg1ujlgCnJerM28Btd1MCU0Mj9EVu3N1v3p6R34etbAR98TVarCE4a0NDj3wDX5HtrAYJ3tji5TGmAtXgmV5zKlIx6K2cp1WAc+JGEbEOhLHU6m7DrZ55Fa0pW5Kbiqd+SWHK0S4clYzr6u/TDS5ZxsDAAIBMeNc4CHEso19tWf38Hg96SnsXn4J6Rl8QpYUt7YpSYbhA4RYALP3ZFm1kk6QBrLdIHQIDgkH34R0jetqGXA3Ec1Y+s6FdyAGWBywI2th4s0dc81vxRUcmY/TIlkVqacflZTsmb34WRKrzx5afn1w5TWuekegqo6UFCYUxFiF0Grjc3oPK/+e5CeA5qwP4CHgc8Vn9Ooz3Wh4Pw8TenwT0G+Z29P12e/fJfgRjDTKI1Uw+eXROr9mp0PzfuwhCRCZfyGJPFOoNrnpKbkHXTXlDClo58TTAnNmAqkdsB7GspwMqXdbni0dBuZcUWdvSsOlcsj8yI0WwLisaSiOyNdjqlAgAOZM+11P40ZUZuYVzGwqQOxEvQnhkUrkH8sjZBnFoepNnlldYwijeS4Munz+o+lkWpuWkCBe8CZ36n0HE8QO0bzJevbSF/rnzHxG0jTwkch7Q/wJza7QTNPrEGGyI3KlBAlJjuIL5qF+ycI/9cFYWbc1REbZn8BqNBI7K0+jpBqHQ8TbUZougtdtF5+JZ0UC+WFDyLNZA8XpuTaCpLpfUfIyr1HJSmv0Zn04D44Q4AXkFlCElwHU9XszGgKZu4N4UpKHCmb8TkN0t3bpz7l/rhzfcpzmdJX25SCJNMSBj/0UoeTKWkHYjeMzNbu071eKQ3y8up41Sgs1kL63BnqckwkrenXgQhitgbL3C/SFC1zDSS7DyQj4Y6JW1KUsBAgFujCZrN51uT/GkTW7Jj+1mC+mgdzSDMvgOKD085PHXMdJ2ynNI73eVCRamL8NtcB6nmjMaUzecDxyIqn3guTZbuP6Vlgvq3zztWcXgjWArKcKYloUw3Ym/FL4rKr9h3TRxC6mvzIQepdDx+pkdHrk/SeuR4hEE0Acfir3AfxRQTDnO2pegZYMAB73DPasbHjpCTlNSk28e8V0Rq+QtMiMHA5OHczocRXNfnf34/Qx3JgZbEOENM9MeeGRNyZ0gwLZar2v+xMSeOsvjNnerPKu2TZnmw1yTgIvpy6wSulMYn1ccBsMPA2cSII3F2CWgNiIUufA8XnIMlaU9MciCSbR6jAOV2SCdy/m2mZ5pjqmkY32U+eXRKuiGdasRWzO39waN0n+zSlDDTRfb3k1N6oTKlPKRvtLY/RZ/frTInNbVCzE/eBlLV/rYjStd1Gqp+jzxM+O/TDGam5SFeHaYe1az1MXd1y9szZ27zKDvE4mKvMi89HFhX3z1lZjzNk7dwPZwtzQD+zt6fKkm+y1CedsClJATIELe+DodDkVjYO96PXpuP/dBN5jTpary4KQ5MHotwyeq4aU7o8tqC71y7xec2y4It0eXBmoCbxspjLyZSWv8GmIrmA9bm5j/bnKKzfX3IwQzvPNXbJbq11pGWHGWs0JfLZ9VnBYWu6f39/04z7mf4lE0CAwHETGrv/5Qw6bmSmNPf9bO4rCRPtjm4O+GLE6kjPYz+xPiB8mIF9xpzlewHbME3524vLo2S9b77kVXU41435yFpAbfEtfzXS7DuLhnPVvjCkidr7KXMp4zjS7Kyu+doe00o80DWYqXEw+i/f8VLFQOYSSKurxdNvmt3VYkr3Dundp85ovifpWgAwLJ4FYcpeDubk8xwSN0ZMkqz6mZ+R2FwzKKyMViKAT4YPkLizK0A/za2GOvXz+BeO1CgkO8kA4Zl1F9Xiw2JKiPahOcGK1CoQAhHpI/e8lTBAX7+LKdd55an5BUOBUGO5qJSfhJ/nrz6z5wUw5dgNifp+GBsLwkJI+T8JEe99dT5T4i+yZs6/v76v/VWJkMBA5go+918Xhun1eT+WC/uKprPyb0m5I0RD4H7RV5n6OcucZQ8J5JOVQ+kVqXvOZ9mgffU65uQMYRxTe5LyZqY0mkkH5ntUWxN6m+Sv5ir1tfdMC9C8ZnJvoT1Xiwf09dvIUjVfNcSDKzTFtMjM+LSvJqTjU15xpmTQ3Y2UNtpicMP4bRZidTkYwCx5Mhsfm4ncZVrSMlCqGw47Js3qAzkqOZHW5C6S4gTkfjnWxORQIXy0GQF7zGRyY4eqLPFpqLog9Y5c25+PoGzI7DCOdP7+6/tV2GFyASyV1vfPlEcKWxT6J9VOS4bePCA3jTKvhR8NkfLQXjJRKLh1HxBZTbNNy6GoZoAQf/D6AfnNKB8ViFyH/yM00Or0MGLAlCCKJIOj0Qj9aIf0sebPrB+tgSbtr0M6YQ8QT/J4MSHBDSglY71cj/PT4gBHwjalW7y+uDxSPg0slJwKs1qEgmQAIbQvtYsIY7Q07wWcIf3wzklm+mJ/mIEFPGGNsfd8P9Yf7sqba+Jka0ChgnZdPecXHlO69UXocfnn8mj9LPvNGbAPlOL97O2Dcj8PfvrcYS10XWGmZLApHAxxGMwODhFCQuJfrMY0zVbTvEBikStI39Ty+rOZMP0NhATxPLJpiC0qkHAF4qhIRM32cfR+BcEFuSOXdCjtNGBKNCWaxHzKl1WKZaLNSH20BT4VfjTdEtyHqSlbxNM7T/6zKlr37XfjMT1NdBPNySupbrxnIE1Z12ITn6QKGbcxUfcM64IyJjOh3vw/yCOakqwmBkyJ1qKygcwu1o8m0rieI4md5O+5O3iWSLXUt/YPigGa+CZXqnCHoWiizZqhHbQXGtosdzL9RrQRiSMIGVIWLUQb5gRFPRhXLu99mSH/WRkjj8wOVqEMc/NZ1oRAhdGtn628VtbP3j+7JEJjzFsPF0pyQbOzeKC/oZoyymwLo+eG2+D0q03rkawjCuvxRa+IpuzvUXhwP5uzel+2PPNeuDKDWRsXoJkcQ2FOy2y1yooemxui5VNkQxRWn9AAL9KJbAom/2cicb0SzPAHlQmW2WqldLl/z6VMk1gCVbshGZHA07el6iPwhvLcEHrF4P9SJb8ztFhRPZLqAULQBBv982V7SJEcPlKlwFV/MS5lqNZuSSpolP1xZZr0vTkgX7YFmdfiOvy8J7pEkvIbNM/TfUDM5O0WVrdLcHq1fBVZonWkEBvChjVwTdZIjjEMrMMwNH0NhqOSY2tgobbXYP0QNMXLhDVCM2ukqLZj0KwaKkBog+IVX673YK7bvBbfz+SpXtuDaVuSrwLm26gSfS5MV+/51+3oOiWYtT6JlfKJd552msDVesMjTutkQYBJ63ttdYx2OEAgrNx7TJ8eRluQmuaBgUTXQRz8mOMpbeyBtd/swxb/Ag3bfB1ZouGQsvrOQZu49Te0/5KdXsRn13JBTGmN4uoO2R5cqCYQkhoVjumg/pxl/vVD5O4TRqLYFWlJMB8NgImH/7boyyxZ+GWmLNiZoRNbnTnt0xSNH4K2WppMH/raz/Uvdlr3wM9IQpj+F+9SmW+aJ2TxmLFL9525+uNilgARYvngNyHoCCuRKH1uSiFXtua54/zfXPxw/Rb367K/ECtWCeENntNCaAO/HpTbPQWSxhHEKKlhRSujTSlMR5DTOYIEFIoSLET1QgeWAv8u9+AceN4NAp5catI2Waet94zgudntQ2BKNos80shjddp/BD/GMhkwOSyt6U7s/U1Lu/IZkC8cc+sxAJiN1gR04Xf8Df8FUwvzaSja+YKnIsJm3SC+CibaP5dFaZJ5UkGTEvSFSFfXYREYnxtoQmQkIzD5eSDk+VIHRE3TLSpBMMcwq8lWGazA+1oOmAzXadnuLE1MoJ8RGooMre4LSKu73gbHilAhP5m2LfRR+jqyVC0Wcq5DspqkrF2ktv10va33ApnSGkgwag9pPkQcCH9DA8LjTKgeprnQp3Npep2zq4CZXN3f1AwRt0ycyzVN4WCaqyCEaEgC0mhuE5E7eZ50vpABwxXWnNC+Miv2HVN/jYLqpbuPqinFpEk0bTiIq2Gy+SdXaQD8zBCl+oUMzmy9T776gT94fb/uJc2s8qrar4hGuNRR2WhT85hwyU/ePqAlZ2AbPAh4oGba1/PAhcAUpiaYxHhan+AGIGh2hBTJoZQGSa40JK6os/5Iac/QmNIadD/DRKB0BqgdAjefX0gVhulUA0W7M0F/02n+Djb7+dzFTNf45T3TzdIiYmmAAyTCY6r6JFUNigRfyICRCaeM/CRRUWYYAfQRUAJBxMQ35/c/evOACgY6BcTlNl5y+U9/o6qpW3M/n1gQpsghIA9J+vnVA6czXsuBEMEHxD/EjQBUoUMfDwS2ipD/Gwb+Ir4mFgmgHT46GAqM+UUIxeEFsskvT76OqZbYErskFHfXZ9WcvjimtAZBXlA/ikExiQhXoG1uGumtsSAL9dInQF9G5hrKhKlBj3nFF/3jBH9twAQ8TtHri0sjZem3WRpvI154KcxoDZjSLK7OlpdWxGg1AWgj+2E9YhCzHY1FAJrOCLSjRJpeiUFfmC2HC7UYF1gfjYl2Lm8cHPW+VoN+uQTuX1sdKzePJfsnQBbuTNeOBIMhptfjIN5KUgVKLKWgUS2wHcHFej9TtybLij1ZEpDeKBWdhlS0nalv6hmi+TrQwFyjJjAkrUbr/4hvcfD4hDAofhrMSkxJszsckPjl0obna1tHQBkmdGTCEJMjTgdjPLMoXCZvTpFPAwrUJBosz/VihmHYNT8Wc5QWkOTJrvPOc8bsyGQhKE05E60riMshTfE7CJ6D5lHsG3WsXvNDAS8AaQbzORGQFU1dilqSpE2PIMAPgBCI/NOgIu2Fi0WD8Nzgm6fXxSo4WtaqMUpQTfbCbpwVTKyJTBfQWJo84+919vQpiELD4cT8RhVArJnStoEGyeash7gglSAwGEKI73dN5mewXnwuMqtIyQMYpJVjQm6jnHAzX0GuiyD4wibdK+6D+yJN7nwo6dzBnpIAn5zfpPdApQ8gDJ/l/hBmrI8EDH5mDyhIJ67M39lz14Fm7D1t1+tiguNDEj9mX1lX9NE6Zcq90WXyiXeurN5/TLVlZG6blJ0QKaw/XV/adubyMKXrAEqmTww5jWsO5sq4jcmaw4jGtIK4ZHFYFQCAK2admmnWneM7OjJtLGZzZUQrOdnyS2E2hAAhE0I3VgtFGICyIRiC5lTA+ZT8QESDtZe8tHEuMbAnBMDf+ChWA99ksrAW1/YhgEmJeU2anQOUTwwM6wIt+8qH0doUmJgfwXTLzyXkAUIJQQHXT9ySLM9/EKFxRyoYFn+VpYRK+iKB8JdXxaqgAskeuz5Rs25At9kbKmnoFwS4lZiH73ZKv4dmUhv9C2T6p6latE2Xgw/3ZcucHelakfPEghBtPEVwn1gyObBW1QZECpETs2XfSTb41/Jo/S4EA3myhCzoX5NW3KT3wmeIC1PXqEyp/XYDtKUHgurUabv63Zi4WDckV9DBASuAHkpk2bz1cZws/faohmEIt7k+4RumQQAi8MivJYmAChcqfkZ5JmopFk2bl3xFJCBdVu45Jl9HHNdrEYKZvDlZ3lmXqBgBbThdM77QiAh5is7HbUjSJ4Qv33NMTVbqRFFapPP5JlVKQl6DpBU1SWh6tfilNkpsqSExBSfqk0t7Lz9Tug66wSEdefoVsSjADTqJcxgcDERnpYyZ+Yt+2tAKZBeG1en4/zm/s37PdOSrwtgwKAHppxaGKsFABMRYd0eVqqRy7U96tQbaDU0Dkb2yKkbNZlDraVuPSFJ+k4JnSFZyeq2nSaNNKUcjBEXYCIHEvT67OEyrMfIq21S7oNVoAD36k0Tdw++/tk8f0UAHNbKwiNftjSlXLQJTgmRioSDErPpHXgHryNL50RsH9HcztqVqy33ixZzdu56JKkxoLszaMfs5N8xwmhL/7yt71RKhfO6zoGIpa+jSrB5eOXfO4tax3vK9V/fJj988qCY066NRMT4116F7H4kU9Eqqb6fAuEL7qd4y2keFMC4GTbGwQNA8lPiBA7Am1v2zt82MIV5/9AZ7cFA/h69OOxArQwihQWz036ui1YL63mv79PO4NDwCkoolfiZOTfYV7sW6Qzla40v39vtnHJb/+8/dun56I5E4YOEAtKkkFxbF8H/++a2e5czPjyiSjBamAVtkVp22mgxOr1XBQGF9aFaz5DXYJb+2u7606QozpfuAAGEMNhenl6A1Xb4Js0zanKxpZxDSC0uj5LklESrtqVhBAjJhtmcWmVIN6U79JO3wSUKgYgGCBWaOyKzVZ0cged211tUerkxJ235MaBIoZn2Wps/74G/kGtMtHYLHmuDnHaFFmhRAAgI9hHAH0Bpo0C0BBUoAdDsgzku2FQRJa0Vyg0FaCc4TBsmt7FDzlIA9bf4RaIBx7B9CkqD9Zv98lexYKXwHLUY+CyzWEERUdp0+J8VqrwmRgk6DHPLZeV+kqYDFArpvRpDZ66ewWTX6nugy/R5S2yB46mVZH9qYPjxTth7RNcMAdC8n9IF5B/FCuGgusoDI3rF6/CTkNqiL9OziCLnpXW+995dWRinCjYZCg6HxuUeY6uHZQRrvRvtjfqPdcK/YL+4HWuIeNvrmq0lJpQmW3a3jfRRkYv1EG7ifo6WtWkFEm0qu/eicIO3/RNdAmnehEXmsBMIGs5uCA87PeiwEyQI8I5V2nGGZdRJwpFrzZo/XdyuV2u32C49TXomBe2TF6sj6QALWt3arRMFXwVThAGKy61RaM2OP1Zv+UnGzZokg9ZDm+GEANBrrI0vC/cuu4TjLlKXKlEhhTGqQ3tjser1XskUoGUMbYqrRq/XVj2JVA/LYBJLOSUcj3xdzliwWsnNI1n5+cbgS2EOzA9XsQotgAprT3GesFSB4TLyfjzio16ICg4fXcAYd3b3aRBkz1GTuMNngW6DEph31NqfI7Y7KCEqc6I1jxVrDs2pUMLJ2LBXWhqmGz0juMG4LmhvmwwzG72VPmCTAY2aSv4yGe+a9MNngk+eI5ZUpUaPJSdHDZKbOlUZjoz8xGzRjWWGq7o8vU9pBY5EYcTi1RsatT9b9QltZPWZpME1FCrWgPEbhkbkhWp2Eu2XulalJ2ddH5wXre9gzOuaRxMD1sTpoiIYbgICDaXED2E+ejwKAh4CiwB6UFd/SfVhPU4Ne2UMLKhhSRs+1GJrxweHxaHRrOojtv2mcx5SjvdX/hSkxXwBmlFAWhWnvF9pe/mdltEzekqyphbwPQufgIWC0DIdNDJTfUWWDxMfcJU0NonIfJIzTsuTZ98Lll+8eUs0GgzS0mckD+LzfRJVoHq0WMC/iWgUKFpFiSGkTLoIFuLiixJhjoMdWiR9/JwMq8mi95oXCVAgCiPfbqLJzUvIAaFgzFhCaB2uAUA2+IhYP30txNEyJRiLOhwXwpkesCRxO9tfeSvF5Z0NJ0Aed5dDYCASsDxpmsceY+sSIH50XoiY01hjCqsylIABLzhJgPN4AC401WmVdVFF5J1Vqd0CrJpN2k7hiCC1qfF9cFilbAwrUWnMHsQYb1z1T3ijDnSm1edcEP2W2hPxGfY4Fj3HDtMPMA6RZ9m2WpBabPjD5wNkV7Rr39E2qkpC0atUwgDtoWHrH8rAeALPxm5LEK75CShRFNC0IfLui6hOOHjUR+v2USq33zZPqZpMYMfPJiaVAAHBsQKacEiDzdqRLhqOrPDYJf6cLnNWMCh8uJqdBXRQe4wdD/cTh3xE2g7EIDwA+UR00bkOi+tD4pmANO8OPK6JLSRsmNQKMR0Z8tD9b80zJgyYfGaCPuDhr/XDfMQVY8qtPKHbAw3uwLvDtYFxqSelOjvlLf6V/LIvUB/ZgkpMEg9nJemmyRrYN+0hCC34j7gLhKleNRxgQvxSfH3Sfa5lYga9qX0xo4pJDTTwZZsqrNGBKkqBJAMdXBlT45TuHtFoBoKe9q1fNcTTMPVPNFv1IZyQ9Cdyb/AoUoaRdBUUB/Iw/XtXUpczJg3AheBgT4AItCLBC/5yVe7MVsAHl3eSP9I/Q0ivAHoAS68nTZC7hH0LgmJH4cDxCAcbnuzCj8YXpc8r383sGTMnTuUAwuSe07NStqYr44jtRaUK9IsAQ2tJaH/FHBBTpmmgyzFs0Kk9Do4YUZBWtxn3w9CrqIwF6+F6EGAxLuiBPMfvBG/s164tib9BantyNb/zjtw7ovcKcnwYWSEVjp4Z3WBv3gFbjPSYjBevn0Y4g9TAk62K9/1gapc/1dNWmoPZYJBQa0CuIMi72jaSMyVtTNPIwlAIGawwz5VUaypQt3drikJAEJhFmKuAU0h2zHMj+aEmreHqbPV4BN0A7AUCYP3nrgKYy4qeoWVSFWdSnAAKm5CY/89EEaJzvvbZf/t+/dysSS8cCCJjWJjtDS+TfK3iPn/x1fpis987TShYGBeS7wkqUAG8Z4ytPLwxXkw1pb/mUaBVKpSjXskq8xKCLe42M9kw0u4VP9FctZmUmEehPLWwSj4M5Zox2vK+mzf3vK/t0nWQXgZrj82FOAywRzyRpnjItHiWANoTJMIUx9dkrLIjDqdX6qAbtfD+GutD9iu5y39ScPjwnSLUgAJurSQ9j4isTouFBRAiDn77tpRYE5je+/d8+iFSLBS2NiYvVAJDjOqigIt6Iq/HTtw4o6AS6zX7XtXUPqdTPGsNMeZUGPg5+VEFVu/pK5HSSnogp5g4CoF0wschmAZXWahkHqkypUl5F2zlPfrYGUhmzDW0MqolWgRH4DMFuYmkEyXnQLECJV3yloreWfweD0/iaWCrpbd4JlSokADeo9WStrAkCR5CwTvX6DbMImbgv98R70MwAS66D0A3AD34ipiXNsVfuy1bzVoXG8ZZz+qZSdFxQ2a7dyvGfMf1JsaNSx0qgIH0NZos4WqsmKegs1121P1uRZvxazGSrgNlKeSPgTwwWhDcotUZ2hRVryhv3zhq5FwAzrBVCRUQAQLurm88m7/N57hNzHHDrZ28fUJfA0yfP5SFJQx+uTLnGMIyMYaa8NmMw3KqPCpIhSlyKWfQzg1z38oyL/wLWN9T7+q4Bs2JWnhlgQ0E5EV6g+Ai5WZ+lasH0rtBiDT1VNHQqGu2fVCFj1ydoCOeXIw6qJiSNlBIwBszN+2nOBfCFFQN6zMN/wQgGeyzDdw0nU/b19W0xDKPE/Q3DY3jciCOxuEvmf1MoDyyIl5snRchds2Pl7x+myjsbj8pbG7LkmWXJ8udZMXLf/DgZsfGYfBnbIJVtZxm9uUdkW3idPLciVX4+JlTunRcri/cWS3p5zyWIKXMYhtFtGMYyWkwuttvtQYZh5BqGUTQ8h+eNPDt6zhRlVXQXbQ2tKRqxIavo3jmxRbdNjii6ZVJ40c2TwotunxKpvxu1+WjRnsTGopoOu/OzDZ19RX7pLUWjt2Tr+347IbzojXXpRfsSaosaO06d910XMRMNw5jw/wHQsSeoUF9P/gAAAABJRU5ErkJggg==", - "DisplayColor": "Brown", - "CanDocking": false, - "DockDirection": 0, - "CanTurnLeft": true, - "CanTurnRight": true, - "DisableCross": false, - "IsActive": true, - "CreatedDate": "2025-12-12T16:29:33.968Z", - "ModifiedDate": "2025-12-12T16:29:33.968Z" - }, - { - "NodeId": "N015", - "Name": "", - "Position": "449, 499", + "NodeTextForeColor": "", + "NodeTextFontSize": 7.0, + "Id": "N026", "Type": 0, + "Position": "649, 87" + }, + { + "Text": "", + "StationType": 0, + "CanDocking": false, + "DockDirection": 0, "ConnectedNodes": [ "N011", "N012", "N016" ], - "RfidId": "0037", - "LabelText": "", - "ForeColor": "White", - "BackColor": "Transparent", - "ImageBase64": "", - "DisplayColor": "Cyan", - "CanDocking": false, - "DockDirection": 0, "CanTurnLeft": true, "CanTurnRight": true, "DisableCross": false, + "SpeedLimit": 0, + "AliasName": "", "IsActive": true, - "CreatedDate": "2025-12-12T16:29:33.968Z", - "ModifiedDate": "2025-12-12T16:29:33.968Z" + "RfidId": "0037", + "NodeTextForeColor": "", + "NodeTextFontSize": 7.0, + "Id": "N015", + "Type": 0, + "Position": "449, 499" }, { - "NodeId": "N016", - "Name": "", - "Position": "422, 537", - "Type": 0, + "Text": "", + "StationType": 0, + "CanDocking": false, + "DockDirection": 0, "ConnectedNodes": [ "N015", "N017" ], - "RfidId": "0036", - "LabelText": "", - "ForeColor": "White", - "BackColor": "Transparent", - "ImageBase64": "", - "DisplayColor": "Cyan", - "CanDocking": false, - "DockDirection": 0, "CanTurnLeft": true, "CanTurnRight": true, "DisableCross": false, + "SpeedLimit": 0, + "AliasName": "", "IsActive": true, - "CreatedDate": "2025-12-12T16:29:33.968Z", - "ModifiedDate": "2025-12-12T16:29:33.968Z" + "RfidId": "0036", + "NodeTextForeColor": "", + "NodeTextFontSize": 7.0, + "Id": "N016", + "Type": 0, + "Position": "422, 537" }, { - "NodeId": "N017", - "Name": "", - "Position": "380, 557", - "Type": 0, + "Text": "", + "StationType": 0, + "CanDocking": false, + "DockDirection": 0, "ConnectedNodes": [ "N016", "N018" ], - "RfidId": "0035", - "LabelText": "", - "ForeColor": "White", - "BackColor": "Transparent", - "ImageBase64": "", - "DisplayColor": "Cyan", - "CanDocking": false, - "DockDirection": 0, "CanTurnLeft": true, "CanTurnRight": true, "DisableCross": false, + "SpeedLimit": 0, + "AliasName": "", "IsActive": true, - "CreatedDate": "2025-12-12T16:29:33.968Z", - "ModifiedDate": "2025-12-12T16:29:33.968Z" + "RfidId": "0035", + "NodeTextForeColor": "", + "NodeTextFontSize": 7.0, + "Id": "N017", + "Type": 0, + "Position": "380, 557" }, { - "NodeId": "N018", - "Name": "", - "Position": "329, 561", - "Type": 0, + "Text": "", + "StationType": 0, + "CanDocking": false, + "DockDirection": 0, "ConnectedNodes": [ "N017", "N030", "N005" ], - "RfidId": "0034", - "LabelText": "", - "ForeColor": "White", - "BackColor": "Transparent", - "ImageBase64": "", - "DisplayColor": "Cyan", - "CanDocking": false, - "DockDirection": 0, "CanTurnLeft": true, "CanTurnRight": true, "DisableCross": false, + "SpeedLimit": 0, + "AliasName": "", "IsActive": true, - "CreatedDate": "2025-12-12T16:29:33.968Z", - "ModifiedDate": "2025-12-12T16:29:33.968Z" + "RfidId": "0034", + "NodeTextForeColor": "", + "NodeTextFontSize": 7.0, + "Id": "N018", + "Type": 0, + "Position": "329, 561" }, { - "NodeId": "N005", - "Name": "", - "Position": "233, 561", - "Type": 0, + "Text": "", + "StationType": 0, + "CanDocking": false, + "DockDirection": 0, "ConnectedNodes": [ "N018", "N020", "N029" ], - "RfidId": "0033", - "LabelText": "", - "ForeColor": "White", - "BackColor": "Transparent", - "ImageBase64": "", - "DisplayColor": "Cyan", - "CanDocking": false, - "DockDirection": 0, "CanTurnLeft": true, "CanTurnRight": true, "DisableCross": false, + "SpeedLimit": 0, + "AliasName": "", "IsActive": true, - "CreatedDate": "2025-12-12T16:29:33.968Z", - "ModifiedDate": "2025-12-12T16:29:33.968Z" + "RfidId": "0033", + "NodeTextForeColor": "", + "NodeTextFontSize": 7.0, + "Id": "N005", + "Type": 0, + "Position": "233, 561" }, { - "NodeId": "N020", - "Name": "", - "Position": "155, 560", - "Type": 0, + "Text": "", + "StationType": 0, + "CanDocking": false, + "DockDirection": 0, "ConnectedNodes": [ "N005", "N021", "N028" ], - "RfidId": "0032", - "LabelText": "", - "ForeColor": "White", - "BackColor": "Transparent", - "ImageBase64": "", - "DisplayColor": "Cyan", - "CanDocking": false, - "DockDirection": 0, "CanTurnLeft": true, "CanTurnRight": true, "DisableCross": false, + "SpeedLimit": 0, + "AliasName": "", "IsActive": true, - "CreatedDate": "2025-12-12T16:29:33.968Z", - "ModifiedDate": "2025-12-12T16:29:33.968Z" + "RfidId": "0032", + "NodeTextForeColor": "", + "NodeTextFontSize": 7.0, + "Id": "N020", + "Type": 0, + "Position": "155, 560" }, { - "NodeId": "N021", - "Name": "", - "Position": "68, 558", - "Type": 0, + "Text": "", + "StationType": 0, + "CanDocking": false, + "DockDirection": 0, "ConnectedNodes": [ "N020", "N027" ], - "RfidId": "0031", - "LabelText": "", - "ForeColor": "White", - "BackColor": "Transparent", - "ImageBase64": "", - "DisplayColor": "Cyan", - "CanDocking": false, - "DockDirection": 0, "CanTurnLeft": true, "CanTurnRight": true, "DisableCross": false, + "SpeedLimit": 0, + "AliasName": "", "IsActive": true, - "CreatedDate": "2025-12-12T16:29:33.968Z", - "ModifiedDate": "2025-12-12T16:29:33.968Z" + "RfidId": "0031", + "NodeTextForeColor": "", + "NodeTextFontSize": 7.0, + "Id": "N021", + "Type": 0, + "Position": "68, 558" }, { - "NodeId": "N027", - "Name": "BUF1", - "Position": "38, 637", - "Type": 4, + "Text": "Buf #1", + "StationType": 4, + "CanDocking": true, + "DockDirection": 2, "ConnectedNodes": [ "N021" ], - "RfidId": "0041", - "LabelText": "", - "ForeColor": "White", - "BackColor": "Transparent", - "ImageBase64": "", - "DisplayColor": "Green", - "CanDocking": true, - "DockDirection": 0, "CanTurnLeft": true, "CanTurnRight": true, - "DisableCross": false, + "DisableCross": true, + "SpeedLimit": 0, + "AliasName": "", "IsActive": true, - "CreatedDate": "2025-12-12T16:29:33.968Z", - "ModifiedDate": "2025-12-12T16:29:33.968Z" + "RfidId": "0041", + "NodeTextForeColor": "", + "NodeTextFontSize": 7.0, + "Id": "N027", + "Type": 0, + "Position": "38, 637" }, { - "NodeId": "N028", - "Name": "BUF2", - "Position": "125, 639", - "Type": 4, + "Text": "Buf #2", + "StationType": 4, + "CanDocking": true, + "DockDirection": 2, "ConnectedNodes": [ "N020" ], - "RfidId": "0040", - "LabelText": "", - "ForeColor": "White", - "BackColor": "Transparent", - "ImageBase64": "", - "DisplayColor": "Green", - "CanDocking": true, - "DockDirection": 0, "CanTurnLeft": true, "CanTurnRight": true, - "DisableCross": false, + "DisableCross": true, + "SpeedLimit": 0, + "AliasName": "", "IsActive": true, - "CreatedDate": "2025-12-12T16:29:33.968Z", - "ModifiedDate": "2025-12-12T16:29:33.968Z" + "RfidId": "0040", + "NodeTextForeColor": "", + "NodeTextFontSize": 7.0, + "Id": "N028", + "Type": 0, + "Position": "125, 639" }, { - "NodeId": "N029", - "Name": "BUF3", - "Position": "203, 635", - "Type": 4, + "Text": "Buf #3", + "StationType": 4, + "CanDocking": true, + "DockDirection": 2, "ConnectedNodes": [ "N005" ], - "RfidId": "0039", - "LabelText": "", - "ForeColor": "White", - "BackColor": "Transparent", - "ImageBase64": "", - "DisplayColor": "Green", - "CanDocking": true, - "DockDirection": 0, "CanTurnLeft": true, "CanTurnRight": true, - "DisableCross": false, + "DisableCross": true, + "SpeedLimit": 0, + "AliasName": "", "IsActive": true, - "CreatedDate": "2025-12-12T16:29:33.968Z", - "ModifiedDate": "2025-12-12T16:29:33.968Z" + "RfidId": "0039", + "NodeTextForeColor": "", + "NodeTextFontSize": 7.0, + "Id": "N029", + "Type": 0, + "Position": "203, 635" }, { - "NodeId": "N030", - "Name": "BUF4", - "Position": "296, 638", - "Type": 4, + "Text": "Buf #4", + "StationType": 4, + "CanDocking": true, + "DockDirection": 2, "ConnectedNodes": [ "N018" ], - "RfidId": "0038", - "LabelText": "", - "ForeColor": "White", - "BackColor": "Transparent", - "ImageBase64": "", - "DisplayColor": "Green", - "CanDocking": true, - "DockDirection": 0, "CanTurnLeft": true, "CanTurnRight": true, - "DisableCross": false, + "DisableCross": true, + "SpeedLimit": 0, + "AliasName": "", "IsActive": true, - "CreatedDate": "2025-12-12T16:29:33.968Z", - "ModifiedDate": "2025-12-12T16:29:33.968Z" + "RfidId": "0038", + "NodeTextForeColor": "", + "NodeTextFontSize": 7.0, + "Id": "N030", + "Type": 0, + "Position": "296, 638" }, { - "NodeId": "N031", - "Name": "", - "Position": "350, 400", - "Type": 0, + "Text": "", + "StationType": 0, + "CanDocking": false, + "DockDirection": 0, "ConnectedNodes": [ "N003", "N008", "N011", "N022" ], - "RfidId": "0030", - "LabelText": "", - "ForeColor": "White", - "BackColor": "Transparent", - "ImageBase64": "", - "DisplayColor": "Cyan", - "CanDocking": false, - "DockDirection": 0, "CanTurnLeft": true, "CanTurnRight": true, "DisableCross": false, + "SpeedLimit": 0, + "AliasName": "", "IsActive": true, - "CreatedDate": "2025-12-12T16:29:33.968Z", - "ModifiedDate": "2025-12-12T16:29:33.968Z" + "RfidId": "0030", + "NodeTextForeColor": "", + "NodeTextFontSize": 7.0, + "Id": "N031", + "Type": 0, + "Position": "350, 400" + } + ], + "Labels": [ + { + "Text": "Amkor Technology Korea", + "ForeColor": "White", + "BackColor": "MidnightBlue", + "FontFamily": "Arial", + "FontSize": 20.0, + "FontStyle": 0, + "Padding": 5, + "Id": "LBL001", + "Type": 1, + "Position": "180, 105" + } + ], + "Images": [ + { + "Name": "Image", + "ImagePath": "", + "ImageBase64": "", + "Scale": "1, 1", + "Opacity": 1.0, + "Rotation": 0.0, + "Id": "IMG001", + "Type": 2, + "Position": "633, 310" + } + ], + "Marks": [ + { + "X": 684.0, + "Y": 539.0, + "Rotation": 119.877727797857, + "Id": "2cb51787-c8cf-4ddb-97f0-b71f519d47dc", + "Type": 3, + "Position": "684, 539" + }, + { + "X": 40.0, + "Y": 559.0, + "Rotation": 90.0, + "Id": "f704ebe0-1653-4559-b06f-1eaecafbefba", + "Type": 3, + "Position": "40, 559" + }, + { + "X": 126.0, + "Y": 560.0, + "Rotation": 90.0, + "Id": "d5b27365-79a2-4351-84c3-6767941ec0be", + "Type": 3, + "Position": "126, 560" + }, + { + "X": 203.0, + "Y": 558.0, + "Rotation": 89.2872271068898, + "Id": "0367cafb-9f85-4440-b6b4-c802a58e6181", + "Type": 3, + "Position": "203, 558" + }, + { + "X": 296.0, + "Y": 560.0, + "Rotation": 88.405167720722616, + "Id": "1f4ab2c9-07f8-4675-802d-9b4824b55198", + "Type": 3, + "Position": "296, 560" + }, + { + "X": 81.0, + "Y": 256.0, + "Rotation": 74.50226651936697, + "Id": "15fddfa4-ff74-48ff-b922-4aacdce1960b", + "Type": 3, + "Position": "81, 256" + }, + { + "X": 73.0, + "Y": 466.0, + "Rotation": 90.0, + "Id": "962bb671-6932-477d-9209-2c2a076cfb22", + "Type": 3, + "Position": "73, 466" + }, + { + "X": 686.0, + "Y": 196.0, + "Rotation": 90.0, + "Id": "cd9f8434-f223-4532-9b3e-5b44c738abbb", + "Type": 3, + "Position": "686, 196" + }, + { + "X": 669.0, + "Y": 84.0, + "Rotation": 90.0, + "Id": "61c6d1dd-6a39-4931-a530-9b44d2010139", + "Type": 3, + "Position": "669, 84" + }, + { + "X": 204.0, + "Y": 655.0, + "Rotation": 0.38243447796178032, + "Id": "4b699847-36d4-471c-b990-4ad37967c2dc", + "Type": 3, + "Position": "204, 655" + }, + { + "X": 296.0, + "Y": 657.0, + "Rotation": -1.3380194104322385, + "Id": "a9f68317-f1c2-47d8-b029-348b5428be9f", + "Type": 3, + "Position": "296, 657" + }, + { + "X": 122.0, + "Y": 657.0, + "Rotation": 0.84311038333069632, + "Id": "fe227205-2a65-4ba9-bb4a-4efb4ed0a7b0", + "Type": 3, + "Position": "122, 657" + }, + { + "X": 40.0, + "Y": 657.0, + "Rotation": 1.659829660758831, + "Id": "5dd29191-798c-480c-b066-7947bfcc4fb7", + "Type": 3, + "Position": "40, 657" + } + ], + "Magnets": [ + { + "P1": { + "X": 52.0, + "Y": 466.0 + }, + "P2": { + "X": 183.0, + "Y": 465.0 + }, + "ControlPoint": null, + "Id": "92130fcc-1d99-4a1c-99d6-7d48072d9a3f", + "Type": 4 + }, + { + "P1": { + "X": 315.71428571428572, + "Y": 562.0 + }, + "P2": { + "X": 449.71428571428572, + "Y": 399.28571428571428 + }, + "ControlPoint": { + "X": 485.39960039960044, + "Y": 568.185814185814 + }, + "Id": "6d5f514a-c84d-42fd-951d-cc1836a02eb9", + "Type": 4 + }, + { + "P1": { + "X": 449.0, + "Y": 498.61904761904759 + }, + "P2": { + "X": 549.28571428571422, + "Y": 449.47619047619042 + }, + "ControlPoint": { + "X": 469.68531468531472, + "Y": 417.5191475191474 + }, + "Id": "fe5f3cc4-f995-4fa2-a55d-1bd77792c062", + "Type": 4 + }, + { + "P1": { + "X": 317.14285714285717, + "Y": 562.71428571428567 + }, + "P2": { + "X": -8.2704062755716912, + "Y": 556.75181625033554 + }, + "ControlPoint": null, + "Id": "5a0edec2-7ac3-4c99-bbb4-8debde0c1d07", + "Type": 4 + }, + { + "P1": { + "X": 38.242424242424313, + "Y": 675.8533133533133 + }, + "P2": { + "X": 40.39960039960053, + "Y": 561.04295704295691 + }, + "ControlPoint": null, + "Id": "def7c4b9-86db-42eb-aae6-0c6c9bedcc30", + "Type": 4 + }, + { + "P1": { + "X": 124.90909090909095, + "Y": 676.51998001998 + }, + "P2": { + "X": 125.39960039960052, + "Y": 559.66200466200439 + }, + "ControlPoint": null, + "Id": "624327ee-be0f-4373-b60a-786a93c1eabf", + "Type": 4 + }, + { + "P1": { + "X": 202.24242424242428, + "Y": 675.18664668664667 + }, + "P2": { + "X": 203.25674325674333, + "Y": 560.32867132867113 + }, + "ControlPoint": null, + "Id": "f1e885ae-55f7-42e9-b3aa-648541e97da0", + "Type": 4 + }, + { + "P1": { + "X": 296.90909090909088, + "Y": 675.18664668664667 + }, + "P2": { + "X": 295.39960039960044, + "Y": 562.47152847152836 + }, + "ControlPoint": null, + "Id": "dc3e8061-2c99-4f24-ac9b-4020dd91fa8b", + "Type": 4 + }, + { + "P1": { + "X": 549.28571428571422, + "Y": 450.14285714285711 + }, + "P2": { + "X": 719.0, + "Y": 558.0 + }, + "ControlPoint": null, + "Id": "e8242b99-ab7a-453f-b074-516cf7f8aa6b", + "Type": 4 + }, + { + "P1": { + "X": 349.85714285714283, + "Y": 399.71428571428567 + }, + "P2": { + "X": 449.71428571428572, + "Y": 399.28571428571428 + }, + "ControlPoint": { + "X": 400.39960039960044, + "Y": 349.61438561438558 + }, + "Id": "d9b18933-d211-4a46-9265-6d2543abc8f2", + "Type": 4 + }, + { + "P1": { + "X": 449.71428571428572, + "Y": 399.28571428571428 + }, + "P2": { + "X": 450.42857142857144, + "Y": 299.85714285714283 + }, + "ControlPoint": { + "X": 399.68531468531472, + "Y": 349.61438561438558 + }, + "Id": "4ec4e1cd-2b6b-4e22-a0e3-1e91f83c90a6", + "Type": 4 + }, + { + "P1": { + "X": 350.14285714285711, + "Y": 300.57142857142861 + }, + "P2": { + "X": 450.42857142857144, + "Y": 299.85714285714283 + }, + "ControlPoint": { + "X": 399.68531468531472, + "Y": 349.61438561438558 + }, + "Id": "c3de9569-3278-4b45-8c73-554e9a2241ad", + "Type": 4 + }, + { + "P1": { + "X": 349.85714285714283, + "Y": 399.71428571428567 + }, + "P2": { + "X": 350.14285714285711, + "Y": 300.57142857142861 + }, + "ControlPoint": { + "X": 398.971028971029, + "Y": 349.61438561438558 + }, + "Id": "c49475f5-5bae-49d1-aec9-8972b35f0624", + "Type": 4 + }, + { + "P1": { + "X": 450.0, + "Y": 300.0 + }, + "P2": { + "X": 480.0, + "Y": 183.0 + }, + "ControlPoint": { + "X": 476.0, + "Y": 235.0 + }, + "Id": "e47ba9a3-4678-4df6-bfbd-0f43cfd9dc40", + "Type": 4 + }, + { + "P1": { + "X": 480.0, + "Y": 183.0 + }, + "P2": { + "X": 700.25674325674322, + "Y": 90.471528471528487 + }, + "ControlPoint": { + "X": 493.88778460102014, + "Y": 76.249775551246131 + }, + "Id": "d183c4a4-7de8-421a-bed2-4caa2e4b6e27", + "Type": 4 + }, + { + "P1": { + "X": 183.0, + "Y": 465.0 + }, + "P2": { + "X": 349.85714285714283, + "Y": 399.71428571428567 + }, + "ControlPoint": { + "X": 265.42857142857144, + "Y": 459.45970695970692 + }, + "Id": "3e2ae683-f783-4001-a2a4-d56664ca89b8", + "Type": 4 + }, + { + "P1": { + "X": 350.14285714285711, + "Y": 300.57142857142861 + }, + "P2": { + "X": 41.113886113886245, + "Y": 266.75724275724275 + }, + "ControlPoint": { + "X": 223.25674325674333, + "Y": 201.75724275724275 + }, + "Id": "b2b3d68a-2fe0-4bcd-b6de-0463a2b604e0", + "Type": 4 + }, + { + "P1": { + "X": 450.0, + "Y": 300.0 + }, + "P2": { + "X": 716.71717407979054, + "Y": 226.11091587291276 + }, + "ControlPoint": { + "X": 550.92770039558, + "Y": 215.05828429396541 + }, + "Id": "9bf4a200-eeb2-4a42-851c-1e596ac16c06", + "Type": 4 + }, + { + "P1": { + "X": 480.0, + "Y": 183.0 + }, + "P2": { + "X": 526.52631578947364, + "Y": 253.52631578947367 + }, + "ControlPoint": { + "X": 469.87506881663262, + "Y": 260.32144218870224 + }, + "Id": "5d340580-bb09-42d9-81ed-43e69f8921c3", + "Type": 4 + }, + { + "P1": { + "X": 350.0, + "Y": 301.0 + }, + "P2": { + "X": 450.0, + "Y": 399.0 + }, + "ControlPoint": null, + "Id": "f659dce6-5b29-44d7-9e51-e148dab3b02e", + "Type": 4 + }, + { + "P1": { + "X": 350.0, + "Y": 400.0 + }, + "P2": { + "X": 450.0, + "Y": 300.0 + }, + "ControlPoint": null, + "Id": "10e46540-7a48-4f44-8b88-029c5a5115f1", + "Type": 4 + }, + { + "P1": { + "X": 450.0, + "Y": 399.0 + }, + "P2": { + "X": 549.0, + "Y": 450.0 + }, + "ControlPoint": null, + "Id": "0417e191-5169-46be-ad45-607abdeae8a6", + "Type": 4 } ], "Settings": { "BackgroundColorArgb": -14671840, "ShowGrid": false }, - "Marks": [ - { - "id": "2cb51787-c8cf-4ddb-97f0-b71f519d47dc", - "x": 684.7353629976581, - "y": 539.3747072599532, - "rotation": 119.877727797857 - }, - { - "id": "f704ebe0-1653-4559-b06f-1eaecafbefba", - "x": 40.428571428571516, - "y": 559.4597069597069, - "rotation": 90 - }, - { - "id": "d5b27365-79a2-4351-84c3-6767941ec0be", - "x": 126.14285714285721, - "y": 560.1739926739926, - "rotation": 90 - }, - { - "id": "0367cafb-9f85-4440-b6b4-c802a58e6181", - "x": 203.28571428571433, - "y": 558.7930402930402, - "rotation": 89.2872271068898 - }, - { - "id": "1f4ab2c9-07f8-4675-802d-9b4824b55198", - "x": 296.14285714285717, - "y": 560.8882783882782, - "rotation": 88.40516772072262 - }, - { - "id": "15fddfa4-ff74-48ff-b922-4aacdce1960b", - "x": 81.82817182817195, - "y": 256.042957042957, - "rotation": 74.50226651936697 - }, - { - "id": "962bb671-6932-477d-9209-2c2a076cfb22", - "x": 73.25674325674338, - "y": 466.0429570429569, - "rotation": 90 - }, - { - "id": "cd9f8434-f223-4532-9b3e-5b44c738abbb", - "x": 686.8281718281718, - "y": 196.04295704295703, - "rotation": 90 - }, - { - "id": "61c6d1dd-6a39-4931-a530-9b44d2010139", - "x": 669.6853146853147, - "y": 84.61438561438564, - "rotation": 90 - }, - { - "id": "4b699847-36d4-471c-b990-4ad37967c2dc", - "x": 204.24242424242428, - "y": 655.8533133533133, - "rotation": 0.3824344779617803 - }, - { - "id": "a9f68317-f1c2-47d8-b029-348b5428be9f", - "x": 296.9090909090909, - "y": 657.1866466866467, - "rotation": -1.3380194104322385 - }, - { - "id": "fe227205-2a65-4ba9-bb4a-4efb4ed0a7b0", - "x": 122.90909090909095, - "y": 657.1866466866467, - "rotation": 0.8431103833306963 - }, - { - "id": "5dd29191-798c-480c-b066-7947bfcc4fb7", - "x": 40.24242424242431, - "y": 657.1866466866467, - "rotation": 1.659829660758831 - } - ], - "Magnets": [ - { - "id": "92130fcc-1d99-4a1c-99d6-7d48072d9a3f", - "type": "STRAIGHT", - "p1": { - "x": 52, - "y": 466 - }, - "p2": { - "x": 183, - "y": 465 - } - }, - { - "id": "6d5f514a-c84d-42fd-951d-cc1836a02eb9", - "type": "CURVE", - "p1": { - "x": 315.7142857142857, - "y": 562 - }, - "p2": { - "x": 449.7142857142857, - "y": 399.2857142857143 - }, - "controlPoint": { - "x": 485.39960039960044, - "y": 568.185814185814 - } - }, - { - "id": "fe5f3cc4-f995-4fa2-a55d-1bd77792c062", - "type": "CURVE", - "p1": { - "x": 449, - "y": 498.6190476190476 - }, - "p2": { - "x": 549.2857142857142, - "y": 449.4761904761904 - }, - "controlPoint": { - "x": 469.6853146853147, - "y": 417.5191475191474 - } - }, - { - "id": "5a0edec2-7ac3-4c99-bbb4-8debde0c1d07", - "type": "STRAIGHT", - "p1": { - "x": 317.14285714285717, - "y": 562.7142857142857 - }, - "p2": { - "x": -8.270406275571691, - "y": 556.7518162503355 - } - }, - { - "id": "def7c4b9-86db-42eb-aae6-0c6c9bedcc30", - "type": "STRAIGHT", - "p1": { - "x": 38.24242424242431, - "y": 675.8533133533133 - }, - "p2": { - "x": 40.39960039960053, - "y": 561.0429570429569 - } - }, - { - "id": "624327ee-be0f-4373-b60a-786a93c1eabf", - "type": "STRAIGHT", - "p1": { - "x": 124.90909090909095, - "y": 676.51998001998 - }, - "p2": { - "x": 125.39960039960052, - "y": 559.6620046620044 - } - }, - { - "id": "f1e885ae-55f7-42e9-b3aa-648541e97da0", - "type": "STRAIGHT", - "p1": { - "x": 202.24242424242428, - "y": 675.1866466866467 - }, - "p2": { - "x": 203.25674325674333, - "y": 560.3286713286711 - } - }, - { - "id": "dc3e8061-2c99-4f24-ac9b-4020dd91fa8b", - "type": "STRAIGHT", - "p1": { - "x": 296.9090909090909, - "y": 675.1866466866467 - }, - "p2": { - "x": 295.39960039960044, - "y": 562.4715284715284 - } - }, - { - "id": "e8242b99-ab7a-453f-b074-516cf7f8aa6b", - "type": "STRAIGHT", - "p1": { - "x": 549.2857142857142, - "y": 450.1428571428571 - }, - "p2": { - "x": 719, - "y": 558 - } - }, - { - "id": "d9b18933-d211-4a46-9265-6d2543abc8f2", - "type": "CURVE", - "p1": { - "x": 349.85714285714283, - "y": 399.71428571428567 - }, - "p2": { - "x": 449.7142857142857, - "y": 399.2857142857143 - }, - "controlPoint": { - "x": 400.39960039960044, - "y": 349.6143856143856 - } - }, - { - "id": "4ec4e1cd-2b6b-4e22-a0e3-1e91f83c90a6", - "type": "CURVE", - "p1": { - "x": 449.7142857142857, - "y": 399.2857142857143 - }, - "p2": { - "x": 450.42857142857144, - "y": 299.85714285714283 - }, - "controlPoint": { - "x": 399.6853146853147, - "y": 349.6143856143856 - } - }, - { - "id": "c3de9569-3278-4b45-8c73-554e9a2241ad", - "type": "CURVE", - "p1": { - "x": 350.1428571428571, - "y": 300.5714285714286 - }, - "p2": { - "x": 450.42857142857144, - "y": 299.85714285714283 - }, - "controlPoint": { - "x": 399.6853146853147, - "y": 349.6143856143856 - } - }, - { - "id": "c49475f5-5bae-49d1-aec9-8972b35f0624", - "type": "CURVE", - "p1": { - "x": 349.85714285714283, - "y": 399.71428571428567 - }, - "p2": { - "x": 350.1428571428571, - "y": 300.5714285714286 - }, - "controlPoint": { - "x": 398.971028971029, - "y": 349.6143856143856 - } - }, - { - "id": "e47ba9a3-4678-4df6-bfbd-0f43cfd9dc40", - "type": "CURVE", - "p1": { - "x": 450.42857142857144, - "y": 299.85714285714283 - }, - "p2": { - "x": 499.85714285714283, - "y": 134.71428571428575 - }, - "controlPoint": { - "x": 443.2567432567433, - "y": 201.04295704295703 - } - }, - { - "id": "d183c4a4-7de8-421a-bed2-4caa2e4b6e27", - "type": "CURVE", - "p1": { - "x": 499.85714285714283, - "y": 134.71428571428575 - }, - "p2": { - "x": 703.2567432567432, - "y": 87.47152847152849 - }, - "controlPoint": { - "x": 560.4285714285713, - "y": 78.03113553113558 - } - }, - { - "id": "3e2ae683-f783-4001-a2a4-d56664ca89b8", - "type": "CURVE", - "p1": { - "x": 183, - "y": 465 - }, - "p2": { - "x": 349.85714285714283, - "y": 399.71428571428567 - }, - "controlPoint": { - "x": 265.42857142857144, - "y": 459.4597069597069 - } - }, - { - "id": "b2b3d68a-2fe0-4bcd-b6de-0463a2b604e0", - "type": "CURVE", - "p1": { - "x": 350.1428571428571, - "y": 300.5714285714286 - }, - "p2": { - "x": 41.113886113886245, - "y": 266.75724275724275 - }, - "controlPoint": { - "x": 223.25674325674333, - "y": 201.75724275724275 - } - }, - { - "id": "9bf4a200-eeb2-4a42-851c-1e596ac16c06", - "type": "CURVE", - "p1": { - "x": 450.42857142857144, - "y": 299.85714285714283 - }, - "p2": { - "x": 713.2567432567432, - "y": 198.18581418581417 - }, - "controlPoint": { - "x": 539.7142857142857, - "y": 185.1739926739927 - } - }, - { - "id": "5d340580-bb09-42d9-81ed-43e69f8921c3", - "type": "CURVE", - "p1": { - "x": 462.7142857142857, - "y": 194.85714285714286 - }, - "p2": { - "x": 507.7142857142858, - "y": 242.28571428571428 - }, - "controlPoint": { - "x": 448.2567432567433, - "y": 259.6143856143856 - } - }, - { - "id": "f659dce6-5b29-44d7-9e51-e148dab3b02e", - "type": "STRAIGHT", - "p1": { - "x": 350, - "y": 301 - }, - "p2": { - "x": 450, - "y": 399 - } - }, - { - "id": "10e46540-7a48-4f44-8b88-029c5a5115f1", - "type": "STRAIGHT", - "p1": { - "x": 350, - "y": 400 - }, - "p2": { - "x": 450, - "y": 300 - } - }, - { - "id": "0417e191-5169-46be-ad45-607abdeae8a6", - "type": "STRAIGHT", - "p1": { - "x": 450, - "y": 399 - }, - "p2": { - "x": 549, - "y": 450 - } - } - ], - "CreatedDate": "2025-12-12T16:29:33.968Z", - "Version": "1.1" + "CreatedDate": "2025-12-14T17:05:15.7222924+09:00", + "Version": "1.3" } \ No newline at end of file diff --git a/Cs_HMI/Data/NewMap_3.json b/Cs_HMI/Data/NewMap_3.json new file mode 100644 index 0000000..2603738 --- /dev/null +++ b/Cs_HMI/Data/NewMap_3.json @@ -0,0 +1,1135 @@ +{ + "Nodes": [ + { + "Id": "N001", + "Text": "Unloader", + "Position": "99, 251", + "Type": 0, + "StationType": 3, + "ConnectedNodes": [ + "N002" + ], + "RfidId": "0001", + "NodeTextForeColor": "White", + "NodeTextFontSize": 30, + "AliasName": "", + "SpeedLimit": 0, + "CanDocking": true, + "DockDirection": 0, + "CanTurnLeft": true, + "CanTurnRight": true, + "DisableCross": false, + "IsActive": true + }, + { + "Id": "N002", + "Text": "", + "Position": "249, 250", + "Type": 0, + "StationType": 0, + "ConnectedNodes": [ + "N001", + "N003" + ], + "RfidId": "0002", + "NodeTextForeColor": "", + "NodeTextFontSize": 7, + "AliasName": "", + "SpeedLimit": 0, + "CanDocking": false, + "DockDirection": 0, + "CanTurnLeft": true, + "CanTurnRight": true, + "DisableCross": false, + "IsActive": true + }, + { + "Id": "N003", + "Text": "", + "Position": "350, 301", + "Type": 0, + "StationType": 0, + "ConnectedNodes": [ + "N002", + "N011", + "N022", + "N031" + ], + "RfidId": "0003", + "NodeTextForeColor": "", + "NodeTextFontSize": 7, + "AliasName": "", + "SpeedLimit": 0, + "CanDocking": false, + "DockDirection": 0, + "CanTurnLeft": true, + "CanTurnRight": true, + "DisableCross": false, + "IsActive": true + }, + { + "Id": "N006", + "Text": "", + "Position": "527, 254", + "Type": 0, + "StationType": 0, + "ConnectedNodes": [ + "N007", + "N022", + "N023" + ], + "RfidId": "0013", + "NodeTextForeColor": "", + "NodeTextFontSize": 7, + "AliasName": "", + "SpeedLimit": 0, + "CanDocking": false, + "DockDirection": 0, + "CanTurnLeft": true, + "CanTurnRight": true, + "DisableCross": false, + "IsActive": true + }, + { + "Id": "N007", + "Text": "", + "Position": "609, 227", + "Type": 0, + "StationType": 0, + "ConnectedNodes": [ + "N006", + "N019" + ], + "RfidId": "0014", + "NodeTextForeColor": "", + "NodeTextFontSize": 7, + "AliasName": "", + "SpeedLimit": 0, + "CanDocking": false, + "DockDirection": 0, + "CanTurnLeft": true, + "CanTurnRight": true, + "DisableCross": false, + "IsActive": true + }, + { + "Id": "N008", + "Text": "", + "Position": "275, 441", + "Type": 0, + "StationType": 0, + "ConnectedNodes": [ + "N009", + "N031" + ], + "RfidId": "0009", + "NodeTextForeColor": "", + "NodeTextFontSize": 7, + "AliasName": "", + "SpeedLimit": 0, + "CanDocking": false, + "DockDirection": 0, + "CanTurnLeft": true, + "CanTurnRight": true, + "DisableCross": false, + "IsActive": true + }, + { + "Id": "N009", + "Text": "", + "Position": "184, 466", + "Type": 0, + "StationType": 0, + "ConnectedNodes": [ + "N008", + "N010" + ], + "RfidId": "0010", + "NodeTextForeColor": "", + "NodeTextFontSize": 7, + "AliasName": "", + "SpeedLimit": 0, + "CanDocking": false, + "DockDirection": 0, + "CanTurnLeft": true, + "CanTurnRight": true, + "DisableCross": false, + "IsActive": true + }, + { + "Id": "N010", + "Text": "Cleaner", + "Position": "92, 465", + "Type": 0, + "StationType": 2, + "ConnectedNodes": [ + "N009" + ], + "RfidId": "0011", + "NodeTextForeColor": "", + "NodeTextFontSize": 7, + "AliasName": "", + "SpeedLimit": 0, + "CanDocking": true, + "DockDirection": 1, + "CanTurnLeft": true, + "CanTurnRight": true, + "DisableCross": false, + "IsActive": true + }, + { + "Id": "N011", + "Text": "", + "Position": "450, 399", + "Type": 0, + "StationType": 0, + "ConnectedNodes": [ + "N003", + "N012", + "N015", + "N031", + "N022" + ], + "RfidId": "0005", + "NodeTextForeColor": "", + "NodeTextFontSize": 7, + "AliasName": "", + "SpeedLimit": 0, + "CanDocking": false, + "DockDirection": 0, + "CanTurnLeft": true, + "CanTurnRight": true, + "DisableCross": false, + "IsActive": true + }, + { + "Id": "N012", + "Text": "", + "Position": "549, 450", + "Type": 0, + "StationType": 0, + "ConnectedNodes": [ + "N011", + "N013", + "N015" + ], + "RfidId": "0006", + "NodeTextForeColor": "", + "NodeTextFontSize": 7, + "AliasName": "", + "SpeedLimit": 0, + "CanDocking": false, + "DockDirection": 0, + "CanTurnLeft": true, + "CanTurnRight": true, + "DisableCross": false, + "IsActive": true + }, + { + "Id": "N013", + "Text": "", + "Position": "616, 492", + "Type": 0, + "StationType": 0, + "ConnectedNodes": [ + "N012", + "N014" + ], + "RfidId": "0007", + "NodeTextForeColor": "", + "NodeTextFontSize": 7, + "AliasName": "", + "SpeedLimit": 0, + "CanDocking": false, + "DockDirection": 0, + "CanTurnLeft": true, + "CanTurnRight": true, + "DisableCross": false, + "IsActive": true + }, + { + "Id": "N014", + "Text": "Loader", + "Position": "670, 526", + "Type": 0, + "StationType": 1, + "ConnectedNodes": [ + "N013" + ], + "RfidId": "0008", + "NodeTextForeColor": "", + "NodeTextFontSize": 7, + "AliasName": "", + "SpeedLimit": 0, + "CanDocking": true, + "DockDirection": 0, + "CanTurnLeft": true, + "CanTurnRight": true, + "DisableCross": false, + "IsActive": true + }, + { + "Id": "N019", + "Text": "Chg #1", + "Position": "668, 223", + "Type": 0, + "StationType": 5, + "ConnectedNodes": [ + "N007" + ], + "RfidId": "0015", + "NodeTextForeColor": "", + "NodeTextFontSize": 7, + "AliasName": "", + "SpeedLimit": 0, + "CanDocking": true, + "DockDirection": 1, + "CanTurnLeft": true, + "CanTurnRight": true, + "DisableCross": false, + "IsActive": true + }, + { + "Id": "N022", + "Text": "", + "Position": "450, 300", + "Type": 0, + "StationType": 0, + "ConnectedNodes": [ + "N003", + "N006", + "N011", + "N023", + "N031" + ], + "RfidId": "0012", + "NodeTextForeColor": "", + "NodeTextFontSize": 7, + "AliasName": "", + "SpeedLimit": 0, + "CanDocking": false, + "DockDirection": 0, + "CanTurnLeft": true, + "CanTurnRight": true, + "DisableCross": false, + "IsActive": true + }, + { + "Id": "N023", + "Text": "", + "Position": "480, 183", + "Type": 0, + "StationType": 0, + "ConnectedNodes": [ + "N006", + "N022", + "N024" + ], + "RfidId": "0016", + "NodeTextForeColor": "", + "NodeTextFontSize": 7, + "AliasName": "", + "SpeedLimit": 0, + "CanDocking": false, + "DockDirection": 0, + "CanTurnLeft": true, + "CanTurnRight": true, + "DisableCross": false, + "IsActive": true + }, + { + "Id": "N024", + "Text": "", + "Position": "500, 135", + "Type": 0, + "StationType": 0, + "ConnectedNodes": [ + "N023", + "N025" + ], + "RfidId": "0017", + "NodeTextForeColor": "", + "NodeTextFontSize": 7, + "AliasName": "", + "SpeedLimit": 0, + "CanDocking": false, + "DockDirection": 0, + "CanTurnLeft": true, + "CanTurnRight": true, + "DisableCross": false, + "IsActive": true + }, + { + "Id": "N025", + "Text": "", + "Position": "573, 97", + "Type": 0, + "StationType": 0, + "ConnectedNodes": [ + "N024", + "N026" + ], + "RfidId": "0018", + "NodeTextForeColor": "", + "NodeTextFontSize": 7, + "AliasName": "", + "SpeedLimit": 0, + "CanDocking": false, + "DockDirection": 0, + "CanTurnLeft": true, + "CanTurnRight": true, + "DisableCross": false, + "IsActive": true + }, + { + "Id": "N026", + "Text": "Chg #2", + "Position": "649, 87", + "Type": 0, + "StationType": 5, + "ConnectedNodes": [ + "N025" + ], + "RfidId": "0019", + "NodeTextForeColor": "", + "NodeTextFontSize": 7, + "AliasName": "", + "SpeedLimit": 0, + "CanDocking": true, + "DockDirection": 1, + "CanTurnLeft": true, + "CanTurnRight": true, + "DisableCross": false, + "IsActive": true + }, + { + "Id": "N015", + "Text": "", + "Position": "449, 499", + "Type": 0, + "StationType": 0, + "ConnectedNodes": [ + "N011", + "N012", + "N016" + ], + "RfidId": "0037", + "NodeTextForeColor": "", + "NodeTextFontSize": 7, + "AliasName": "", + "SpeedLimit": 0, + "CanDocking": false, + "DockDirection": 0, + "CanTurnLeft": true, + "CanTurnRight": true, + "DisableCross": false, + "IsActive": true + }, + { + "Id": "N016", + "Text": "", + "Position": "422, 537", + "Type": 0, + "StationType": 0, + "ConnectedNodes": [ + "N015", + "N017" + ], + "RfidId": "0036", + "NodeTextForeColor": "", + "NodeTextFontSize": 7, + "AliasName": "", + "SpeedLimit": 0, + "CanDocking": false, + "DockDirection": 0, + "CanTurnLeft": true, + "CanTurnRight": true, + "DisableCross": false, + "IsActive": true + }, + { + "Id": "N017", + "Text": "", + "Position": "380, 557", + "Type": 0, + "StationType": 0, + "ConnectedNodes": [ + "N016", + "N018" + ], + "RfidId": "0035", + "NodeTextForeColor": "", + "NodeTextFontSize": 7, + "AliasName": "", + "SpeedLimit": 0, + "CanDocking": false, + "DockDirection": 0, + "CanTurnLeft": true, + "CanTurnRight": true, + "DisableCross": false, + "IsActive": true + }, + { + "Id": "N018", + "Text": "", + "Position": "329, 561", + "Type": 0, + "StationType": 0, + "ConnectedNodes": [ + "N017", + "N030", + "N005" + ], + "RfidId": "0034", + "NodeTextForeColor": "", + "NodeTextFontSize": 7, + "AliasName": "", + "SpeedLimit": 0, + "CanDocking": false, + "DockDirection": 0, + "CanTurnLeft": true, + "CanTurnRight": true, + "DisableCross": false, + "IsActive": true + }, + { + "Id": "N005", + "Text": "", + "Position": "233, 561", + "Type": 0, + "StationType": 0, + "ConnectedNodes": [ + "N018", + "N020", + "N029" + ], + "RfidId": "0033", + "NodeTextForeColor": "", + "NodeTextFontSize": 7, + "AliasName": "", + "SpeedLimit": 0, + "CanDocking": false, + "DockDirection": 0, + "CanTurnLeft": true, + "CanTurnRight": true, + "DisableCross": false, + "IsActive": true + }, + { + "Id": "N020", + "Text": "", + "Position": "155, 560", + "Type": 0, + "StationType": 0, + "ConnectedNodes": [ + "N005", + "N021", + "N028" + ], + "RfidId": "0032", + "NodeTextForeColor": "", + "NodeTextFontSize": 7, + "AliasName": "", + "SpeedLimit": 0, + "CanDocking": false, + "DockDirection": 0, + "CanTurnLeft": true, + "CanTurnRight": true, + "DisableCross": false, + "IsActive": true + }, + { + "Id": "N021", + "Text": "", + "Position": "68, 558", + "Type": 0, + "StationType": 0, + "ConnectedNodes": [ + "N020", + "N027" + ], + "RfidId": "0031", + "NodeTextForeColor": "", + "NodeTextFontSize": 7, + "AliasName": "", + "SpeedLimit": 0, + "CanDocking": false, + "DockDirection": 0, + "CanTurnLeft": true, + "CanTurnRight": true, + "DisableCross": false, + "IsActive": true + }, + { + "Id": "N027", + "Text": "Buf #1", + "Position": "38, 637", + "Type": 0, + "StationType": 4, + "ConnectedNodes": [ + "N021" + ], + "RfidId": "0041", + "NodeTextForeColor": "", + "NodeTextFontSize": 7, + "AliasName": "", + "SpeedLimit": 0, + "CanDocking": true, + "DockDirection": 0, + "CanTurnLeft": true, + "CanTurnRight": true, + "DisableCross": false, + "IsActive": true + }, + { + "Id": "N028", + "Text": "Buf #2", + "Position": "125, 639", + "Type": 0, + "StationType": 4, + "ConnectedNodes": [ + "N020" + ], + "RfidId": "0040", + "NodeTextForeColor": "", + "NodeTextFontSize": 7, + "AliasName": "", + "SpeedLimit": 0, + "CanDocking": true, + "DockDirection": 0, + "CanTurnLeft": true, + "CanTurnRight": true, + "DisableCross": false, + "IsActive": true + }, + { + "Id": "N029", + "Text": "Buf #3", + "Position": "203, 635", + "Type": 0, + "StationType": 4, + "ConnectedNodes": [ + "N005" + ], + "RfidId": "0039", + "NodeTextForeColor": "", + "NodeTextFontSize": 7, + "AliasName": "", + "SpeedLimit": 0, + "CanDocking": true, + "DockDirection": 0, + "CanTurnLeft": true, + "CanTurnRight": true, + "DisableCross": false, + "IsActive": true + }, + { + "Id": "N030", + "Text": "Buf #4", + "Position": "296, 638", + "Type": 0, + "StationType": 4, + "ConnectedNodes": [ + "N018" + ], + "RfidId": "0038", + "NodeTextForeColor": "", + "NodeTextFontSize": 7, + "AliasName": "", + "SpeedLimit": 0, + "CanDocking": true, + "DockDirection": 0, + "CanTurnLeft": true, + "CanTurnRight": true, + "DisableCross": false, + "IsActive": true + }, + { + "Id": "N031", + "Text": "", + "Position": "350, 400", + "Type": 0, + "StationType": 0, + "ConnectedNodes": [ + "N003", + "N008", + "N011", + "N022" + ], + "RfidId": "0030", + "NodeTextForeColor": "", + "NodeTextFontSize": 7, + "AliasName": "", + "SpeedLimit": 0, + "CanDocking": false, + "DockDirection": 0, + "CanTurnLeft": true, + "CanTurnRight": true, + "DisableCross": false, + "IsActive": true + } + ], + "Labels": [ + { + "Id": "LBL001", + "Type": 1, + "Text": "Amkor Technology Korea", + "Position": "180, 105", + "ForeColor": "White", + "BackColor": "MidnightBlue", + "FontFamily": "Arial", + "FontSize": 20, + "FontStyle": 0, + "Padding": 5 + } + ], + "Images": [ + { + "Id": "IMG001", + "Type": 2, + "Name": "Image", + "Position": "633, 310", + "ImagePath": "", + "ImageBase64": "", + "Scale": "1, 1", + "Opacity": 1, + "Rotation": 0 + } + ], + "Magnets": [ + { + "Id": "92130fcc-1d99-4a1c-99d6-7d48072d9a3f", + "Type": 4, + "P1": { + "X": 52, + "Y": 466 + }, + "P2": { + "X": 183, + "Y": 465 + }, + "ControlPoint": null + }, + { + "Id": "6d5f514a-c84d-42fd-951d-cc1836a02eb9", + "Type": 4, + "P1": { + "X": 315.7142857142857, + "Y": 562 + }, + "P2": { + "X": 449.7142857142857, + "Y": 399.2857142857143 + }, + "ControlPoint": { + "X": 485.39960039960044, + "Y": 568.185814185814 + } + }, + { + "Id": "fe5f3cc4-f995-4fa2-a55d-1bd77792c062", + "Type": 4, + "P1": { + "X": 449, + "Y": 498.6190476190476 + }, + "P2": { + "X": 549.2857142857142, + "Y": 449.4761904761904 + }, + "ControlPoint": { + "X": 469.6853146853147, + "Y": 417.5191475191474 + } + }, + { + "Id": "5a0edec2-7ac3-4c99-bbb4-8debde0c1d07", + "Type": 4, + "P1": { + "X": 317.14285714285717, + "Y": 562.7142857142857 + }, + "P2": { + "X": -8.270406275571691, + "Y": 556.7518162503355 + }, + "ControlPoint": null + }, + { + "Id": "def7c4b9-86db-42eb-aae6-0c6c9bedcc30", + "Type": 4, + "P1": { + "X": 38.24242424242431, + "Y": 675.8533133533133 + }, + "P2": { + "X": 40.39960039960053, + "Y": 561.0429570429569 + }, + "ControlPoint": null + }, + { + "Id": "624327ee-be0f-4373-b60a-786a93c1eabf", + "Type": 4, + "P1": { + "X": 124.90909090909095, + "Y": 676.51998001998 + }, + "P2": { + "X": 125.39960039960052, + "Y": 559.6620046620044 + }, + "ControlPoint": null + }, + { + "Id": "f1e885ae-55f7-42e9-b3aa-648541e97da0", + "Type": 4, + "P1": { + "X": 202.24242424242428, + "Y": 675.1866466866467 + }, + "P2": { + "X": 203.25674325674333, + "Y": 560.3286713286711 + }, + "ControlPoint": null + }, + { + "Id": "dc3e8061-2c99-4f24-ac9b-4020dd91fa8b", + "Type": 4, + "P1": { + "X": 296.9090909090909, + "Y": 675.1866466866467 + }, + "P2": { + "X": 295.39960039960044, + "Y": 562.4715284715284 + }, + "ControlPoint": null + }, + { + "Id": "e8242b99-ab7a-453f-b074-516cf7f8aa6b", + "Type": 4, + "P1": { + "X": 549.2857142857142, + "Y": 450.1428571428571 + }, + "P2": { + "X": 719, + "Y": 558 + }, + "ControlPoint": null + }, + { + "Id": "d9b18933-d211-4a46-9265-6d2543abc8f2", + "Type": 4, + "P1": { + "X": 349.85714285714283, + "Y": 399.71428571428567 + }, + "P2": { + "X": 449.7142857142857, + "Y": 399.2857142857143 + }, + "ControlPoint": { + "X": 400.39960039960044, + "Y": 349.6143856143856 + } + }, + { + "Id": "4ec4e1cd-2b6b-4e22-a0e3-1e91f83c90a6", + "Type": 4, + "P1": { + "X": 449.7142857142857, + "Y": 399.2857142857143 + }, + "P2": { + "X": 450.42857142857144, + "Y": 299.85714285714283 + }, + "ControlPoint": { + "X": 399.6853146853147, + "Y": 349.6143856143856 + } + }, + { + "Id": "c3de9569-3278-4b45-8c73-554e9a2241ad", + "Type": 4, + "P1": { + "X": 350.1428571428571, + "Y": 300.5714285714286 + }, + "P2": { + "X": 450.42857142857144, + "Y": 299.85714285714283 + }, + "ControlPoint": { + "X": 399.6853146853147, + "Y": 349.6143856143856 + } + }, + { + "Id": "c49475f5-5bae-49d1-aec9-8972b35f0624", + "Type": 4, + "P1": { + "X": 349.85714285714283, + "Y": 399.71428571428567 + }, + "P2": { + "X": 350.1428571428571, + "Y": 300.5714285714286 + }, + "ControlPoint": { + "X": 398.971028971029, + "Y": 349.6143856143856 + } + }, + { + "Id": "e47ba9a3-4678-4df6-bfbd-0f43cfd9dc40", + "Type": 4, + "P1": { + "X": 450, + "Y": 300 + }, + "P2": { + "X": 480, + "Y": 183 + }, + "ControlPoint": { + "X": 476, + "Y": 235 + } + }, + { + "Id": "d183c4a4-7de8-421a-bed2-4caa2e4b6e27", + "Type": 4, + "P1": { + "X": 480, + "Y": 183 + }, + "P2": { + "X": 700.2567432567432, + "Y": 90.47152847152849 + }, + "ControlPoint": { + "X": 493.88778460102014, + "Y": 76.24977555124613 + } + }, + { + "Id": "3e2ae683-f783-4001-a2a4-d56664ca89b8", + "Type": 4, + "P1": { + "X": 183, + "Y": 465 + }, + "P2": { + "X": 349.85714285714283, + "Y": 399.71428571428567 + }, + "ControlPoint": { + "X": 265.42857142857144, + "Y": 459.4597069597069 + } + }, + { + "Id": "b2b3d68a-2fe0-4bcd-b6de-0463a2b604e0", + "Type": 4, + "P1": { + "X": 350.1428571428571, + "Y": 300.5714285714286 + }, + "P2": { + "X": 41.113886113886245, + "Y": 266.75724275724275 + }, + "ControlPoint": { + "X": 223.25674325674333, + "Y": 201.75724275724275 + } + }, + { + "Id": "9bf4a200-eeb2-4a42-851c-1e596ac16c06", + "Type": 4, + "P1": { + "X": 450, + "Y": 300 + }, + "P2": { + "X": 716.7171740797905, + "Y": 226.11091587291276 + }, + "ControlPoint": { + "X": 550.92770039558, + "Y": 215.05828429396541 + } + }, + { + "Id": "5d340580-bb09-42d9-81ed-43e69f8921c3", + "Type": 4, + "P1": { + "X": 480, + "Y": 183 + }, + "P2": { + "X": 526.5263157894736, + "Y": 253.52631578947367 + }, + "ControlPoint": { + "X": 469.8750688166326, + "Y": 260.32144218870224 + } + }, + { + "Id": "f659dce6-5b29-44d7-9e51-e148dab3b02e", + "Type": 4, + "P1": { + "X": 350, + "Y": 301 + }, + "P2": { + "X": 450, + "Y": 399 + }, + "ControlPoint": null + }, + { + "Id": "10e46540-7a48-4f44-8b88-029c5a5115f1", + "Type": 4, + "P1": { + "X": 350, + "Y": 400 + }, + "P2": { + "X": 450, + "Y": 300 + }, + "ControlPoint": null + }, + { + "Id": "0417e191-5169-46be-ad45-607abdeae8a6", + "Type": 4, + "P1": { + "X": 450, + "Y": 399 + }, + "P2": { + "X": 549, + "Y": 450 + }, + "ControlPoint": null + } + ], + "Marks": [ + { + "Id": "2cb51787-c8cf-4ddb-97f0-b71f519d47dc", + "Type": 3, + "Position": "684, 539", + "X": 684, + "Y": 539, + "Rotation": 119.877727797857 + }, + { + "Id": "f704ebe0-1653-4559-b06f-1eaecafbefba", + "Type": 3, + "Position": "40, 559", + "X": 40, + "Y": 559, + "Rotation": 90 + }, + { + "Id": "d5b27365-79a2-4351-84c3-6767941ec0be", + "Type": 3, + "Position": "126, 560", + "X": 126, + "Y": 560, + "Rotation": 90 + }, + { + "Id": "0367cafb-9f85-4440-b6b4-c802a58e6181", + "Type": 3, + "Position": "203, 558", + "X": 203, + "Y": 558, + "Rotation": 89.2872271068898 + }, + { + "Id": "1f4ab2c9-07f8-4675-802d-9b4824b55198", + "Type": 3, + "Position": "296, 560", + "X": 296, + "Y": 560, + "Rotation": 88.40516772072262 + }, + { + "Id": "15fddfa4-ff74-48ff-b922-4aacdce1960b", + "Type": 3, + "Position": "81, 256", + "X": 81, + "Y": 256, + "Rotation": 74.50226651936697 + }, + { + "Id": "962bb671-6932-477d-9209-2c2a076cfb22", + "Type": 3, + "Position": "73, 466", + "X": 73, + "Y": 466, + "Rotation": 90 + }, + { + "Id": "cd9f8434-f223-4532-9b3e-5b44c738abbb", + "Type": 3, + "Position": "686, 196", + "X": 686, + "Y": 196, + "Rotation": 90 + }, + { + "Id": "61c6d1dd-6a39-4931-a530-9b44d2010139", + "Type": 3, + "Position": "669, 84", + "X": 669, + "Y": 84, + "Rotation": 90 + }, + { + "Id": "4b699847-36d4-471c-b990-4ad37967c2dc", + "Type": 3, + "Position": "204, 655", + "X": 204, + "Y": 655, + "Rotation": 0.3824344779617803 + }, + { + "Id": "a9f68317-f1c2-47d8-b029-348b5428be9f", + "Type": 3, + "Position": "296, 657", + "X": 296, + "Y": 657, + "Rotation": -1.3380194104322385 + }, + { + "Id": "fe227205-2a65-4ba9-bb4a-4efb4ed0a7b0", + "Type": 3, + "Position": "122, 657", + "X": 122, + "Y": 657, + "Rotation": 0.8431103833306963 + }, + { + "Id": "5dd29191-798c-480c-b066-7947bfcc4fb7", + "Type": 3, + "Position": "40, 657", + "X": 40, + "Y": 657, + "Rotation": 1.659829660758831 + } + ], + "Settings": { + "BackgroundColorArgb": -14671840, + "ShowGrid": false + }, + "CreatedDate": "2025-12-14T08:13:36.810Z", + "Version": "1.3" +} \ No newline at end of file diff --git a/Cs_HMI/Project/Device/_DeviceManagement.cs b/Cs_HMI/Project/Device/_DeviceManagement.cs index e389051..93af0a2 100644 --- a/Cs_HMI/Project/Device/_DeviceManagement.cs +++ b/Cs_HMI/Project/Device/_DeviceManagement.cs @@ -227,8 +227,10 @@ namespace Project /// /// 시리얼 포트 연결 (arDev.arRS232) /// - void ConnectSerialPort(arDev.arRS232 dev, string port, int baud, eVarTime conn, eVarTime conntry, eVarTime recvtime) + bool ConnectSerialPort(arDev.arRS232 dev, string port, int baud, eVarTime conn, eVarTime conntry, eVarTime recvtime) { + if(port.isEmpty()) return false; + if (dev.IsOpen == false && port.isEmpty() == false) { var tsPLC = VAR.TIME.RUN(conntry); @@ -246,8 +248,17 @@ namespace Project } else { - var errmessage = dev.errorMessage; - PUB.log.AddE($"[AGV:{port}:{baud}] {errmessage}"); + //존재하지 않는 포트라면 sync를 벗어난다 + var ports = System.IO.Ports.SerialPort.GetPortNames().Select(t => t.ToLower()).ToList(); + if (ports.Contains(PUB.setting.Port_AGV.ToLower()) == false) + { + return false; + } + else + { + var errmessage = dev.errorMessage; + PUB.log.AddE($"[AGV:{port}:{baud}] {errmessage}"); + } } VAR.TIME.Update(conn); VAR.TIME.Update(conntry); @@ -264,6 +275,7 @@ namespace Project dev.Close(); VAR.TIME.Update(conntry); } + return true; } /// diff --git a/Cs_HMI/Project/PUB.cs b/Cs_HMI/Project/PUB.cs index 5503e4a..5ab0f34 100644 --- a/Cs_HMI/Project/PUB.cs +++ b/Cs_HMI/Project/PUB.cs @@ -614,7 +614,7 @@ namespace Project { if (_mapNodes == null || _mapNodes.Any() == false) return null; if (nodeidx.isEmpty()) return null; - return _mapNodes.Where(t => t.NodeId.Equals(nodeidx)).FirstOrDefault(); + return _mapNodes.Where(t => t.Id.Equals(nodeidx)).FirstOrDefault(); } public static MapNode FindByRFID(string rfidValue) { @@ -626,7 +626,7 @@ namespace Project { if (_mapNodes == null || _mapNodes.Any() == false) return null; if (alias.isEmpty()) return null; - var lst = _mapNodes.Where(t => t.NodeAlias.Equals(alias)); + var lst = _mapNodes.Where(t => t.AliasName.Equals(alias)); if (lst.Any() == false) return null; return lst.ToList(); } @@ -654,7 +654,7 @@ namespace Project _virtualAGV.SetPosition(node, motorDirection); RefreshAGVCanvas(); - log.Add($"[AGV] RFID {rfidId} 감지 → 노드 {node.NodeId} 위치 업데이트 (방향: {motorDirection})"); + log.Add($"[AGV] RFID {rfidId} 감지 → 노드 {node.Id} 위치 업데이트 (방향: {motorDirection})"); return true; } @@ -672,7 +672,7 @@ namespace Project { if (_virtualAGV == null || _mapNodes == null) return false; - var node = _mapNodes.FirstOrDefault(n => n.NodeId == nodeId); + var node = _mapNodes.FirstOrDefault(n => n.Id == nodeId); if (node != null) { _virtualAGV.SetPosition(node, motorDirection); @@ -744,42 +744,7 @@ namespace Project } } - /// - /// 현재 AGV의 노드 ID 가져오기 - /// - /// 현재 노드 ID - public static string GetCurrentAGVNodeId() - { - return _virtualAGV?.CurrentNodeId ?? string.Empty; - } - - /// - /// 현재 AGV 위치 가져오기 - /// - /// 현재 위치 - public static Point GetCurrentAGVPosition() - { - return _virtualAGV?.CurrentPosition ?? Point.Empty; - } - - /// - /// 현재 AGV 방향 가져오기 - /// - /// 현재 방향 - public static AgvDirection GetCurrentAGVDirection() - { - return _virtualAGV?.CurrentDirection ?? AgvDirection.Forward; - } - - /// - /// 현재 AGV 상태 가져오기 - /// - /// 현재 상태 - public static AGVState GetCurrentAGVState() - { - return _virtualAGV?.CurrentState ?? AGVState.Idle; - } - + #endregion } diff --git a/Cs_HMI/Project/StateMachine/Step/_SM_RUN.cs b/Cs_HMI/Project/StateMachine/Step/_SM_RUN.cs index e46069b..a9c7625 100644 --- a/Cs_HMI/Project/StateMachine/Step/_SM_RUN.cs +++ b/Cs_HMI/Project/StateMachine/Step/_SM_RUN.cs @@ -108,49 +108,30 @@ namespace Project //목적지가 BUFFER라면 버퍼투입대기위치까지 완료했다는 시그널을 보낸다. var target = PUB._virtualAGV.TargetNode; PUB.log.Add($"목적지({target.RfidId}) 도착완료 타입:{target.Type}, 출발지:{PUB._virtualAGV.StartNode.RfidId}"); - if (target.Type == AGVNavigationCore.Models.NodeType.Buffer) + + switch(target.StationType) { - - //현재위치가 마지막경로의 NODEID와 일치해야한다 - var lastPath = PUB._virtualAGV.CurrentPath.DetailedPath.LastOrDefault(); - if (lastPath.NodeId.Equals(PUB._virtualAGV.CurrentNodeId)) - { - //버퍼진입전 노드에 도착완료했따 - PUB.XBE.BufferInReady = true; - PUB.XBE.BufferReadyError = false; - } - else - { - //마지막위치가 아닌 다른 위치에 있으니 버퍼 작업을 할 수없다 - PUB.log.AddAT("목적지 버퍼이동완료 했지만 마지막 노드가 아닙니다"); - PUB.XBE.BufferInReady = false; - PUB.XBE.BufferReadyError = true; - } - PUB.XBE.BufferInComplete = false; - PUB.XBE.BufferOutComplete = false; - } - - else if (target.Type == AGVNavigationCore.Models.NodeType.Charging) - { - - } - else if (target.Type == AGVNavigationCore.Models.NodeType.Loader) - { - - } - else if (target.Type == AGVNavigationCore.Models.NodeType.Clearner) - { - - } - else if (target.Type == AGVNavigationCore.Models.NodeType.UnLoader) - { - - } - else - { - //목적지다 다른 형태이다 - + case AGVNavigationCore.Models.StationType.Buffer: + //현재위치가 마지막경로의 NODEID와 일치해야한다 + var lastPath = PUB._virtualAGV.CurrentPath.DetailedPath.LastOrDefault(); + if (lastPath.NodeId.Equals(PUB._virtualAGV.CurrentNode.Id)) + { + //버퍼진입전 노드에 도착완료했따 + PUB.XBE.BufferInReady = true; + PUB.XBE.BufferReadyError = false; + } + else + { + //마지막위치가 아닌 다른 위치에 있으니 버퍼 작업을 할 수없다 + PUB.log.AddAT("목적지 버퍼이동완료 했지만 마지막 노드가 아닙니다"); + PUB.XBE.BufferInReady = false; + PUB.XBE.BufferReadyError = true; + } + PUB.XBE.BufferInComplete = false; + PUB.XBE.BufferOutComplete = false; + break; } + PUB._virtualAGV.Turn = AGVNavigationCore.Models.AGVTurn.None; PUB.sm.SetNewRunStep(ERunStep.READY); } diff --git a/Cs_HMI/Project/StateMachine/Step/_SM_RUN_POSCHK.cs b/Cs_HMI/Project/StateMachine/Step/_SM_RUN_POSCHK.cs index 3fb4cd6..62fc7d7 100644 --- a/Cs_HMI/Project/StateMachine/Step/_SM_RUN_POSCHK.cs +++ b/Cs_HMI/Project/StateMachine/Step/_SM_RUN_POSCHK.cs @@ -13,7 +13,7 @@ namespace Project public Boolean _SM_RUN_POSCHK(bool isFirst, TimeSpan stepTime) { //현재위치가 설정되어있는지 확인한다, 현재위치값이 있는 경우 True 를 반환 - var currentnode = PUB.FindByNodeID(PUB._virtualAGV.CurrentNodeId); + var currentnode = PUB.FindByNodeID(PUB._virtualAGV.CurrentNode.Id); if (currentnode != null) return true; //이동을 하지 않고있다면 전진을 진행한다 diff --git a/Cs_HMI/Project/StateMachine/Step/_SM_RUN_SYNC.cs b/Cs_HMI/Project/StateMachine/Step/_SM_RUN_SYNC.cs index c42d5d3..d28fb0d 100644 --- a/Cs_HMI/Project/StateMachine/Step/_SM_RUN_SYNC.cs +++ b/Cs_HMI/Project/StateMachine/Step/_SM_RUN_SYNC.cs @@ -24,8 +24,14 @@ namespace Project if (PUB.AGV.IsOpen == false) { //agv connect - ConnectSerialPort(PUB.AGV, PUB.setting.Port_AGV, PUB.setting.Baud_AGV, + var rlt = ConnectSerialPort(PUB.AGV, PUB.setting.Port_AGV, PUB.setting.Baud_AGV, eVarTime.LastConn_AGV, eVarTime.LastConnTry_AGV, eVarTime.LastRecv_AGV); + if (rlt == false) + { + //존재하지 않는 포트라면 sync를 벗어난다 + PUB.log.AddE($"AGV포트({PUB.setting.Port_AGV}) 가 존재하지않아 SYNC를 중단합니다"); + PUB.sm.SetNewStep(eSMStep.IDLE); + } } else if (PUB.AGV.IsValid == true) { @@ -81,7 +87,7 @@ namespace Project synlist.Add("SGS", PUB.setting.GDSValue.ToString("0000")); VAR.I32[eVarInt32.SyncItemCount] = synlist.Count; - + PUB.AddEEDB($"SYNC시작({PUB.Result.TargetPos})"); @@ -129,7 +135,7 @@ namespace Project if (PUB.AGV.ACKData.Equals(item.Key)) { synidx += 1; - if(ts.TotalSeconds < 0.15) PUB.sm.UpdateRunStepSeq(-2); //싱크중에 추가 지연시간 확보 + if (ts.TotalSeconds < 0.15) PUB.sm.UpdateRunStepSeq(-2); //싱크중에 추가 지연시간 확보 else PUB.sm.UpdateRunStepSeq(-1); LastCommandTime = DateTime.Now; } @@ -150,11 +156,11 @@ namespace Project { PUB.AddEEDB($"SYNC완료({PUB.Result.TargetPos})"); UpdateProgressStatus(stepTime.TotalSeconds, 5, "SYNC : 완료"); - + // 동기화 완료 시 캔버스 모드 복귀 if (PUB._mapCanvas != null) PUB._mapCanvas.SetSyncStatus("동기화 완료!", 1.0f, "잠시 후 메인 화면으로 이동합니다."); - + LastCommandTime = DateTime.Now; PUB.sm.UpdateRunStepSeq(); return false; diff --git a/Cs_HMI/Project/StateMachine/Step/_Util.cs b/Cs_HMI/Project/StateMachine/Step/_Util.cs index bb81fd5..a52993e 100644 --- a/Cs_HMI/Project/StateMachine/Step/_Util.cs +++ b/Cs_HMI/Project/StateMachine/Step/_Util.cs @@ -71,7 +71,7 @@ namespace Project if (_SM_RUN_POSCHK(false, new TimeSpan()) == false) return false; //현재위치노드 오류 - var currentNode = PUB.FindByNodeID(PUB._virtualAGV.CurrentNodeId); + var currentNode = PUB.FindByNodeID(PUB._virtualAGV.CurrentNode.Id); if (currentNode == null) { PUB.log.AddE($"현재위치노드가 없습니다"); @@ -81,7 +81,7 @@ namespace Project //시작노드값이 없다면 현재위치를 노드로 결정한다 if (PUB._virtualAGV.StartNode == null) - PUB._virtualAGV.StartNode = PUB.FindByNodeID(PUB._virtualAGV.CurrentNodeId); + PUB._virtualAGV.StartNode = PUB.FindByNodeID(PUB._virtualAGV.CurrentNode.Id); //시작노드가없다면 오류 if (PUB._virtualAGV.StartNode == null) @@ -102,7 +102,7 @@ namespace Project //경로 생성(경로정보가 없거나 현재노드가 경로에 없는경우) if (PUB._virtualAGV.CurrentPath == null || PUB._virtualAGV.CurrentPath.DetailedPath.Any() == false || - PUB._virtualAGV.CurrentPath.DetailedPath.Where(t => t.NodeId.Equals(currentNode.NodeId)).Any() == false) + PUB._virtualAGV.CurrentPath.DetailedPath.Where(t => t.NodeId.Equals(currentNode.Id)).Any() == false) { if (PUB.AGV.system1.agv_run) { @@ -145,7 +145,7 @@ namespace Project $"현재 상태: {PUB._virtualAGV.CurrentState}\n" + $"현재 방향: {PUB._virtualAGV.CurrentDirection}\n" + $"위치 확정: {PUB._virtualAGV.IsPositionConfirmed} (RFID {PUB._virtualAGV.DetectedRfidCount}개)\n" + - $"현재 노드: {PUB._virtualAGV.CurrentNodeId ?? "없음"}"; + $"현재 노드: {PUB._virtualAGV.CurrentNode.Id ?? "없음"}"; //모터에서 정지를 요청했다 if (nextAction.Motor == AGVNavigationCore.Models.MotorCommand.Stop) @@ -169,11 +169,11 @@ namespace Project // 현재 노드가 타겟 노드와 같고, 위치가 확정된 상태라면 도착으로 간주 // 단, AGV가 실제로 멈췄는지 확인 (agv_run == false) if (PUB._virtualAGV.IsPositionConfirmed && - PUB._virtualAGV.CurrentNodeId == PUB._virtualAGV.TargetNode.NodeId) + PUB._virtualAGV.CurrentNode.Id == PUB._virtualAGV.TargetNode.Id) { if (PUB.AGV.system1.agv_run == false) { - PUB.log.AddI($"목표 도착 및 정지 확인됨(MarkStop 완료). Node:{PUB._virtualAGV.CurrentNodeId}"); + PUB.log.AddI($"목표 도착 및 정지 확인됨(MarkStop 완료). Node:{PUB._virtualAGV.CurrentNode.Id}"); return true; } } @@ -251,7 +251,7 @@ namespace Project else { //현재위치가 충전위치이고, 움직이지 않았다면 완료된 경우라 할수 있따 - if (PUB._virtualAGV.CurrentNodeId.Equals(PUB.setting.NodeMAP_RFID_Charger) && + if (PUB._virtualAGV.CurrentNode.Id.Equals(PUB.setting.NodeMAP_RFID_Charger) && VAR.BOOL[eVarBool.MARK_SENSOR] == true) { PUB.log.AddI("충전위치 검색 완료"); diff --git a/Cs_HMI/Project/StateMachine/_AGV.cs b/Cs_HMI/Project/StateMachine/_AGV.cs index 8700665..10d99d8 100644 --- a/Cs_HMI/Project/StateMachine/_AGV.cs +++ b/Cs_HMI/Project/StateMachine/_AGV.cs @@ -175,13 +175,10 @@ namespace Project var newNodeId = $"AUTO_{PUB.Result.LastTAG}"; var newNode = new MapNode { - NodeId = newNodeId, + Id = newNodeId, RfidId = PUB.Result.LastTAG, - Name = $"자동추가_{PUB.Result.LastTAG}", - Type = NodeType.Normal, Position = new Point(100, 100), // 기본 위치 IsActive = true, - DisplayColor = Color.Orange, // 자동 추가된 노드는 오렌지색으로 표시 CreatedDate = DateTime.Now, ModifiedDate = DateTime.Now }; @@ -232,7 +229,7 @@ namespace Project $"현재 상태: {PUB._virtualAGV.CurrentState}\n" + $"현재 방향: {PUB._virtualAGV.CurrentDirection}\n" + $"위치 확정: {PUB._virtualAGV.IsPositionConfirmed} (RFID {PUB._virtualAGV.DetectedRfidCount}개)\n" + - $"현재 노드: {PUB._virtualAGV.CurrentNodeId ?? "없음"}"; + $"현재 노드: {PUB._virtualAGV.CurrentNode.Id ?? "없음"}"; PUB._mapCanvas.PredictMessage = message; } diff --git a/Cs_HMI/Project/StateMachine/_Xbee.cs b/Cs_HMI/Project/StateMachine/_Xbee.cs index 15ee4c1..e483d6d 100644 --- a/Cs_HMI/Project/StateMachine/_Xbee.cs +++ b/Cs_HMI/Project/StateMachine/_Xbee.cs @@ -8,6 +8,7 @@ using AGVNavigationCore.Utils; using AR; using arDev; using COMM; +using Project.StateMachine; namespace Project { @@ -56,7 +57,7 @@ namespace Project } else { - PUB.log.AddI($"XBEE:현재위치설정:[{node.RfidId}]{node.NodeId}"); + PUB.log.AddI($"XBEE:현재위치설정:[{node.RfidId}]{node.Id}"); } PUB._mapCanvas.SetAGVPosition(PUB.setting.MCID, node, PUB._virtualAGV.CurrentDirection); @@ -69,25 +70,25 @@ namespace Project case ENIGProtocol.AGVCommandHE.PickOff: // 111 { PUB.log.AddI($"XBEE:작업명령수신:{cmd}"); - + // 현재 위치 확인 (TargetNode가 아닌 CurrentNode 기준) var currNode = PUB._virtualAGV.CurrentNode; if (currNode == null) { - PUB.log.AddE($"[{logPrefix}-{cmd}] 현재 노드를 알 수 없습니다 NodeID:{PUB._virtualAGV.CurrentNodeId}"); + PUB.log.AddE($"[{logPrefix}-{cmd}] 현재 노드를 알 수 없습니다 NodeID:{PUB._virtualAGV.CurrentNode.Id}"); PUB.XBE.SendError(ENIGProtocol.AGVErrorCode.EmptyNode, "Unknown Node"); return; } PUB.NextWorkCmd = cmd; - ERunStep nextStep = ERunStep.IDLE; + ERunStep nextStep = ERunStep.READY; - switch (currNode.Type) + switch (currNode.StationType) { - case NodeType.Loader: nextStep = ERunStep.LOADER_IN; break; - case NodeType.UnLoader: nextStep = ERunStep.UNLOADER_IN; break; - case NodeType.Buffer: nextStep = ERunStep.BUFFER_IN; break; - case NodeType.Clearner: nextStep = ERunStep.CLEANER_IN; break; + case StationType.Loader: nextStep = ERunStep.LOADER_IN; break; + case StationType.UnLoader: nextStep = ERunStep.UNLOADER_IN; break; + case StationType.Buffer: nextStep = ERunStep.BUFFER_IN; break; + case StationType.Clearner: nextStep = ERunStep.CLEANER_IN; break; default: PUB.log.AddE($"[{logPrefix}-{cmd}] 해당 노드타입({currNode.Type})은 작업을 지원하지 않습니다."); return; @@ -130,11 +131,11 @@ namespace Project } ///출발지 - var startNode = PUB._mapNodes.FirstOrDefault(t => t.RfidId == PUB._virtualAGV.CurrentNodeId); + var startNode = PUB._mapNodes.FirstOrDefault(t => t.RfidId == PUB._virtualAGV.CurrentNode.Id); PUB._virtualAGV.StartNode = startNode; if (startNode == null) { - PUB.log.AddE($"[{logPrefix}-Goto] 시작노드가 없습니다(현재위치 없음) NodeID:{PUB._virtualAGV.CurrentNodeId}"); + PUB.log.AddE($"[{logPrefix}-Goto] 시작노드가 없습니다(현재위치 없음) NodeID:{PUB._virtualAGV.CurrentNode.Id}"); } if (startNode != null) diff --git a/Cs_HMI/Project/ViewForm/fAuto.cs b/Cs_HMI/Project/ViewForm/fAuto.cs index 8021e81..a349220 100644 --- a/Cs_HMI/Project/ViewForm/fAuto.cs +++ b/Cs_HMI/Project/ViewForm/fAuto.cs @@ -51,7 +51,7 @@ namespace Project.ViewForm //PUB._mapCanvas.NodeAdded += OnNodeAdded; // 이벤트 연결 //PUB._mapCanvas.NodeAdded += OnNodeAdded; - PUB._mapCanvas.NodesSelected += OnNodeSelected; + PUB._mapCanvas.NodeSelect += OnNodeSelected; //PUB._mapCanvas.NodeMoved += OnNodeMoved; //PUB._mapCanvas.NodeDeleted += OnNodeDeleted; //PUB._mapCanvas.ConnectionDeleted += OnConnectionDeleted; @@ -62,40 +62,40 @@ namespace Project.ViewForm panel1.Controls.Add(PUB._mapCanvas); } - private void OnNodeSelected(object sender, List nodes, MouseEventArgs e) + + private void OnNodeSelected(object sender, NodeBase node, MouseEventArgs e) { if (e.Button != MouseButtons.Right) return; - var node = nodes.FirstOrDefault(); - if (nodes == null) return; + if (node == null) return; + if ((node is MapNode mapnode) == false) return; // 도킹 가능한 노드인지 또는 작업 노드인지 확인 - bool isDockingNode = node.Type == NodeType.Loader || node.Type == NodeType.UnLoader - || node.Type == NodeType.Buffer || node.Type == NodeType.Clearner - || node.Type == NodeType.Charging; - - if (!isDockingNode) return; + if (mapnode.isDockingNode == false) return; ContextMenuStrip menu = new ContextMenuStrip(); // PickOn var pickOn = new ToolStripMenuItem("Pick On (Move & Pick)"); - pickOn.Click += (s, args) => ExecuteManualCommand(node, ENIGProtocol.AGVCommandHE.PickOn); + pickOn.Click += (s, args) => ExecuteManualCommand(mapnode, ENIGProtocol.AGVCommandHE.PickOn); menu.Items.Add(pickOn); // PickOff var pickOff = new ToolStripMenuItem("Pick Off (Move & Drop)"); - pickOff.Click += (s, args) => ExecuteManualCommand(node, ENIGProtocol.AGVCommandHE.PickOff); + pickOff.Click += (s, args) => ExecuteManualCommand(mapnode, ENIGProtocol.AGVCommandHE.PickOff); menu.Items.Add(pickOff); // Charge - if (node.Type == NodeType.Charging) + if (mapnode.StationType == StationType.Charger) { var charge = new ToolStripMenuItem("Charge (Move & Charge)"); - charge.Click += (s, args) => ExecuteManualCommand(node, ENIGProtocol.AGVCommandHE.Charger); + charge.Click += (s, args) => ExecuteManualCommand(mapnode, ENIGProtocol.AGVCommandHE.Charger); menu.Items.Add(charge); } menu.Show(Cursor.Position); + + + } private void ExecuteManualCommand(MapNode targetNode, ENIGProtocol.AGVCommandHE cmd) @@ -105,13 +105,19 @@ namespace Project.ViewForm MessageBox.Show("AGV의 현재 위치를 알 수 없습니다."); return; } - if (PUB.sm.RunStep != eSMStep.IDLE && PUB.sm.RunStep != eSMStep.READY) + if (PUB.sm.Step == eSMStep.IDLE) { if (MessageBox.Show("현재 대기상태가 아닙니다. 강제로 실행하시겠습니까?", "Warning", MessageBoxButtons.YesNo) == DialogResult.No) return; } + if (targetNode.isDockingNode == false) + { + UTIL.MsgE("이동 가능한 노드가 아닙니다"); + } + // 1. 경로 생성 var pathFinder = new AGVNavigationCore.PathFinding.Planning.AGVPathfinder(PUB._mapNodes); + // 현재위치에서 목표위치까지 var result = pathFinder.FindPath(PUB._virtualAGV.CurrentNode, targetNode); @@ -122,29 +128,22 @@ namespace Project.ViewForm } // 2. 상태 설정 - PUB.log.AddI($"[Manual Command] {cmd} to {targetNode.Name}({targetNode.NodeId})"); - // Path 변환 (Node 리스트 -> AGVPathResult) - // VirtualAGV.SetPath가 필요할 수 있음. 혹은 Goto 로직을 수동으로 구성. - // _SM_RUN_GOTO에서는 PUB._virtualAGV.CurrentPath를 사용함. - // AGVPathResult 생성 필요. + // 2. 상태 설정 + if (targetNode is MapNode mapno) + PUB.log.AddI($"[Manual Command] {cmd} to {mapno.RfidId}({targetNode.Id})"); + else + PUB.log.AddI($"[Manual Command] {cmd} to ({targetNode.Id})"); - var detailedPath = AGVNavigationCore.PathFinding.Planning.AGVPathfinder.MakeDetailData(result.Path, PUB._mapNodes); - PUB._virtualAGV.SetPath(result.Path, detailedPath); - PUB._virtualAGV.TargetNode = targetNode; + // FindPathResult contains DetailedPath already. + PUB._virtualAGV.SetPath(result); + PUB._virtualAGV.TargetNode = targetNode as MapNode; // 3. 작업 설정 PUB.NextWorkCmd = cmd; // 4. 실행 PUB.sm.SetNewRunStep(ERunStep.GOTO); // GOTO -> Arrive -> _IN sequence execution - } - - // 툴바 버튼 이벤트 연결 - //WireToolbarButtonEvents(); - - - } @@ -208,7 +207,7 @@ namespace Project.ViewForm var agvList = new System.Collections.Generic.List { PUB._virtualAGV }; PUB._mapCanvas.AGVList = agvList; - PUB.log.Add($"가상 AGV 생성: {startNode.NodeId} 위치"); + PUB.log.Add($"가상 AGV 생성: {startNode.Id} 위치"); } } else if (PUB._virtualAGV != null) diff --git a/Cs_HMI/Project/ViewForm/fManual.cs b/Cs_HMI/Project/ViewForm/fManual.cs index a75756c..312f7f2 100644 --- a/Cs_HMI/Project/ViewForm/fManual.cs +++ b/Cs_HMI/Project/ViewForm/fManual.cs @@ -88,7 +88,7 @@ namespace Project.ViewForm // [Manual Safety] Clear previous auto-task state PUB._virtualAGV.TargetNode = null; - PUB._virtualAGV.CurrentPath = null; + PUB._virtualAGV.StopPath(); PUB.NextWorkCmd = ENIGProtocol.AGVCommandHE.Stop; // Clear ACS Command PUB.sm.ClearRunStep(); // Clear RunStep sequence } @@ -117,7 +117,7 @@ namespace Project.ViewForm // [Manual Safety] Clear previous auto-task state PUB._virtualAGV.TargetNode = null; - PUB._virtualAGV.CurrentPath = null; + PUB._virtualAGV.StopPath(); PUB.NextWorkCmd = ENIGProtocol.AGVCommandHE.Stop; PUB.sm.ClearRunStep(); } @@ -146,7 +146,7 @@ namespace Project.ViewForm // [Manual Safety] Clear previous auto-task state PUB._virtualAGV.TargetNode = null; - PUB._virtualAGV.CurrentPath = null; + PUB._virtualAGV.StopPath(); PUB.NextWorkCmd = ENIGProtocol.AGVCommandHE.Stop; PUB.sm.ClearRunStep(); } @@ -175,7 +175,7 @@ namespace Project.ViewForm // [Manual Safety] Clear previous auto-task state PUB._virtualAGV.TargetNode = null; - PUB._virtualAGV.CurrentPath = null; + PUB._virtualAGV.StopPath(); PUB.NextWorkCmd = ENIGProtocol.AGVCommandHE.Stop; PUB.sm.ClearRunStep(); } diff --git a/Cs_HMI/Project/fMain.cs b/Cs_HMI/Project/fMain.cs index 21767e3..6a3a4d2 100644 --- a/Cs_HMI/Project/fMain.cs +++ b/Cs_HMI/Project/fMain.cs @@ -806,6 +806,10 @@ namespace Project // 맵 캔버스에 데이터 설정 _mapCanvas.Nodes = result.Nodes; + _mapCanvas.Labels = result.Labels; + _mapCanvas.Images = result.Images; + _mapCanvas.Marks = result.Marks; + _mapCanvas.Magnets = result.Magnets; // RfidMappings 제거됨 - MapNode에 통합 // 🔥 맵 설정 적용 (배경색, 그리드 표시) @@ -865,7 +869,7 @@ namespace Project ShowGrid = _mapCanvas.ShowGrid }; - if (MapLoader.SaveMapToFile(filePath, _mapNodes, settings)) + if (MapLoader.SaveMapToFile(filePath, _mapNodes, _mapCanvas.Labels, _mapCanvas.Images, _mapCanvas.Marks, _mapCanvas.Magnets, settings)) { // 설정에 마지막 맵 파일 경로 저장 PUB.setting.LastMapFile = filePath; diff --git a/Cs_HMI/TestProject/mcpServers b/Cs_HMI/TestProject/mcpServers new file mode 160000 index 0000000..792c474 --- /dev/null +++ b/Cs_HMI/TestProject/mcpServers @@ -0,0 +1 @@ +Subproject commit 792c47442d1b81f8aeeff2f6de2a2ce6560072ae diff --git a/Cs_HMI/build.bat b/Cs_HMI/build.bat index 44627b5..471bd03 100644 --- a/Cs_HMI/build.bat +++ b/Cs_HMI/build.bat @@ -1,13 +1,14 @@ -@echo off + + echo Building AGV C# HMI Project... REM Set MSBuild path -REM set MSBUILD="C:\Program Files\Microsoft Visual Studio\2022\Professional\MSBuild\Current\Bin\MSBuild.exe" + set MSBUILD="C:\Program Files (x86)\Microsoft Visual Studio\2017\WDExpress\MSBuild\15.0\Bin\MSBuild.exe" +set MSBUILD="C:\Program Files\Microsoft Visual Studio\2022\Professional\MSBuild\Current\Bin\MSBuild.exe" +set MSBUILD="C:\Program Files\Microsoft Visual Studio\2022\Community\MSBuild\Current\Bin\MSBuild.exe" REM Rebuild Debug x86 configuration (VS-style Rebuild) -%MSBUILD% AGVCSharp.sln -property:Configuration=Debug -property:Platform=x86 -verbosity:quiet -nologo -t:Rebuild - -pause \ No newline at end of file +%MSBUILD% "S:\Source\Amkor\ENIG\Cs_HMI\AGVCSharp.sln" -property:Configuration=Debug -property:Platform=x86 -verbosity:quiet -nologo -t:Rebuild \ No newline at end of file