"feat:Enable-hover-highlight-and-refactor"
This commit is contained in:
3
Cs_HMI/.cursorrules
Normal file
3
Cs_HMI/.cursorrules
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# User Preferences
|
||||||
|
- **프로젝트 컴파일은 build.bat 를 사용하세요(윈도우 환경)
|
||||||
|
- **콘솔명령 실행시에는 항상 cmd /c 를 사용해서 실행하세요
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
|
|
||||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
# Visual Studio Express 15 for Windows Desktop
|
# Visual Studio Version 17
|
||||||
VisualStudioVersion = 15.0.36324.19
|
VisualStudioVersion = 17.14.36804.6 d17.14
|
||||||
MinimumVisualStudioVersion = 10.0.40219.1
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Sub", "Sub", "{C423C39A-44E7-4F09-B2F7-7943975FF948}"
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Sub", "Sub", "{C423C39A-44E7-4F09-B2F7-7943975FF948}"
|
||||||
EndProject
|
EndProject
|
||||||
@@ -38,8 +38,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AGVSimulator", "AGVLogic\AG
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "arCommUtil", "SubProject\commutil\arCommUtil.csproj", "{14E8C9A5-013E-49BA-B435-FFFFFF7DD623}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "arCommUtil", "SubProject\commutil\arCommUtil.csproj", "{14E8C9A5-013E-49BA-B435-FFFFFF7DD623}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Supertonic.Netfx48", "SubProject\SuperTonic\Supertonic.Netfx48.csproj", "{19675E19-EB91-493E-88C3-32B3C094B749}"
|
|
||||||
EndProject
|
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
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|x64.Build.0 = Release|Any CPU
|
||||||
{14E8C9A5-013E-49BA-B435-FFFFFF7DD623}.Release|x86.ActiveCfg = Release|x86
|
{14E8C9A5-013E-49BA-B435-FFFFFF7DD623}.Release|x86.ActiveCfg = Release|x86
|
||||||
{14E8C9A5-013E-49BA-B435-FFFFFF7DD623}.Release|x86.Build.0 = 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
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
@@ -207,7 +193,6 @@ Global
|
|||||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890} = {E5C75D32-5AD6-44DD-8F27-E32023206EBB}
|
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890} = {E5C75D32-5AD6-44DD-8F27-E32023206EBB}
|
||||||
{B2C3D4E5-0000-0000-0000-000000000000} = {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}
|
{14E8C9A5-013E-49BA-B435-FFFFFF7DD623} = {C423C39A-44E7-4F09-B2F7-7943975FF948}
|
||||||
{19675E19-EB91-493E-88C3-32B3C094B749} = {C423C39A-44E7-4F09-B2F7-7943975FF948}
|
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
SolutionGuid = {B5B1FD72-356F-4840-83E8-B070AC21C8D9}
|
SolutionGuid = {B5B1FD72-356F-4840-83E8-B070AC21C8D9}
|
||||||
|
|||||||
@@ -13,9 +13,9 @@ namespace AGVMapEditor.Forms
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class ImageEditorForm : Form
|
public partial class ImageEditorForm : Form
|
||||||
{
|
{
|
||||||
private MapNode _targetNode;
|
private MapImage _targetNode;
|
||||||
|
|
||||||
public ImageEditorForm(MapNode imageNode = null)
|
public ImageEditorForm(MapImage imageNode = null)
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
_targetNode = imageNode;
|
_targetNode = imageNode;
|
||||||
@@ -25,7 +25,7 @@ namespace AGVMapEditor.Forms
|
|||||||
{
|
{
|
||||||
LoadImageFromNode(imageNode);
|
LoadImageFromNode(imageNode);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.KeyPreview = true;
|
this.KeyPreview = true;
|
||||||
this.KeyDown += (s1, e1) => {
|
this.KeyDown += (s1, e1) => {
|
||||||
if (e1.KeyCode == Keys.Escape) this.Close();
|
if (e1.KeyCode == Keys.Escape) this.Close();
|
||||||
@@ -38,7 +38,7 @@ namespace AGVMapEditor.Forms
|
|||||||
imageCanvas.BrushSize = trackBrush.Value;
|
imageCanvas.BrushSize = trackBrush.Value;
|
||||||
imageCanvas.BrushModeEnabled = chkBrushMode.Checked;
|
imageCanvas.BrushModeEnabled = chkBrushMode.Checked;
|
||||||
imageCanvas.BackColor = Color.FromArgb(32,32,32);
|
imageCanvas.BackColor = Color.FromArgb(32,32,32);
|
||||||
|
|
||||||
// 이벤트 연결
|
// 이벤트 연결
|
||||||
chkBrushMode.CheckedChanged += (s, e) => imageCanvas.BrushModeEnabled = chkBrushMode.Checked;
|
chkBrushMode.CheckedChanged += (s, e) => imageCanvas.BrushModeEnabled = chkBrushMode.Checked;
|
||||||
}
|
}
|
||||||
@@ -48,7 +48,7 @@ namespace AGVMapEditor.Forms
|
|||||||
imageCanvas.BrushSize = trackBrush.Value;
|
imageCanvas.BrushSize = trackBrush.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void LoadImageFromNode(MapNode node)
|
private void LoadImageFromNode(MapImage node)
|
||||||
{
|
{
|
||||||
if (node.LoadedImage != null)
|
if (node.LoadedImage != null)
|
||||||
{
|
{
|
||||||
@@ -66,7 +66,7 @@ namespace AGVMapEditor.Forms
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void LoadImageFromFile(string filePath)
|
private void LoadImageFromFile(string filePath)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -160,7 +160,7 @@ namespace AGVMapEditor.Forms
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_targetNode != null && _targetNode.Type == NodeType.Image)
|
if (_targetNode != null)
|
||||||
{
|
{
|
||||||
// 표시 크기로 리사이즈된 이미지 가져오기
|
// 표시 크기로 리사이즈된 이미지 가져오기
|
||||||
var finalImage = imageCanvas.GetResizedImage();
|
var finalImage = imageCanvas.GetResizedImage();
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ using System.Windows.Forms;
|
|||||||
using AGVMapEditor.Models;
|
using AGVMapEditor.Models;
|
||||||
using AGVNavigationCore.Controls;
|
using AGVNavigationCore.Controls;
|
||||||
using AGVNavigationCore.Models;
|
using AGVNavigationCore.Models;
|
||||||
|
using MapImage = AGVNavigationCore.Models.MapImage;
|
||||||
|
using MapLabel = AGVNavigationCore.Models.MapLabel;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
namespace AGVMapEditor.Forms
|
namespace AGVMapEditor.Forms
|
||||||
@@ -18,11 +20,11 @@ namespace AGVMapEditor.Forms
|
|||||||
{
|
{
|
||||||
#region Fields
|
#region Fields
|
||||||
|
|
||||||
private List<MapNode> _mapNodes;
|
// private List<MapNode> this._mapCanvas.Nodes;
|
||||||
private UnifiedAGVCanvas _mapCanvas;
|
private UnifiedAGVCanvas _mapCanvas;
|
||||||
|
|
||||||
// 현재 선택된 노드
|
// 현재 선택된 노드
|
||||||
private MapNode _selectedNode;
|
private NodeBase _selectedNode;
|
||||||
|
|
||||||
// 파일 경로
|
// 파일 경로
|
||||||
private string _currentMapFile = string.Empty;
|
private string _currentMapFile = string.Empty;
|
||||||
@@ -93,20 +95,18 @@ namespace AGVMapEditor.Forms
|
|||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
#region Initialization
|
#region Initialization
|
||||||
|
|
||||||
private void InitializeData()
|
private void InitializeData()
|
||||||
{
|
{
|
||||||
_mapNodes = new List<MapNode>();
|
// this._mapCanvas.Nodes = new List<MapNode>();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void InitializeMapCanvas()
|
private void InitializeMapCanvas()
|
||||||
{
|
{
|
||||||
_mapCanvas = new UnifiedAGVCanvas();
|
_mapCanvas = new UnifiedAGVCanvas();
|
||||||
_mapCanvas.Dock = DockStyle.Fill;
|
_mapCanvas.Dock = DockStyle.Fill;
|
||||||
_mapCanvas.Nodes = _mapNodes;
|
|
||||||
// RfidMappings 제거 - MapNode에 통합됨
|
|
||||||
|
|
||||||
// 이벤트 연결
|
// 이벤트 연결
|
||||||
_mapCanvas.NodeAdded += OnNodeAdded;
|
_mapCanvas.NodeAdded += OnNodeAdded;
|
||||||
@@ -115,12 +115,14 @@ namespace AGVMapEditor.Forms
|
|||||||
_mapCanvas.NodeMoved += OnNodeMoved;
|
_mapCanvas.NodeMoved += OnNodeMoved;
|
||||||
_mapCanvas.NodeDeleted += OnNodeDeleted;
|
_mapCanvas.NodeDeleted += OnNodeDeleted;
|
||||||
_mapCanvas.ConnectionDeleted += OnConnectionDeleted;
|
_mapCanvas.ConnectionDeleted += OnConnectionDeleted;
|
||||||
_mapCanvas.ImageNodeDoubleClicked += OnImageNodeDoubleClicked;
|
_mapCanvas.ImageDoubleClicked += OnImageDoubleClicked;
|
||||||
_mapCanvas.MapChanged += OnMapChanged;
|
_mapCanvas.MapChanged += OnMapChanged;
|
||||||
|
|
||||||
// 스플리터 패널에 맵 캔버스 추가
|
// 스플리터 패널에 맵 캔버스 추가
|
||||||
panel1.Controls.Add(_mapCanvas);
|
panel1.Controls.Add(_mapCanvas);
|
||||||
|
|
||||||
|
// ...
|
||||||
|
|
||||||
// 툴바 버튼 이벤트 연결
|
// 툴바 버튼 이벤트 연결
|
||||||
WireToolbarButtonEvents();
|
WireToolbarButtonEvents();
|
||||||
}
|
}
|
||||||
@@ -160,7 +162,7 @@ namespace AGVMapEditor.Forms
|
|||||||
|
|
||||||
private void MainForm_Load(object sender, EventArgs e)
|
private void MainForm_Load(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
|
|
||||||
RefreshNodeList();
|
RefreshNodeList();
|
||||||
// 속성 변경 시 이벤트 연결
|
// 속성 변경 시 이벤트 연결
|
||||||
_propertyGrid.PropertyValueChanged += PropertyGrid_PropertyValueChanged;
|
_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;
|
_hasChanges = true;
|
||||||
UpdateTitle();
|
UpdateTitle();
|
||||||
@@ -199,7 +201,7 @@ namespace AGVMapEditor.Forms
|
|||||||
// }
|
// }
|
||||||
//}
|
//}
|
||||||
|
|
||||||
private void OnNodesSelected(object sender, List<MapNode> nodes)
|
private void OnNodesSelected(object sender, List<NodeBase> nodes)
|
||||||
{
|
{
|
||||||
// 다중 선택 시 처리
|
// 다중 선택 시 처리
|
||||||
if (nodes == null || nodes.Count == 0)
|
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;
|
_hasChanges = true;
|
||||||
UpdateTitle();
|
UpdateTitle();
|
||||||
RefreshNodeList();
|
RefreshNodeList();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnNodeDeleted(object sender, MapNode node)
|
private void OnNodeDeleted(object sender, NodeBase node)
|
||||||
{
|
{
|
||||||
_hasChanges = true;
|
_hasChanges = true;
|
||||||
UpdateTitle();
|
UpdateTitle();
|
||||||
@@ -258,20 +260,17 @@ namespace AGVMapEditor.Forms
|
|||||||
UpdateNodeProperties(); // 연결 정보 업데이트
|
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();
|
||||||
_hasChanges = true;
|
_mapCanvas.Invalidate(); // 캔버스 다시 그리기
|
||||||
UpdateTitle();
|
UpdateNodeProperties(); // 속성 업데이트
|
||||||
_mapCanvas.Invalidate(); // 캔버스 다시 그리기
|
|
||||||
UpdateNodeProperties(); // 속성 업데이트
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -401,12 +400,11 @@ namespace AGVMapEditor.Forms
|
|||||||
private void AddNewNode()
|
private void AddNewNode()
|
||||||
{
|
{
|
||||||
var nodeId = GenerateNodeId();
|
var nodeId = GenerateNodeId();
|
||||||
var nodeName = $"노드{_mapNodes.Count + 1}";
|
var position = new Point(100 + this._mapCanvas.Nodes.Count * 50, 100 + this._mapCanvas.Nodes.Count * 50);
|
||||||
var position = new Point(100 + _mapNodes.Count * 50, 100 + _mapNodes.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;
|
_hasChanges = true;
|
||||||
|
|
||||||
RefreshNodeList();
|
RefreshNodeList();
|
||||||
@@ -422,13 +420,14 @@ namespace AGVMapEditor.Forms
|
|||||||
return;
|
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);
|
"삭제 확인", MessageBoxButtons.YesNo, MessageBoxIcon.Question);
|
||||||
|
|
||||||
if (result == DialogResult.Yes)
|
if (result == DialogResult.Yes)
|
||||||
{
|
{
|
||||||
// 노드 제거
|
// 노드 제거
|
||||||
_mapNodes.Remove(_selectedNode);
|
_mapCanvas.RemoveItem(_selectedNode);
|
||||||
_selectedNode = null;
|
_selectedNode = null;
|
||||||
_hasChanges = true;
|
_hasChanges = true;
|
||||||
|
|
||||||
@@ -441,15 +440,15 @@ namespace AGVMapEditor.Forms
|
|||||||
|
|
||||||
private void AddConnectionToSelectedNode()
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 다른 노드들 중에서 선택
|
// 다른 노드들 중에서 선택
|
||||||
var availableNodes = _mapNodes.Where(n => n.NodeId != _selectedNode.NodeId &&
|
var availableNodes = this._mapCanvas.Nodes.Where(n => n.Id != selectedMapNode.Id &&
|
||||||
!_selectedNode.ConnectedNodes.Contains(n.NodeId)).ToList();
|
!selectedMapNode.ConnectedNodes.Contains(n.Id)).ToList();
|
||||||
|
|
||||||
if (availableNodes.Count == 0)
|
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 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)
|
if (targetNode != null)
|
||||||
{
|
{
|
||||||
_selectedNode.AddConnection(targetNode.NodeId);
|
selectedMapNode.AddConnection(targetNode.Id);
|
||||||
_hasChanges = true;
|
_hasChanges = true;
|
||||||
RefreshMapCanvas();
|
RefreshMapCanvas();
|
||||||
UpdateNodeProperties();
|
UpdateNodeProperties();
|
||||||
@@ -474,25 +473,25 @@ namespace AGVMapEditor.Forms
|
|||||||
|
|
||||||
private void RemoveConnectionFromSelectedNode()
|
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);
|
MessageBox.Show("연결을 제거할 노드를 선택하거나 연결된 노드가 없습니다.", "알림", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 연결된 노드들 중에서 선택
|
// 연결된 노드들 중에서 선택
|
||||||
var connectedNodeNames = _selectedNode.ConnectedNodes.Select(connectedNodeId =>
|
var connectedNodeNames = selectedMapNode.ConnectedNodes.Select(connectedNodeId =>
|
||||||
{
|
{
|
||||||
var node = _mapNodes.FirstOrDefault(n => n.NodeId == connectedNodeId);
|
var node = this._mapCanvas.Nodes.FirstOrDefault(n => n.Id == connectedNodeId);
|
||||||
return node != null ? $"{node.NodeId}: {node.Name}" : connectedNodeId;
|
return node != null ? $"{node.Id}: {node.RfidId}" : connectedNodeId;
|
||||||
}).ToArray();
|
}).ToArray();
|
||||||
|
|
||||||
var input = Microsoft.VisualBasic.Interaction.InputBox("제거할 연결을 선택하세요:", "연결 제거", connectedNodeNames[0]);
|
var input = Microsoft.VisualBasic.Interaction.InputBox("제거할 연결을 선택하세요:", "연결 제거", connectedNodeNames[0]);
|
||||||
|
|
||||||
var targetNodeId = input.Split(':')[0];
|
var targetNodeId = input.Split(':')[0];
|
||||||
if (_selectedNode.ConnectedNodes.Contains(targetNodeId))
|
if (selectedMapNode.ConnectedNodes.Contains(targetNodeId))
|
||||||
{
|
{
|
||||||
_selectedNode.RemoveConnection(targetNodeId);
|
selectedMapNode.RemoveConnection(targetNodeId);
|
||||||
_hasChanges = true;
|
_hasChanges = true;
|
||||||
RefreshMapCanvas();
|
RefreshMapCanvas();
|
||||||
UpdateNodeProperties();
|
UpdateNodeProperties();
|
||||||
@@ -509,7 +508,7 @@ namespace AGVMapEditor.Forms
|
|||||||
{
|
{
|
||||||
nodeId = $"N{counter:D3}";
|
nodeId = $"N{counter:D3}";
|
||||||
counter++;
|
counter++;
|
||||||
} while (_mapNodes.Any(n => n.NodeId == nodeId));
|
} while (this._mapCanvas.Nodes.Any(n => n.Id == nodeId));
|
||||||
|
|
||||||
return nodeId;
|
return nodeId;
|
||||||
}
|
}
|
||||||
@@ -520,7 +519,7 @@ namespace AGVMapEditor.Forms
|
|||||||
|
|
||||||
private void NewMap()
|
private void NewMap()
|
||||||
{
|
{
|
||||||
_mapNodes.Clear();
|
this._mapCanvas.Nodes.Clear();
|
||||||
_selectedNode = null;
|
_selectedNode = null;
|
||||||
_currentMapFile = string.Empty;
|
_currentMapFile = string.Empty;
|
||||||
_hasChanges = false;
|
_hasChanges = false;
|
||||||
@@ -533,7 +532,7 @@ namespace AGVMapEditor.Forms
|
|||||||
{
|
{
|
||||||
if (CheckSaveChanges())
|
if (CheckSaveChanges())
|
||||||
{
|
{
|
||||||
_mapNodes.Clear();
|
this._mapCanvas.Nodes.Clear();
|
||||||
_selectedNode = null;
|
_selectedNode = null;
|
||||||
_currentMapFile = string.Empty;
|
_currentMapFile = string.Empty;
|
||||||
_hasChanges = false;
|
_hasChanges = false;
|
||||||
@@ -547,7 +546,7 @@ namespace AGVMapEditor.Forms
|
|||||||
{
|
{
|
||||||
var openFileDialog = new OpenFileDialog
|
var openFileDialog = new OpenFileDialog
|
||||||
{
|
{
|
||||||
Filter = "AGV Map Files (*.agvmap)|*.agvmap|All Files (*.*)|*.*",
|
Filter = "AGV Map Files (*.agvmap;*.json)|*.agvmap;*.json|All Files (*.*)|*.*",
|
||||||
DefaultExt = "agvmap",
|
DefaultExt = "agvmap",
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -624,10 +623,14 @@ namespace AGVMapEditor.Forms
|
|||||||
|
|
||||||
if (result.Success)
|
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에 통합
|
// RfidMappings 제거됨 - MapNode에 통합
|
||||||
|
|
||||||
// 🔥 맵 설정 적용 (배경색, 그리드 표시)
|
// 🔥 맵 설정 적용 (배경색, 그리드 표시)
|
||||||
@@ -693,7 +696,7 @@ namespace AGVMapEditor.Forms
|
|||||||
ShowGrid = _mapCanvas.ShowGrid
|
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;
|
_currentMapFile = filePath;
|
||||||
@@ -718,7 +721,7 @@ namespace AGVMapEditor.Forms
|
|||||||
private void UpdateRfidMappings()
|
private void UpdateRfidMappings()
|
||||||
{
|
{
|
||||||
// RFID 자동 할당 제거 - 사용자가 직접 입력한 값 유지
|
// RFID 자동 할당 제거 - 사용자가 직접 입력한 값 유지
|
||||||
// MapLoader.AssignAutoRfidIds(_mapNodes);
|
// MapLoader.AssignAutoRfidIds(this._mapCanvas.Nodes);
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool CheckSaveChanges()
|
private bool CheckSaveChanges()
|
||||||
@@ -780,14 +783,14 @@ namespace AGVMapEditor.Forms
|
|||||||
private void RefreshNodeList()
|
private void RefreshNodeList()
|
||||||
{
|
{
|
||||||
listBoxNodes.DataSource = null;
|
listBoxNodes.DataSource = null;
|
||||||
listBoxNodes.DataSource = _mapNodes;
|
listBoxNodes.DataSource = this._mapCanvas.Nodes;
|
||||||
listBoxNodes.DisplayMember = "DisplayText";
|
listBoxNodes.DisplayMember = "DisplayText";
|
||||||
listBoxNodes.ValueMember = "NodeId";
|
listBoxNodes.ValueMember = "Id";
|
||||||
|
|
||||||
// 노드 목록 클릭 이벤트 연결
|
// 노드 목록 클릭 이벤트 연결
|
||||||
listBoxNodes.SelectedIndexChanged -= ListBoxNodes_SelectedIndexChanged;
|
listBoxNodes.SelectedIndexChanged -= ListBoxNodes_SelectedIndexChanged;
|
||||||
listBoxNodes.SelectedIndexChanged += ListBoxNodes_SelectedIndexChanged;
|
listBoxNodes.SelectedIndexChanged += ListBoxNodes_SelectedIndexChanged;
|
||||||
|
|
||||||
// 노드 타입별 색상 적용
|
// 노드 타입별 색상 적용
|
||||||
listBoxNodes.DrawMode = DrawMode.OwnerDrawFixed;
|
listBoxNodes.DrawMode = DrawMode.OwnerDrawFixed;
|
||||||
listBoxNodes.DrawItem -= ListBoxNodes_DrawItem;
|
listBoxNodes.DrawItem -= ListBoxNodes_DrawItem;
|
||||||
@@ -812,14 +815,14 @@ namespace AGVMapEditor.Forms
|
|||||||
{
|
{
|
||||||
e.DrawBackground();
|
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 foreColor = Color.Black;
|
||||||
Color backColor = e.BackColor;
|
Color backColor = e.BackColor;
|
||||||
|
|
||||||
if ((e.State & DrawItemState.Selected) == DrawItemState.Selected)
|
if ((e.State & DrawItemState.Selected) == DrawItemState.Selected)
|
||||||
{
|
{
|
||||||
backColor = SystemColors.Highlight;
|
backColor = SystemColors.Highlight;
|
||||||
@@ -827,22 +830,30 @@ namespace AGVMapEditor.Forms
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
backColor = Color.White;
|
||||||
switch (node.Type)
|
switch (node.Type)
|
||||||
{
|
{
|
||||||
case NodeType.Normal:
|
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;
|
break;
|
||||||
case NodeType.Loader:
|
|
||||||
case NodeType.UnLoader:
|
case NodeType.Label:
|
||||||
case NodeType.Clearner:
|
case NodeType.Mark:
|
||||||
case NodeType.Buffer:
|
case NodeType.Image:
|
||||||
foreColor = Color.DarkGreen;
|
foreColor = Color.DarkBlue;
|
||||||
backColor = Color.LightGreen;
|
|
||||||
break;
|
break;
|
||||||
case NodeType.Charging:
|
case NodeType.Magnet:
|
||||||
|
foreColor = Color.DarkMagenta;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
foreColor = Color.DarkRed;
|
foreColor = Color.DarkRed;
|
||||||
backColor = Color.LightPink;
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -854,17 +865,21 @@ namespace AGVMapEditor.Forms
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 텍스트 그리기 (노드ID - 노드명 - RFID 순서)
|
// 텍스트 그리기 (노드ID - 노드명 - RFID 순서)
|
||||||
var displayText = node.NodeId;
|
string displayText;
|
||||||
|
if (node.Type == NodeType.Normal)
|
||||||
if (!string.IsNullOrEmpty(node.Name))
|
|
||||||
{
|
{
|
||||||
displayText += $" - {node.Name}";
|
var item = node as MapNode;
|
||||||
|
if (item.HasRfid())
|
||||||
|
displayText = $"[{node.Id}-{item.RfidId}] {item.StationType}";
|
||||||
|
else
|
||||||
|
displayText = $"[{node.Id}] {item.StationType}";
|
||||||
}
|
}
|
||||||
|
else if(node.Type == NodeType.Label)
|
||||||
if (!string.IsNullOrEmpty(node.RfidId))
|
|
||||||
{
|
{
|
||||||
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))
|
using (var brush = new SolidBrush(foreColor))
|
||||||
{
|
{
|
||||||
@@ -881,31 +896,31 @@ namespace AGVMapEditor.Forms
|
|||||||
var processedPairs = new HashSet<string>();
|
var processedPairs = new HashSet<string>();
|
||||||
|
|
||||||
// 모든 노드의 연결 정보를 수집 (중복 방지)
|
// 모든 노드의 연결 정보를 수집 (중복 방지)
|
||||||
foreach (var fromNode in _mapNodes)
|
foreach (var fromNode in this._mapCanvas.Nodes)
|
||||||
{
|
{
|
||||||
foreach (var toNodeId in fromNode.ConnectedNodes)
|
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)
|
if (toNode != null)
|
||||||
{
|
{
|
||||||
// 중복 체크 (단일 연결만 표시)
|
// 중복 체크 (단일 연결만 표시)
|
||||||
string pairKey1 = $"{fromNode.NodeId}-{toNode.NodeId}";
|
string pairKey1 = $"{fromNode.Id}-{toNode.Id}";
|
||||||
string pairKey2 = $"{toNode.NodeId}-{fromNode.NodeId}";
|
string pairKey2 = $"{toNode.Id}-{fromNode.Id}";
|
||||||
|
|
||||||
if (!processedPairs.Contains(pairKey1) && !processedPairs.Contains(pairKey2))
|
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)
|
? (fromNode, toNode)
|
||||||
: (toNode, fromNode);
|
: (toNode, fromNode);
|
||||||
|
|
||||||
connections.Add(new NodeConnectionInfo
|
connections.Add(new NodeConnectionInfo
|
||||||
{
|
{
|
||||||
FromNodeId = firstNode.NodeId,
|
FromNodeId = firstNode.Id,
|
||||||
FromNodeName = firstNode.Name,
|
FromNodeName = "",
|
||||||
FromRfidId = firstNode.RfidId,
|
FromRfidId = firstNode.RfidId,
|
||||||
ToNodeId = secondNode.NodeId,
|
ToNodeId = secondNode.Id,
|
||||||
ToNodeName = secondNode.Name,
|
ToNodeName = "",
|
||||||
ToRfidId = secondNode.RfidId,
|
ToRfidId = secondNode.RfidId,
|
||||||
ConnectionType = "양방향" // 모든 연결이 양방향
|
ConnectionType = "양방향" // 모든 연결이 양방향
|
||||||
});
|
});
|
||||||
@@ -940,7 +955,7 @@ namespace AGVMapEditor.Forms
|
|||||||
_mapCanvas?.HighlightConnection(connectionInfo.FromNodeId, connectionInfo.ToNodeId);
|
_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)
|
if (fromNode != null)
|
||||||
{
|
{
|
||||||
_selectedNode = fromNode;
|
_selectedNode = fromNode;
|
||||||
@@ -1002,7 +1017,7 @@ namespace AGVMapEditor.Forms
|
|||||||
private void UpdateImageEditButton()
|
private void UpdateImageEditButton()
|
||||||
{
|
{
|
||||||
// ToolStripButton으로 변경됨
|
// ToolStripButton으로 변경됨
|
||||||
btnEditImage.Enabled = (_selectedNode != null && _selectedNode.Type == NodeType.Image);
|
btnEditImage.Enabled = (_mapCanvas.SelectedImage != null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -1019,9 +1034,10 @@ namespace AGVMapEditor.Forms
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private void BtnToolbarEditImage_Click(object sender, EventArgs e)
|
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)
|
if (editor.ShowDialog(this) == DialogResult.OK)
|
||||||
{
|
{
|
||||||
@@ -1059,7 +1075,7 @@ namespace AGVMapEditor.Forms
|
|||||||
if (listBoxNodes != null)
|
if (listBoxNodes != null)
|
||||||
{
|
{
|
||||||
listBoxNodes.DataSource = null;
|
listBoxNodes.DataSource = null;
|
||||||
listBoxNodes.DataSource = _mapNodes;
|
listBoxNodes.DataSource = this._mapCanvas.Items;
|
||||||
listBoxNodes.DisplayMember = "DisplayText";
|
listBoxNodes.DisplayMember = "DisplayText";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1121,13 +1137,13 @@ namespace AGVMapEditor.Forms
|
|||||||
|
|
||||||
// 🔥 다중 선택 여부 확인 및 선택된 노드들 저장
|
// 🔥 다중 선택 여부 확인 및 선택된 노드들 저장
|
||||||
bool isMultiSelect = _propertyGrid.SelectedObjects is MapNode[]; // _propertyGrid.SelectedObject is MapNode[];
|
bool isMultiSelect = _propertyGrid.SelectedObjects is MapNode[]; // _propertyGrid.SelectedObject is MapNode[];
|
||||||
|
|
||||||
//var a = _propertyGrid.SelectedObject;
|
//var a = _propertyGrid.SelectedObject;
|
||||||
List<MapNode> selectedNodes = null;
|
List<NodeBase> selectedNodes = null;
|
||||||
if (isMultiSelect)
|
if (isMultiSelect)
|
||||||
{
|
{
|
||||||
// 캔버스에서 현재 선택된 노드들 가져오기
|
// 캔버스에서 현재 선택된 노드들 가져오기
|
||||||
selectedNodes = new List<MapNode>(_mapCanvas.SelectedNodes);
|
selectedNodes = new List<NodeBase>(_mapCanvas.SelectedNodes);
|
||||||
System.Diagnostics.Debug.WriteLine($"[PropertyGrid] 다중 선택 노드 수: {selectedNodes.Count}");
|
System.Diagnostics.Debug.WriteLine($"[PropertyGrid] 다중 선택 노드 수: {selectedNodes.Count}");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1144,7 +1160,7 @@ namespace AGVMapEditor.Forms
|
|||||||
// 🔥 다중 선택인 경우 MultiNodePropertyWrapper를 다시 생성하여 바인딩
|
// 🔥 다중 선택인 경우 MultiNodePropertyWrapper를 다시 생성하여 바인딩
|
||||||
if (isMultiSelect && selectedNodes != null && selectedNodes.Count > 0)
|
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);
|
//var multiWrapper = new MultiNodePropertyWrapper(selectedNodes);
|
||||||
_propertyGrid.SelectedObjects = selectedNodes.ToArray();// multiWrapper;
|
_propertyGrid.SelectedObjects = selectedNodes.ToArray();// multiWrapper;
|
||||||
}
|
}
|
||||||
@@ -1153,9 +1169,9 @@ namespace AGVMapEditor.Forms
|
|||||||
_propertyGrid.Refresh();
|
_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)
|
if (nodeIndex >= 0)
|
||||||
{
|
{
|
||||||
listBoxNodes.SelectedIndex = nodeIndex;
|
listBoxNodes.SelectedIndex = nodeIndex;
|
||||||
@@ -1170,7 +1186,7 @@ namespace AGVMapEditor.Forms
|
|||||||
/// <returns>중복되면 true, 아니면 false</returns>
|
/// <returns>중복되면 true, 아니면 false</returns>
|
||||||
private bool CheckRfidDuplicate(string rfidValue)
|
private bool CheckRfidDuplicate(string rfidValue)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(rfidValue) || _mapNodes == null)
|
if (string.IsNullOrEmpty(rfidValue) || this._mapCanvas.Nodes == null)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
// 현재 편집 중인 노드 제외하고 중복 검사
|
// 현재 편집 중인 노드 제외하고 중복 검사
|
||||||
@@ -1192,10 +1208,10 @@ namespace AGVMapEditor.Forms
|
|||||||
//}
|
//}
|
||||||
|
|
||||||
int duplicateCount = 0;
|
int duplicateCount = 0;
|
||||||
foreach (var node in _mapNodes)
|
foreach (var node in this._mapCanvas.Nodes)
|
||||||
{
|
{
|
||||||
// 현재 편집 중인 노드는 제외
|
// 현재 편집 중인 노드는 제외
|
||||||
if (node.NodeId == currentNodeId)
|
if (node.Id == currentNodeId)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
// 같은 RFID 값을 가진 노드가 있는지 확인
|
// 같은 RFID 값을 가진 노드가 있는지 확인
|
||||||
@@ -1234,23 +1250,23 @@ namespace AGVMapEditor.Forms
|
|||||||
if (result == DialogResult.Yes)
|
if (result == DialogResult.Yes)
|
||||||
{
|
{
|
||||||
// 단일 연결 삭제
|
// 단일 연결 삭제
|
||||||
var fromNode = _mapNodes.FirstOrDefault(n => n.NodeId == connectionInfo.FromNodeId);
|
var fromNode = this._mapCanvas.Nodes.FirstOrDefault(n => n.Id == connectionInfo.FromNodeId);
|
||||||
var toNode = _mapNodes.FirstOrDefault(n => n.NodeId == connectionInfo.ToNodeId);
|
var toNode = this._mapCanvas.Nodes.FirstOrDefault(n => n.Id == connectionInfo.ToNodeId);
|
||||||
|
|
||||||
if (fromNode != null && toNode != null)
|
if (fromNode != null && toNode != null)
|
||||||
{
|
{
|
||||||
// 양방향 연결 삭제 (양쪽 방향 모두 제거)
|
// 양방향 연결 삭제 (양쪽 방향 모두 제거)
|
||||||
bool removed = false;
|
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;
|
removed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (toNode.ConnectedNodes.Contains(fromNode.NodeId))
|
if (toNode.ConnectedNodes.Contains(fromNode.Id))
|
||||||
{
|
{
|
||||||
toNode.RemoveConnection(fromNode.NodeId);
|
toNode.RemoveConnection(fromNode.Id);
|
||||||
removed = true;
|
removed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1277,108 +1293,18 @@ namespace AGVMapEditor.Forms
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#region Multi-Node Selection
|
|
||||||
|
|
||||||
private void ShowMultiNodeContextMenu(List<MapNode> 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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 선택된 노드들의 글자색 일괄 변경
|
|
||||||
/// </summary>
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 선택된 노드들의 배경색 일괄 변경
|
|
||||||
/// </summary>
|
|
||||||
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)
|
private void allTurnLeftRightCrossOnToolStripMenuItem_Click(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
//모든노드의 trun left/right/ cross 속성을 true로 변경합니다
|
//모든노드의 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);
|
MessageBox.Show("맵에 노드가 없습니다.", "알림", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var result = MessageBox.Show(
|
var result = MessageBox.Show(
|
||||||
$"모든 노드({_mapNodes.Count}개)의 회전/교차 속성을 활성화하시겠습니까?\n\n" +
|
$"모든 노드({this._mapCanvas.Nodes.Count}개)의 회전/교차 속성을 활성화하시겠습니까?\n\n" +
|
||||||
"• CanTurnLeft = true\n" +
|
"• CanTurnLeft = true\n" +
|
||||||
"• CanTurnRight = true\n" +
|
"• CanTurnRight = true\n" +
|
||||||
"• DisableCross = false",
|
"• DisableCross = false",
|
||||||
@@ -1388,20 +1314,20 @@ namespace AGVMapEditor.Forms
|
|||||||
|
|
||||||
if (result == DialogResult.Yes)
|
if (result == DialogResult.Yes)
|
||||||
{
|
{
|
||||||
foreach (var node in _mapNodes)
|
foreach (var node in this._mapCanvas.Nodes)
|
||||||
{
|
{
|
||||||
node.CanTurnLeft = true;
|
node.CanTurnLeft = true;
|
||||||
node.CanTurnRight = true;
|
node.CanTurnRight = true;
|
||||||
node.DisableCross =false;
|
node.DisableCross = false;
|
||||||
node.ModifiedDate = DateTime.Now;
|
node.ModifiedDate = DateTime.Now;
|
||||||
}
|
}
|
||||||
|
|
||||||
_hasChanges = true;
|
_hasChanges = true;
|
||||||
UpdateTitle();
|
UpdateTitle();
|
||||||
RefreshMapCanvas();
|
RefreshMapCanvas();
|
||||||
|
|
||||||
MessageBox.Show(
|
MessageBox.Show(
|
||||||
$"{_mapNodes.Count}개 노드의 회전/교차 속성이 모두 활성화되었습니다.",
|
$"{this._mapCanvas.Nodes.Count}개 노드의 회전/교차 속성이 모두 활성화되었습니다.",
|
||||||
"완료",
|
"완료",
|
||||||
MessageBoxButtons.OK,
|
MessageBoxButtons.OK,
|
||||||
MessageBoxIcon.Information);
|
MessageBoxIcon.Information);
|
||||||
|
|||||||
@@ -78,7 +78,12 @@
|
|||||||
<Compile Include="Models\IMovableAGV.cs" />
|
<Compile Include="Models\IMovableAGV.cs" />
|
||||||
<Compile Include="Models\VirtualAGV.cs" />
|
<Compile Include="Models\VirtualAGV.cs" />
|
||||||
<Compile Include="Models\MapLoader.cs" />
|
<Compile Include="Models\MapLoader.cs" />
|
||||||
|
<Compile Include="Models\MapMagnet.cs" />
|
||||||
|
<Compile Include="Models\MapMark.cs" />
|
||||||
<Compile Include="Models\MapNode.cs" />
|
<Compile Include="Models\MapNode.cs" />
|
||||||
|
<Compile Include="Models\NodeBase.cs" />
|
||||||
|
<Compile Include="Models\MapLabel.cs" />
|
||||||
|
<Compile Include="Models\MapImage.cs" />
|
||||||
<Compile Include="PathFinding\Planning\AGVPathfinder.cs" />
|
<Compile Include="PathFinding\Planning\AGVPathfinder.cs" />
|
||||||
<Compile Include="PathFinding\Planning\DirectionChangePlanner.cs" />
|
<Compile Include="PathFinding\Planning\DirectionChangePlanner.cs" />
|
||||||
<Compile Include="PathFinding\Planning\DirectionalPathfinder.cs" />
|
<Compile Include="PathFinding\Planning\DirectionalPathfinder.cs" />
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ namespace AGVNavigationCore.Controls
|
|||||||
|
|
||||||
// 이동 경로 정보 추가
|
// 이동 경로 정보 추가
|
||||||
Point? PrevPosition { get; }
|
Point? PrevPosition { get; }
|
||||||
string CurrentNodeId { get; }
|
MapNode CurrentNode { get; }
|
||||||
string PrevNodeId { get; }
|
MapNode PrevNode { get; }
|
||||||
DockingDirection DockingDirection { get; }
|
DockingDirection DockingDirection { get; }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,10 @@
|
|||||||
|
using AGVNavigationCore.Models;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Drawing;
|
using System.Drawing;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Windows.Forms;
|
using System.Windows.Forms;
|
||||||
using AGVNavigationCore.Models;
|
using System.Xml.Linq;
|
||||||
|
|
||||||
namespace AGVNavigationCore.Controls
|
namespace AGVNavigationCore.Controls
|
||||||
{
|
{
|
||||||
@@ -15,7 +17,7 @@ namespace AGVNavigationCore.Controls
|
|||||||
Focus(); // 포커스 설정
|
Focus(); // 포커스 설정
|
||||||
|
|
||||||
var worldPoint = ScreenToWorld(e.Location);
|
var worldPoint = ScreenToWorld(e.Location);
|
||||||
var hitNode = GetNodeAt(worldPoint);
|
var hitNode = GetItemAt(worldPoint);
|
||||||
|
|
||||||
// 에뮬레이터 모드 처리
|
// 에뮬레이터 모드 처리
|
||||||
if (_canvasMode == CanvasMode.Emulator)
|
if (_canvasMode == CanvasMode.Emulator)
|
||||||
@@ -48,7 +50,11 @@ namespace AGVNavigationCore.Controls
|
|||||||
// 마지막 선택된 노드 업데이트 (단일 참조용)
|
// 마지막 선택된 노드 업데이트 (단일 참조용)
|
||||||
_selectedNode = _selectedNodes.Count > 0 ? _selectedNodes[_selectedNodes.Count - 1] : null;
|
_selectedNode = _selectedNodes.Count > 0 ? _selectedNodes[_selectedNodes.Count - 1] : null;
|
||||||
|
|
||||||
// 다중 선택 이벤트만 발생 (OnNodesSelected에서 단일/다중 구분 처리)
|
// 단일/다중 선택 이벤트 발생
|
||||||
|
if (_selectedNodes.Count == 1)
|
||||||
|
{
|
||||||
|
NodeSelect?.Invoke(this, _selectedNodes[0], e);
|
||||||
|
}
|
||||||
NodesSelected?.Invoke(this, _selectedNodes);
|
NodesSelected?.Invoke(this, _selectedNodes);
|
||||||
Invalidate();
|
Invalidate();
|
||||||
}
|
}
|
||||||
@@ -59,7 +65,8 @@ namespace AGVNavigationCore.Controls
|
|||||||
_selectedNodes.Clear();
|
_selectedNodes.Clear();
|
||||||
_selectedNodes.Add(hitNode);
|
_selectedNodes.Add(hitNode);
|
||||||
|
|
||||||
// NodesSelected 이벤트만 발생 (OnNodesSelected에서 단일/다중 구분 처리)
|
// 단일/다중 선택 이벤트 발생
|
||||||
|
NodeSelect?.Invoke(this, hitNode, e);
|
||||||
NodesSelected?.Invoke(this, _selectedNodes);
|
NodesSelected?.Invoke(this, _selectedNodes);
|
||||||
Invalidate();
|
Invalidate();
|
||||||
}
|
}
|
||||||
@@ -94,7 +101,7 @@ namespace AGVNavigationCore.Controls
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case EditMode.Connect:
|
case EditMode.Connect:
|
||||||
HandleConnectClick(hitNode);
|
HandleConnectClick(hitNode as MapNode);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case EditMode.Delete:
|
case EditMode.Delete:
|
||||||
@@ -110,38 +117,29 @@ namespace AGVNavigationCore.Controls
|
|||||||
private void UnifiedAGVCanvas_MouseDoubleClick(object sender, MouseEventArgs e)
|
private void UnifiedAGVCanvas_MouseDoubleClick(object sender, MouseEventArgs e)
|
||||||
{
|
{
|
||||||
var worldPoint = ScreenToWorld(e.Location);
|
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)
|
||||||
{
|
{
|
||||||
// 노드 타입별 더블클릭 액션
|
HandleNormalNodeDoubleClick(hitNode as MapNode);
|
||||||
switch (hitNode.Type)
|
}
|
||||||
{
|
else if (hitNode.Type == NodeType.Label)
|
||||||
case NodeType.Normal:
|
{
|
||||||
case NodeType.Loader:
|
HandleLabelDoubleClick(hitNode as MapLabel);
|
||||||
case NodeType.UnLoader:
|
}
|
||||||
case NodeType.Clearner:
|
else if (hitNode.Type == NodeType.Image)
|
||||||
case NodeType.Buffer:
|
{
|
||||||
case NodeType.Charging:
|
HandleImageDoubleClick(hitNode as MapImage);
|
||||||
HandleNormalNodeDoubleClick(hitNode);
|
}
|
||||||
break;
|
else if (hitNode.Type == NodeType.Mark)
|
||||||
|
{
|
||||||
case NodeType.Label:
|
HandleMarkDoubleClick(hitNode as MapMark);
|
||||||
HandleLabelNodeDoubleClick(hitNode);
|
}
|
||||||
break;
|
else if (hitNode.Type == NodeType.Magnet)
|
||||||
|
{
|
||||||
case NodeType.Image:
|
HandleMagnetDoubleClick(hitNode as MapMagnet);
|
||||||
HandleImageNodeDoubleClick(hitNode);
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
// 기본 동작: 노드 선택 이벤트 발생
|
|
||||||
_selectedNode = hitNode;
|
|
||||||
_selectedNodes.Clear();
|
|
||||||
_selectedNodes.Add(hitNode);
|
|
||||||
NodesSelected?.Invoke(this, _selectedNodes);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,7 +148,7 @@ namespace AGVNavigationCore.Controls
|
|||||||
// RFID 입력창 표시
|
// RFID 입력창 표시
|
||||||
string currentRfid = node.RfidId ?? "";
|
string currentRfid = node.RfidId ?? "";
|
||||||
string newRfid = Microsoft.VisualBasic.Interaction.InputBox(
|
string newRfid = Microsoft.VisualBasic.Interaction.InputBox(
|
||||||
$"노드 '{node.Name}'의 RFID를 입력하세요:",
|
$"노드 '{node.RfidId}[{node.Id}]'의 RFID를 입력하세요:",
|
||||||
"RFID 설정",
|
"RFID 설정",
|
||||||
currentRfid);
|
currentRfid);
|
||||||
|
|
||||||
@@ -167,11 +165,19 @@ namespace AGVNavigationCore.Controls
|
|||||||
_selectedNodes.Add(node);
|
_selectedNodes.Add(node);
|
||||||
NodesSelected?.Invoke(this, _selectedNodes);
|
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(
|
string newText = Microsoft.VisualBasic.Interaction.InputBox(
|
||||||
"라벨 텍스트를 입력하세요:",
|
"라벨 텍스트를 입력하세요:",
|
||||||
"라벨 편집",
|
"라벨 편집",
|
||||||
@@ -179,28 +185,24 @@ namespace AGVNavigationCore.Controls
|
|||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(newText) && newText != currentText)
|
if (!string.IsNullOrWhiteSpace(newText) && newText != currentText)
|
||||||
{
|
{
|
||||||
node.LabelText = newText.Trim();
|
label.Text = newText.Trim();
|
||||||
MapChanged?.Invoke(this, EventArgs.Empty);
|
MapChanged?.Invoke(this, EventArgs.Empty);
|
||||||
Invalidate();
|
Invalidate();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 더블클릭 시 해당 노드만 선택 (다중 선택 해제)
|
_selectedNode = label;
|
||||||
_selectedNode = node;
|
LabelDoubleClicked?.Invoke(this, label);
|
||||||
_selectedNodes.Clear();
|
Invalidate();
|
||||||
_selectedNodes.Add(node);
|
|
||||||
NodesSelected?.Invoke(this, _selectedNodes);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void HandleImageNodeDoubleClick(MapNode node)
|
private void HandleImageDoubleClick(MapImage image)
|
||||||
{
|
{
|
||||||
// 더블클릭 시 해당 노드만 선택 (다중 선택 해제)
|
_selectedNode = image;
|
||||||
_selectedNode = node;
|
|
||||||
_selectedNodes.Clear();
|
|
||||||
_selectedNodes.Add(node);
|
|
||||||
NodesSelected?.Invoke(this, _selectedNodes);
|
|
||||||
|
|
||||||
// 이미지 편집 이벤트 발생 (MainForm에서 처리)
|
// 이미지 편집 이벤트 발생 (MainForm에서 처리)
|
||||||
ImageNodeDoubleClicked?.Invoke(this, node);
|
ImageDoubleClicked?.Invoke(this, image);
|
||||||
|
|
||||||
|
Invalidate();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UnifiedAGVCanvas_MouseDown(object sender, MouseEventArgs e)
|
private void UnifiedAGVCanvas_MouseDown(object sender, MouseEventArgs e)
|
||||||
@@ -211,24 +213,23 @@ namespace AGVNavigationCore.Controls
|
|||||||
{
|
{
|
||||||
if (_editMode == EditMode.Move)
|
if (_editMode == EditMode.Move)
|
||||||
{
|
{
|
||||||
var hitNode = GetNodeAt(worldPoint);
|
// 1. 노드 선택 확인
|
||||||
|
var hitNode = GetItemAt(worldPoint);
|
||||||
if (hitNode != null)
|
if (hitNode != null)
|
||||||
{
|
{
|
||||||
_isDragging = true;
|
_isDragging = true;
|
||||||
_isPanning = false; // 🔥 팬 모드 비활성화 - 중요!
|
_isPanning = false;
|
||||||
_selectedNode = hitNode;
|
_selectedNode = hitNode;
|
||||||
_dragStartPosition = hitNode.Position; // 원래 위치 저장 (고스트용)
|
_dragStartPosition = hitNode.Position;
|
||||||
_dragOffset = new Point(
|
_dragOffset = new Point(worldPoint.X - hitNode.Position.X, worldPoint.Y - hitNode.Position.Y);
|
||||||
worldPoint.X - hitNode.Position.X,
|
_mouseMoveCounter = 0;
|
||||||
worldPoint.Y - hitNode.Position.Y
|
|
||||||
);
|
|
||||||
_mouseMoveCounter = 0; // 디버그: 카운터 리셋
|
|
||||||
Cursor = Cursors.SizeAll;
|
Cursor = Cursors.SizeAll;
|
||||||
Capture = true; // 🔥 마우스 캡처 활성화 - 이게 핵심!
|
Capture = true;
|
||||||
//System.Diagnostics.Debug.WriteLine($"MouseDown: 드래그 시작! Capture={Capture}, isDragging={_isDragging}, isPanning={_isPanning}, Node={hitNode.NodeId}");
|
|
||||||
Invalidate();
|
Invalidate();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 팬 시작 (좌클릭 - 모드에 따라)
|
// 팬 시작 (좌클릭 - 모드에 따라)
|
||||||
@@ -250,7 +251,9 @@ namespace AGVNavigationCore.Controls
|
|||||||
// 컨텍스트 메뉴 (편집 모드에서만)
|
// 컨텍스트 메뉴 (편집 모드에서만)
|
||||||
if (_canvasMode == CanvasMode.Edit)
|
if (_canvasMode == CanvasMode.Edit)
|
||||||
{
|
{
|
||||||
var hitNode = GetNodeAt(worldPoint);
|
var hitNode = GetItemAt(worldPoint);
|
||||||
|
// TODO: 라벨/이미지에 대한 컨텍스트 메뉴도 지원하려면 여기서 hitLabel/hitImage 확인해서 전달
|
||||||
|
// 현재는 ShowContextMenu가 MapNode만 받으므로 노드만 처리
|
||||||
ShowContextMenu(e.Location, hitNode);
|
ShowContextMenu(e.Location, hitNode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -266,9 +269,12 @@ namespace AGVNavigationCore.Controls
|
|||||||
_mouseMoveCounter++;
|
_mouseMoveCounter++;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 호버 노드 업데이트
|
// 호버 업데이트
|
||||||
var newHoveredNode = GetNodeAt(worldPoint);
|
var newHoveredNode = GetItemAt(worldPoint);
|
||||||
if (newHoveredNode != _hoveredNode)
|
|
||||||
|
bool hoverChanged = (newHoveredNode != _hoveredNode) ;
|
||||||
|
|
||||||
|
if (hoverChanged)
|
||||||
{
|
{
|
||||||
_hoveredNode = newHoveredNode;
|
_hoveredNode = newHoveredNode;
|
||||||
Invalidate();
|
Invalidate();
|
||||||
@@ -291,24 +297,31 @@ namespace AGVNavigationCore.Controls
|
|||||||
}
|
}
|
||||||
else if (_isDragging && _canvasMode == CanvasMode.Edit)
|
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)
|
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;
|
_selectedNode.Position = newPosition;
|
||||||
NodeMoved?.Invoke(this, _selectedNode);
|
NodeMoved?.Invoke(this, _selectedNode);
|
||||||
|
moved = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (moved)
|
||||||
|
{
|
||||||
MapChanged?.Invoke(this, EventArgs.Empty);
|
MapChanged?.Invoke(this, EventArgs.Empty);
|
||||||
Invalidate();
|
Invalidate();
|
||||||
Update(); // 🔥 즉시 Paint 이벤트 처리하여 화면 업데이트
|
Update(); // 🔥 즉시 Paint 이벤트 처리하여 화면 업데이트
|
||||||
@@ -409,51 +422,77 @@ namespace AGVNavigationCore.Controls
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private MapNode GetNodeAt(Point worldPoint)
|
private NodeBase GetItemAt(Point worldPoint)
|
||||||
{
|
{
|
||||||
if (_nodes == null) return null;
|
if (_labels != null)
|
||||||
|
|
||||||
// 역순으로 검사하여 위에 그려진 노드부터 확인
|
|
||||||
for (int i = _nodes.Count - 1; i >= 0; i--)
|
|
||||||
{
|
{
|
||||||
var node = _nodes[i];
|
// 역순으로 검사하여 위에 그려진 노드부터 확인
|
||||||
if (IsPointInNode(worldPoint, node))
|
for (int i = _labels.Count - 1; i >= 0; i--)
|
||||||
return node;
|
{
|
||||||
|
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;
|
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 IsPointInLabel(point, label);
|
||||||
return IsPointInLabelNode(point, node);
|
|
||||||
case NodeType.Image:
|
|
||||||
return IsPointInImageNode(point, node);
|
|
||||||
default:
|
|
||||||
return IsPointInCircularNode(point, node);
|
|
||||||
}
|
}
|
||||||
|
if (node is MapImage image)
|
||||||
|
{
|
||||||
|
return IsPointInImage(point, image);
|
||||||
|
}
|
||||||
|
// 라벨과 이미지는 별도 리스트로 관리되므로 여기서 처리하지 않음
|
||||||
|
// 하지만 혹시 모를 하위 호환성을 위해 타입 체크는 유지하되,
|
||||||
|
// 실제 로직은 CircularNode 등으로 분기
|
||||||
|
return IsPointInCircularNode(point, node as MapNode);
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool IsPointInCircularNode(Point point, MapNode node)
|
private bool IsPointInCircularNode(Point point, MapNode node)
|
||||||
{
|
{
|
||||||
switch (node.Type)
|
switch (node.StationType)
|
||||||
{
|
{
|
||||||
case NodeType.Loader:
|
case StationType.Loader:
|
||||||
case NodeType.UnLoader:
|
case StationType.UnLoader:
|
||||||
case NodeType.Clearner:
|
case StationType.Clearner:
|
||||||
case NodeType.Buffer:
|
case StationType.Buffer:
|
||||||
return IsPointInPentagon(point, node);
|
return IsPointInPentagon(point, node);
|
||||||
case NodeType.Charging:
|
case StationType.Charger:
|
||||||
return IsPointInTriangle(point, node);
|
return IsPointInTriangle(point, node);
|
||||||
default:
|
default:
|
||||||
return IsPointInCircle(point, node);
|
return IsPointInCircle(point, node);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool IsPointInCircle(Point point, MapNode node)
|
private bool IsPointInCircle(Point point, NodeBase node)
|
||||||
{
|
{
|
||||||
// 화면에서 최소 20픽셀 정도의 히트 영역을 확보하되, 노드 크기보다 작아지지 않게 함
|
// 화면에서 최소 20픽셀 정도의 히트 영역을 확보하되, 노드 크기보다 작아지지 않게 함
|
||||||
var minHitRadiusInScreen = 20;
|
var minHitRadiusInScreen = 20;
|
||||||
@@ -466,7 +505,7 @@ namespace AGVNavigationCore.Controls
|
|||||||
return distance <= hitRadius;
|
return distance <= hitRadius;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool IsPointInPentagon(Point point, MapNode node)
|
private bool IsPointInPentagon(Point point, NodeBase node)
|
||||||
{
|
{
|
||||||
// 화면에서 최소 20픽셀 정도의 히트 영역을 확보
|
// 화면에서 최소 20픽셀 정도의 히트 영역을 확보
|
||||||
var minHitRadiusInScreen = 20;
|
var minHitRadiusInScreen = 20;
|
||||||
@@ -487,7 +526,7 @@ namespace AGVNavigationCore.Controls
|
|||||||
return IsPointInPolygon(point, points);
|
return IsPointInPolygon(point, points);
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool IsPointInTriangle(Point point, MapNode node)
|
private bool IsPointInTriangle(Point point, NodeBase node)
|
||||||
{
|
{
|
||||||
// 화면에서 최소 20픽셀 정도의 히트 영역을 확보하되, 노드 크기보다 작아지지 않게 함
|
// 화면에서 최소 20픽셀 정도의 히트 영역을 확보하되, 노드 크기보다 작아지지 않게 함
|
||||||
var minHitRadiusInScreen = 20;
|
var minHitRadiusInScreen = 20;
|
||||||
@@ -532,38 +571,68 @@ namespace AGVNavigationCore.Controls
|
|||||||
return inside;
|
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로 텍스트 크기 측정
|
// Graphics 객체 임시 생성 (Using CreateGraphics is faster than new Bitmap)
|
||||||
using (var tempBitmap = new Bitmap(1, 1))
|
using (var g = this.CreateGraphics())
|
||||||
using (var tempGraphics = Graphics.FromImage(tempBitmap))
|
|
||||||
{
|
{
|
||||||
var font = new Font(node.FontFamily, node.FontSize, node.FontStyle);
|
// Font 생성 로직: 사용자 정의 폰트가 있으면 생성, 없으면 기본 폰트 사용 (Dispose 주의)
|
||||||
var textSize = tempGraphics.MeasureString(text, font);
|
Font fontToUse = null;
|
||||||
|
bool shouldDisposeFont = false;
|
||||||
|
|
||||||
var textRect = new Rectangle(
|
try
|
||||||
(int)(node.Position.X - textSize.Width / 2),
|
{
|
||||||
(int)(node.Position.Y - textSize.Height / 2),
|
if (string.IsNullOrEmpty(label.FontFamily) || label.FontSize <= 0)
|
||||||
(int)textSize.Width,
|
{
|
||||||
(int)textSize.Height
|
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();
|
var textSize = g.MeasureString(text, fontToUse);
|
||||||
return textRect.Contains(point);
|
|
||||||
|
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)
|
if (displaySize.IsEmpty)
|
||||||
displaySize = new Size(50, 50); // 기본 크기
|
displaySize = new Size(50, 50); // 기본 크기
|
||||||
|
|
||||||
var imageRect = new Rectangle(
|
var imageRect = new Rectangle(
|
||||||
node.Position.X - displaySize.Width / 2,
|
image.Position.X - displaySize.Width / 2,
|
||||||
node.Position.Y - displaySize.Height / 2,
|
image.Position.Y - displaySize.Height / 2,
|
||||||
displaySize.Width,
|
displaySize.Width,
|
||||||
displaySize.Height
|
displaySize.Height
|
||||||
);
|
);
|
||||||
@@ -571,6 +640,32 @@ namespace AGVNavigationCore.Controls
|
|||||||
return imageRect.Contains(point);
|
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)
|
private IAGV GetAGVAt(Point worldPoint)
|
||||||
{
|
{
|
||||||
if (_agvList == null) return null;
|
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)
|
if (hitNode != null)
|
||||||
{
|
{
|
||||||
@@ -605,8 +700,8 @@ namespace AGVNavigationCore.Controls
|
|||||||
{
|
{
|
||||||
// 연결선을 클릭했을 때 삭제 확인
|
// 연결선을 클릭했을 때 삭제 확인
|
||||||
var (fromNode, toNode) = connection.Value;
|
var (fromNode, toNode) = connection.Value;
|
||||||
string fromDisplay = !string.IsNullOrEmpty(fromNode.RfidId) ? fromNode.RfidId : fromNode.NodeId;
|
string fromDisplay = !string.IsNullOrEmpty(fromNode.RfidId) ? fromNode.RfidId : fromNode.Id;
|
||||||
string toDisplay = !string.IsNullOrEmpty(toNode.RfidId) ? toNode.RfidId : toNode.NodeId;
|
string toDisplay = !string.IsNullOrEmpty(toNode.RfidId) ? toNode.RfidId : toNode.Id;
|
||||||
|
|
||||||
var result = MessageBox.Show(
|
var result = MessageBox.Show(
|
||||||
$"연결을 삭제하시겠습니까?\n\n{fromDisplay} ↔ {toDisplay}",
|
$"연결을 삭제하시겠습니까?\n\n{fromDisplay} ↔ {toDisplay}",
|
||||||
@@ -617,13 +712,13 @@ namespace AGVNavigationCore.Controls
|
|||||||
if (result == DialogResult.Yes)
|
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
|
var newNode = new MapNode
|
||||||
{
|
{
|
||||||
NodeId = newNodeId,
|
Id = newNodeId,
|
||||||
Position = worldPoint,
|
Position = worldPoint
|
||||||
Type = NodeType.Normal
|
|
||||||
};
|
};
|
||||||
|
|
||||||
_nodes.Add(newNode);
|
_nodes.Add(newNode);
|
||||||
@@ -681,20 +775,22 @@ namespace AGVNavigationCore.Controls
|
|||||||
worldPoint.Y = (worldPoint.Y / GRID_SIZE) * GRID_SIZE;
|
worldPoint.Y = (worldPoint.Y / GRID_SIZE) * GRID_SIZE;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 고유한 NodeId 생성
|
// 고유한 NodeId 생성 (라벨도 ID 공유 권장)
|
||||||
string newNodeId = GenerateUniqueNodeId();
|
string newNodeId = GenerateUniqueNodeId();
|
||||||
|
|
||||||
var newNode = new MapNode
|
var newLabel = new MapLabel
|
||||||
{
|
{
|
||||||
NodeId = newNodeId,
|
Id = newNodeId,
|
||||||
Position = worldPoint,
|
Position = worldPoint,
|
||||||
Type = NodeType.Label,
|
Text = "New Label",
|
||||||
Name = "새 라벨"
|
FontSize = 10,
|
||||||
|
FontFamily = "Arial"
|
||||||
};
|
};
|
||||||
|
|
||||||
_nodes.Add(newNode);
|
if (_labels == null) _labels = new List<MapLabel>();
|
||||||
|
_labels.Add(newLabel);
|
||||||
|
|
||||||
NodeAdded?.Invoke(this, newNode);
|
//NodeAdded?.Invoke(this, newNode); // TODO: 라벨 추가 이벤트 필요?
|
||||||
MapChanged?.Invoke(this, EventArgs.Empty);
|
MapChanged?.Invoke(this, EventArgs.Empty);
|
||||||
Invalidate();
|
Invalidate();
|
||||||
}
|
}
|
||||||
@@ -711,17 +807,17 @@ namespace AGVNavigationCore.Controls
|
|||||||
// 고유한 NodeId 생성
|
// 고유한 NodeId 생성
|
||||||
string newNodeId = GenerateUniqueNodeId();
|
string newNodeId = GenerateUniqueNodeId();
|
||||||
|
|
||||||
var newNode = new MapNode
|
var newImage = new MapImage
|
||||||
{
|
{
|
||||||
NodeId = newNodeId,
|
Id = newNodeId,
|
||||||
Position = worldPoint,
|
Position = worldPoint,
|
||||||
Type = NodeType.Image,
|
Name = "New Image"
|
||||||
Name = "새 이미지"
|
|
||||||
};
|
};
|
||||||
|
|
||||||
_nodes.Add(newNode);
|
if (_images == null) _images = new List<MapImage>();
|
||||||
|
_images.Add(newImage);
|
||||||
|
|
||||||
NodeAdded?.Invoke(this, newNode);
|
//NodeAdded?.Invoke(this, newNode); // TODO: 이미지 추가 이벤트 필요?
|
||||||
MapChanged?.Invoke(this, EventArgs.Empty);
|
MapChanged?.Invoke(this, EventArgs.Empty);
|
||||||
Invalidate();
|
Invalidate();
|
||||||
}
|
}
|
||||||
@@ -739,7 +835,9 @@ namespace AGVNavigationCore.Controls
|
|||||||
nodeId = $"N{counter:D3}";
|
nodeId = $"N{counter:D3}";
|
||||||
counter++;
|
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;
|
_nodeCounter = counter;
|
||||||
return nodeId;
|
return nodeId;
|
||||||
@@ -776,7 +874,7 @@ namespace AGVNavigationCore.Controls
|
|||||||
// 연결된 모든 연결선도 제거
|
// 연결된 모든 연결선도 제거
|
||||||
foreach (var node in _nodes)
|
foreach (var node in _nodes)
|
||||||
{
|
{
|
||||||
node.RemoveConnection(hitNode.NodeId);
|
node.RemoveConnection(hitNode.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
_nodes.Remove(hitNode);
|
_nodes.Remove(hitNode);
|
||||||
@@ -792,13 +890,13 @@ namespace AGVNavigationCore.Controls
|
|||||||
private void CreateConnection(MapNode fromNode, MapNode toNode)
|
private void CreateConnection(MapNode fromNode, MapNode toNode)
|
||||||
{
|
{
|
||||||
// 중복 연결 체크 (양방향)
|
// 중복 연결 체크 (양방향)
|
||||||
if (fromNode.ConnectedNodes.Contains(toNode.NodeId) ||
|
if (fromNode.ConnectedNodes.Contains(toNode.Id) ||
|
||||||
toNode.ConnectedNodes.Contains(fromNode.NodeId))
|
toNode.ConnectedNodes.Contains(fromNode.Id))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// 양방향 연결 생성 (AGV가 양쪽 방향으로 이동 가능하도록)
|
// 양방향 연결 생성 (AGV가 양쪽 방향으로 이동 가능하도록)
|
||||||
fromNode.AddConnection(toNode.NodeId);
|
fromNode.AddConnection(toNode.Id);
|
||||||
toNode.AddConnection(fromNode.NodeId);
|
toNode.AddConnection(fromNode.Id);
|
||||||
|
|
||||||
MapChanged?.Invoke(this, EventArgs.Empty);
|
MapChanged?.Invoke(this, EventArgs.Empty);
|
||||||
}
|
}
|
||||||
@@ -810,20 +908,26 @@ namespace AGVNavigationCore.Controls
|
|||||||
return (float)Math.Sqrt(dx * dx + dy * dy);
|
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();
|
_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.Clear();
|
||||||
_selectedNodes.Add(hitNode);
|
_selectedNodes.Add(hitItem);
|
||||||
NodesSelected?.Invoke(this, _selectedNodes);
|
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("-");
|
_contextMenu.Items.Add("-");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -848,13 +952,13 @@ namespace AGVNavigationCore.Controls
|
|||||||
var (fromNode, toNode) = connection.Value;
|
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)
|
private (MapNode From, MapNode To)? GetConnectionAt(Point worldPoint)
|
||||||
{
|
{
|
||||||
const int CONNECTION_HIT_TOLERANCE = 10;
|
const int CONNECTION_HIT_TOLERANCE = 10;
|
||||||
|
if (_nodes == null) return null;
|
||||||
|
|
||||||
// 모든 연결선을 확인하여 클릭한 위치와 가장 가까운 연결선 찾기
|
// 모든 연결선을 확인하여 클릭한 위치와 가장 가까운 연결선 찾기
|
||||||
foreach (var fromNode in _nodes)
|
foreach (var fromNode in _nodes)
|
||||||
{
|
{
|
||||||
foreach (var toNodeId in fromNode.ConnectedNodes)
|
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)
|
if (toNode != null)
|
||||||
{
|
{
|
||||||
// 연결선과 클릭 위치 간의 거리 계산
|
// 연결선과 클릭 위치 간의 거리 계산
|
||||||
@@ -889,6 +994,49 @@ namespace AGVNavigationCore.Controls
|
|||||||
return null;
|
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)
|
private float CalculatePointToLineDistance(Point point, Point lineStart, Point lineEnd)
|
||||||
{
|
{
|
||||||
// 점에서 선분까지의 거리 계산
|
// 점에서 선분까지의 거리 계산
|
||||||
@@ -928,27 +1076,21 @@ namespace AGVNavigationCore.Controls
|
|||||||
{
|
{
|
||||||
string tooltipText = "";
|
string tooltipText = "";
|
||||||
|
|
||||||
// 노드 툴팁
|
var hitNode = GetItemAt(worldPoint);
|
||||||
var hitNode = GetNodeAt(worldPoint);
|
var hitAGV = GetAGVAt(worldPoint);
|
||||||
|
|
||||||
if (hitNode != null)
|
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})";
|
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})";
|
||||||
else
|
|
||||||
{
|
|
||||||
// AGV 툴팁
|
|
||||||
var hitAGV = GetAGVAt(worldPoint);
|
|
||||||
if (hitAGV != null)
|
|
||||||
{
|
|
||||||
var state = _agvStates.ContainsKey(hitAGV.AgvId) ? _agvStates[hitAGV.AgvId] : AGVState.Idle;
|
|
||||||
tooltipText = $"AGV: {hitAGV.AgvId}\n상태: {state}\n배터리: {hitAGV.BatteryLevel:F1}%\n위치: ({hitAGV.CurrentPosition.X}, {hitAGV.CurrentPosition.Y})";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 툴팁 업데이트 (기존 ToolTip 컨트롤 사용)
|
// 툴팁 텍스트 갱신 (변경되었을 때만)
|
||||||
if (!string.IsNullOrEmpty(tooltipText))
|
if (_tooltip != null && _tooltip.GetToolTip(this) != tooltipText)
|
||||||
{
|
{
|
||||||
// ToolTip 설정 (필요시 추가 구현)
|
_tooltip.SetToolTip(this, tooltipText);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1016,7 +1158,7 @@ namespace AGVNavigationCore.Controls
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public void PanToNode(string nodeId)
|
public void PanToNode(string nodeId)
|
||||||
{
|
{
|
||||||
var node = _nodes?.FirstOrDefault(n => n.NodeId == nodeId);
|
var node = _nodes?.FirstOrDefault(n => n.Id == nodeId);
|
||||||
if (node != null)
|
if (node != null)
|
||||||
{
|
{
|
||||||
PanTo(node.Position);
|
PanTo(node.Position);
|
||||||
|
|||||||
@@ -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.Models;
|
||||||
using AGVNavigationCore.PathFinding;
|
using AGVNavigationCore.PathFinding;
|
||||||
using AGVNavigationCore.PathFinding.Core;
|
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
|
namespace AGVNavigationCore.Controls
|
||||||
{
|
{
|
||||||
@@ -65,10 +67,19 @@ namespace AGVNavigationCore.Controls
|
|||||||
|
|
||||||
// 맵 데이터
|
// 맵 데이터
|
||||||
private List<MapNode> _nodes;
|
private List<MapNode> _nodes;
|
||||||
private MapNode _selectedNode;
|
private List<MapLabel> _labels; // 추가
|
||||||
private List<MapNode> _selectedNodes; // 다중 선택
|
private List<MapImage> _images; // 추가
|
||||||
private MapNode _hoveredNode;
|
private List<MapMark> _marks;
|
||||||
private MapNode _destinationNode;
|
private List<MapMagnet> _magnets;
|
||||||
|
|
||||||
|
// 선택된 객체들 (나중에 NodeBase로 통일 필요)
|
||||||
|
private NodeBase _selectedNode;
|
||||||
|
|
||||||
|
private List<NodeBase> _selectedNodes; // 다중 선택 (NodeBase로 변경 고려)
|
||||||
|
|
||||||
|
private NodeBase _hoveredNode;
|
||||||
|
|
||||||
|
private NodeBase _destinationNode;
|
||||||
|
|
||||||
// AGV 관련
|
// AGV 관련
|
||||||
private List<IAGV> _agvList;
|
private List<IAGV> _agvList;
|
||||||
@@ -143,24 +154,31 @@ namespace AGVNavigationCore.Controls
|
|||||||
private Pen _pathPen;
|
private Pen _pathPen;
|
||||||
private Pen _agvPen;
|
private Pen _agvPen;
|
||||||
private Pen _highlightedConnectionPen;
|
private Pen _highlightedConnectionPen;
|
||||||
|
private Pen _magnetPen;
|
||||||
|
private Pen _markPen;
|
||||||
|
private ToolTip _tooltip;
|
||||||
|
|
||||||
// 컨텍스트 메뉴
|
// 컨텍스트 메뉴
|
||||||
private ContextMenuStrip _contextMenu;
|
private ContextMenuStrip _contextMenu;
|
||||||
|
|
||||||
// 이벤트
|
// 이벤트
|
||||||
public event EventHandler<MapNode> NodeRightClicked;
|
public event EventHandler<NodeBase> NodeRightClicked;
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Events
|
#region Events
|
||||||
|
|
||||||
// 맵 편집 이벤트
|
// 맵 편집 이벤트
|
||||||
public event EventHandler<MapNode> NodeAdded;
|
public delegate void NodeSelectHandler(object sender, NodeBase node, MouseEventArgs e);
|
||||||
public event EventHandler<List<MapNode>> NodesSelected; // 다중 선택 이벤트
|
public event NodeSelectHandler NodeSelect;
|
||||||
public event EventHandler<MapNode> NodeDeleted;
|
|
||||||
public event EventHandler<MapNode> NodeMoved;
|
public event EventHandler<NodeBase> NodeAdded;
|
||||||
|
public event EventHandler<List<NodeBase>> NodesSelected; // 다중 선택 이벤트
|
||||||
|
public event EventHandler<NodeBase> NodeDeleted;
|
||||||
|
public event EventHandler<NodeBase> NodeMoved;
|
||||||
public event EventHandler<(MapNode From, MapNode To)> ConnectionDeleted;
|
public event EventHandler<(MapNode From, MapNode To)> ConnectionDeleted;
|
||||||
public event EventHandler<MapNode> ImageNodeDoubleClicked;
|
public event EventHandler<MapImage> ImageDoubleClicked;
|
||||||
|
public event EventHandler<MapLabel> LabelDoubleClicked;
|
||||||
public event EventHandler MapChanged;
|
public event EventHandler MapChanged;
|
||||||
|
|
||||||
#endregion
|
#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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 편집 모드 (CanvasMode.Edit일 때만 적용)
|
/// 편집 모드 (CanvasMode.Edit일 때만 적용)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -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; }
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 선택된 노드 (단일)
|
/// 선택된 노드 (단일)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public MapNode SelectedNode => _selectedNode;
|
public MapNode SelectedNode
|
||||||
|
{
|
||||||
|
get { return this._selectedNode as MapNode; }
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 선택된 노드들 (다중)
|
/// 선택된 노드들 (다중)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public List<MapNode> SelectedNodes => _selectedNodes ?? new List<MapNode>();
|
public List<NodeBase> SelectedNodes => _selectedNodes ?? new List<NodeBase>();
|
||||||
|
|
||||||
|
|
||||||
|
public List<NodeBase> Items
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
List<NodeBase> items = new List<NodeBase>();
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 노드 목록
|
/// 노드 목록
|
||||||
@@ -260,6 +375,58 @@ namespace AGVNavigationCore.Controls
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 라벨 목록
|
||||||
|
/// </summary>
|
||||||
|
public List<MapLabel> Labels
|
||||||
|
{
|
||||||
|
get => _labels ?? new List<MapLabel>();
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_labels = value ?? new List<MapLabel>();
|
||||||
|
Invalidate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 이미지 목록
|
||||||
|
/// </summary>
|
||||||
|
public List<MapImage> Images
|
||||||
|
{
|
||||||
|
get => _images ?? new List<MapImage>();
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_images = value ?? new List<MapImage>();
|
||||||
|
Invalidate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 마크 목록
|
||||||
|
/// </summary>
|
||||||
|
public List<MapMark> Marks
|
||||||
|
{
|
||||||
|
get => _marks ?? new List<MapMark>();
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_marks = value ?? new List<MapMark>();
|
||||||
|
Invalidate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 마그넷 목록
|
||||||
|
/// </summary>
|
||||||
|
public List<MapMagnet> Magnets
|
||||||
|
{
|
||||||
|
get => _magnets ?? new List<MapMagnet>();
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_magnets = value ?? new List<MapMagnet>();
|
||||||
|
Invalidate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// AGV 목록
|
/// AGV 목록
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -389,7 +556,12 @@ namespace AGVNavigationCore.Controls
|
|||||||
ControlStyles.ResizeRedraw, true);
|
ControlStyles.ResizeRedraw, true);
|
||||||
|
|
||||||
_nodes = new List<MapNode>();
|
_nodes = new List<MapNode>();
|
||||||
_selectedNodes = new List<MapNode>(); // 다중 선택 리스트 초기화
|
_labels = new List<MapLabel>();
|
||||||
|
_images = new List<MapImage>();
|
||||||
|
_marks = new List<MapMark>();
|
||||||
|
_magnets = new List<MapMagnet>();
|
||||||
|
|
||||||
|
_selectedNodes = new List<NodeBase>(); // 다중 선택 리스트 초기화
|
||||||
_agvList = new List<IAGV>();
|
_agvList = new List<IAGV>();
|
||||||
_agvPositions = new Dictionary<string, Point>();
|
_agvPositions = new Dictionary<string, Point>();
|
||||||
_agvDirections = new Dictionary<string, AgvDirection>();
|
_agvDirections = new Dictionary<string, AgvDirection>();
|
||||||
@@ -399,6 +571,12 @@ namespace AGVNavigationCore.Controls
|
|||||||
|
|
||||||
InitializeBrushesAndPens();
|
InitializeBrushesAndPens();
|
||||||
CreateContextMenu();
|
CreateContextMenu();
|
||||||
|
|
||||||
|
_tooltip = new ToolTip();
|
||||||
|
_tooltip.AutoPopDelay = 5000;
|
||||||
|
_tooltip.InitialDelay = 1000;
|
||||||
|
_tooltip.ReshowDelay = 500;
|
||||||
|
_tooltip.ShowAlways = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void InitializeBrushesAndPens()
|
private void InitializeBrushesAndPens()
|
||||||
@@ -421,6 +599,7 @@ namespace AGVNavigationCore.Controls
|
|||||||
|
|
||||||
// 펜
|
// 펜
|
||||||
_connectionPen = new Pen(Color.DarkBlue, CONNECTION_WIDTH);
|
_connectionPen = new Pen(Color.DarkBlue, CONNECTION_WIDTH);
|
||||||
|
_connectionPen.DashStyle = DashStyle.Dash;
|
||||||
_connectionPen.EndCap = LineCap.ArrowAnchor;
|
_connectionPen.EndCap = LineCap.ArrowAnchor;
|
||||||
|
|
||||||
_gridPen = new Pen(Color.LightGray, 1);
|
_gridPen = new Pen(Color.LightGray, 1);
|
||||||
@@ -430,6 +609,8 @@ namespace AGVNavigationCore.Controls
|
|||||||
_pathPen = new Pen(Color.Purple, 3);
|
_pathPen = new Pen(Color.Purple, 3);
|
||||||
_agvPen = new Pen(Color.Red, 3);
|
_agvPen = new Pen(Color.Red, 3);
|
||||||
_highlightedConnectionPen = new Pen(Color.Red, 4) { DashStyle = DashStyle.Solid };
|
_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()
|
private void CreateContextMenu()
|
||||||
@@ -621,6 +802,8 @@ namespace AGVNavigationCore.Controls
|
|||||||
_pathPen?.Dispose();
|
_pathPen?.Dispose();
|
||||||
_agvPen?.Dispose();
|
_agvPen?.Dispose();
|
||||||
_highlightedConnectionPen?.Dispose();
|
_highlightedConnectionPen?.Dispose();
|
||||||
|
_magnetPen?.Dispose();
|
||||||
|
_markPen?.Dispose();
|
||||||
|
|
||||||
// 컨텍스트 메뉴 정리
|
// 컨텍스트 메뉴 정리
|
||||||
_contextMenu?.Dispose();
|
_contextMenu?.Dispose();
|
||||||
@@ -671,7 +854,7 @@ namespace AGVNavigationCore.Controls
|
|||||||
for (int i = 1; i < kvp.Value.Count; i++)
|
for (int i = 1; i < kvp.Value.Count; i++)
|
||||||
{
|
{
|
||||||
int duplicateNodeIndex = kvp.Value[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)
|
foreach (var node in _nodes)
|
||||||
{
|
{
|
||||||
// NodeId에서 숫자 부분 추출 (예: "N001" -> 1)
|
// 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);
|
maxNumber = Math.Max(maxNumber, number);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,26 +11,18 @@ namespace AGVNavigationCore.Models
|
|||||||
{
|
{
|
||||||
/// <summary>일반 경로 노드</summary>
|
/// <summary>일반 경로 노드</summary>
|
||||||
Normal,
|
Normal,
|
||||||
/// <summary>로더</summary>
|
|
||||||
Loader,
|
|
||||||
/// <summary>
|
|
||||||
/// 언로더
|
|
||||||
/// </summary>
|
|
||||||
UnLoader,
|
|
||||||
/// <summary>
|
|
||||||
/// 클리너
|
|
||||||
/// </summary>
|
|
||||||
Clearner,
|
|
||||||
/// <summary>
|
|
||||||
/// 버퍼
|
|
||||||
/// </summary>
|
|
||||||
Buffer,
|
|
||||||
/// <summary>충전 스테이션</summary>
|
|
||||||
Charging,
|
|
||||||
/// <summary>라벨 (UI 요소)</summary>
|
|
||||||
Label,
|
Label,
|
||||||
/// <summary>이미지 (UI 요소)</summary>
|
/// <summary>이미지 (UI 요소)</summary>
|
||||||
Image
|
Image,
|
||||||
|
/// <summary>
|
||||||
|
/// 마크센서
|
||||||
|
/// </summary>
|
||||||
|
Mark,
|
||||||
|
/// <summary>
|
||||||
|
/// 마그넷라인
|
||||||
|
/// </summary>
|
||||||
|
Magnet
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -71,13 +63,13 @@ namespace AGVNavigationCore.Models
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 일반노드
|
/// 일반노드
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Node,
|
Normal,
|
||||||
/// <summary>로더</summary>
|
/// <summary>로더</summary>
|
||||||
Loader,
|
Loader,
|
||||||
/// <summary>클리너</summary>
|
/// <summary>클리너</summary>
|
||||||
Cleaner,
|
Clearner,
|
||||||
/// <summary>오프로더</summary>
|
/// <summary>오프로더</summary>
|
||||||
Offloader,
|
UnLoader,
|
||||||
/// <summary>버퍼</summary>
|
/// <summary>버퍼</summary>
|
||||||
Buffer,
|
Buffer,
|
||||||
/// <summary>충전기</summary>
|
/// <summary>충전기</summary>
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ namespace AGVNavigationCore.Models
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 현재 노드 ID
|
/// 현재 노드 ID
|
||||||
/// </summary>
|
/// </summary>
|
||||||
string CurrentNodeId { get; }
|
MapNode CurrentNode { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 목표 위치
|
/// 목표 위치
|
||||||
@@ -92,7 +92,7 @@ namespace AGVNavigationCore.Models
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 목표 노드 ID
|
/// 목표 노드 ID
|
||||||
/// </summary>
|
/// </summary>
|
||||||
string PrevNodeId { get; }
|
MapNode PrevNode { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 도킹 방향
|
/// 도킹 방향
|
||||||
|
|||||||
88
Cs_HMI/AGVLogic/AGVNavigationCore/Models/MapImage.cs
Normal file
88
Cs_HMI/AGVLogic/AGVNavigationCore/Models/MapImage.cs
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
42
Cs_HMI/AGVLogic/AGVNavigationCore/Models/MapLabel.cs
Normal file
42
Cs_HMI/AGVLogic/AGVNavigationCore/Models/MapLabel.cs
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,6 +28,10 @@ namespace AGVNavigationCore.Models
|
|||||||
{
|
{
|
||||||
public bool Success { get; set; }
|
public bool Success { get; set; }
|
||||||
public List<MapNode> Nodes { get; set; } = new List<MapNode>();
|
public List<MapNode> Nodes { get; set; } = new List<MapNode>();
|
||||||
|
public List<MapLabel> Labels { get; set; } = new List<MapLabel>(); // 추가
|
||||||
|
public List<MapImage> Images { get; set; } = new List<MapImage>(); // 추가
|
||||||
|
public List<MapMark> Marks { get; set; } = new List<MapMark>();
|
||||||
|
public List<MapMagnet> Magnets { get; set; } = new List<MapMagnet>();
|
||||||
public MapSettings Settings { get; set; } = new MapSettings();
|
public MapSettings Settings { get; set; } = new MapSettings();
|
||||||
public string ErrorMessage { get; set; } = string.Empty;
|
public string ErrorMessage { get; set; } = string.Empty;
|
||||||
public string Version { get; set; } = string.Empty;
|
public string Version { get; set; } = string.Empty;
|
||||||
@@ -40,9 +44,13 @@ namespace AGVNavigationCore.Models
|
|||||||
public class MapFileData
|
public class MapFileData
|
||||||
{
|
{
|
||||||
public List<MapNode> Nodes { get; set; } = new List<MapNode>();
|
public List<MapNode> Nodes { get; set; } = new List<MapNode>();
|
||||||
|
public List<MapLabel> Labels { get; set; } = new List<MapLabel>(); // 추가
|
||||||
|
public List<MapImage> Images { get; set; } = new List<MapImage>(); // 추가
|
||||||
|
public List<MapMark> Marks { get; set; } = new List<MapMark>();
|
||||||
|
public List<MapMagnet> Magnets { get; set; } = new List<MapMagnet>();
|
||||||
public MapSettings Settings { get; set; } = new MapSettings();
|
public MapSettings Settings { get; set; } = new MapSettings();
|
||||||
public DateTime CreatedDate { get; set; }
|
public DateTime CreatedDate { get; set; }
|
||||||
public string Version { get; set; } = "1.1"; // 버전 업그레이드 (설정 추가)
|
public string Version { get; set; } = "1.3"; // 버전 업그레이드
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -64,7 +72,7 @@ namespace AGVNavigationCore.Models
|
|||||||
|
|
||||||
var json = File.ReadAllText(filePath);
|
var json = File.ReadAllText(filePath);
|
||||||
|
|
||||||
// JSON 역직렬화 설정: 누락된 속성 무시, 안전한 처리
|
// JSON 역직렬화 설정
|
||||||
var settings = new JsonSerializerSettings
|
var settings = new JsonSerializerSettings
|
||||||
{
|
{
|
||||||
MissingMemberHandling = MissingMemberHandling.Ignore,
|
MissingMemberHandling = MissingMemberHandling.Ignore,
|
||||||
@@ -72,36 +80,83 @@ namespace AGVNavigationCore.Models
|
|||||||
DefaultValueHandling = DefaultValueHandling.Populate
|
DefaultValueHandling = DefaultValueHandling.Populate
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 먼저 구조 파악을 위해 동적 객체로 로드하거나, MapFileData로 시도
|
||||||
var mapData = JsonConvert.DeserializeObject<MapFileData>(json, settings);
|
var mapData = JsonConvert.DeserializeObject<MapFileData>(json, settings);
|
||||||
|
|
||||||
if (mapData != null)
|
if (mapData != null)
|
||||||
{
|
{
|
||||||
result.Nodes = mapData.Nodes ?? new List<MapNode>();
|
result.Nodes = new List<MapNode>();
|
||||||
result.Settings = mapData.Settings ?? new MapSettings(); // 설정 로드
|
result.Labels = mapData.Labels ?? new List<MapLabel>();
|
||||||
|
result.Images = mapData.Images ?? new List<MapImage>();
|
||||||
|
result.Marks = mapData.Marks ?? new List<MapMark>();
|
||||||
|
result.Magnets = mapData.Magnets ?? new List<MapMagnet>();
|
||||||
|
result.Settings = mapData.Settings ?? new MapSettings();
|
||||||
result.Version = mapData.Version ?? "1.0";
|
result.Version = mapData.Version ?? "1.0";
|
||||||
result.CreatedDate = mapData.CreatedDate;
|
result.CreatedDate = mapData.CreatedDate;
|
||||||
|
|
||||||
// 기존 Description 데이터를 Name으로 마이그레이션
|
if (mapData.Nodes != null)
|
||||||
MigrateDescriptionToName(result.Nodes);
|
{
|
||||||
|
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 기반으로 설정)
|
// 중복된 NodeId 정리 (Nav Node만)
|
||||||
MigrateDockingDirection(result.Nodes);
|
|
||||||
|
|
||||||
// 중복된 NodeId 정리
|
|
||||||
FixDuplicateNodeIds(result.Nodes);
|
FixDuplicateNodeIds(result.Nodes);
|
||||||
|
|
||||||
// 존재하지 않는 노드에 대한 연결 정리 (고아 연결 제거)
|
// 고아 연결 정리
|
||||||
CleanupOrphanConnections(result.Nodes);
|
CleanupOrphanConnections(result.Nodes);
|
||||||
|
|
||||||
// 양방향 연결 자동 설정 (A→B가 있으면 B→A도 설정)
|
// 양방향 연결 자동 설정
|
||||||
// 주의: CleanupDuplicateConnections()는 제거됨 - 양방향 연결을 단방향으로 변환하는 버그가 있었음
|
|
||||||
EnsureBidirectionalConnections(result.Nodes);
|
EnsureBidirectionalConnections(result.Nodes);
|
||||||
|
|
||||||
// ConnectedMapNodes 채우기 (string ID → MapNode 객체 참조)
|
// ConnectedMapNodes 채우기
|
||||||
ResolveConnectedMapNodes(result.Nodes);
|
ResolveConnectedMapNodes(result.Nodes);
|
||||||
|
|
||||||
// 이미지 노드들의 이미지 로드
|
// 이미지 로드 (MapImage 객체에서)
|
||||||
LoadImageNodes(result.Nodes);
|
foreach (var img in result.Images)
|
||||||
|
{
|
||||||
|
img.LoadImage();
|
||||||
|
}
|
||||||
|
|
||||||
result.Success = true;
|
result.Success = true;
|
||||||
}
|
}
|
||||||
@@ -121,23 +176,23 @@ namespace AGVNavigationCore.Models
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 맵 데이터를 파일로 저장
|
/// 맵 데이터를 파일로 저장
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="filePath">저장할 파일 경로</param>
|
public static bool SaveMapToFile(string filePath, List<MapNode> nodes, List<MapLabel> labels = null, List<MapImage> images = null, List<MapMark> marks = null, List<MapMagnet> magnets = null, MapSettings settings = null)
|
||||||
/// <param name="nodes">맵 노드 목록</param>
|
|
||||||
/// <param name="settings">맵 설정 (배경색, 그리드 표시 등)</param>
|
|
||||||
/// <returns>저장 성공 여부</returns>
|
|
||||||
public static bool SaveMapToFile(string filePath, List<MapNode> nodes, MapSettings settings = null)
|
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// 저장 전 고아 연결 정리 (삭제된 노드에 대한 연결 제거)
|
// 저장 전 고아 연결 정리
|
||||||
CleanupOrphanConnections(nodes);
|
CleanupOrphanConnections(nodes);
|
||||||
|
|
||||||
var mapData = new MapFileData
|
var mapData = new MapFileData
|
||||||
{
|
{
|
||||||
Nodes = nodes,
|
Nodes = nodes,
|
||||||
Settings = settings ?? new MapSettings(), // 설정 저장
|
Labels = labels ?? new List<MapLabel>(),
|
||||||
|
Images = images ?? new List<MapImage>(),
|
||||||
|
Marks = marks ?? new List<MapMark>(),
|
||||||
|
Magnets = magnets ?? new List<MapMagnet>(),
|
||||||
|
Settings = settings ?? new MapSettings(),
|
||||||
CreatedDate = DateTime.Now,
|
CreatedDate = DateTime.Now,
|
||||||
Version = "1.1"
|
Version = "1.3"
|
||||||
};
|
};
|
||||||
|
|
||||||
var json = JsonConvert.SerializeObject(mapData, Formatting.Indented);
|
var json = JsonConvert.SerializeObject(mapData, Formatting.Indented);
|
||||||
@@ -145,27 +200,13 @@ namespace AGVNavigationCore.Models
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
catch (Exception)
|
catch (Exception)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 이미지 노드들의 이미지 로드
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="nodes">노드 목록</param>
|
|
||||||
private static void LoadImageNodes(List<MapNode> nodes)
|
|
||||||
{
|
|
||||||
foreach (var node in nodes)
|
|
||||||
{
|
|
||||||
if (node.Type == NodeType.Image)
|
|
||||||
{
|
|
||||||
node.LoadImage();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ConnectedMapNodes 채우기 (ConnectedNodes의 string ID → MapNode 객체 변환)
|
/// ConnectedMapNodes 채우기 (ConnectedNodes의 string ID → MapNode 객체 변환)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -175,7 +216,7 @@ namespace AGVNavigationCore.Models
|
|||||||
if (mapNodes == null || mapNodes.Count == 0) return;
|
if (mapNodes == null || mapNodes.Count == 0) return;
|
||||||
|
|
||||||
// 빠른 조회를 위한 Dictionary 생성
|
// 빠른 조회를 위한 Dictionary 생성
|
||||||
var nodeDict = mapNodes.ToDictionary(n => n.NodeId, n => n);
|
var nodeDict = mapNodes.ToDictionary(n => n.Id, n => n);
|
||||||
|
|
||||||
foreach (var node in mapNodes)
|
foreach (var node in mapNodes)
|
||||||
{
|
{
|
||||||
@@ -192,6 +233,8 @@ namespace AGVNavigationCore.Models
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -208,39 +251,6 @@ namespace AGVNavigationCore.Models
|
|||||||
// 기존 파일들은 다시 저장될 때 Description 없이 저장됨
|
// 기존 파일들은 다시 저장될 때 Description 없이 저장됨
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 기존 맵 파일의 DockingDirection을 NodeType 기반으로 마이그레이션
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="mapNodes">맵 노드 목록</param>
|
|
||||||
private static void MigrateDockingDirection(List<MapNode> 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 중복된 NodeId를 가진 노드들을 고유한 NodeId로 수정
|
/// 중복된 NodeId를 가진 노드들을 고유한 NodeId로 수정
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -255,13 +265,13 @@ namespace AGVNavigationCore.Models
|
|||||||
// 첫 번째 패스: 중복된 노드들 식별
|
// 첫 번째 패스: 중복된 노드들 식별
|
||||||
foreach (var node in mapNodes)
|
foreach (var node in mapNodes)
|
||||||
{
|
{
|
||||||
if (usedIds.Contains(node.NodeId))
|
if (usedIds.Contains(node.Id))
|
||||||
{
|
{
|
||||||
duplicateNodes.Add(node);
|
duplicateNodes.Add(node);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
usedIds.Add(node.NodeId);
|
usedIds.Add(node.Id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -271,9 +281,9 @@ namespace AGVNavigationCore.Models
|
|||||||
string newNodeId = GenerateUniqueNodeId(usedIds);
|
string newNodeId = GenerateUniqueNodeId(usedIds);
|
||||||
|
|
||||||
// 다른 노드들의 연결에서 기존 NodeId를 새 NodeId로 업데이트
|
// 다른 노드들의 연결에서 기존 NodeId를 새 NodeId로 업데이트
|
||||||
UpdateConnections(mapNodes, duplicateNode.NodeId, newNodeId);
|
UpdateConnections(mapNodes, duplicateNode.Id, newNodeId);
|
||||||
|
|
||||||
duplicateNode.NodeId = newNodeId;
|
duplicateNode.Id = newNodeId;
|
||||||
usedIds.Add(newNodeId);
|
usedIds.Add(newNodeId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -331,7 +341,7 @@ namespace AGVNavigationCore.Models
|
|||||||
if (mapNodes == null || mapNodes.Count == 0) return;
|
if (mapNodes == null || mapNodes.Count == 0) return;
|
||||||
|
|
||||||
// 존재하는 모든 노드 ID 집합 생성
|
// 존재하는 모든 노드 ID 집합 생성
|
||||||
var validNodeIds = new HashSet<string>(mapNodes.Select(n => n.NodeId));
|
var validNodeIds = new HashSet<string>(mapNodes.Select(n => n.Id));
|
||||||
|
|
||||||
// 각 노드의 연결을 검증하고 존재하지 않는 노드 ID 제거
|
// 각 노드의 연결을 검증하고 존재하지 않는 노드 ID 제거
|
||||||
foreach (var node in mapNodes)
|
foreach (var node in mapNodes)
|
||||||
@@ -369,13 +379,13 @@ namespace AGVNavigationCore.Models
|
|||||||
|
|
||||||
foreach (var connectedNodeId in node.ConnectedNodes.ToList())
|
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;
|
if (connectedNode == null) continue;
|
||||||
|
|
||||||
// 연결 쌍의 키 생성 (사전순 정렬)
|
// 연결 쌍의 키 생성 (사전순 정렬)
|
||||||
string pairKey = string.Compare(node.NodeId, connectedNodeId, StringComparison.Ordinal) < 0
|
string pairKey = string.Compare(node.Id, connectedNodeId, StringComparison.Ordinal) < 0
|
||||||
? $"{node.NodeId}-{connectedNodeId}"
|
? $"{node.Id}-{connectedNodeId}"
|
||||||
: $"{connectedNodeId}-{node.NodeId}";
|
: $"{connectedNodeId}-{node.Id}";
|
||||||
|
|
||||||
if (processedPairs.Contains(pairKey))
|
if (processedPairs.Contains(pairKey))
|
||||||
{
|
{
|
||||||
@@ -388,17 +398,17 @@ namespace AGVNavigationCore.Models
|
|||||||
processedPairs.Add(pairKey);
|
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);
|
connectionsToRemove.Add(connectedNodeId);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// 반대 방향 연결 제거
|
// 반대 방향 연결 제거
|
||||||
connectedNode.RemoveConnection(node.NodeId);
|
connectedNode.RemoveConnection(node.Id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -433,16 +443,16 @@ namespace AGVNavigationCore.Models
|
|||||||
// 1단계: 모든 명시적 연결 수집
|
// 1단계: 모든 명시적 연결 수집
|
||||||
foreach (var node in mapNodes)
|
foreach (var node in mapNodes)
|
||||||
{
|
{
|
||||||
if (!allConnections.ContainsKey(node.NodeId))
|
if (!allConnections.ContainsKey(node.Id))
|
||||||
{
|
{
|
||||||
allConnections[node.NodeId] = new HashSet<string>();
|
allConnections[node.Id] = new HashSet<string>();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (node.ConnectedNodes != null)
|
if (node.ConnectedNodes != null)
|
||||||
{
|
{
|
||||||
foreach (var connectedId in node.ConnectedNodes)
|
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)
|
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에 그 노드를 추가 (중복 방지)
|
// 이 노드의 ConnectedNodes에 그 노드를 추가 (중복 방지)
|
||||||
if (!node.ConnectedNodes.Contains(otherNodeId))
|
if (!node.ConnectedNodes.Contains(otherNodeId))
|
||||||
|
|||||||
72
Cs_HMI/AGVLogic/AGVNavigationCore/Models/MapMagnet.cs
Normal file
72
Cs_HMI/AGVLogic/AGVNavigationCore/Models/MapMagnet.cs
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
using System;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Drawing;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace AGVNavigationCore.Models
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 맵 상의 마그넷(Magnet) 정보를 나타내는 클래스
|
||||||
|
/// </summary>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 시작점 Point 반환
|
||||||
|
/// </summary>
|
||||||
|
[Browsable(false)]
|
||||||
|
[JsonIgnore]
|
||||||
|
public Point StartPoint => new Point((int)P1.X, (int)P1.Y);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 끝점 Point 반환
|
||||||
|
/// </summary>
|
||||||
|
[Browsable(false)]
|
||||||
|
[JsonIgnore]
|
||||||
|
public Point EndPoint => new Point((int)P2.X, (int)P2.Y);
|
||||||
|
}
|
||||||
|
}
|
||||||
37
Cs_HMI/AGVLogic/AGVNavigationCore/Models/MapMark.cs
Normal file
37
Cs_HMI/AGVLogic/AGVNavigationCore/Models/MapMark.cs
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
using System;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Drawing;
|
||||||
|
|
||||||
|
namespace AGVNavigationCore.Models
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 맵 상의 마크(Mark) 정보를 나타내는 클래스
|
||||||
|
/// </summary>
|
||||||
|
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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,81 +1,63 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel;
|
||||||
using System.Drawing;
|
using System.Drawing;
|
||||||
using System.Drawing.Drawing2D;
|
using System.Drawing.Drawing2D;
|
||||||
using AGVNavigationCore.Utils;
|
using AGVNavigationCore.Utils;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
namespace AGVNavigationCore.Models
|
namespace AGVNavigationCore.Models
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 맵 노드 정보를 관리하는 클래스
|
/// 맵 노드 정보를 관리하는 클래스 (주행 경로용 노드)
|
||||||
/// 논리적 노드로서 실제 맵의 위치와 속성을 정의
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class MapNode
|
public class MapNode : NodeBase
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// 논리적 노드 ID (맵 에디터에서 관리하는 고유 ID)
|
|
||||||
/// 예: "N001", "N002", "LOADER1", "CHARGER1"
|
|
||||||
/// </summary>
|
|
||||||
public string NodeId { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 노드 표시 이름 (사용자 친화적)
|
|
||||||
/// 예: "로더1", "충전기1", "교차점A", "회전지점1"
|
|
||||||
/// </summary>
|
|
||||||
public string Name { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
/// <summary>
|
[Category("라벨 설정")]
|
||||||
/// 맵 상의 위치 좌표 (픽셀 단위)
|
[Description("표시할 텍스트입니다.")]
|
||||||
/// </summary>
|
public string Text { get; set; } = "";
|
||||||
public Point Position { get; set; } = Point.Empty;
|
|
||||||
|
|
||||||
/// <summary>
|
public StationType StationType { get; set; }
|
||||||
/// 노드 타입
|
|
||||||
/// </summary>
|
|
||||||
public NodeType Type { get; set; } = NodeType.Normal;
|
|
||||||
|
|
||||||
|
[Browsable(false)]
|
||||||
public bool CanDocking
|
public bool CanDocking
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
if (Type == NodeType.Buffer) return true;
|
if (StationType == StationType.Buffer) return true;
|
||||||
if (Type == NodeType.Loader) return true;
|
if (StationType == StationType.Loader) return true;
|
||||||
if (Type == NodeType.UnLoader) return true;
|
if (StationType == StationType.UnLoader) return true;
|
||||||
if (Type == NodeType.Clearner) return true;
|
if (StationType == StationType.Clearner) return true;
|
||||||
if (Type == NodeType.Charging) return true;
|
if (StationType == StationType.Charger) return true;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
[Category("노드 설정")]
|
||||||
/// 도킹 방향 (도킹/충전 노드인 경우에만 Forward/Backward, 일반 노드는 DontCare)
|
[Description("도킹/충전 노드의 진입 방향입니다.")]
|
||||||
/// </summary>
|
|
||||||
public DockingDirection DockDirection { get; set; } = DockingDirection.DontCare;
|
public DockingDirection DockDirection { get; set; } = DockingDirection.DontCare;
|
||||||
|
|
||||||
/// <summary>
|
[Category("연결 정보")]
|
||||||
/// 연결된 노드 ID 목록 (경로 정보)
|
[Description("연결된 노드 ID 목록입니다.")]
|
||||||
/// </summary>
|
[ReadOnly(true)]
|
||||||
public List<string> ConnectedNodes { get; set; } = new List<string>();
|
public List<string> ConnectedNodes { get; set; } = new List<string>();
|
||||||
|
|
||||||
/// <summary>
|
[JsonIgnore]
|
||||||
/// 연결된 노드 객체 목록 (런타임 전용, JSON 무시)
|
[Browsable(false)]
|
||||||
/// </summary>
|
|
||||||
[Newtonsoft.Json.JsonIgnore]
|
|
||||||
public List<MapNode> ConnectedMapNodes { get; set; } = new List<MapNode>();
|
public List<MapNode> ConnectedMapNodes { get; set; } = new List<MapNode>();
|
||||||
|
|
||||||
/// <summary>
|
[Category("주행 설정")]
|
||||||
/// 회전 가능 여부 (180도 회전 가능한 지점)
|
[Description("제자리 회전(좌) 가능 여부입니다.")]
|
||||||
/// </summary>
|
|
||||||
public bool CanTurnLeft { get; set; } = true;
|
public bool CanTurnLeft { get; set; } = true;
|
||||||
|
|
||||||
/// <summary>
|
[Category("주행 설정")]
|
||||||
/// 회전 가능 여부 (180도 회전 가능한 지점)
|
[Description("제자리 회전(우) 가능 여부입니다.")]
|
||||||
/// </summary>
|
|
||||||
public bool CanTurnRight { get; set; } = true;
|
public bool CanTurnRight { get; set; } = true;
|
||||||
|
|
||||||
/// <summary>
|
[Category("주행 설정")]
|
||||||
/// 교차로로 이용가능한지
|
[Description("교차로 주행 가능 여부입니다.")]
|
||||||
/// </summary>
|
|
||||||
public bool DisableCross
|
public bool DisableCross
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
@@ -85,216 +67,61 @@ namespace AGVNavigationCore.Models
|
|||||||
}
|
}
|
||||||
set { _disablecross = value; }
|
set { _disablecross = value; }
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool _disablecross = false;
|
private bool _disablecross = false;
|
||||||
|
|
||||||
/// <summary>
|
[Category("주행 설정")]
|
||||||
/// 해당 노드 통과 시 제한 속도 (기본값: M - Normal)
|
[Description("노드 통과 시 제한 속도입니다.")]
|
||||||
/// Predict 단계에서 이 값을 참조하여 속도 명령을 생성합니다.
|
|
||||||
/// </summary>
|
|
||||||
public SpeedLevel SpeedLimit { get; set; } = SpeedLevel.M;
|
public SpeedLevel SpeedLimit { get; set; } = SpeedLevel.M;
|
||||||
|
|
||||||
/// <summary>
|
[Category("노드 설정")]
|
||||||
/// 장비 ID (도킹/충전 스테이션인 경우)
|
[Description("장비 ID 또는 별칭입니다.")]
|
||||||
/// 예: "LOADER1", "CLEANER1", "BUFFER1", "CHARGER1"
|
public string AliasName { get; set; } = string.Empty;
|
||||||
/// </summary>
|
|
||||||
public string NodeAlias { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
/// <summary>
|
[Category("기본 정보")]
|
||||||
/// 노드 생성 일자
|
[Description("노드 사용 여부입니다.")]
|
||||||
/// </summary>
|
|
||||||
public DateTime CreatedDate { get; set; } = DateTime.Now;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 노드 수정 일자
|
|
||||||
/// </summary>
|
|
||||||
public DateTime ModifiedDate { get; set; } = DateTime.Now;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 노드 활성화 여부
|
|
||||||
/// </summary>
|
|
||||||
public bool IsActive { get; set; } = true;
|
public bool IsActive { get; set; } = true;
|
||||||
|
|
||||||
/// <summary>
|
[Category("RFID 정보")]
|
||||||
/// 노드 색상 (맵 에디터 표시용)
|
[Description("물리적 RFID 태그 ID입니다.")]
|
||||||
/// </summary>
|
|
||||||
public Color DisplayColor { get; set; } = Color.Blue;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// RFID 태그 ID (이 노드에 매핑된 RFID)
|
|
||||||
/// </summary>
|
|
||||||
public string RfidId { get; set; } = string.Empty;
|
public string RfidId { get; set; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// RFID 상태 (정상, 손상, 교체예정 등)
|
|
||||||
/// </summary>
|
|
||||||
public string RfidStatus { get; set; } = "정상";
|
|
||||||
|
|
||||||
/// <summary>
|
[Category("노드 텍스트"), DisplayName("TextColor")]
|
||||||
/// RFID 설치 위치 설명 (현장 작업자용)
|
[Description("텍스트 색상입니다.")]
|
||||||
/// 예: "로더1번 앞", "충전기2번 입구", "복도 교차점" 등
|
public Color NodeTextForeColor { get; set; } = Color.Black;
|
||||||
/// </summary>
|
|
||||||
public string RfidDescription { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 라벨 텍스트 (NodeType.Label인 경우 사용)
|
|
||||||
/// </summary>
|
|
||||||
public string LabelText { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 라벨 폰트 패밀리 (NodeType.Label인 경우 사용)
|
|
||||||
/// </summary>
|
|
||||||
public string FontFamily { get; set; } = "Arial";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 라벨 폰트 크기 (NodeType.Label인 경우 사용)
|
|
||||||
/// </summary>
|
|
||||||
public float FontSize { get; set; } = 12.0f;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 라벨 폰트 스타일 (NodeType.Label인 경우 사용)
|
|
||||||
/// </summary>
|
|
||||||
public FontStyle FontStyle { get; set; } = FontStyle.Regular;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 텍스트 전경색 (모든 노드 타입에서 사용)
|
|
||||||
/// </summary>
|
|
||||||
public Color ForeColor { get; set; } = Color.Black;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 라벨 배경색 (NodeType.Label인 경우 사용)
|
|
||||||
/// </summary>
|
|
||||||
public Color BackColor { get; set; } = Color.Transparent;
|
|
||||||
|
|
||||||
private float _textFontSize = 7.0f;
|
private float _textFontSize = 7.0f;
|
||||||
|
[Category("노드 텍스트"), DisplayName("TextSize")]
|
||||||
/// <summary>
|
[Description("일반 노드 텍스트의 크기입니다.")]
|
||||||
/// 텍스트 폰트 크기 (모든 노드 타입의 텍스트 표시에 사용, 픽셀 단위)
|
public float NodeTextFontSize
|
||||||
/// 0 이하의 값이 설정되면 기본값 7.0f로 자동 설정
|
|
||||||
/// </summary>
|
|
||||||
public float TextFontSize
|
|
||||||
{
|
{
|
||||||
get => _textFontSize;
|
get => _textFontSize;
|
||||||
set => _textFontSize = value > 0 ? value : 7.0f;
|
set => _textFontSize = value > 0 ? value : 7.0f;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
public MapNode() : base()
|
||||||
/// 텍스트 볼드체 여부 (모든 노드 타입의 텍스트 표시에 사용)
|
|
||||||
/// </summary>
|
|
||||||
public bool TextFontBold { get; set; } = true;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 노드 이름 말풍선 배경색 (하단에 표시되는 노드 이름의 배경색)
|
|
||||||
/// </summary>
|
|
||||||
public Color NameBubbleBackColor { get; set; } = Color.Gold;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 노드 이름 말풍선 글자색 (하단에 표시되는 노드 이름의 글자색)
|
|
||||||
/// </summary>
|
|
||||||
public Color NameBubbleForeColor { get; set; } = Color.Black;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 라벨 배경 표시 여부 (NodeType.Label인 경우 사용)
|
|
||||||
/// </summary>
|
|
||||||
public bool ShowBackground { get; set; } = false;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 라벨 패딩 (NodeType.Label인 경우 사용, 픽셀 단위)
|
|
||||||
/// </summary>
|
|
||||||
public int Padding { get; set; } = 8;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 이미지 파일 경로 (편집용, 저장시엔 사용되지 않음)
|
|
||||||
/// </summary>
|
|
||||||
[Newtonsoft.Json.JsonIgnore]
|
|
||||||
public string ImagePath { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Base64 인코딩된 이미지 데이터 (JSON 저장용)
|
|
||||||
/// </summary>
|
|
||||||
public string ImageBase64 { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 이미지 크기 배율 (NodeType.Image인 경우 사용)
|
|
||||||
/// </summary>
|
|
||||||
public SizeF Scale { get; set; } = new SizeF(1.0f, 1.0f);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 이미지 투명도 (NodeType.Image인 경우 사용, 0.0~1.0)
|
|
||||||
/// </summary>
|
|
||||||
public float Opacity { get; set; } = 1.0f;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 이미지 회전 각도 (NodeType.Image인 경우 사용, 도 단위)
|
|
||||||
/// </summary>
|
|
||||||
public float Rotation { get; set; } = 0.0f;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 로딩된 이미지 (런타임에서만 사용, JSON 직렬화 제외)
|
|
||||||
/// </summary>
|
|
||||||
[Newtonsoft.Json.JsonIgnore]
|
|
||||||
public Image LoadedImage { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 기본 생성자
|
|
||||||
/// </summary>
|
|
||||||
public MapNode()
|
|
||||||
{
|
{
|
||||||
|
Type = NodeType.Normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
public MapNode(string nodeId, Point position, StationType type) : base(nodeId, position)
|
||||||
/// 매개변수 생성자
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="nodeId">노드 ID</param>
|
|
||||||
/// <param name="name">노드 이름</param>
|
|
||||||
/// <param name="position">위치</param>
|
|
||||||
/// <param name="type">노드 타입</param>
|
|
||||||
public MapNode(string nodeId, string name, Point position, NodeType type)
|
|
||||||
{
|
{
|
||||||
NodeId = nodeId;
|
Type = NodeType.Normal;
|
||||||
Name = name;
|
|
||||||
Position = position;
|
|
||||||
Type = type;
|
|
||||||
CreatedDate = DateTime.Now;
|
|
||||||
ModifiedDate = DateTime.Now;
|
|
||||||
|
|
||||||
// 타입별 기본 색상 설정
|
|
||||||
SetDefaultColorByType(type);
|
|
||||||
}
|
}
|
||||||
|
[Category("기본 정보")]
|
||||||
/// <summary>
|
[JsonIgnore]
|
||||||
/// 노드 타입에 따른 기본 색상 설정
|
[ReadOnly(true), Browsable(false)]
|
||||||
/// </summary>
|
public bool isDockingNode
|
||||||
/// <param name="type">노드 타입</param>
|
|
||||||
public void SetDefaultColorByType(NodeType type)
|
|
||||||
{
|
{
|
||||||
switch (type)
|
get
|
||||||
{
|
{
|
||||||
case NodeType.Normal:
|
if (StationType == StationType.Charger || StationType == StationType.Buffer ||
|
||||||
DisplayColor = Color.Blue;
|
StationType == StationType.Clearner || StationType == StationType.Loader ||
|
||||||
break;
|
StationType == StationType.UnLoader) return true;
|
||||||
case NodeType.UnLoader:
|
return false;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 다른 노드와의 연결 추가
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="nodeId">연결할 노드 ID</param>
|
|
||||||
public void AddConnection(string nodeId)
|
public void AddConnection(string nodeId)
|
||||||
{
|
{
|
||||||
if (!ConnectedNodes.Contains(nodeId))
|
if (!ConnectedNodes.Contains(nodeId))
|
||||||
@@ -304,10 +131,6 @@ namespace AGVNavigationCore.Models
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 다른 노드와의 연결 제거
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="nodeId">연결 해제할 노드 ID</param>
|
|
||||||
public void RemoveConnection(string nodeId)
|
public void RemoveConnection(string nodeId)
|
||||||
{
|
{
|
||||||
if (ConnectedNodes.Remove(nodeId))
|
if (ConnectedNodes.Remove(nodeId))
|
||||||
@@ -316,290 +139,29 @@ namespace AGVNavigationCore.Models
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
///// <summary>
|
|
||||||
///// 도킹 스테이션 설정
|
|
||||||
///// </summary>
|
|
||||||
///// <param name="stationId">장비 ID</param>
|
|
||||||
///// <param name="stationType">장비 타입</param>
|
|
||||||
///// <param name="dockDirection">도킹 방향</param>
|
|
||||||
//public void SetDockingStation(string stationId, StationType stationType, DockingDirection dockDirection)
|
|
||||||
//{
|
|
||||||
// Type = NodeType.Docking;
|
|
||||||
// NodeAlias = stationId;
|
|
||||||
// DockDirection = dockDirection;
|
|
||||||
// SetDefaultColorByType(NodeType.Docking);
|
|
||||||
// ModifiedDate = DateTime.Now;
|
|
||||||
//}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 충전 스테이션 설정
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="stationId">충전기 ID</param>
|
|
||||||
public void SetChargingStation(string stationId)
|
public void SetChargingStation(string stationId)
|
||||||
{
|
{
|
||||||
Type = NodeType.Charging;
|
StationType = StationType.Charger;
|
||||||
NodeAlias = stationId;
|
Id = stationId;
|
||||||
DockDirection = DockingDirection.Forward; // 충전기는 항상 전진 도킹
|
DockDirection = DockingDirection.Forward;
|
||||||
SetDefaultColorByType(NodeType.Charging);
|
|
||||||
ModifiedDate = DateTime.Now;
|
ModifiedDate = DateTime.Now;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 문자열 표현
|
|
||||||
/// </summary>
|
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
return $"{RfidId}({NodeId}): {Name} ({Type}) at ({Position.X}, {Position.Y})";
|
return $"{RfidId}({Id}): {AliasName} ({Type}) at ({Position.X}, {Position.Y})";
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 리스트박스 표시용 텍스트 (노드ID - 설명 - RFID 순서)
|
|
||||||
/// </summary>
|
|
||||||
public string DisplayText
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
var displayText = NodeId;
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(Name))
|
|
||||||
{
|
|
||||||
displayText += $" - {Name}";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(RfidId))
|
|
||||||
{
|
|
||||||
displayText += $" - [{RfidId}]";
|
|
||||||
}
|
|
||||||
|
|
||||||
return displayText;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 노드 복사
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>복사된 노드</returns>
|
|
||||||
public MapNode Clone()
|
|
||||||
{
|
|
||||||
var clone = new MapNode
|
|
||||||
{
|
|
||||||
NodeId = NodeId,
|
|
||||||
Name = Name,
|
|
||||||
Position = Position,
|
|
||||||
Type = Type,
|
|
||||||
DockDirection = DockDirection,
|
|
||||||
ConnectedNodes = new List<string>(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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 이미지 로드 (Base64 또는 파일 경로에서, 256x256 이상일 경우 자동 리사이즈)
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>로드 성공 여부</returns>
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 이미지 리사이즈 (비율 유지)
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="image">원본 이미지</param>
|
|
||||||
/// <param name="maxWidth">최대 너비</param>
|
|
||||||
/// <param name="maxHeight">최대 높이</param>
|
|
||||||
/// <returns>리사이즈된 이미지</returns>
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 실제 표시될 크기 계산 (이미지 노드인 경우)
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>실제 크기</returns>
|
|
||||||
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)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 파일 경로에서 이미지를 Base64로 변환하여 저장
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="filePath">이미지 파일 경로</param>
|
|
||||||
/// <returns>변환 성공 여부</returns>
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 리소스 정리
|
|
||||||
/// </summary>
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
LoadedImage?.Dispose();
|
|
||||||
LoadedImage = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 경로 찾기에 사용 가능한 노드인지 확인
|
|
||||||
/// (라벨, 이미지 노드는 경로 찾기에서 제외)
|
|
||||||
/// </summary>
|
|
||||||
public bool IsNavigationNode()
|
public bool IsNavigationNode()
|
||||||
{
|
{
|
||||||
return Type != NodeType.Label && Type != NodeType.Image && IsActive;
|
// 이제 MapNode는 항상 내비게이션 노드임 (Label, Image 분리됨)
|
||||||
|
// 하지만 기존 로직 호환성을 위해 Active 체크만 유지
|
||||||
|
return IsActive;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// RFID가 할당되어 있는지 확인
|
|
||||||
/// </summary>
|
|
||||||
public bool HasRfid()
|
public bool HasRfid()
|
||||||
{
|
{
|
||||||
return !string.IsNullOrEmpty(RfidId);
|
return !string.IsNullOrEmpty(RfidId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// RFID 정보 설정
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="rfidId">RFID ID</param>
|
|
||||||
/// <param name="rfidDescription">설치 위치 설명</param>
|
|
||||||
/// <param name="rfidStatus">RFID 상태</param>
|
|
||||||
public void SetRfidInfo(string rfidId, string rfidDescription = "", string rfidStatus = "정상")
|
|
||||||
{
|
|
||||||
RfidId = rfidId;
|
|
||||||
RfidDescription = rfidDescription;
|
|
||||||
RfidStatus = rfidStatus;
|
|
||||||
ModifiedDate = DateTime.Now;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// RFID 정보 삭제
|
|
||||||
/// </summary>
|
|
||||||
public void ClearRfidInfo()
|
|
||||||
{
|
|
||||||
RfidId = string.Empty;
|
|
||||||
RfidDescription = string.Empty;
|
|
||||||
RfidStatus = "정상";
|
|
||||||
ModifiedDate = DateTime.Now;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// RFID 기반 표시 텍스트 (RFID ID 우선, 없으면 노드ID)
|
|
||||||
/// </summary>
|
|
||||||
public string GetRfidDisplayText()
|
|
||||||
{
|
|
||||||
return HasRfid() ? RfidId : NodeId;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
61
Cs_HMI/AGVLogic/AGVNavigationCore/Models/NodeBase.cs
Normal file
61
Cs_HMI/AGVLogic/AGVNavigationCore/Models/NodeBase.cs
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
using System;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Drawing;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace AGVNavigationCore.Models
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 맵 상의 모든 객체의 최상위 기본 클래스
|
||||||
|
/// 위치, 선택 상태, 기본 식별자 등을 관리
|
||||||
|
/// </summary>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -146,7 +146,12 @@ namespace AGVNavigationCore.Models
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 현재 노드 ID
|
/// 현재 노드 ID
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string CurrentNodeId => _currentNode?.NodeId;
|
public MapNode CurrentNode => _currentNode;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 현재 노드 ID (CurrentNode.Id)
|
||||||
|
/// </summary>
|
||||||
|
public string CurrentNodeId => _currentNode?.Id;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 이전 위치
|
/// 이전 위치
|
||||||
@@ -158,10 +163,6 @@ namespace AGVNavigationCore.Models
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public float BatteryLevel { get; set; } = 100.0f;
|
public float BatteryLevel { get; set; } = 100.0f;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 이전 노드 ID
|
|
||||||
/// </summary>
|
|
||||||
public string PrevNodeId => _prevNode?.NodeId;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 이전 노드
|
/// 이전 노드
|
||||||
@@ -314,7 +315,7 @@ namespace AGVNavigationCore.Models
|
|||||||
MagnetPosition.S,
|
MagnetPosition.S,
|
||||||
SpeedLevel.L,
|
SpeedLevel.L,
|
||||||
eAGVCommandReason.NoPath,
|
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 (_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) //이미완료되었다.
|
if (lastNode.IsPass) //이미완료되었다.
|
||||||
{
|
{
|
||||||
@@ -332,7 +333,7 @@ namespace AGVNavigationCore.Models
|
|||||||
MagnetPosition.S,
|
MagnetPosition.S,
|
||||||
SpeedLevel.L,
|
SpeedLevel.L,
|
||||||
eAGVCommandReason.Complete,
|
eAGVCommandReason.Complete,
|
||||||
$"목적지 도착 - 최종:{_currentNode?.NodeId ?? "알수없음"}"
|
$"목적지 도착 - 최종:{_currentNode?.Id ?? "알수없음"}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -343,7 +344,7 @@ namespace AGVNavigationCore.Models
|
|||||||
MagnetPosition.S,
|
MagnetPosition.S,
|
||||||
SpeedLevel.L,
|
SpeedLevel.L,
|
||||||
eAGVCommandReason.MarkStop,
|
eAGVCommandReason.MarkStop,
|
||||||
$"목적지 도착 전(MarkStop) - 최종:{_currentNode?.NodeId ?? "알수없음"}"
|
$"목적지 도착 전(MarkStop) - 최종:{_currentNode?.Id ?? "알수없음"}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -351,7 +352,7 @@ namespace AGVNavigationCore.Models
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 4. 경로이탈
|
// 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)
|
if (TargetNode == null)
|
||||||
{
|
{
|
||||||
return new AGVCommand(
|
return new AGVCommand(
|
||||||
@@ -359,11 +360,11 @@ namespace AGVNavigationCore.Models
|
|||||||
MagnetPosition.S,
|
MagnetPosition.S,
|
||||||
SpeedLevel.L,
|
SpeedLevel.L,
|
||||||
eAGVCommandReason.PathOut,
|
eAGVCommandReason.PathOut,
|
||||||
$"(재탐색요청)경로이탈 현재위치:{_currentNode.NodeId}"
|
$"(재탐색요청)경로이탈 현재위치:{_currentNode.Id}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return GetCommandFromPath(CurrentNodeId, "경로 실행 시작");
|
return GetCommandFromPath(CurrentNode, "경로 실행 시작");
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -414,13 +415,13 @@ namespace AGVNavigationCore.Models
|
|||||||
}
|
}
|
||||||
|
|
||||||
_currentPath = path;
|
_currentPath = path;
|
||||||
_remainingNodes = path.Path.Select(n => n.NodeId).ToList(); // MapNode → NodeId 변환
|
_remainingNodes = path.Path.Select(n => n.Id).ToList(); // MapNode → NodeId 변환
|
||||||
_currentNodeIndex = 0;
|
_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)
|
public void SetPosition(MapNode node, AgvDirection motorDirection)
|
||||||
{
|
{
|
||||||
// 현재 위치를 이전 위치로 저장 (리프트 방향 계산용)
|
// 현재 위치를 이전 위치로 저장 (리프트 방향 계산용)
|
||||||
if (_currentNode != null && _currentNode.NodeId != node.NodeId)
|
if (_currentNode != null && _currentNode.Id != node.Id)
|
||||||
{
|
{
|
||||||
_prevPosition = _currentPosition; // 이전 위치
|
_prevPosition = _currentPosition; // 이전 위치
|
||||||
_prevNode = _currentNode;
|
_prevNode = _currentNode;
|
||||||
@@ -574,9 +575,9 @@ namespace AGVNavigationCore.Models
|
|||||||
_currentNode = node;
|
_currentNode = node;
|
||||||
|
|
||||||
// 🔥 노드 ID를 RFID로 간주하여 감지 목록에 추가 (시뮬레이터용)
|
// 🔥 노드 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개 이상 감지 시 위치 확정
|
// 🔥 RFID 2개 이상 감지 시 위치 확정
|
||||||
@@ -588,7 +589,7 @@ namespace AGVNavigationCore.Models
|
|||||||
//현재 경로값이 있는지 확인한다.
|
//현재 경로값이 있는지 확인한다.
|
||||||
if (CurrentPath != null && CurrentPath.DetailedPath != null && CurrentPath.DetailedPath.Any())
|
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)
|
if (item != null)
|
||||||
{
|
{
|
||||||
// [PathJump Check] 점프한 노드 개수 확인
|
// [PathJump Check] 점프한 노드 개수 확인
|
||||||
@@ -596,7 +597,7 @@ namespace AGVNavigationCore.Models
|
|||||||
int skippedCount = CurrentPath.DetailedPath.Count(t => t.seq < item.seq && t.IsPass == false);
|
int skippedCount = CurrentPath.DetailedPath.Count(t => t.seq < item.seq && t.IsPass == false);
|
||||||
if (skippedCount > 2)
|
if (skippedCount > 2)
|
||||||
{
|
{
|
||||||
OnError($"PathJump: {skippedCount}개의 노드를 건너뛰었습니다. (허용: 2개, 현재노드: {node.NodeId})");
|
OnError($"PathJump: {skippedCount}개의 노드를 건너뛰었습니다. (허용: 2개, 현재노드: {node.Id})");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -631,7 +632,7 @@ namespace AGVNavigationCore.Models
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string GetRfidByNodeId(List<MapNode> _mapNodes, string nodeId)
|
public string GetRfidByNodeId(List<MapNode> _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;
|
return node?.HasRfid() == true ? node.RfidId : nodeId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -642,7 +643,7 @@ namespace AGVNavigationCore.Models
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// DetailedPath에서 노드 정보를 찾아 AGVCommand 생성
|
/// DetailedPath에서 노드 정보를 찾아 AGVCommand 생성
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private AGVCommand GetCommandFromPath(string targetNodeId, string actionDescription)
|
private AGVCommand GetCommandFromPath(MapNode targetNode, string actionDescription)
|
||||||
{
|
{
|
||||||
// DetailedPath가 없으면 기본 명령 반환
|
// DetailedPath가 없으면 기본 명령 반환
|
||||||
if (_currentPath == null || _currentPath.DetailedPath == null || _currentPath.DetailedPath.Count == 0)
|
if (_currentPath == null || _currentPath.DetailedPath == null || _currentPath.DetailedPath.Count == 0)
|
||||||
@@ -659,7 +660,7 @@ namespace AGVNavigationCore.Models
|
|||||||
|
|
||||||
// DetailedPath에서 targetNodeId에 해당하는 NodeMotorInfo 찾기
|
// 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)
|
if (nodeInfo == null)
|
||||||
{
|
{
|
||||||
@@ -673,7 +674,7 @@ namespace AGVNavigationCore.Models
|
|||||||
MagnetPosition.S,
|
MagnetPosition.S,
|
||||||
SpeedLevel.M,
|
SpeedLevel.M,
|
||||||
eAGVCommandReason.NoTarget,
|
eAGVCommandReason.NoTarget,
|
||||||
$"{actionDescription} (노드 {targetNodeId} 정보 없음)"
|
$"{actionDescription} (노드 {targetNode.Id} 정보 없음)"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -721,7 +722,7 @@ namespace AGVNavigationCore.Models
|
|||||||
magnetPos,
|
magnetPos,
|
||||||
speed,
|
speed,
|
||||||
eAGVCommandReason.Normal,
|
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)
|
private void OnError(string message)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ namespace AGVNavigationCore.PathFinding.Analysis
|
|||||||
if (node.IsNavigationNode())
|
if (node.IsNavigationNode())
|
||||||
{
|
{
|
||||||
var junctionInfo = AnalyzeNode(node);
|
var junctionInfo = AnalyzeNode(node);
|
||||||
_junctions[node.NodeId] = junctionInfo;
|
_junctions[node.Id] = junctionInfo;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -69,7 +69,7 @@ namespace AGVNavigationCore.PathFinding.Analysis
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private JunctionInfo AnalyzeNode(MapNode node)
|
private JunctionInfo AnalyzeNode(MapNode node)
|
||||||
{
|
{
|
||||||
var junction = new JunctionInfo(node.NodeId);
|
var junction = new JunctionInfo(node.Id);
|
||||||
|
|
||||||
// 양방향 연결을 고려하여 모든 연결된 노드 찾기
|
// 양방향 연결을 고려하여 모든 연결된 노드 찾기
|
||||||
var connectedNodes = GetAllConnectedNodes(node);
|
var connectedNodes = GetAllConnectedNodes(node);
|
||||||
@@ -96,16 +96,16 @@ namespace AGVNavigationCore.PathFinding.Analysis
|
|||||||
{
|
{
|
||||||
if (connectedNode != null)
|
if (connectedNode != null)
|
||||||
{
|
{
|
||||||
connected.Add(connectedNode.NodeId);
|
connected.Add(connectedNode.Id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 역방향 연결된 노드들 (다른 노드에서 이 노드로 연결)
|
// 역방향 연결된 노드들 (다른 노드에서 이 노드로 연결)
|
||||||
foreach (var otherNode in _mapNodes)
|
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)
|
foreach (var connectedId in connectedNodes)
|
||||||
{
|
{
|
||||||
var connectedNode = _mapNodes.FirstOrDefault(n => n.NodeId == connectedId);
|
var connectedNode = _mapNodes.FirstOrDefault(n => n.Id == connectedId);
|
||||||
if (connectedNode != null)
|
if (connectedNode != null)
|
||||||
{
|
{
|
||||||
double angle = CalculateAngle(junctionNode.Position, connectedNode.Position);
|
double angle = CalculateAngle(junctionNode.Position, connectedNode.Position);
|
||||||
@@ -226,9 +226,9 @@ namespace AGVNavigationCore.PathFinding.Analysis
|
|||||||
return MagnetDirection.Straight;
|
return MagnetDirection.Straight;
|
||||||
|
|
||||||
// 실제 각도 기반으로 마그넷 방향 계산
|
// 실제 각도 기반으로 마그넷 방향 계산
|
||||||
var fromNode = _mapNodes.FirstOrDefault(n => n.NodeId == fromNodeId);
|
var fromNode = _mapNodes.FirstOrDefault(n => n.Id == fromNodeId);
|
||||||
var currentNode = _mapNodes.FirstOrDefault(n => n.NodeId == currentNodeId);
|
var currentNode = _mapNodes.FirstOrDefault(n => n.Id == currentNodeId);
|
||||||
var toNode = _mapNodes.FirstOrDefault(n => n.NodeId == toNodeId);
|
var toNode = _mapNodes.FirstOrDefault(n => n.Id == toNodeId);
|
||||||
|
|
||||||
if (fromNode == null || currentNode == null || toNode == null)
|
if (fromNode == null || currentNode == null || toNode == null)
|
||||||
return MagnetDirection.Straight;
|
return MagnetDirection.Straight;
|
||||||
|
|||||||
@@ -320,7 +320,7 @@ namespace AGVNavigationCore.PathFinding.Core
|
|||||||
{
|
{
|
||||||
return DetailedPath.Select(n => n.NodeId).ToList();
|
return DetailedPath.Select(n => n.NodeId).ToList();
|
||||||
}
|
}
|
||||||
return Path?.Select(n => n.NodeId).ToList() ?? new List<string>();
|
return Path?.Select(n => n.Id).ToList() ?? new List<string>();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -50,36 +50,36 @@ namespace AGVNavigationCore.PathFinding.Core
|
|||||||
// 모든 네비게이션 노드를 PathNode로 변환하고 양방향 연결 생성
|
// 모든 네비게이션 노드를 PathNode로 변환하고 양방향 연결 생성
|
||||||
foreach (var mapNode in _mapNodes)
|
foreach (var mapNode in _mapNodes)
|
||||||
{
|
{
|
||||||
_mapNodeLookup[mapNode.NodeId] = mapNode; // Add to lookup table
|
_mapNodeLookup[mapNode.Id] = mapNode; // Add to lookup table
|
||||||
|
|
||||||
if (mapNode.IsNavigationNode())
|
if (mapNode.IsNavigationNode())
|
||||||
{
|
{
|
||||||
var pathNode = new PathNode(mapNode.NodeId, mapNode.Position);
|
var pathNode = new PathNode(mapNode.Id, mapNode.Position);
|
||||||
_nodeMap[mapNode.NodeId] = pathNode;
|
_nodeMap[mapNode.Id] = pathNode;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 단일 연결을 양방향으로 확장
|
// 단일 연결을 양방향으로 확장
|
||||||
foreach (var mapNode in _mapNodes)
|
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)
|
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];
|
var connectedPathNode = _nodeMap[connectedNode.Id];
|
||||||
if (!connectedPathNode.ConnectedNodes.Contains(mapNode.NodeId))
|
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<NodeMotorInfo>(previousResult.DetailedPath ?? new List<NodeMotorInfo>());
|
var combinedDetailedPath = new List<NodeMotorInfo>(previousResult.DetailedPath ?? new List<NodeMotorInfo>());
|
||||||
|
|
||||||
// 이전 경로의 마지막 노드와 현재 경로의 시작 노드 비교
|
// 이전 경로의 마지막 노드와 현재 경로의 시작 노드 비교
|
||||||
string lastNodeOfPrevious = previousResult.Path[previousResult.Path.Count - 1].NodeId;
|
string lastNodeOfPrevious = previousResult.Path[previousResult.Path.Count - 1].Id;
|
||||||
string firstNodeOfCurrent = currentResult.Path[0].NodeId;
|
string firstNodeOfCurrent = currentResult.Path[0].Id;
|
||||||
|
|
||||||
if (lastNodeOfPrevious == firstNodeOfCurrent)
|
if (lastNodeOfPrevious == firstNodeOfCurrent)
|
||||||
{
|
{
|
||||||
@@ -508,8 +508,8 @@ namespace AGVNavigationCore.PathFinding.Core
|
|||||||
float totalDistance = 0;
|
float totalDistance = 0;
|
||||||
for (int i = 0; i < path.Count - 1; i++)
|
for (int i = 0; i < path.Count - 1; i++)
|
||||||
{
|
{
|
||||||
var nodeId1 = path[i].NodeId;
|
var nodeId1 = path[i].Id;
|
||||||
var nodeId2 = path[i + 1].NodeId;
|
var nodeId2 = path[i + 1].Id;
|
||||||
|
|
||||||
if (_nodeMap.ContainsKey(nodeId1) && _nodeMap.ContainsKey(nodeId2))
|
if (_nodeMap.ContainsKey(nodeId1) && _nodeMap.ContainsKey(nodeId2))
|
||||||
{
|
{
|
||||||
@@ -577,7 +577,7 @@ namespace AGVNavigationCore.PathFinding.Core
|
|||||||
return null;
|
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)
|
if (junctionNode == null || junctionNode.ConnectedNodes == null || junctionNode.ConnectedNodes.Count == 0)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
@@ -602,7 +602,7 @@ namespace AGVNavigationCore.PathFinding.Core
|
|||||||
if (connectedNodeId == targetNodeId) continue;
|
if (connectedNodeId == targetNodeId) continue;
|
||||||
|
|
||||||
// 조건 3, 4, 5: 존재하고, 활성 상태이고, 네비게이션 가능
|
// 조건 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())
|
if (connectedNode != null && connectedNode.IsActive && connectedNode.IsNavigationNode())
|
||||||
{
|
{
|
||||||
alternateNodes.Add(connectedNode);
|
alternateNodes.Add(connectedNode);
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ namespace AGVNavigationCore.PathFinding.Planning
|
|||||||
n.DisableCross == false &&
|
n.DisableCross == false &&
|
||||||
n.ConnectedNodes.Count >= 3 &&
|
n.ConnectedNodes.Count >= 3 &&
|
||||||
n.ConnectedMapNodes.Where(t => t.CanDocking).Any() == false &&
|
n.ConnectedMapNodes.Where(t => t.CanDocking).Any() == false &&
|
||||||
n.NodeId != startNode.NodeId
|
n.Id != startNode.Id
|
||||||
).ToList();
|
).ToList();
|
||||||
|
|
||||||
// docking 포인트가 연결된 노드는 제거한다.
|
// docking 포인트가 연결된 노드는 제거한다.
|
||||||
@@ -103,7 +103,7 @@ namespace AGVNavigationCore.PathFinding.Planning
|
|||||||
pathNode.ConnectedNodes.Count >= 3 &&
|
pathNode.ConnectedNodes.Count >= 3 &&
|
||||||
pathNode.ConnectedMapNodes.Where(t => t.CanDocking).Any() == false)
|
pathNode.ConnectedMapNodes.Where(t => t.CanDocking).Any() == false)
|
||||||
{
|
{
|
||||||
if (pathNode.NodeId.Equals(StartNode.NodeId) == false)
|
if (pathNode.Id.Equals(StartNode.Id) == false)
|
||||||
return pathNode;
|
return pathNode;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -111,6 +111,12 @@ namespace AGVNavigationCore.PathFinding.Planning
|
|||||||
return null;
|
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,
|
public AGVPathResult FindPath(MapNode startNode, MapNode targetNode,
|
||||||
MapNode prevNode, AgvDirection prevDirection, AgvDirection currentDirection, bool crossignore = false)
|
MapNode prevNode, AgvDirection prevDirection, AgvDirection currentDirection, bool crossignore = false)
|
||||||
{
|
{
|
||||||
@@ -121,13 +127,17 @@ namespace AGVNavigationCore.PathFinding.Planning
|
|||||||
return AGVPathResult.CreateFailure("목적지 노드가 null입니다.", 0, 0);
|
return AGVPathResult.CreateFailure("목적지 노드가 null입니다.", 0, 0);
|
||||||
if (prevNode == null)
|
if (prevNode == null)
|
||||||
return AGVPathResult.CreateFailure("이전위치 노드가 null입니다.", 0, 0);
|
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<MapNode> { startNode, startNode }, new List<AgvDirection>(), 0, 0);
|
return AGVPathResult.CreateSuccess(new List<MapNode> { startNode, startNode }, new List<AgvDirection>(), 0, 0);
|
||||||
|
|
||||||
var ReverseDirection = (currentDirection == AgvDirection.Forward ? AgvDirection.Backward : AgvDirection.Forward);
|
var ReverseDirection = (currentDirection == AgvDirection.Forward ? AgvDirection.Backward : AgvDirection.Forward);
|
||||||
|
|
||||||
//1.목적지까지의 최단거리 경로를 찾는다.
|
//1.목적지까지의 최단거리 경로를 찾는다.
|
||||||
var pathResult = _basicPathfinder.FindPathAStar(startNode.NodeId, targetNode.NodeId);
|
var pathResult = _basicPathfinder.FindPathAStar(startNode.Id, targetNode.Id);
|
||||||
pathResult.PrevNode = prevNode;
|
pathResult.PrevNode = prevNode;
|
||||||
pathResult.PrevDirection = prevDirection;
|
pathResult.PrevDirection = prevDirection;
|
||||||
if (!pathResult.Success || pathResult.Path == null || pathResult.Path.Count == 0)
|
if (!pathResult.Success || pathResult.Path == null || pathResult.Path.Count == 0)
|
||||||
@@ -146,11 +156,11 @@ namespace AGVNavigationCore.PathFinding.Planning
|
|||||||
|
|
||||||
|
|
||||||
//2.AGV방향과 목적지에 설정된 방향이 일치하면 그대로 진행하면된다.(목적지에 방향이 없는 경우에도 그대로 진행)
|
//2.AGV방향과 목적지에 설정된 방향이 일치하면 그대로 진행하면된다.(목적지에 방향이 없는 경우에도 그대로 진행)
|
||||||
if (targetNode.DockDirection == DockingDirection.DontCare ||
|
if (tnode.DockDirection == DockingDirection.DontCare ||
|
||||||
(targetNode.DockDirection == DockingDirection.Forward && currentDirection == AgvDirection.Forward) ||
|
(tnode.DockDirection == DockingDirection.Forward && currentDirection == AgvDirection.Forward) ||
|
||||||
(targetNode.DockDirection == DockingDirection.Backward && currentDirection == AgvDirection.Backward))
|
(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);
|
MakeDetailData(pathResult, currentDirection);
|
||||||
MakeMagnetDirection(pathResult);
|
MakeMagnetDirection(pathResult);
|
||||||
@@ -177,10 +187,10 @@ namespace AGVNavigationCore.PathFinding.Planning
|
|||||||
//뒤로 이동시 경로상의 처음 만나는 노드가 같다면 그 방향으로 이동하면 된다.
|
//뒤로 이동시 경로상의 처음 만나는 노드가 같다면 그 방향으로 이동하면 된다.
|
||||||
// ⚠️ 단, 현재 방향과 목적지 도킹 방향이 일치해야 함!
|
// ⚠️ 단, 현재 방향과 목적지 도킹 방향이 일치해야 함!
|
||||||
if (nextNodeBackward != null && pathResult.Path.Count > 1 &&
|
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 ||
|
if (tnode.DockDirection == DockingDirection.Forward && ReverseDirection == AgvDirection.Forward ||
|
||||||
targetNode.DockDirection == DockingDirection.Backward && ReverseDirection == AgvDirection.Backward)
|
tnode.DockDirection == DockingDirection.Backward && ReverseDirection == AgvDirection.Backward)
|
||||||
{
|
{
|
||||||
MakeDetailData(pathResult, ReverseDirection);
|
MakeDetailData(pathResult, ReverseDirection);
|
||||||
MakeMagnetDirection(pathResult);
|
MakeMagnetDirection(pathResult);
|
||||||
@@ -191,12 +201,12 @@ namespace AGVNavigationCore.PathFinding.Planning
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (nextNodeForward != null && pathResult.Path.Count > 1 &&
|
if (nextNodeForward != null && pathResult.Path.Count > 1 &&
|
||||||
nextNodeForward.NodeId == pathResult.Path[1].NodeId &&
|
nextNodeForward.Id == pathResult.Path[1].Id &&
|
||||||
targetNode.DockDirection == DockingDirection.Forward &&
|
tnode.DockDirection == DockingDirection.Forward &&
|
||||||
currentDirection == AgvDirection.Forward) // ✅ 추가: 현재도 Forward여야 함
|
currentDirection == AgvDirection.Forward) // ✅ 추가: 현재도 Forward여야 함
|
||||||
{
|
{
|
||||||
if (targetNode.DockDirection == DockingDirection.Forward && currentDirection == AgvDirection.Forward ||
|
if (tnode.DockDirection == DockingDirection.Forward && currentDirection == AgvDirection.Forward ||
|
||||||
targetNode.DockDirection == DockingDirection.Backward && currentDirection == AgvDirection.Backward)
|
tnode.DockDirection == DockingDirection.Backward && currentDirection == AgvDirection.Backward)
|
||||||
{
|
{
|
||||||
MakeDetailData(pathResult, currentDirection);
|
MakeDetailData(pathResult, currentDirection);
|
||||||
MakeMagnetDirection(pathResult);
|
MakeMagnetDirection(pathResult);
|
||||||
@@ -221,7 +231,7 @@ namespace AGVNavigationCore.PathFinding.Planning
|
|||||||
//진행방향으로 이동했을때 나오는 노드를 사용한다.
|
//진행방향으로 이동했을때 나오는 노드를 사용한다.
|
||||||
if (nextNodeForward != null)
|
if (nextNodeForward != null)
|
||||||
{
|
{
|
||||||
var Path0 = _basicPathfinder.FindPathAStar(startNode.NodeId, nextNodeForward.NodeId);
|
var Path0 = _basicPathfinder.FindPathAStar(startNode.Id, nextNodeForward.Id);
|
||||||
Path0.PrevNode = prevNode;
|
Path0.PrevNode = prevNode;
|
||||||
Path0.PrevDirection = prevDirection;
|
Path0.PrevDirection = prevDirection;
|
||||||
MakeDetailData(Path0, prevDirection);
|
MakeDetailData(Path0, prevDirection);
|
||||||
@@ -259,7 +269,7 @@ namespace AGVNavigationCore.PathFinding.Planning
|
|||||||
JunctionInPath = FindNearestJunction(startNode);
|
JunctionInPath = FindNearestJunction(startNode);
|
||||||
|
|
||||||
//종료노드로부터 가까운 교차로 검색
|
//종료노드로부터 가까운 교차로 검색
|
||||||
if (JunctionInPath == null) JunctionInPath = FindNearestJunction(targetNode);
|
if (JunctionInPath == null) JunctionInPath = FindNearestJunction(tnode);
|
||||||
}
|
}
|
||||||
if (JunctionInPath == null)
|
if (JunctionInPath == null)
|
||||||
return AGVPathResult.CreateFailure("교차로가 없어 경로계산을 할 수 없습니다", 0, 0);
|
return AGVPathResult.CreateFailure("교차로가 없어 경로계산을 할 수 없습니다", 0, 0);
|
||||||
@@ -267,7 +277,7 @@ namespace AGVNavigationCore.PathFinding.Planning
|
|||||||
//경유지를 포함하여 경로를 다시 계산한다.
|
//경유지를 포함하여 경로를 다시 계산한다.
|
||||||
|
|
||||||
//1.시작위치 - 교차로(여기까지는 현재 방향으로 그대로 이동을 한다)
|
//1.시작위치 - 교차로(여기까지는 현재 방향으로 그대로 이동을 한다)
|
||||||
var path1 = _basicPathfinder.FindPathAStar(startNode.NodeId, JunctionInPath.NodeId);
|
var path1 = _basicPathfinder.FindPathAStar(startNode.Id, JunctionInPath.Id);
|
||||||
path1.PrevNode = prevNode;
|
path1.PrevNode = prevNode;
|
||||||
path1.PrevDirection = prevDirection;
|
path1.PrevDirection = prevDirection;
|
||||||
|
|
||||||
@@ -278,7 +288,7 @@ namespace AGVNavigationCore.PathFinding.Planning
|
|||||||
// ReverseCheck = false; //현재 진행 방향으로 이동해야한다
|
// ReverseCheck = false; //현재 진행 방향으로 이동해야한다
|
||||||
// MakeDetailData(path1, currentDirection); // path1의 상세 경로 정보 채우기 (모터 방향 설정)
|
// 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; //현재 방향의 반대방향으로 이동해야한다
|
ReverseCheck = true; //현재 방향의 반대방향으로 이동해야한다
|
||||||
MakeDetailData(path1, ReverseDirection); // path1의 상세 경로 정보 채우기 (모터 방향 설정)
|
MakeDetailData(path1, ReverseDirection); // path1의 상세 경로 정보 채우기 (모터 방향 설정)
|
||||||
@@ -295,7 +305,7 @@ namespace AGVNavigationCore.PathFinding.Planning
|
|||||||
|
|
||||||
|
|
||||||
//2.교차로 - 종료위치
|
//2.교차로 - 종료위치
|
||||||
var path2 = _basicPathfinder.FindPathAStar(JunctionInPath.NodeId, targetNode.NodeId);
|
var path2 = _basicPathfinder.FindPathAStar(JunctionInPath.Id, targetNode.Id);
|
||||||
path2.PrevNode = prevNode;
|
path2.PrevNode = prevNode;
|
||||||
path2.PrevDirection = prevDirection;
|
path2.PrevDirection = prevDirection;
|
||||||
|
|
||||||
@@ -315,9 +325,9 @@ namespace AGVNavigationCore.PathFinding.Planning
|
|||||||
|
|
||||||
|
|
||||||
//3.방향전환을 위환 대체 노드찾기
|
//3.방향전환을 위환 대체 노드찾기
|
||||||
tempNode = _basicPathfinder.FindAlternateNodeForDirectionChange(JunctionInPath.NodeId,
|
tempNode = _basicPathfinder.FindAlternateNodeForDirectionChange(JunctionInPath.Id,
|
||||||
path1.Path[path1.Path.Count - 2].NodeId,
|
path1.Path[path1.Path.Count - 2].Id,
|
||||||
path2.Path[1].NodeId);
|
path2.Path[1].Id);
|
||||||
|
|
||||||
//4. path1 + tempnode + path2 가 최종 위치가 된다.
|
//4. path1 + tempnode + path2 가 최종 위치가 된다.
|
||||||
if (tempNode == null)
|
if (tempNode == null)
|
||||||
@@ -332,7 +342,7 @@ namespace AGVNavigationCore.PathFinding.Planning
|
|||||||
//if (tempNode != null)
|
//if (tempNode != null)
|
||||||
{
|
{
|
||||||
// 교차로 → 대체노드 경로 계산
|
// 교차로 → 대체노드 경로 계산
|
||||||
var pathToTemp = _basicPathfinder.FindPathAStar(JunctionInPath.NodeId, tempNode.NodeId);
|
var pathToTemp = _basicPathfinder.FindPathAStar(JunctionInPath.Id, tempNode.Id);
|
||||||
pathToTemp.PrevNode = JunctionInPath;
|
pathToTemp.PrevNode = JunctionInPath;
|
||||||
pathToTemp.PrevDirection = (ReverseCheck ? ReverseDirection : currentDirection);
|
pathToTemp.PrevDirection = (ReverseCheck ? ReverseDirection : currentDirection);
|
||||||
if (!pathToTemp.Success)
|
if (!pathToTemp.Success)
|
||||||
@@ -348,7 +358,7 @@ namespace AGVNavigationCore.PathFinding.Planning
|
|||||||
combinedResult = _basicPathfinder.CombineResults(combinedResult, pathToTemp);
|
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.PrevNode = JunctionInPath;
|
||||||
pathFromTemp.PrevDirection = (ReverseCheck ? ReverseDirection : currentDirection);
|
pathFromTemp.PrevDirection = (ReverseCheck ? ReverseDirection : currentDirection);
|
||||||
if (!pathFromTemp.Success)
|
if (!pathFromTemp.Success)
|
||||||
@@ -362,14 +372,14 @@ namespace AGVNavigationCore.PathFinding.Planning
|
|||||||
|
|
||||||
//현재까지 노드에서 목적지까지의 방향이 일치하면 그대로 사용한다.
|
//현재까지 노드에서 목적지까지의 방향이 일치하면 그대로 사용한다.
|
||||||
bool temp3ok = false;
|
bool temp3ok = false;
|
||||||
var TempCheck3 = _basicPathfinder.FindPathAStar(combinedResult.Path.Last().NodeId, targetNode.NodeId);
|
var TempCheck3 = _basicPathfinder.FindPathAStar(combinedResult.Path.Last().Id, targetNode.Id);
|
||||||
if (TempCheck3.Path.First().NodeId.Equals(combinedResult.Path.Last().NodeId))
|
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;
|
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;
|
temp3ok = true;
|
||||||
}
|
}
|
||||||
@@ -381,11 +391,11 @@ namespace AGVNavigationCore.PathFinding.Planning
|
|||||||
if (temp3ok == false)
|
if (temp3ok == false)
|
||||||
{
|
{
|
||||||
//목적지와 방향이 맞지 않다. 그러므로 대체노드를 추가로 더 찾아야한다.
|
//목적지와 방향이 맞지 않다. 그러므로 대체노드를 추가로 더 찾아야한다.
|
||||||
var tempNode2 = _basicPathfinder.FindAlternateNodeForDirectionChange(JunctionInPath.NodeId,
|
var tempNode2 = _basicPathfinder.FindAlternateNodeForDirectionChange(JunctionInPath.Id,
|
||||||
combinedResult.Path[combinedResult.Path.Count - 2].NodeId,
|
combinedResult.Path[combinedResult.Path.Count - 2].Id,
|
||||||
path2.Path[1].NodeId);
|
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);
|
if (ReverseCheck) MakeDetailData(pathToTemp2, currentDirection);
|
||||||
else MakeDetailData(pathToTemp2, ReverseDirection);
|
else MakeDetailData(pathToTemp2, ReverseDirection);
|
||||||
|
|
||||||
@@ -400,7 +410,7 @@ namespace AGVNavigationCore.PathFinding.Planning
|
|||||||
combinedResult.DetailedPath[combinedResult.DetailedPath.Count - 1].MotorDirection = currentDirection;
|
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);
|
if (ReverseCheck) MakeDetailData(pathToTemp3, ReverseDirection);
|
||||||
else MakeDetailData(pathToTemp3, currentDirection);
|
else MakeDetailData(pathToTemp3, currentDirection);
|
||||||
|
|
||||||
@@ -420,12 +430,15 @@ namespace AGVNavigationCore.PathFinding.Planning
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 이 작업후에 MakeMagnetDirection 를 추가로 실행 하세요
|
||||||
|
/// </summary>
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 이 작업후에 MakeMagnetDirection 를 추가로 실행 하세요
|
/// 이 작업후에 MakeMagnetDirection 를 추가로 실행 하세요
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="path1"></param>
|
/// <param name="path1"></param>
|
||||||
/// <param name="currentDirection"></param>
|
/// <param name="currentDirection"></param>
|
||||||
private void MakeDetailData(AGVPathResult path1, AgvDirection currentDirection)
|
public void MakeDetailData(AGVPathResult path1, AgvDirection currentDirection)
|
||||||
{
|
{
|
||||||
if (path1.Success && path1.Path != null && path1.Path.Count > 0)
|
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++)
|
for (int i = 0; i < path1.Path.Count; i++)
|
||||||
{
|
{
|
||||||
var node = path1.Path[i];
|
var node = path1.Path[i];
|
||||||
string nodeId = node.NodeId;
|
string nodeId = node.Id;
|
||||||
string RfidId = node.RfidId;
|
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,
|
var nodeInfo = new NodeMotorInfo(i + 1,
|
||||||
@@ -446,7 +459,7 @@ namespace AGVNavigationCore.PathFinding.Planning
|
|||||||
);
|
);
|
||||||
|
|
||||||
// [Speed Control] MapNode의 속도 설정 적용
|
// [Speed Control] MapNode의 속도 설정 적용
|
||||||
var mapNode = _mapNodes.FirstOrDefault(n => n.NodeId == nodeId);
|
var mapNode = _mapNodes.FirstOrDefault(n => n.Id == nodeId);
|
||||||
if (mapNode != null)
|
if (mapNode != null)
|
||||||
{
|
{
|
||||||
nodeInfo.Speed = mapNode.SpeedLimit;
|
nodeInfo.Speed = mapNode.SpeedLimit;
|
||||||
@@ -470,13 +483,13 @@ namespace AGVNavigationCore.PathFinding.Planning
|
|||||||
for (int i = 0; i < path1.DetailedPath.Count; i++)
|
for (int i = 0; i < path1.DetailedPath.Count; i++)
|
||||||
{
|
{
|
||||||
var detailPath = path1.DetailedPath[i];
|
var detailPath = path1.DetailedPath[i];
|
||||||
string nodeId = path1.Path[i].NodeId;
|
string nodeId = path1.Path[i].Id;
|
||||||
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;
|
||||||
|
|
||||||
// 마그넷 방향 계산 (3개 이상 연결된 교차로에서만 좌/우 가중치 적용)
|
// 마그넷 방향 계산 (3개 이상 연결된 교차로에서만 좌/우 가중치 적용)
|
||||||
if (i > 0 && nextNodeId != null)
|
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)
|
if (path1.DetailedPath[i - 1].MotorDirection != detailPath.MotorDirection)
|
||||||
detailPath.MagnetDirection = MagnetDirection.Straight;
|
detailPath.MagnetDirection = MagnetDirection.Straight;
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -91,14 +91,14 @@ namespace AGVNavigationCore.PathFinding.Planning
|
|||||||
// 직접 경로에 갈림길이 포함된 경우 그 갈림길에서 방향 전환
|
// 직접 경로에 갈림길이 포함된 경우 그 갈림길에서 방향 전환
|
||||||
foreach (var node in directPath2.Path.Skip(1).Take(directPath2.Path.Count - 2)) // 시작과 끝 제외
|
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)
|
if (junctionInfo != null && junctionInfo.IsJunction)
|
||||||
{
|
{
|
||||||
// 간단한 방향 전환: 직접 경로 사용하되 방향 전환 노드 표시
|
// 간단한 방향 전환: 직접 경로 사용하되 방향 전환 노드 표시
|
||||||
return DirectionChangePlan.CreateSuccess(
|
return DirectionChangePlan.CreateSuccess(
|
||||||
directPath2.Path,
|
directPath2.Path,
|
||||||
node.NodeId,
|
node.Id,
|
||||||
$"갈림길 {node.NodeId}에서 방향 전환: {currentDirection} → {requiredDirection}"
|
$"갈림길 {node.Id}에서 방향 전환: {currentDirection} → {requiredDirection}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -165,14 +165,14 @@ namespace AGVNavigationCore.PathFinding.Planning
|
|||||||
{
|
{
|
||||||
foreach (var node in directPath.Path.Skip(2)) // 시작점과 다음 노드는 제외
|
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 (junctionInfo != null && junctionInfo.IsJunction)
|
||||||
{
|
{
|
||||||
// 직진 경로상에서는 더 엄격한 조건 적용
|
// 직진 경로상에서는 더 엄격한 조건 적용
|
||||||
if (!suitableJunctions.Contains(node.NodeId) &&
|
if (!suitableJunctions.Contains(node.Id) &&
|
||||||
HasMultipleExitOptions(node.NodeId))
|
HasMultipleExitOptions(node.Id))
|
||||||
{
|
{
|
||||||
suitableJunctions.Add(node.NodeId);
|
suitableJunctions.Add(node.Id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -226,7 +226,7 @@ namespace AGVNavigationCore.PathFinding.Planning
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private List<string> GetAllConnectedNodes(string nodeId)
|
private List<string> 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<string>();
|
if (node == null) return new List<string>();
|
||||||
|
|
||||||
var connected = new HashSet<string>();
|
var connected = new HashSet<string>();
|
||||||
@@ -236,16 +236,16 @@ namespace AGVNavigationCore.PathFinding.Planning
|
|||||||
{
|
{
|
||||||
if (connectedNode != null)
|
if (connectedNode != null)
|
||||||
{
|
{
|
||||||
connected.Add(connectedNode.NodeId);
|
connected.Add(connectedNode.Id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 역방향 연결
|
// 역방향 연결
|
||||||
foreach (var otherNode in _mapNodes)
|
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 actualDirectionChangeNode = FindActualDirectionChangeNode(changePath, junctionNodeId);
|
||||||
|
|
||||||
string description = $"갈림길 {GetDisplayName(junctionNodeId)}를 통해 {GetDisplayName(actualDirectionChangeNode)}에서 방향 전환: {currentDirection} → {requiredDirection}";
|
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);
|
return DirectionChangePlan.CreateSuccess(changePath, actualDirectionChangeNode, description);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -319,7 +319,7 @@ namespace AGVNavigationCore.PathFinding.Planning
|
|||||||
|
|
||||||
// 2. 인근 갈림길을 통한 우회인지, 직진 경로상 갈림길인지 판단
|
// 2. 인근 갈림길을 통한 우회인지, 직진 경로상 갈림길인지 판단
|
||||||
var directPath = _pathfinder.FindPathAStar(startNodeId, targetNodeId);
|
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)
|
if (isNearbyDetour)
|
||||||
{
|
{
|
||||||
@@ -376,17 +376,17 @@ namespace AGVNavigationCore.PathFinding.Planning
|
|||||||
if (currentDirection != requiredDirection)
|
if (currentDirection != requiredDirection)
|
||||||
{
|
{
|
||||||
string fromNodeId = toJunctionPath.Path.Count >= 2 ?
|
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);
|
var changeSequence = GenerateDirectionChangeSequence(junctionNodeId, fromNodeId, currentDirection, requiredDirection);
|
||||||
if (changeSequence.Count > 1)
|
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. 갈림길에서 목표점까지의 경로
|
// 3. 갈림길에서 목표점까지의 경로
|
||||||
string lastNode = fullPath.LastOrDefault()?.NodeId ?? junctionNodeId;
|
string lastNode = fullPath.LastOrDefault()?.Id ?? junctionNodeId;
|
||||||
var fromJunctionPath = _pathfinder.FindPathAStar(lastNode, targetNodeId);
|
var fromJunctionPath = _pathfinder.FindPathAStar(lastNode, targetNodeId);
|
||||||
if (fromJunctionPath.Success && fromJunctionPath.Path.Count > 1)
|
if (fromJunctionPath.Success && fromJunctionPath.Path.Count > 1)
|
||||||
{
|
{
|
||||||
@@ -461,8 +461,8 @@ namespace AGVNavigationCore.PathFinding.Planning
|
|||||||
// 왔던 길(excludeNodeId)를 제외한 노드 중에서 최적의 우회 노드 선택
|
// 왔던 길(excludeNodeId)를 제외한 노드 중에서 최적의 우회 노드 선택
|
||||||
// 우선순위: 1) 막다른 길이 아닌 노드 (우회 후 복귀 가능) 2) 직진방향 3) 목적지 방향
|
// 우선순위: 1) 막다른 길이 아닌 노드 (우회 후 복귀 가능) 2) 직진방향 3) 목적지 방향
|
||||||
|
|
||||||
var junctionNode = _mapNodes.FirstOrDefault(n => n.NodeId == junctionNodeId);
|
var junctionNode = _mapNodes.FirstOrDefault(n => n.Id == junctionNodeId);
|
||||||
var fromNode = _mapNodes.FirstOrDefault(n => n.NodeId == excludeNodeId);
|
var fromNode = _mapNodes.FirstOrDefault(n => n.Id == excludeNodeId);
|
||||||
|
|
||||||
if (junctionNode == null || fromNode == null)
|
if (junctionNode == null || fromNode == null)
|
||||||
return availableNodes.FirstOrDefault();
|
return availableNodes.FirstOrDefault();
|
||||||
@@ -478,7 +478,7 @@ namespace AGVNavigationCore.PathFinding.Planning
|
|||||||
{
|
{
|
||||||
if (nodeId == excludeNodeId) continue; // 왔던 길 제외
|
if (nodeId == excludeNodeId) continue; // 왔던 길 제외
|
||||||
|
|
||||||
var candidateNode = _mapNodes.FirstOrDefault(n => n.NodeId == nodeId);
|
var candidateNode = _mapNodes.FirstOrDefault(n => n.Id == nodeId);
|
||||||
if (candidateNode == null) continue;
|
if (candidateNode == null) continue;
|
||||||
|
|
||||||
// 갈림길에서 후보 노드로의 방향 벡터 계산 (junctionNode → candidateNode)
|
// 갈림길에서 후보 노드로의 방향 벡터 계산 (junctionNode → candidateNode)
|
||||||
@@ -561,11 +561,11 @@ namespace AGVNavigationCore.PathFinding.Planning
|
|||||||
return junctionNodeId; // 기본값으로 갈림길 반환
|
return junctionNodeId; // 기본값으로 갈림길 반환
|
||||||
|
|
||||||
// 갈림길이 두 번 나타나는 위치 찾기
|
// 갈림길이 두 번 나타나는 위치 찾기
|
||||||
int firstJunctionIndex = changePath.FindIndex(n => n.NodeId == junctionNodeId);
|
int firstJunctionIndex = changePath.FindIndex(n => n.Id == junctionNodeId);
|
||||||
int lastJunctionIndex = -1;
|
int lastJunctionIndex = -1;
|
||||||
for (int i = changePath.Count - 1; i >= 0; i--)
|
for (int i = changePath.Count - 1; i >= 0; i--)
|
||||||
{
|
{
|
||||||
if (changePath[i].NodeId == junctionNodeId)
|
if (changePath[i].Id == junctionNodeId)
|
||||||
{
|
{
|
||||||
lastJunctionIndex = i;
|
lastJunctionIndex = i;
|
||||||
break;
|
break;
|
||||||
@@ -577,7 +577,7 @@ namespace AGVNavigationCore.PathFinding.Planning
|
|||||||
firstJunctionIndex != lastJunctionIndex && lastJunctionIndex - firstJunctionIndex == 2)
|
firstJunctionIndex != lastJunctionIndex && lastJunctionIndex - firstJunctionIndex == 2)
|
||||||
{
|
{
|
||||||
// 첫 번째와 두 번째 갈림길 사이에 있는 노드가 실제 방향전환 노드
|
// 첫 번째와 두 번째 갈림길 사이에 있는 노드가 실제 방향전환 노드
|
||||||
string detourNode = changePath[firstJunctionIndex + 1].NodeId;
|
string detourNode = changePath[firstJunctionIndex + 1].Id;
|
||||||
return detourNode;
|
return detourNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -647,11 +647,11 @@ namespace AGVNavigationCore.PathFinding.Planning
|
|||||||
}
|
}
|
||||||
|
|
||||||
string errorMessage = $"되돌아가기 패턴 검출 ({backtrackingPatterns.Count}개): {string.Join(", ", issues)}";
|
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}");
|
System.Diagnostics.Debug.WriteLine($"[PathValidation] ❌ 되돌아가기 패턴: {errorMessage}");
|
||||||
|
|
||||||
return PathValidationResult.CreateInvalidWithBacktracking(
|
return PathValidationResult.CreateInvalidWithBacktracking(
|
||||||
path.Select(n => n.NodeId).ToList(), backtrackingPatterns, startNodeId, "", junctionNodeId, errorMessage);
|
path.Select(n => n.Id).ToList(), backtrackingPatterns, startNodeId, "", junctionNodeId, errorMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 연속된 중복 노드 검증
|
// 2. 연속된 중복 노드 검증
|
||||||
@@ -670,13 +670,13 @@ namespace AGVNavigationCore.PathFinding.Planning
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 4. 갈림길 포함 여부 검증
|
// 4. 갈림길 포함 여부 검증
|
||||||
if (!path.Any(n => n.NodeId == junctionNodeId))
|
if (!path.Any(n => n.Id == junctionNodeId))
|
||||||
{
|
{
|
||||||
return PathValidationResult.CreateInvalid(startNodeId, "", $"갈림길 {junctionNodeId}이 경로에 포함되지 않음");
|
return PathValidationResult.CreateInvalid(startNodeId, "", $"갈림길 {junctionNodeId}이 경로에 포함되지 않음");
|
||||||
}
|
}
|
||||||
|
|
||||||
System.Diagnostics.Debug.WriteLine($"[PathValidation] ✅ 유효한 경로: {string.Join(" → ", path.Select(n => n.NodeId))}");
|
System.Diagnostics.Debug.WriteLine($"[PathValidation] ✅ 유효한 경로: {string.Join(" → ", path.Select(n => n.Id))}");
|
||||||
return PathValidationResult.CreateValid(path.Select(n => n.NodeId).ToList(), startNodeId, "", junctionNodeId);
|
return PathValidationResult.CreateValid(path.Select(n => n.Id).ToList(), startNodeId, "", junctionNodeId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -688,9 +688,9 @@ namespace AGVNavigationCore.PathFinding.Planning
|
|||||||
|
|
||||||
for (int i = 0; i < path.Count - 2; i++)
|
for (int i = 0; i < path.Count - 2; i++)
|
||||||
{
|
{
|
||||||
string nodeA = path[i].NodeId;
|
string nodeA = path[i].Id;
|
||||||
string nodeB = path[i + 1].NodeId;
|
string nodeB = path[i + 1].Id;
|
||||||
string nodeC = path[i + 2].NodeId;
|
string nodeC = path[i + 2].Id;
|
||||||
|
|
||||||
// A → B → A 패턴 검출
|
// A → B → A 패턴 검출
|
||||||
if (nodeA == nodeC && nodeA != nodeB)
|
if (nodeA == nodeC && nodeA != nodeB)
|
||||||
@@ -712,9 +712,9 @@ namespace AGVNavigationCore.PathFinding.Planning
|
|||||||
|
|
||||||
for (int i = 0; i < path.Count - 1; i++)
|
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++)
|
for (int i = 0; i < path.Count - 1; i++)
|
||||||
{
|
{
|
||||||
string currentNode = path[i].NodeId;
|
string currentNode = path[i].Id;
|
||||||
string nextNode = path[i + 1].NodeId;
|
string nextNode = path[i + 1].Id;
|
||||||
|
|
||||||
// 두 노드간 직접 연결성 확인 (맵 노드의 ConnectedMapNodes 리스트 사용)
|
// 두 노드간 직접 연결성 확인 (맵 노드의 ConnectedMapNodes 리스트 사용)
|
||||||
var currentMapNode = _mapNodes.FirstOrDefault(n => n.NodeId == currentNode);
|
var currentMapNode = _mapNodes.FirstOrDefault(n => n.Id == currentNode);
|
||||||
if (currentMapNode == null || !currentMapNode.ConnectedMapNodes.Any(n => n.NodeId == nextNode))
|
if (currentMapNode == null || !currentMapNode.ConnectedMapNodes.Any(n => n.Id == nextNode))
|
||||||
{
|
{
|
||||||
return PathValidationResult.CreateInvalid(currentNode, nextNode, $"노드 {currentNode}와 {nextNode} 사이에 연결이 없음");
|
return PathValidationResult.CreateInvalid(currentNode, nextNode, $"노드 {currentNode}와 {nextNode} 사이에 연결이 없음");
|
||||||
}
|
}
|
||||||
@@ -769,7 +769,7 @@ namespace AGVNavigationCore.PathFinding.Planning
|
|||||||
/// <returns>표시할 이름</returns>
|
/// <returns>표시할 이름</returns>
|
||||||
private string GetDisplayName(string nodeId)
|
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))
|
if (node != null && !string.IsNullOrEmpty(node.RfidId))
|
||||||
{
|
{
|
||||||
return node.RfidId;
|
return node.RfidId;
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ namespace AGVNavigationCore.PathFinding.Planning
|
|||||||
|
|
||||||
// 연결된 노드 중 현재 노드가 아닌 것들만 필터링
|
// 연결된 노드 중 현재 노드가 아닌 것들만 필터링
|
||||||
var candidateNodes = allNodes.Where(n =>
|
var candidateNodes = allNodes.Where(n =>
|
||||||
connectedNodeIds.Contains(n.NodeId) && n.NodeId != currentNode.NodeId
|
connectedNodeIds.Contains(n.Id) && n.Id != currentNode.Id
|
||||||
).ToList();
|
).ToList();
|
||||||
|
|
||||||
if (candidateNodes.Count == 0)
|
if (candidateNodes.Count == 0)
|
||||||
@@ -88,7 +88,7 @@ namespace AGVNavigationCore.PathFinding.Planning
|
|||||||
|
|
||||||
if (movementLength < 0.001f) // 거의 이동하지 않음
|
if (movementLength < 0.001f) // 거의 이동하지 않음
|
||||||
{
|
{
|
||||||
return candidateNodes[0].NodeId; // 첫 번째 연결 노드 반환
|
return candidateNodes[0].Id; // 첫 번째 연결 노드 반환
|
||||||
}
|
}
|
||||||
|
|
||||||
var normalizedMovement = new PointF(
|
var normalizedMovement = new PointF(
|
||||||
@@ -138,7 +138,7 @@ namespace AGVNavigationCore.PathFinding.Planning
|
|||||||
|
|
||||||
// 가장 높은 점수를 가진 노드 반환
|
// 가장 높은 점수를 가진 노드 반환
|
||||||
var bestCandidate = scoredCandidates.OrderByDescending(x => x.score).First();
|
var bestCandidate = scoredCandidates.OrderByDescending(x => x.score).First();
|
||||||
return bestCandidate.node.NodeId;
|
return bestCandidate.node.Id;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ namespace AGVNavigationCore.Utils
|
|||||||
List<MapNode> candidateNodes = new List<MapNode>();
|
List<MapNode> candidateNodes = new List<MapNode>();
|
||||||
if (prevDirection == direction)
|
if (prevDirection == direction)
|
||||||
{
|
{
|
||||||
candidateNodes = connectedMapNodes.Where(n => n.NodeId != prevNode.NodeId).ToList();
|
candidateNodes = connectedMapNodes.Where(n => n.Id != prevNode.Id).ToList();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -112,9 +112,9 @@ namespace AGVNavigationCore.Utils
|
|||||||
Console.WriteLine(
|
Console.WriteLine(
|
||||||
$"\n[GetNextNodeByDirection] ========== 다음 노드 선택 시작 ==========");
|
$"\n[GetNextNodeByDirection] ========== 다음 노드 선택 시작 ==========");
|
||||||
Console.WriteLine(
|
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(
|
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(
|
Console.WriteLine(
|
||||||
$" 이동벡터: ({movementVector.X:F2}, {movementVector.Y:F2}) → 정규화: ({normalizedMovement.X:F3}, {normalizedMovement.Y:F3})");
|
$" 이동벡터: ({movementVector.X:F2}, {movementVector.Y:F2}) → 정규화: ({normalizedMovement.X:F3}, {normalizedMovement.Y:F3})");
|
||||||
Console.WriteLine(
|
Console.WriteLine(
|
||||||
@@ -159,7 +159,7 @@ namespace AGVNavigationCore.Utils
|
|||||||
}
|
}
|
||||||
|
|
||||||
Console.WriteLine(
|
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(
|
Console.WriteLine(
|
||||||
$" 벡터: ({toNextVector.X:F2}, {toNextVector.Y:F2}), 길이: {toNextLength:F2}");
|
$" 벡터: ({toNextVector.X:F2}, {toNextVector.Y:F2}), 길이: {toNextLength:F2}");
|
||||||
Console.WriteLine(
|
Console.WriteLine(
|
||||||
@@ -204,7 +204,7 @@ namespace AGVNavigationCore.Utils
|
|||||||
}
|
}
|
||||||
|
|
||||||
Console.WriteLine(
|
Console.WriteLine(
|
||||||
$"\n 최종선택: {bestNode?.RfidId ?? "null"}[{bestNode?.NodeId ?? "null"}] (점수: {bestScore:F4})");
|
$"\n 최종선택: {bestNode?.RfidId ?? "null"}[{bestNode?.Id ?? "null"}] (점수: {bestScore:F4})");
|
||||||
Console.WriteLine(
|
Console.WriteLine(
|
||||||
$"[GetNextNodeByDirection] ========== 다음 노드 선택 종료 ==========\n");
|
$"[GetNextNodeByDirection] ========== 다음 노드 선택 종료 ==========\n");
|
||||||
|
|
||||||
@@ -445,15 +445,15 @@ namespace AGVNavigationCore.Utils
|
|||||||
// 선택 이유 생성
|
// 선택 이유 생성
|
||||||
if (prevMotorDirection.HasValue && direction == prevMotorDirection)
|
if (prevMotorDirection.HasValue && direction == prevMotorDirection)
|
||||||
{
|
{
|
||||||
reason = $"모터 방향 일관성 유지 ({direction}) → {candidate.NodeId}";
|
reason = $"모터 방향 일관성 유지 ({direction}) → {candidate.Id}";
|
||||||
}
|
}
|
||||||
else if (prevMotorDirection.HasValue)
|
else if (prevMotorDirection.HasValue)
|
||||||
{
|
{
|
||||||
reason = $"모터 방향 변경 ({prevMotorDirection} → {direction}) → {candidate.NodeId}";
|
reason = $"모터 방향 변경 ({prevMotorDirection} → {direction}) → {candidate.Id}";
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
reason = $"방향 기반 선택 ({direction}) → {candidate.NodeId}";
|
reason = $"방향 기반 선택 ({direction}) → {candidate.Id}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,8 +90,8 @@ namespace AGVNavigationCore.Utils
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Console.WriteLine($"이전 노드: {previousNode.NodeId} (RFID: {previousNode.RfidId}) - 위치: {previousNode.Position}");
|
Console.WriteLine($"이전 노드: {previousNode.Id} (RFID: {previousNode.RfidId}) - 위치: {previousNode.Position}");
|
||||||
Console.WriteLine($"현재 노드: {currentNode.NodeId} (RFID: {currentNode.RfidId}) - 위치: {currentNode.Position}");
|
Console.WriteLine($"현재 노드: {currentNode.Id} (RFID: {currentNode.RfidId}) - 위치: {currentNode.Position}");
|
||||||
Console.WriteLine($"이동 벡터: ({currentNode.Position.X - previousNode.Position.X}, " +
|
Console.WriteLine($"이동 벡터: ({currentNode.Position.X - previousNode.Position.X}, " +
|
||||||
$"{currentNode.Position.Y - previousNode.Position.Y})");
|
$"{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)
|
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($" ├─ 노드 타입: {GetNodeTypeName(nextNode.Type)}");
|
||||||
Console.WriteLine($" └─ 연결된 노드: {string.Join(", ", nextNode.ConnectedNodes)}");
|
Console.WriteLine($" └─ 연결된 노드: {string.Join(", ", nextNode.ConnectedNodes)}");
|
||||||
}
|
}
|
||||||
@@ -132,7 +132,7 @@ namespace AGVNavigationCore.Utils
|
|||||||
Console.WriteLine("\n========== 모든 노드 정보 ==========");
|
Console.WriteLine("\n========== 모든 노드 정보 ==========");
|
||||||
foreach (var node in _allNodes.OrderBy(n => n.RfidId))
|
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)}");
|
Console.WriteLine($" 위치: {node.Position}, 연결: {string.Join(", ", node.ConnectedNodes)}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -149,8 +149,9 @@ namespace AGVNavigationCore.Utils
|
|||||||
}
|
}
|
||||||
|
|
||||||
Console.WriteLine($"\n========== RFID {rfidId} 상세 정보 ==========");
|
Console.WriteLine($"\n========== RFID {rfidId} 상세 정보 ==========");
|
||||||
Console.WriteLine($"노드 ID: {node.NodeId}");
|
Console.WriteLine($"노드 ID: {node.Id}");
|
||||||
Console.WriteLine($"이름: {node.Name}");
|
Console.WriteLine($"RFID: {node.RfidId}");
|
||||||
|
Console.WriteLine($"ALIAS: {node.AliasName}");
|
||||||
Console.WriteLine($"위치: {node.Position}");
|
Console.WriteLine($"위치: {node.Position}");
|
||||||
Console.WriteLine($"타입: {GetNodeTypeName(node.Type)}");
|
Console.WriteLine($"타입: {GetNodeTypeName(node.Type)}");
|
||||||
Console.WriteLine($"TurnLeft/Right/교차로 : {(node.CanTurnLeft ? "O":"X")}/{(node.CanTurnRight ? "O" : "X")}/{(node.DisableCross ? "X" : "O")}");
|
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)
|
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)
|
if (connectedNode != null)
|
||||||
{
|
{
|
||||||
Console.WriteLine($" → {connectedId} (RFID: {connectedNode.RfidId}) - 위치: {connectedNode.Position}");
|
Console.WriteLine($" → {connectedId} (RFID: {connectedNode.RfidId}) - 위치: {connectedNode.Position}");
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ namespace AGVNavigationCore.Utils
|
|||||||
return DockingValidationResult.CreateNotRequired();
|
return DockingValidationResult.CreateNotRequired();
|
||||||
}
|
}
|
||||||
if (pathResult.DetailedPath.Any() == false && pathResult.Path.Any() && pathResult.Path.Count == 2 &&
|
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] 도킹 검증 불필요: 동일포인트");
|
System.Diagnostics.Debug.WriteLine($"[DockingValidator] 도킹 검증 불필요: 동일포인트");
|
||||||
return DockingValidationResult.CreateNotRequired();
|
return DockingValidationResult.CreateNotRequired();
|
||||||
@@ -44,7 +44,7 @@ namespace AGVNavigationCore.Utils
|
|||||||
return DockingValidationResult.CreateNotRequired();
|
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 경로 이동 예측 검증
|
//detail 경로 이동 예측 검증
|
||||||
for (int i = 0; i < pathResult.DetailedPath.Count - 1; i++)
|
for (int i = 0; i < pathResult.DetailedPath.Count - 1; i++)
|
||||||
@@ -52,8 +52,8 @@ namespace AGVNavigationCore.Utils
|
|||||||
var curNodeId = pathResult.DetailedPath[i].NodeId;
|
var curNodeId = pathResult.DetailedPath[i].NodeId;
|
||||||
var nextNodeId = pathResult.DetailedPath[i + 1].NodeId;
|
var nextNodeId = pathResult.DetailedPath[i + 1].NodeId;
|
||||||
|
|
||||||
var curNode = mapNodes?.FirstOrDefault(n => n.NodeId == curNodeId);
|
var curNode = mapNodes?.FirstOrDefault(n => n.Id == curNodeId);
|
||||||
var nextNode = mapNodes?.FirstOrDefault(n => n.NodeId == nextNodeId);
|
var nextNode = mapNodes?.FirstOrDefault(n => n.Id == nextNodeId);
|
||||||
|
|
||||||
if (curNode != null && nextNode != null)
|
if (curNode != null && nextNode != null)
|
||||||
{
|
{
|
||||||
@@ -67,7 +67,7 @@ namespace AGVNavigationCore.Utils
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
var prevNodeId = pathResult.DetailedPath[i - 1].NodeId;
|
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;
|
prevDir = pathResult.DetailedPath[i - 1].MotorDirection;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,7 +78,7 @@ namespace AGVNavigationCore.Utils
|
|||||||
Console.WriteLine(
|
Console.WriteLine(
|
||||||
$"\n[ValidateDockingDirection] 경로 검증 단계 {i}:");
|
$"\n[ValidateDockingDirection] 경로 검증 단계 {i}:");
|
||||||
Console.WriteLine(
|
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(
|
Console.WriteLine(
|
||||||
$" 현재 노드 위치: ({curNode.Position.X:F1}, {curNode.Position.Y:F1})");
|
$" 현재 노드 위치: ({curNode.Position.X:F1}, {curNode.Position.Y:F1})");
|
||||||
Console.WriteLine(
|
Console.WriteLine(
|
||||||
@@ -96,16 +96,16 @@ namespace AGVNavigationCore.Utils
|
|||||||
);
|
);
|
||||||
|
|
||||||
Console.WriteLine(
|
Console.WriteLine(
|
||||||
$" [예상] GetNextNodeByDirection 결과: {expectedNextNode?.NodeId ?? "null"}");
|
$" [예상] GetNextNodeByDirection 결과: {expectedNextNode?.Id ?? "null"}");
|
||||||
Console.WriteLine(
|
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 =
|
string error =
|
||||||
$"[DockingValidator] ⚠️ 경로 방향 불일치" +
|
$"[DockingValidator] ⚠️ 경로 방향 불일치" +
|
||||||
$"\n현재={curNode.RfidId}[{curNodeId}] 이전={prevNode.RfidId}[{(prevNode?.NodeId ?? string.Empty)}] " +
|
$"\n현재={curNode.RfidId}[{curNodeId}] 이전={prevNode.RfidId}[{(prevNode?.Id ?? string.Empty)}] " +
|
||||||
$"\n예상다음={expectedNextNode.RfidId}[{expectedNextNode.NodeId}] 실제다음={nextNode.RfidId}[{nextNodeId}]";
|
$"\n예상다음={expectedNextNode.RfidId}[{expectedNextNode.Id}] 실제다음={nextNode.RfidId}[{nextNodeId}]";
|
||||||
Console.WriteLine(
|
Console.WriteLine(
|
||||||
$"[ValidateDockingDirection] ❌ 경로 방향 불일치 검출!");
|
$"[ValidateDockingDirection] ❌ 경로 방향 불일치 검출!");
|
||||||
Console.WriteLine(
|
Console.WriteLine(
|
||||||
@@ -118,7 +118,7 @@ namespace AGVNavigationCore.Utils
|
|||||||
$" 현재→실제: ({(nextNode.Position.X - curNode.Position.X):F2}, {(nextNode.Position.Y - curNode.Position.Y):F2})");
|
$" 현재→실제: ({(nextNode.Position.X - curNode.Position.X):F2}, {(nextNode.Position.Y - curNode.Position.Y):F2})");
|
||||||
Console.WriteLine($"[ValidateDockingDirection] 에러메시지: {error}");
|
Console.WriteLine($"[ValidateDockingDirection] 에러메시지: {error}");
|
||||||
return DockingValidationResult.CreateInvalid(
|
return DockingValidationResult.CreateInvalid(
|
||||||
LastNode.NodeId,
|
LastNode.Id,
|
||||||
LastNode.Type,
|
LastNode.Type,
|
||||||
pathResult.DetailedPath[i].MotorDirection,
|
pathResult.DetailedPath[i].MotorDirection,
|
||||||
pathResult.DetailedPath[i].MotorDirection,
|
pathResult.DetailedPath[i].MotorDirection,
|
||||||
@@ -150,12 +150,12 @@ namespace AGVNavigationCore.Utils
|
|||||||
System.Diagnostics.Debug.WriteLine($"[DockingValidator] 필요한 도킹 방향: {requiredDirection}");
|
System.Diagnostics.Debug.WriteLine($"[DockingValidator] 필요한 도킹 방향: {requiredDirection}");
|
||||||
|
|
||||||
var LastNodeInfo = pathResult.DetailedPath.Last();
|
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}");
|
System.Diagnostics.Debug.WriteLine($"[DockingValidator] ❌ 도킹 검증 실패: {error}");
|
||||||
return DockingValidationResult.CreateInvalid(
|
return DockingValidationResult.CreateInvalid(
|
||||||
LastNode.NodeId,
|
LastNode.Id,
|
||||||
LastNode.Type,
|
LastNode.Type,
|
||||||
requiredDirection,
|
requiredDirection,
|
||||||
LastNodeInfo.MotorDirection,
|
LastNodeInfo.MotorDirection,
|
||||||
@@ -167,7 +167,7 @@ namespace AGVNavigationCore.Utils
|
|||||||
{
|
{
|
||||||
System.Diagnostics.Debug.WriteLine($"[DockingValidator] ✅ 도킹 검증 성공");
|
System.Diagnostics.Debug.WriteLine($"[DockingValidator] ✅ 도킹 검증 성공");
|
||||||
return DockingValidationResult.CreateValid(
|
return DockingValidationResult.CreateValid(
|
||||||
LastNode.NodeId,
|
LastNode.Id,
|
||||||
LastNode.Type,
|
LastNode.Type,
|
||||||
requiredDirection,
|
requiredDirection,
|
||||||
LastNodeInfo.MotorDirection);
|
LastNodeInfo.MotorDirection);
|
||||||
@@ -177,7 +177,7 @@ namespace AGVNavigationCore.Utils
|
|||||||
string error = $"도킹 방향 불일치: 필요={GetDirectionText(requiredDirection)}, 계산됨={GetDirectionText(LastNodeInfo.MotorDirection)}";
|
string error = $"도킹 방향 불일치: 필요={GetDirectionText(requiredDirection)}, 계산됨={GetDirectionText(LastNodeInfo.MotorDirection)}";
|
||||||
System.Diagnostics.Debug.WriteLine($"[DockingValidator] ❌ 도킹 검증 실패: {error}");
|
System.Diagnostics.Debug.WriteLine($"[DockingValidator] ❌ 도킹 검증 실패: {error}");
|
||||||
return DockingValidationResult.CreateInvalid(
|
return DockingValidationResult.CreateInvalid(
|
||||||
LastNode.NodeId,
|
LastNode.Id,
|
||||||
LastNode.Type,
|
LastNode.Type,
|
||||||
requiredDirection,
|
requiredDirection,
|
||||||
LastNodeInfo.MotorDirection,
|
LastNodeInfo.MotorDirection,
|
||||||
@@ -264,7 +264,7 @@ namespace AGVNavigationCore.Utils
|
|||||||
var deltaY = lastNode.Position.Y - secondLastNode.Position.Y;
|
var deltaY = lastNode.Position.Y - secondLastNode.Position.Y;
|
||||||
var distance = Math.Sqrt(deltaX * deltaX + deltaY * deltaY);
|
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)
|
if (distance < 1.0)
|
||||||
|
|||||||
@@ -28,10 +28,10 @@ namespace AGVNavigationCore.Utils
|
|||||||
Console.WriteLine("================================================\n");
|
Console.WriteLine("================================================\n");
|
||||||
|
|
||||||
// 테스트 노드 생성
|
// 테스트 노드 생성
|
||||||
var node001 = new MapNode { NodeId = "N001", RfidId = "001", Position = new Point(65, 229), ConnectedNodes = new List<string> { "N002" } };
|
var node001 = new MapNode { Id = "N001", RfidId = "001", Position = new Point(65, 229), ConnectedNodes = new List<string> { "N002" } };
|
||||||
var node002 = new MapNode { NodeId = "N002", RfidId = "002", Position = new Point(206, 244), ConnectedNodes = new List<string> { "N001", "N003" } };
|
var node002 = new MapNode { Id = "N002", RfidId = "002", Position = new Point(206, 244), ConnectedNodes = new List<string> { "N001", "N003" } };
|
||||||
var node003 = new MapNode { NodeId = "N003", RfidId = "003", Position = new Point(278, 278), ConnectedNodes = new List<string> { "N002", "N004" } };
|
var node003 = new MapNode { Id = "N003", RfidId = "003", Position = new Point(278, 278), ConnectedNodes = new List<string> { "N002", "N004" } };
|
||||||
var node004 = new MapNode { NodeId = "N004", RfidId = "004", Position = new Point(380, 340), ConnectedNodes = new List<string> { "N003", "N022", "N031" } };
|
var node004 = new MapNode { Id = "N004", RfidId = "004", Position = new Point(380, 340), ConnectedNodes = new List<string> { "N003", "N022", "N031" } };
|
||||||
|
|
||||||
var allNodes = new List<MapNode> { node001, node002, node003, node004 };
|
var allNodes = new List<MapNode> { node001, node002, node003, node004 };
|
||||||
|
|
||||||
@@ -115,7 +115,7 @@ namespace AGVNavigationCore.Utils
|
|||||||
|
|
||||||
Console.WriteLine($"설명: {description}");
|
Console.WriteLine($"설명: {description}");
|
||||||
Console.WriteLine($"이전 위치: {prevPos} (RFID: {allNodes.First(n => n.Position == prevPos)?.RfidId ?? "?"})");
|
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($"현재 모터 방향: {motorDir}");
|
||||||
Console.WriteLine($"요청 방향: {direction}");
|
Console.WriteLine($"요청 방향: {direction}");
|
||||||
|
|
||||||
@@ -128,32 +128,32 @@ namespace AGVNavigationCore.Utils
|
|||||||
Console.WriteLine($"이동 벡터: ({movementVector.X}, {movementVector.Y})");
|
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($"가능한 다음 노드들:");
|
Console.WriteLine($"가능한 다음 노드들:");
|
||||||
|
|
||||||
var candidateNodes = allNodes.Where(n =>
|
var candidateNodes = allNodes.Where(n =>
|
||||||
currentNode.ConnectedNodes.Contains(n.NodeId) && n.NodeId != currentNode.NodeId
|
currentNode.ConnectedNodes.Contains(n.Id) && n.Id != currentNode.Id
|
||||||
).ToList();
|
).ToList();
|
||||||
|
|
||||||
foreach (var candidate in candidateNodes)
|
foreach (var candidate in candidateNodes)
|
||||||
{
|
{
|
||||||
var score = CalculateScoreAndPrint(movementVector, currentNode.Position, candidate, direction);
|
var score = CalculateScoreAndPrint(movementVector, currentNode.Position, candidate, direction);
|
||||||
string isExpected = (candidate.NodeId == expectedNextNode.NodeId) ? " ← 예상 노드" : "";
|
string isExpected = (candidate.Id == expectedNextNode.Id) ? " ← 예상 노드" : "";
|
||||||
Console.WriteLine($" {candidate.NodeId} (RFID: {candidate.RfidId}) - 위치: {candidate.Position} - 점수: {score:F1}{isExpected}");
|
Console.WriteLine($" {candidate.Id} (RFID: {candidate.RfidId}) - 위치: {candidate.Position} - 점수: {score:F1}{isExpected}");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 최고 점수 노드 선택
|
// 최고 점수 노드 선택
|
||||||
var bestCandidate = GetBestCandidate(movementVector, currentNode.Position, candidateNodes, direction);
|
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})");
|
Console.WriteLine($"✅ 정답! ({expectedNodeIdStr})");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Console.WriteLine($"❌ 오답! 예상: {expectedNextNode.NodeId}, 실제: {bestCandidate.NodeId}");
|
Console.WriteLine($"❌ 오답! 예상: {expectedNextNode.Id}, 실제: {bestCandidate.Id}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -63,14 +63,14 @@ namespace AGVNavigationCore.Utils
|
|||||||
// 이전 노드 제외 (되돌아가는 방향 제외)
|
// 이전 노드 제외 (되돌아가는 방향 제외)
|
||||||
if (previousNode != null)
|
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)
|
if (nextNodes.Count == 1)
|
||||||
{
|
{
|
||||||
// 직선 경로: 다음 노드 방향으로 예측
|
// 직선 경로: 다음 노드 방향으로 예측
|
||||||
targetPosition = nextNodes.First().Position;
|
targetPosition = nextNodes.First().Position;
|
||||||
calculationMethod = $"전진 경로 예측 ({currentNode.NodeId}→{nextNodes.First().NodeId})";
|
calculationMethod = $"전진 경로 예측 ({currentNode.Id}→{nextNodes.First().Id})";
|
||||||
}
|
}
|
||||||
else if (nextNodes.Count > 1)
|
else if (nextNodes.Count > 1)
|
||||||
{
|
{
|
||||||
@@ -268,7 +268,7 @@ namespace AGVNavigationCore.Utils
|
|||||||
|
|
||||||
foreach (var nodeId in currentNode.ConnectedNodes)
|
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)
|
if (connectedNode != null)
|
||||||
{
|
{
|
||||||
connectedNodes.Add(connectedNode);
|
connectedNodes.Add(connectedNode);
|
||||||
|
|||||||
@@ -427,7 +427,7 @@ namespace AGVSimulator.Forms
|
|||||||
if(targetNode.Type == NodeType.Buffer)
|
if(targetNode.Type == NodeType.Buffer)
|
||||||
{
|
{
|
||||||
var lastDetailPath = advancedResult.DetailedPath.Last();
|
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();
|
advancedResult.DetailedPath = advancedResult.DetailedPath.Take(advancedResult.DetailedPath.Count - 1).ToList();
|
||||||
@@ -492,7 +492,7 @@ namespace AGVSimulator.Forms
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnTargetNodeSelected(object sender, List<MapNode> selectedNodes)
|
private void OnTargetNodeSelected(object sender, List<NodeBase> selectedNodes)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -511,9 +511,9 @@ namespace AGVSimulator.Forms
|
|||||||
if (selectedNode == null) return;
|
if (selectedNode == null) return;
|
||||||
|
|
||||||
// 목적지를 선택된 노드로 설정
|
// 목적지를 선택된 노드로 설정
|
||||||
SetTargetNodeInCombo(selectedNode.NodeId);
|
SetTargetNodeInCombo(selectedNode.Id);
|
||||||
|
|
||||||
var displayText = GetDisplayName(selectedNode.NodeId);
|
var displayText = GetDisplayName(selectedNode.Id);
|
||||||
_statusLabel.Text = $"타겟계산 - 목적지: {displayText}";
|
_statusLabel.Text = $"타겟계산 - 목적지: {displayText}";
|
||||||
|
|
||||||
// 자동으로 경로 계산 수행
|
// 자동으로 경로 계산 수행
|
||||||
@@ -578,7 +578,7 @@ namespace AGVSimulator.Forms
|
|||||||
for (int i = 0; i < _startNodeCombo.Items.Count; i++)
|
for (int i = 0; i < _startNodeCombo.Items.Count; i++)
|
||||||
{
|
{
|
||||||
var item = _startNodeCombo.Items[i].ToString();
|
var item = _startNodeCombo.Items[i].ToString();
|
||||||
if (item.Contains($"[{closestNode.NodeId}]"))
|
if (item.Contains($"[{closestNode.Id}]"))
|
||||||
{
|
{
|
||||||
_startNodeCombo.SelectedIndex = i;
|
_startNodeCombo.SelectedIndex = i;
|
||||||
break;
|
break;
|
||||||
@@ -649,7 +649,7 @@ namespace AGVSimulator.Forms
|
|||||||
if (existingNode != null)
|
if (existingNode != null)
|
||||||
{
|
{
|
||||||
// 이미 존재하는 노드로 이동
|
// 이미 존재하는 노드로 이동
|
||||||
Program.WriteLine($"[맵 스캔] RFID '{rfidId}'는 이미 존재합니다 (노드: {existingNode.NodeId})");
|
Program.WriteLine($"[맵 스캔] RFID '{rfidId}'는 이미 존재합니다 (노드: {existingNode.Id})");
|
||||||
|
|
||||||
// 기존 노드로 AGV 위치 설정
|
// 기존 노드로 AGV 위치 설정
|
||||||
_simulatorCanvas.SetAGVPosition(selectedAGV.AgvId, existingNode, currentDirection);
|
_simulatorCanvas.SetAGVPosition(selectedAGV.AgvId, existingNode, currentDirection);
|
||||||
@@ -659,7 +659,7 @@ namespace AGVSimulator.Forms
|
|||||||
_lastNodeAddTime = DateTime.Now;
|
_lastNodeAddTime = DateTime.Now;
|
||||||
_lastScanDirection = currentDirection; // 방향 업데이트
|
_lastScanDirection = currentDirection; // 방향 업데이트
|
||||||
|
|
||||||
_statusLabel.Text = $"기존 노드로 이동: {existingNode.NodeId} [{GetDirectionSymbol(currentDirection)}]";
|
_statusLabel.Text = $"기존 노드로 이동: {existingNode.Id} [{GetDirectionSymbol(currentDirection)}]";
|
||||||
_rfidTextBox.Text = "";
|
_rfidTextBox.Text = "";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -720,12 +720,11 @@ namespace AGVSimulator.Forms
|
|||||||
var newNodeId = $"{_scanNodeCounter:D3}";
|
var newNodeId = $"{_scanNodeCounter:D3}";
|
||||||
var newNode = new MapNode
|
var newNode = new MapNode
|
||||||
{
|
{
|
||||||
NodeId = newNodeId,
|
Id = newNodeId,
|
||||||
RfidId = rfidId,
|
RfidId = rfidId,
|
||||||
Position = new Point(newX, newY),
|
Position = new Point(newX, newY),
|
||||||
Type = NodeType.Normal,
|
Type = NodeType.Normal,
|
||||||
IsActive = true,
|
IsActive = true
|
||||||
Name = $"N{_scanNodeCounter}"
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 맵에 추가
|
// 맵에 추가
|
||||||
@@ -738,10 +737,10 @@ namespace AGVSimulator.Forms
|
|||||||
if (_lastScannedNode != null)
|
if (_lastScannedNode != null)
|
||||||
{
|
{
|
||||||
// 양방향 연결 (ConnectedNodes에 추가 - JSON 저장됨)
|
// 양방향 연결 (ConnectedNodes에 추가 - JSON 저장됨)
|
||||||
_lastScannedNode.AddConnection(newNode.NodeId);
|
_lastScannedNode.AddConnection(newNode.Id);
|
||||||
newNode.AddConnection(_lastScannedNode.NodeId);
|
newNode.AddConnection(_lastScannedNode.Id);
|
||||||
|
|
||||||
Program.WriteLine($"[맵 스캔] 연결 생성: {_lastScannedNode.NodeId} ↔ {newNode.NodeId}");
|
Program.WriteLine($"[맵 스캔] 연결 생성: {_lastScannedNode.Id} ↔ {newNode.Id}");
|
||||||
}
|
}
|
||||||
|
|
||||||
// AGV 위치 설정
|
// AGV 위치 설정
|
||||||
@@ -752,7 +751,7 @@ namespace AGVSimulator.Forms
|
|||||||
_simulatorCanvas.Nodes = _mapNodes;
|
_simulatorCanvas.Nodes = _mapNodes;
|
||||||
|
|
||||||
// 화면을 새 노드 위치로 이동
|
// 화면을 새 노드 위치로 이동
|
||||||
_simulatorCanvas.PanToNode(newNode.NodeId);
|
_simulatorCanvas.PanToNode(newNode.Id);
|
||||||
_simulatorCanvas.Invalidate();
|
_simulatorCanvas.Invalidate();
|
||||||
|
|
||||||
// 상태 업데이트
|
// 상태 업데이트
|
||||||
@@ -764,10 +763,10 @@ namespace AGVSimulator.Forms
|
|||||||
// UI 업데이트
|
// UI 업데이트
|
||||||
UpdateNodeComboBoxes();
|
UpdateNodeComboBoxes();
|
||||||
|
|
||||||
_statusLabel.Text = $"노드 생성: {newNode.NodeId} (RFID: {rfidId}) [{GetDirectionSymbol(currentDirection)}] - 총 {_mapNodes.Count}개";
|
_statusLabel.Text = $"노드 생성: {newNode.Id} (RFID: {rfidId}) [{GetDirectionSymbol(currentDirection)}] - 총 {_mapNodes.Count}개";
|
||||||
_rfidTextBox.Text = "";
|
_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)
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 콘솔 출력 (상세한 리프트 방향 계산 과정)
|
// 콘솔 출력 (상세한 리프트 방향 계산 과정)
|
||||||
Program.WriteLine($"[AGV-{selectedAGV.AgvId}] 위치 설정:");
|
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($" 위치: ({targetNode.Position.X}, {targetNode.Position.Y})");
|
||||||
Program.WriteLine($" 방향: {selectedDirectionItem?.DisplayText ?? "전진"} ({selectedDirection})");
|
Program.WriteLine($" 방향: {selectedDirectionItem?.DisplayText ?? "전진"} ({selectedDirection})");
|
||||||
|
|
||||||
@@ -912,14 +911,14 @@ namespace AGVSimulator.Forms
|
|||||||
CalculateLiftDirectionDetailed(selectedAGV);
|
CalculateLiftDirectionDetailed(selectedAGV);
|
||||||
Program.WriteLine("");
|
Program.WriteLine("");
|
||||||
|
|
||||||
_statusLabel.Text = $"{selectedAGV.AgvId} 위치를 RFID '{rfidId}' (노드: {targetNode.NodeId}), 방향: {selectedDirectionItem?.DisplayText ?? "전진"}로 설정했습니다.";
|
_statusLabel.Text = $"{selectedAGV.AgvId} 위치를 RFID '{rfidId}' (노드: {targetNode.Id}), 방향: {selectedDirectionItem?.DisplayText ?? "전진"}로 설정했습니다.";
|
||||||
_rfidTextBox.Text = ""; // 입력 필드 초기화
|
_rfidTextBox.Text = ""; // 입력 필드 초기화
|
||||||
|
|
||||||
// 시뮬레이터 캔버스의 해당 노드로 이동
|
// 시뮬레이터 캔버스의 해당 노드로 이동
|
||||||
//_simulatorCanvas.PanToNode(targetNode.NodeId);
|
//_simulatorCanvas.PanToNode(targetNode.NodeId);
|
||||||
|
|
||||||
// 시작 노드 콤보박스를 현재 위치로 자동 선택
|
// 시작 노드 콤보박스를 현재 위치로 자동 선택
|
||||||
SetStartNodeToCombo(targetNode.NodeId);
|
SetStartNodeToCombo(targetNode.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -956,11 +955,10 @@ namespace AGVSimulator.Forms
|
|||||||
return "RFID가 할당된 노드가 없습니다.";
|
return "RFID가 할당된 노드가 없습니다.";
|
||||||
|
|
||||||
// 처음 10개의 RFID만 표시 (노드 이름 포함)
|
// 처음 10개의 RFID만 표시 (노드 이름 포함)
|
||||||
var rfidList = nodesWithRfid.Take(10).Select(n =>
|
var rfidList = nodesWithRfid.Take(10).Select((Func<MapNode, string>)(n =>
|
||||||
{
|
{
|
||||||
var nodeNamePart = !string.IsNullOrEmpty(n.Name) ? $" {n.Name}" : "";
|
return $"- {n.RfidId} → {n.Id}";
|
||||||
return $"- {n.RfidId} → {n.NodeId}{nodeNamePart}";
|
}));
|
||||||
});
|
|
||||||
var result = string.Join("\n", rfidList);
|
var result = string.Join("\n", rfidList);
|
||||||
|
|
||||||
if (nodesWithRfid.Count > 10)
|
if (nodesWithRfid.Count > 10)
|
||||||
@@ -1060,8 +1058,7 @@ namespace AGVSimulator.Forms
|
|||||||
if (node.IsActive && node.HasRfid())
|
if (node.IsActive && node.HasRfid())
|
||||||
{
|
{
|
||||||
// {rfid} - [{node}] {name} 형식으로 ComboBoxItem 생성
|
// {rfid} - [{node}] {name} 형식으로 ComboBoxItem 생성
|
||||||
var nodeNamePart = !string.IsNullOrEmpty(node.Name) ? $" {node.Name}" : "";
|
var displayText = $"{node.RfidId} - [{node.Id}]";
|
||||||
var displayText = $"{node.RfidId} - [{node.NodeId}]{nodeNamePart}";
|
|
||||||
var item = new ComboBoxItem<MapNode>(node, displayText);
|
var item = new ComboBoxItem<MapNode>(node, displayText);
|
||||||
|
|
||||||
_startNodeCombo.Items.Add(item);
|
_startNodeCombo.Items.Add(item);
|
||||||
@@ -1262,7 +1259,7 @@ namespace AGVSimulator.Forms
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private string GetRfidByNodeId(string nodeId)
|
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;
|
return node?.HasRfid() == true ? node.RfidId : nodeId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1271,7 +1268,7 @@ namespace AGVSimulator.Forms
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private string GetDisplayName(string nodeId)
|
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))
|
if (node != null && !string.IsNullOrEmpty(node.RfidId))
|
||||||
{
|
{
|
||||||
return node.RfidId;
|
return node.RfidId;
|
||||||
@@ -1628,7 +1625,7 @@ namespace AGVSimulator.Forms
|
|||||||
if (!string.IsNullOrEmpty(node.RfidId))
|
if (!string.IsNullOrEmpty(node.RfidId))
|
||||||
return node.RfidId;
|
return node.RfidId;
|
||||||
|
|
||||||
return $"({node.NodeId})";
|
return $"({node.Id})";
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -1655,7 +1652,7 @@ namespace AGVSimulator.Forms
|
|||||||
for (int i = 0; i < _targetNodeCombo.Items.Count; i++)
|
for (int i = 0; i < _targetNodeCombo.Items.Count; i++)
|
||||||
{
|
{
|
||||||
var item = _targetNodeCombo.Items[i] as ComboBoxItem<MapNode>;
|
var item = _targetNodeCombo.Items[i] as ComboBoxItem<MapNode>;
|
||||||
if (item?.Value?.NodeId == nodeId)
|
if (item?.Value?.Id == nodeId)
|
||||||
{
|
{
|
||||||
_targetNodeCombo.SelectedIndex = i;
|
_targetNodeCombo.SelectedIndex = i;
|
||||||
return;
|
return;
|
||||||
@@ -1669,7 +1666,7 @@ namespace AGVSimulator.Forms
|
|||||||
private PathTestLogItem CreateTestResultFromUI(MapNode prevNode, MapNode targetNode,
|
private PathTestLogItem CreateTestResultFromUI(MapNode prevNode, MapNode targetNode,
|
||||||
string directionName, (bool result, string message) calcResult)
|
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);
|
(_agvListCombo.SelectedItem as VirtualAGV)?.CurrentNodeId);
|
||||||
|
|
||||||
var logItem = new PathTestLogItem
|
var logItem = new PathTestLogItem
|
||||||
@@ -1741,8 +1738,8 @@ namespace AGVSimulator.Forms
|
|||||||
continue;
|
continue;
|
||||||
|
|
||||||
// 중복 방지 (A→B와 B→A를 같은 것으로 간주)
|
// 중복 방지 (A→B와 B→A를 같은 것으로 간주)
|
||||||
var pairKey1 = $"{nodeA.NodeId}→{nodeB.NodeId}";
|
var pairKey1 = $"{nodeA.Id}→{nodeB.Id}";
|
||||||
var pairKey2 = $"{nodeB.NodeId}→{nodeA.NodeId}";
|
var pairKey2 = $"{nodeB.Id}→{nodeA.Id}";
|
||||||
|
|
||||||
if (nodeA.HasRfid() && nodeB.HasRfid() && !processedPairs.Contains(pairKey1) && !processedPairs.Contains(pairKey2))
|
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);
|
prb1.Value = (int)((double)currentTest / totalTests * 100);
|
||||||
|
|
||||||
// 목표 노드 콤보박스 선택
|
// 목표 노드 콤보박스 선택
|
||||||
SetTargetNodeComboBox(dockingTarget.NodeId);
|
SetTargetNodeComboBox(dockingTarget.Id);
|
||||||
|
|
||||||
// 경로 계산 버튼 클릭 (실제 사용자 동작)
|
// 경로 계산 버튼 클릭 (실제 사용자 동작)
|
||||||
var calcResult = CalcPath();
|
var calcResult = CalcPath();
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"Nodes": [
|
"Nodes": [
|
||||||
{
|
{
|
||||||
"NodeId": "N001",
|
"Id": "N001",
|
||||||
"Name": "UNLOADER",
|
"Name": "UNLOADER",
|
||||||
"Position": "65, 229",
|
"Position": "65, 229",
|
||||||
"Type": 2,
|
"Type": 2,
|
||||||
@@ -40,7 +40,7 @@
|
|||||||
"DisplayText": "N001 - UNLOADER - [0001]"
|
"DisplayText": "N001 - UNLOADER - [0001]"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"NodeId": "N002",
|
"Id": "N002",
|
||||||
"Name": "N002",
|
"Name": "N002",
|
||||||
"Position": "190, 230",
|
"Position": "190, 230",
|
||||||
"Type": 0,
|
"Type": 0,
|
||||||
@@ -80,7 +80,7 @@
|
|||||||
"DisplayText": "N002 - N002 - [0002]"
|
"DisplayText": "N002 - N002 - [0002]"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"NodeId": "N003",
|
"Id": "N003",
|
||||||
"Name": "N003",
|
"Name": "N003",
|
||||||
"Position": "296, 266",
|
"Position": "296, 266",
|
||||||
"Type": 0,
|
"Type": 0,
|
||||||
@@ -120,7 +120,7 @@
|
|||||||
"DisplayText": "N003 - N003 - [0003]"
|
"DisplayText": "N003 - N003 - [0003]"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"NodeId": "N004",
|
"Id": "N004",
|
||||||
"Name": "N004",
|
"Name": "N004",
|
||||||
"Position": "388, 330",
|
"Position": "388, 330",
|
||||||
"Type": 0,
|
"Type": 0,
|
||||||
@@ -162,7 +162,7 @@
|
|||||||
"DisplayText": "N004 - N004 - [0004]"
|
"DisplayText": "N004 - N004 - [0004]"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"NodeId": "N006",
|
"Id": "N006",
|
||||||
"Name": "N006",
|
"Name": "N006",
|
||||||
"Position": "530, 220",
|
"Position": "530, 220",
|
||||||
"Type": 0,
|
"Type": 0,
|
||||||
@@ -202,7 +202,7 @@
|
|||||||
"DisplayText": "N006 - N006 - [0013]"
|
"DisplayText": "N006 - N006 - [0013]"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"NodeId": "N007",
|
"Id": "N007",
|
||||||
"Name": "N007",
|
"Name": "N007",
|
||||||
"Position": "589, 184",
|
"Position": "589, 184",
|
||||||
"Type": 0,
|
"Type": 0,
|
||||||
@@ -242,7 +242,7 @@
|
|||||||
"DisplayText": "N007 - N007 - [0014]"
|
"DisplayText": "N007 - N007 - [0014]"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"NodeId": "N008",
|
"Id": "N008",
|
||||||
"Name": "N008",
|
"Name": "N008",
|
||||||
"Position": "282, 452",
|
"Position": "282, 452",
|
||||||
"Type": 0,
|
"Type": 0,
|
||||||
@@ -282,7 +282,7 @@
|
|||||||
"DisplayText": "N008 - N008 - [0009]"
|
"DisplayText": "N008 - N008 - [0009]"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"NodeId": "N009",
|
"Id": "N009",
|
||||||
"Name": "N009",
|
"Name": "N009",
|
||||||
"Position": "183, 465",
|
"Position": "183, 465",
|
||||||
"Type": 0,
|
"Type": 0,
|
||||||
@@ -322,7 +322,7 @@
|
|||||||
"DisplayText": "N009 - N009 - [0010]"
|
"DisplayText": "N009 - N009 - [0010]"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"NodeId": "N010",
|
"Id": "N010",
|
||||||
"Name": "TOPS",
|
"Name": "TOPS",
|
||||||
"Position": "52, 466",
|
"Position": "52, 466",
|
||||||
"Type": 3,
|
"Type": 3,
|
||||||
@@ -361,7 +361,7 @@
|
|||||||
"DisplayText": "N010 - TOPS - [0011]"
|
"DisplayText": "N010 - TOPS - [0011]"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"NodeId": "N011",
|
"Id": "N011",
|
||||||
"Name": "N011",
|
"Name": "N011",
|
||||||
"Position": "481, 399",
|
"Position": "481, 399",
|
||||||
"Type": 0,
|
"Type": 0,
|
||||||
@@ -402,7 +402,7 @@
|
|||||||
"DisplayText": "N011 - N011 - [0005]"
|
"DisplayText": "N011 - N011 - [0005]"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"NodeId": "N012",
|
"Id": "N012",
|
||||||
"Name": "N012",
|
"Name": "N012",
|
||||||
"Position": "559, 464",
|
"Position": "559, 464",
|
||||||
"Type": 0,
|
"Type": 0,
|
||||||
@@ -442,7 +442,7 @@
|
|||||||
"DisplayText": "N012 - N012 - [0006]"
|
"DisplayText": "N012 - N012 - [0006]"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"NodeId": "N013",
|
"Id": "N013",
|
||||||
"Name": "N013",
|
"Name": "N013",
|
||||||
"Position": "640, 513",
|
"Position": "640, 513",
|
||||||
"Type": 0,
|
"Type": 0,
|
||||||
@@ -482,7 +482,7 @@
|
|||||||
"DisplayText": "N013 - N013 - [0007]"
|
"DisplayText": "N013 - N013 - [0007]"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"NodeId": "N014",
|
"Id": "N014",
|
||||||
"Name": "LOADER",
|
"Name": "LOADER",
|
||||||
"Position": "728, 573",
|
"Position": "728, 573",
|
||||||
"Type": 1,
|
"Type": 1,
|
||||||
@@ -521,7 +521,7 @@
|
|||||||
"DisplayText": "N014 - LOADER - [0008]"
|
"DisplayText": "N014 - LOADER - [0008]"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"NodeId": "N019",
|
"Id": "N019",
|
||||||
"Name": "CHARGER #2",
|
"Name": "CHARGER #2",
|
||||||
"Position": "679, 199",
|
"Position": "679, 199",
|
||||||
"Type": 5,
|
"Type": 5,
|
||||||
@@ -560,7 +560,7 @@
|
|||||||
"DisplayText": "N019 - CHARGER #2 - [0015]"
|
"DisplayText": "N019 - CHARGER #2 - [0015]"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"NodeId": "N022",
|
"Id": "N022",
|
||||||
"Name": "N022",
|
"Name": "N022",
|
||||||
"Position": "461, 267",
|
"Position": "461, 267",
|
||||||
"Type": 0,
|
"Type": 0,
|
||||||
@@ -601,7 +601,7 @@
|
|||||||
"DisplayText": "N022 - N022 - [0012]"
|
"DisplayText": "N022 - N022 - [0012]"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"NodeId": "N023",
|
"Id": "N023",
|
||||||
"Name": "N023",
|
"Name": "N023",
|
||||||
"Position": "418, 206",
|
"Position": "418, 206",
|
||||||
"Type": 0,
|
"Type": 0,
|
||||||
@@ -641,7 +641,7 @@
|
|||||||
"DisplayText": "N023 - N023 - [0016]"
|
"DisplayText": "N023 - N023 - [0016]"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"NodeId": "N024",
|
"Id": "N024",
|
||||||
"Name": "N024",
|
"Name": "N024",
|
||||||
"Position": "476, 141",
|
"Position": "476, 141",
|
||||||
"Type": 0,
|
"Type": 0,
|
||||||
@@ -681,7 +681,7 @@
|
|||||||
"DisplayText": "N024 - N024 - [0017]"
|
"DisplayText": "N024 - N024 - [0017]"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"NodeId": "N025",
|
"Id": "N025",
|
||||||
"Name": "N025",
|
"Name": "N025",
|
||||||
"Position": "548, 99",
|
"Position": "548, 99",
|
||||||
"Type": 0,
|
"Type": 0,
|
||||||
@@ -721,7 +721,7 @@
|
|||||||
"DisplayText": "N025 - N025 - [0018]"
|
"DisplayText": "N025 - N025 - [0018]"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"NodeId": "N026",
|
"Id": "N026",
|
||||||
"Name": "CHARGER #1",
|
"Name": "CHARGER #1",
|
||||||
"Position": "670, 88",
|
"Position": "670, 88",
|
||||||
"Type": 5,
|
"Type": 5,
|
||||||
@@ -760,7 +760,7 @@
|
|||||||
"DisplayText": "N026 - CHARGER #1 - [0019]"
|
"DisplayText": "N026 - CHARGER #1 - [0019]"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"NodeId": "LBL001",
|
"Id": "LBL001",
|
||||||
"Name": "Amkor Technology Korea",
|
"Name": "Amkor Technology Korea",
|
||||||
"Position": "183, 103",
|
"Position": "183, 103",
|
||||||
"Type": 6,
|
"Type": 6,
|
||||||
@@ -797,7 +797,7 @@
|
|||||||
"DisplayText": "LBL001 - Amkor Technology Korea"
|
"DisplayText": "LBL001 - Amkor Technology Korea"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"NodeId": "IMG001",
|
"Id": "IMG001",
|
||||||
"Name": "logo",
|
"Name": "logo",
|
||||||
"Position": "633, 310",
|
"Position": "633, 310",
|
||||||
"Type": 7,
|
"Type": 7,
|
||||||
@@ -834,7 +834,7 @@
|
|||||||
"DisplayText": "IMG001 - logo"
|
"DisplayText": "IMG001 - logo"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"NodeId": "N015",
|
"Id": "N015",
|
||||||
"Name": "",
|
"Name": "",
|
||||||
"Position": "448, 476",
|
"Position": "448, 476",
|
||||||
"Type": 0,
|
"Type": 0,
|
||||||
@@ -874,7 +874,7 @@
|
|||||||
"DisplayText": "N015 - [0037]"
|
"DisplayText": "N015 - [0037]"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"NodeId": "N016",
|
"Id": "N016",
|
||||||
"Name": "",
|
"Name": "",
|
||||||
"Position": "425, 524",
|
"Position": "425, 524",
|
||||||
"Type": 0,
|
"Type": 0,
|
||||||
@@ -914,7 +914,7 @@
|
|||||||
"DisplayText": "N016 - [0036]"
|
"DisplayText": "N016 - [0036]"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"NodeId": "N017",
|
"Id": "N017",
|
||||||
"Name": "",
|
"Name": "",
|
||||||
"Position": "389, 559",
|
"Position": "389, 559",
|
||||||
"Type": 0,
|
"Type": 0,
|
||||||
@@ -954,7 +954,7 @@
|
|||||||
"DisplayText": "N017 - [0035]"
|
"DisplayText": "N017 - [0035]"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"NodeId": "N018",
|
"Id": "N018",
|
||||||
"Name": "",
|
"Name": "",
|
||||||
"Position": "315, 562",
|
"Position": "315, 562",
|
||||||
"Type": 0,
|
"Type": 0,
|
||||||
@@ -995,7 +995,7 @@
|
|||||||
"DisplayText": "N018 - [0034]"
|
"DisplayText": "N018 - [0034]"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"NodeId": "N005",
|
"Id": "N005",
|
||||||
"Name": "",
|
"Name": "",
|
||||||
"Position": "227, 560",
|
"Position": "227, 560",
|
||||||
"Type": 0,
|
"Type": 0,
|
||||||
@@ -1036,7 +1036,7 @@
|
|||||||
"DisplayText": "N005 - [0033]"
|
"DisplayText": "N005 - [0033]"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"NodeId": "N020",
|
"Id": "N020",
|
||||||
"Name": "",
|
"Name": "",
|
||||||
"Position": "142, 557",
|
"Position": "142, 557",
|
||||||
"Type": 0,
|
"Type": 0,
|
||||||
@@ -1077,7 +1077,7 @@
|
|||||||
"DisplayText": "N020 - [0032]"
|
"DisplayText": "N020 - [0032]"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"NodeId": "N021",
|
"Id": "N021",
|
||||||
"Name": "",
|
"Name": "",
|
||||||
"Position": "60, 559",
|
"Position": "60, 559",
|
||||||
"Type": 0,
|
"Type": 0,
|
||||||
@@ -1117,7 +1117,7 @@
|
|||||||
"DisplayText": "N021 - [0031]"
|
"DisplayText": "N021 - [0031]"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"NodeId": "N027",
|
"Id": "N027",
|
||||||
"Name": "BUF1",
|
"Name": "BUF1",
|
||||||
"Position": "61, 645",
|
"Position": "61, 645",
|
||||||
"Type": 4,
|
"Type": 4,
|
||||||
@@ -1156,7 +1156,7 @@
|
|||||||
"DisplayText": "N027 - BUF1 - [0041]"
|
"DisplayText": "N027 - BUF1 - [0041]"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"NodeId": "N028",
|
"Id": "N028",
|
||||||
"Name": "BUF2",
|
"Name": "BUF2",
|
||||||
"Position": "141, 643",
|
"Position": "141, 643",
|
||||||
"Type": 4,
|
"Type": 4,
|
||||||
@@ -1195,7 +1195,7 @@
|
|||||||
"DisplayText": "N028 - BUF2 - [0040]"
|
"DisplayText": "N028 - BUF2 - [0040]"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"NodeId": "N029",
|
"Id": "N029",
|
||||||
"Name": "BUF3",
|
"Name": "BUF3",
|
||||||
"Position": "229, 638",
|
"Position": "229, 638",
|
||||||
"Type": 4,
|
"Type": 4,
|
||||||
@@ -1234,7 +1234,7 @@
|
|||||||
"DisplayText": "N029 - BUF3 - [0039]"
|
"DisplayText": "N029 - BUF3 - [0039]"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"NodeId": "N030",
|
"Id": "N030",
|
||||||
"Name": "BUF4",
|
"Name": "BUF4",
|
||||||
"Position": "316, 638",
|
"Position": "316, 638",
|
||||||
"Type": 4,
|
"Type": 4,
|
||||||
@@ -1273,7 +1273,7 @@
|
|||||||
"DisplayText": "N030 - BUF4 - [0038]"
|
"DisplayText": "N030 - BUF4 - [0038]"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"NodeId": "N031",
|
"Id": "N031",
|
||||||
"Name": "",
|
"Name": "",
|
||||||
"Position": "337, 397",
|
"Position": "337, 397",
|
||||||
"Type": 0,
|
"Type": 0,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"Nodes": [
|
"Nodes": [
|
||||||
{
|
{
|
||||||
"NodeId": "N001",
|
"Id": "N001",
|
||||||
"Name": "UNLOADER",
|
"Name": "UNLOADER",
|
||||||
"Position": "65, 229",
|
"Position": "65, 229",
|
||||||
"Type": 2,
|
"Type": 2,
|
||||||
@@ -40,7 +40,7 @@
|
|||||||
"DisplayText": "N001 - UNLOADER - [0001]"
|
"DisplayText": "N001 - UNLOADER - [0001]"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"NodeId": "N002",
|
"Id": "N002",
|
||||||
"Name": "N002",
|
"Name": "N002",
|
||||||
"Position": "190, 230",
|
"Position": "190, 230",
|
||||||
"Type": 0,
|
"Type": 0,
|
||||||
@@ -80,7 +80,7 @@
|
|||||||
"DisplayText": "N002 - N002 - [0002]"
|
"DisplayText": "N002 - N002 - [0002]"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"NodeId": "N003",
|
"Id": "N003",
|
||||||
"Name": "N003",
|
"Name": "N003",
|
||||||
"Position": "296, 266",
|
"Position": "296, 266",
|
||||||
"Type": 0,
|
"Type": 0,
|
||||||
@@ -120,7 +120,7 @@
|
|||||||
"DisplayText": "N003 - N003 - [0003]"
|
"DisplayText": "N003 - N003 - [0003]"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"NodeId": "N004",
|
"Id": "N004",
|
||||||
"Name": "N004",
|
"Name": "N004",
|
||||||
"Position": "388, 330",
|
"Position": "388, 330",
|
||||||
"Type": 0,
|
"Type": 0,
|
||||||
@@ -162,7 +162,7 @@
|
|||||||
"DisplayText": "N004 - N004 - [0004]"
|
"DisplayText": "N004 - N004 - [0004]"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"NodeId": "N006",
|
"Id": "N006",
|
||||||
"Name": "N006",
|
"Name": "N006",
|
||||||
"Position": "530, 220",
|
"Position": "530, 220",
|
||||||
"Type": 0,
|
"Type": 0,
|
||||||
@@ -202,7 +202,7 @@
|
|||||||
"DisplayText": "N006 - N006 - [0013]"
|
"DisplayText": "N006 - N006 - [0013]"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"NodeId": "N007",
|
"Id": "N007",
|
||||||
"Name": "N007",
|
"Name": "N007",
|
||||||
"Position": "589, 184",
|
"Position": "589, 184",
|
||||||
"Type": 0,
|
"Type": 0,
|
||||||
@@ -242,7 +242,7 @@
|
|||||||
"DisplayText": "N007 - N007 - [0014]"
|
"DisplayText": "N007 - N007 - [0014]"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"NodeId": "N008",
|
"Id": "N008",
|
||||||
"Name": "N008",
|
"Name": "N008",
|
||||||
"Position": "282, 452",
|
"Position": "282, 452",
|
||||||
"Type": 0,
|
"Type": 0,
|
||||||
@@ -282,7 +282,7 @@
|
|||||||
"DisplayText": "N008 - N008 - [0009]"
|
"DisplayText": "N008 - N008 - [0009]"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"NodeId": "N009",
|
"Id": "N009",
|
||||||
"Name": "N009",
|
"Name": "N009",
|
||||||
"Position": "183, 465",
|
"Position": "183, 465",
|
||||||
"Type": 0,
|
"Type": 0,
|
||||||
@@ -322,7 +322,7 @@
|
|||||||
"DisplayText": "N009 - N009 - [0010]"
|
"DisplayText": "N009 - N009 - [0010]"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"NodeId": "N010",
|
"Id": "N010",
|
||||||
"Name": "TOPS",
|
"Name": "TOPS",
|
||||||
"Position": "52, 466",
|
"Position": "52, 466",
|
||||||
"Type": 3,
|
"Type": 3,
|
||||||
@@ -361,7 +361,7 @@
|
|||||||
"DisplayText": "N010 - TOPS - [0011]"
|
"DisplayText": "N010 - TOPS - [0011]"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"NodeId": "N011",
|
"Id": "N011",
|
||||||
"Name": "N011",
|
"Name": "N011",
|
||||||
"Position": "481, 399",
|
"Position": "481, 399",
|
||||||
"Type": 0,
|
"Type": 0,
|
||||||
@@ -402,7 +402,7 @@
|
|||||||
"DisplayText": "N011 - N011 - [0005]"
|
"DisplayText": "N011 - N011 - [0005]"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"NodeId": "N012",
|
"Id": "N012",
|
||||||
"Name": "N012",
|
"Name": "N012",
|
||||||
"Position": "559, 464",
|
"Position": "559, 464",
|
||||||
"Type": 0,
|
"Type": 0,
|
||||||
@@ -442,7 +442,7 @@
|
|||||||
"DisplayText": "N012 - N012 - [0006]"
|
"DisplayText": "N012 - N012 - [0006]"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"NodeId": "N013",
|
"Id": "N013",
|
||||||
"Name": "N013",
|
"Name": "N013",
|
||||||
"Position": "640, 513",
|
"Position": "640, 513",
|
||||||
"Type": 0,
|
"Type": 0,
|
||||||
@@ -482,7 +482,7 @@
|
|||||||
"DisplayText": "N013 - N013 - [0007]"
|
"DisplayText": "N013 - N013 - [0007]"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"NodeId": "N014",
|
"Id": "N014",
|
||||||
"Name": "LOADER",
|
"Name": "LOADER",
|
||||||
"Position": "728, 573",
|
"Position": "728, 573",
|
||||||
"Type": 1,
|
"Type": 1,
|
||||||
@@ -521,7 +521,7 @@
|
|||||||
"DisplayText": "N014 - LOADER - [0008]"
|
"DisplayText": "N014 - LOADER - [0008]"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"NodeId": "N019",
|
"Id": "N019",
|
||||||
"Name": "CHARGER #2",
|
"Name": "CHARGER #2",
|
||||||
"Position": "679, 199",
|
"Position": "679, 199",
|
||||||
"Type": 5,
|
"Type": 5,
|
||||||
@@ -560,7 +560,7 @@
|
|||||||
"DisplayText": "N019 - CHARGER #2 - [0015]"
|
"DisplayText": "N019 - CHARGER #2 - [0015]"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"NodeId": "N022",
|
"Id": "N022",
|
||||||
"Name": "N022",
|
"Name": "N022",
|
||||||
"Position": "461, 267",
|
"Position": "461, 267",
|
||||||
"Type": 0,
|
"Type": 0,
|
||||||
@@ -601,7 +601,7 @@
|
|||||||
"DisplayText": "N022 - N022 - [0012]"
|
"DisplayText": "N022 - N022 - [0012]"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"NodeId": "N023",
|
"Id": "N023",
|
||||||
"Name": "N023",
|
"Name": "N023",
|
||||||
"Position": "418, 206",
|
"Position": "418, 206",
|
||||||
"Type": 0,
|
"Type": 0,
|
||||||
@@ -641,7 +641,7 @@
|
|||||||
"DisplayText": "N023 - N023 - [0016]"
|
"DisplayText": "N023 - N023 - [0016]"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"NodeId": "N024",
|
"Id": "N024",
|
||||||
"Name": "N024",
|
"Name": "N024",
|
||||||
"Position": "476, 141",
|
"Position": "476, 141",
|
||||||
"Type": 0,
|
"Type": 0,
|
||||||
@@ -681,7 +681,7 @@
|
|||||||
"DisplayText": "N024 - N024 - [0017]"
|
"DisplayText": "N024 - N024 - [0017]"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"NodeId": "N025",
|
"Id": "N025",
|
||||||
"Name": "N025",
|
"Name": "N025",
|
||||||
"Position": "548, 99",
|
"Position": "548, 99",
|
||||||
"Type": 0,
|
"Type": 0,
|
||||||
@@ -721,7 +721,7 @@
|
|||||||
"DisplayText": "N025 - N025 - [0018]"
|
"DisplayText": "N025 - N025 - [0018]"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"NodeId": "N026",
|
"Id": "N026",
|
||||||
"Name": "CHARGER #1",
|
"Name": "CHARGER #1",
|
||||||
"Position": "670, 88",
|
"Position": "670, 88",
|
||||||
"Type": 5,
|
"Type": 5,
|
||||||
@@ -760,7 +760,7 @@
|
|||||||
"DisplayText": "N026 - CHARGER #1 - [0019]"
|
"DisplayText": "N026 - CHARGER #1 - [0019]"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"NodeId": "LBL001",
|
"Id": "LBL001",
|
||||||
"Name": "Amkor Technology Korea",
|
"Name": "Amkor Technology Korea",
|
||||||
"Position": "183, 103",
|
"Position": "183, 103",
|
||||||
"Type": 6,
|
"Type": 6,
|
||||||
@@ -797,7 +797,7 @@
|
|||||||
"DisplayText": "LBL001 - Amkor Technology Korea"
|
"DisplayText": "LBL001 - Amkor Technology Korea"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"NodeId": "IMG001",
|
"Id": "IMG001",
|
||||||
"Name": "logo",
|
"Name": "logo",
|
||||||
"Position": "633, 310",
|
"Position": "633, 310",
|
||||||
"Type": 7,
|
"Type": 7,
|
||||||
@@ -834,7 +834,7 @@
|
|||||||
"DisplayText": "IMG001 - logo"
|
"DisplayText": "IMG001 - logo"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"NodeId": "N015",
|
"Id": "N015",
|
||||||
"Name": "",
|
"Name": "",
|
||||||
"Position": "448, 476",
|
"Position": "448, 476",
|
||||||
"Type": 0,
|
"Type": 0,
|
||||||
@@ -874,7 +874,7 @@
|
|||||||
"DisplayText": "N015 - [0037]"
|
"DisplayText": "N015 - [0037]"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"NodeId": "N016",
|
"Id": "N016",
|
||||||
"Name": "",
|
"Name": "",
|
||||||
"Position": "425, 524",
|
"Position": "425, 524",
|
||||||
"Type": 0,
|
"Type": 0,
|
||||||
@@ -914,7 +914,7 @@
|
|||||||
"DisplayText": "N016 - [0036]"
|
"DisplayText": "N016 - [0036]"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"NodeId": "N017",
|
"Id": "N017",
|
||||||
"Name": "",
|
"Name": "",
|
||||||
"Position": "389, 559",
|
"Position": "389, 559",
|
||||||
"Type": 0,
|
"Type": 0,
|
||||||
@@ -954,7 +954,7 @@
|
|||||||
"DisplayText": "N017 - [0035]"
|
"DisplayText": "N017 - [0035]"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"NodeId": "N018",
|
"Id": "N018",
|
||||||
"Name": "",
|
"Name": "",
|
||||||
"Position": "315, 562",
|
"Position": "315, 562",
|
||||||
"Type": 0,
|
"Type": 0,
|
||||||
@@ -995,7 +995,7 @@
|
|||||||
"DisplayText": "N018 - [0034]"
|
"DisplayText": "N018 - [0034]"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"NodeId": "N005",
|
"Id": "N005",
|
||||||
"Name": "",
|
"Name": "",
|
||||||
"Position": "227, 560",
|
"Position": "227, 560",
|
||||||
"Type": 0,
|
"Type": 0,
|
||||||
@@ -1036,7 +1036,7 @@
|
|||||||
"DisplayText": "N005 - [0033]"
|
"DisplayText": "N005 - [0033]"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"NodeId": "N020",
|
"Id": "N020",
|
||||||
"Name": "",
|
"Name": "",
|
||||||
"Position": "142, 557",
|
"Position": "142, 557",
|
||||||
"Type": 0,
|
"Type": 0,
|
||||||
@@ -1077,7 +1077,7 @@
|
|||||||
"DisplayText": "N020 - [0032]"
|
"DisplayText": "N020 - [0032]"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"NodeId": "N021",
|
"Id": "N021",
|
||||||
"Name": "",
|
"Name": "",
|
||||||
"Position": "60, 559",
|
"Position": "60, 559",
|
||||||
"Type": 0,
|
"Type": 0,
|
||||||
@@ -1117,7 +1117,7 @@
|
|||||||
"DisplayText": "N021 - [0031]"
|
"DisplayText": "N021 - [0031]"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"NodeId": "N027",
|
"Id": "N027",
|
||||||
"Name": "BUF1",
|
"Name": "BUF1",
|
||||||
"Position": "61, 645",
|
"Position": "61, 645",
|
||||||
"Type": 4,
|
"Type": 4,
|
||||||
@@ -1156,7 +1156,7 @@
|
|||||||
"DisplayText": "N027 - BUF1 - [0041]"
|
"DisplayText": "N027 - BUF1 - [0041]"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"NodeId": "N028",
|
"Id": "N028",
|
||||||
"Name": "BUF2",
|
"Name": "BUF2",
|
||||||
"Position": "141, 643",
|
"Position": "141, 643",
|
||||||
"Type": 4,
|
"Type": 4,
|
||||||
@@ -1195,7 +1195,7 @@
|
|||||||
"DisplayText": "N028 - BUF2 - [0040]"
|
"DisplayText": "N028 - BUF2 - [0040]"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"NodeId": "N029",
|
"Id": "N029",
|
||||||
"Name": "BUF3",
|
"Name": "BUF3",
|
||||||
"Position": "229, 638",
|
"Position": "229, 638",
|
||||||
"Type": 4,
|
"Type": 4,
|
||||||
@@ -1234,7 +1234,7 @@
|
|||||||
"DisplayText": "N029 - BUF3 - [0039]"
|
"DisplayText": "N029 - BUF3 - [0039]"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"NodeId": "N030",
|
"Id": "N030",
|
||||||
"Name": "BUF4",
|
"Name": "BUF4",
|
||||||
"Position": "316, 638",
|
"Position": "316, 638",
|
||||||
"Type": 4,
|
"Type": 4,
|
||||||
@@ -1273,7 +1273,7 @@
|
|||||||
"DisplayText": "N030 - BUF4 - [0038]"
|
"DisplayText": "N030 - BUF4 - [0038]"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"NodeId": "N031",
|
"Id": "N031",
|
||||||
"Name": "",
|
"Name": "",
|
||||||
"Position": "337, 397",
|
"Position": "337, 397",
|
||||||
"Type": 0,
|
"Type": 0,
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
1135
Cs_HMI/Data/NewMap_3.json
Normal file
1135
Cs_HMI/Data/NewMap_3.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -227,8 +227,10 @@ namespace Project
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 시리얼 포트 연결 (arDev.arRS232)
|
/// 시리얼 포트 연결 (arDev.arRS232)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
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)
|
if (dev.IsOpen == false && port.isEmpty() == false)
|
||||||
{
|
{
|
||||||
var tsPLC = VAR.TIME.RUN(conntry);
|
var tsPLC = VAR.TIME.RUN(conntry);
|
||||||
@@ -246,8 +248,17 @@ namespace Project
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var errmessage = dev.errorMessage;
|
//존재하지 않는 포트라면 sync를 벗어난다
|
||||||
PUB.log.AddE($"[AGV:{port}:{baud}] {errmessage}");
|
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(conn);
|
||||||
VAR.TIME.Update(conntry);
|
VAR.TIME.Update(conntry);
|
||||||
@@ -264,6 +275,7 @@ namespace Project
|
|||||||
dev.Close();
|
dev.Close();
|
||||||
VAR.TIME.Update(conntry);
|
VAR.TIME.Update(conntry);
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -614,7 +614,7 @@ namespace Project
|
|||||||
{
|
{
|
||||||
if (_mapNodes == null || _mapNodes.Any() == false) return null;
|
if (_mapNodes == null || _mapNodes.Any() == false) return null;
|
||||||
if (nodeidx.isEmpty()) 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)
|
public static MapNode FindByRFID(string rfidValue)
|
||||||
{
|
{
|
||||||
@@ -626,7 +626,7 @@ namespace Project
|
|||||||
{
|
{
|
||||||
if (_mapNodes == null || _mapNodes.Any() == false) return null;
|
if (_mapNodes == null || _mapNodes.Any() == false) return null;
|
||||||
if (alias.isEmpty()) 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;
|
if (lst.Any() == false) return null;
|
||||||
return lst.ToList();
|
return lst.ToList();
|
||||||
}
|
}
|
||||||
@@ -654,7 +654,7 @@ namespace Project
|
|||||||
_virtualAGV.SetPosition(node, motorDirection);
|
_virtualAGV.SetPosition(node, motorDirection);
|
||||||
RefreshAGVCanvas();
|
RefreshAGVCanvas();
|
||||||
|
|
||||||
log.Add($"[AGV] RFID {rfidId} 감지 → 노드 {node.NodeId} 위치 업데이트 (방향: {motorDirection})");
|
log.Add($"[AGV] RFID {rfidId} 감지 → 노드 {node.Id} 위치 업데이트 (방향: {motorDirection})");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -672,7 +672,7 @@ namespace Project
|
|||||||
{
|
{
|
||||||
if (_virtualAGV == null || _mapNodes == null) return false;
|
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)
|
if (node != null)
|
||||||
{
|
{
|
||||||
_virtualAGV.SetPosition(node, motorDirection);
|
_virtualAGV.SetPosition(node, motorDirection);
|
||||||
@@ -744,42 +744,7 @@ namespace Project
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 현재 AGV의 노드 ID 가져오기
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>현재 노드 ID</returns>
|
|
||||||
public static string GetCurrentAGVNodeId()
|
|
||||||
{
|
|
||||||
return _virtualAGV?.CurrentNodeId ?? string.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 현재 AGV 위치 가져오기
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>현재 위치</returns>
|
|
||||||
public static Point GetCurrentAGVPosition()
|
|
||||||
{
|
|
||||||
return _virtualAGV?.CurrentPosition ?? Point.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 현재 AGV 방향 가져오기
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>현재 방향</returns>
|
|
||||||
public static AgvDirection GetCurrentAGVDirection()
|
|
||||||
{
|
|
||||||
return _virtualAGV?.CurrentDirection ?? AgvDirection.Forward;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 현재 AGV 상태 가져오기
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>현재 상태</returns>
|
|
||||||
public static AGVState GetCurrentAGVState()
|
|
||||||
{
|
|
||||||
return _virtualAGV?.CurrentState ?? AGVState.Idle;
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -108,49 +108,30 @@ namespace Project
|
|||||||
//목적지가 BUFFER라면 버퍼투입대기위치까지 완료했다는 시그널을 보낸다.
|
//목적지가 BUFFER라면 버퍼투입대기위치까지 완료했다는 시그널을 보낸다.
|
||||||
var target = PUB._virtualAGV.TargetNode;
|
var target = PUB._virtualAGV.TargetNode;
|
||||||
PUB.log.Add($"목적지({target.RfidId}) 도착완료 타입:{target.Type}, 출발지:{PUB._virtualAGV.StartNode.RfidId}");
|
PUB.log.Add($"목적지({target.RfidId}) 도착완료 타입:{target.Type}, 출발지:{PUB._virtualAGV.StartNode.RfidId}");
|
||||||
if (target.Type == AGVNavigationCore.Models.NodeType.Buffer)
|
|
||||||
|
switch(target.StationType)
|
||||||
{
|
{
|
||||||
|
case AGVNavigationCore.Models.StationType.Buffer:
|
||||||
//현재위치가 마지막경로의 NODEID와 일치해야한다
|
//현재위치가 마지막경로의 NODEID와 일치해야한다
|
||||||
var lastPath = PUB._virtualAGV.CurrentPath.DetailedPath.LastOrDefault();
|
var lastPath = PUB._virtualAGV.CurrentPath.DetailedPath.LastOrDefault();
|
||||||
if (lastPath.NodeId.Equals(PUB._virtualAGV.CurrentNodeId))
|
if (lastPath.NodeId.Equals(PUB._virtualAGV.CurrentNode.Id))
|
||||||
{
|
{
|
||||||
//버퍼진입전 노드에 도착완료했따
|
//버퍼진입전 노드에 도착완료했따
|
||||||
PUB.XBE.BufferInReady = true;
|
PUB.XBE.BufferInReady = true;
|
||||||
PUB.XBE.BufferReadyError = false;
|
PUB.XBE.BufferReadyError = false;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
//마지막위치가 아닌 다른 위치에 있으니 버퍼 작업을 할 수없다
|
//마지막위치가 아닌 다른 위치에 있으니 버퍼 작업을 할 수없다
|
||||||
PUB.log.AddAT("목적지 버퍼이동완료 했지만 마지막 노드가 아닙니다");
|
PUB.log.AddAT("목적지 버퍼이동완료 했지만 마지막 노드가 아닙니다");
|
||||||
PUB.XBE.BufferInReady = false;
|
PUB.XBE.BufferInReady = false;
|
||||||
PUB.XBE.BufferReadyError = true;
|
PUB.XBE.BufferReadyError = true;
|
||||||
}
|
}
|
||||||
PUB.XBE.BufferInComplete = false;
|
PUB.XBE.BufferInComplete = false;
|
||||||
PUB.XBE.BufferOutComplete = false;
|
PUB.XBE.BufferOutComplete = false;
|
||||||
}
|
break;
|
||||||
|
|
||||||
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
|
|
||||||
{
|
|
||||||
//목적지다 다른 형태이다
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
PUB._virtualAGV.Turn = AGVNavigationCore.Models.AGVTurn.None;
|
PUB._virtualAGV.Turn = AGVNavigationCore.Models.AGVTurn.None;
|
||||||
PUB.sm.SetNewRunStep(ERunStep.READY);
|
PUB.sm.SetNewRunStep(ERunStep.READY);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ namespace Project
|
|||||||
public Boolean _SM_RUN_POSCHK(bool isFirst, TimeSpan stepTime)
|
public Boolean _SM_RUN_POSCHK(bool isFirst, TimeSpan stepTime)
|
||||||
{
|
{
|
||||||
//현재위치가 설정되어있는지 확인한다, 현재위치값이 있는 경우 True 를 반환
|
//현재위치가 설정되어있는지 확인한다, 현재위치값이 있는 경우 True 를 반환
|
||||||
var currentnode = PUB.FindByNodeID(PUB._virtualAGV.CurrentNodeId);
|
var currentnode = PUB.FindByNodeID(PUB._virtualAGV.CurrentNode.Id);
|
||||||
if (currentnode != null) return true;
|
if (currentnode != null) return true;
|
||||||
|
|
||||||
//이동을 하지 않고있다면 전진을 진행한다
|
//이동을 하지 않고있다면 전진을 진행한다
|
||||||
|
|||||||
@@ -24,8 +24,14 @@ namespace Project
|
|||||||
if (PUB.AGV.IsOpen == false)
|
if (PUB.AGV.IsOpen == false)
|
||||||
{
|
{
|
||||||
//agv connect
|
//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);
|
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)
|
else if (PUB.AGV.IsValid == true)
|
||||||
{
|
{
|
||||||
@@ -81,7 +87,7 @@ namespace Project
|
|||||||
|
|
||||||
synlist.Add("SGS", PUB.setting.GDSValue.ToString("0000"));
|
synlist.Add("SGS", PUB.setting.GDSValue.ToString("0000"));
|
||||||
VAR.I32[eVarInt32.SyncItemCount] = synlist.Count;
|
VAR.I32[eVarInt32.SyncItemCount] = synlist.Count;
|
||||||
|
|
||||||
|
|
||||||
PUB.AddEEDB($"SYNC시작({PUB.Result.TargetPos})");
|
PUB.AddEEDB($"SYNC시작({PUB.Result.TargetPos})");
|
||||||
|
|
||||||
@@ -129,7 +135,7 @@ namespace Project
|
|||||||
if (PUB.AGV.ACKData.Equals(item.Key))
|
if (PUB.AGV.ACKData.Equals(item.Key))
|
||||||
{
|
{
|
||||||
synidx += 1;
|
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);
|
else PUB.sm.UpdateRunStepSeq(-1);
|
||||||
LastCommandTime = DateTime.Now;
|
LastCommandTime = DateTime.Now;
|
||||||
}
|
}
|
||||||
@@ -150,11 +156,11 @@ namespace Project
|
|||||||
{
|
{
|
||||||
PUB.AddEEDB($"SYNC완료({PUB.Result.TargetPos})");
|
PUB.AddEEDB($"SYNC완료({PUB.Result.TargetPos})");
|
||||||
UpdateProgressStatus(stepTime.TotalSeconds, 5, "SYNC : 완료");
|
UpdateProgressStatus(stepTime.TotalSeconds, 5, "SYNC : 완료");
|
||||||
|
|
||||||
// 동기화 완료 시 캔버스 모드 복귀
|
// 동기화 완료 시 캔버스 모드 복귀
|
||||||
if (PUB._mapCanvas != null)
|
if (PUB._mapCanvas != null)
|
||||||
PUB._mapCanvas.SetSyncStatus("동기화 완료!", 1.0f, "잠시 후 메인 화면으로 이동합니다.");
|
PUB._mapCanvas.SetSyncStatus("동기화 완료!", 1.0f, "잠시 후 메인 화면으로 이동합니다.");
|
||||||
|
|
||||||
LastCommandTime = DateTime.Now;
|
LastCommandTime = DateTime.Now;
|
||||||
PUB.sm.UpdateRunStepSeq();
|
PUB.sm.UpdateRunStepSeq();
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ namespace Project
|
|||||||
if (_SM_RUN_POSCHK(false, new TimeSpan()) == false) return false;
|
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)
|
if (currentNode == null)
|
||||||
{
|
{
|
||||||
PUB.log.AddE($"현재위치노드가 없습니다");
|
PUB.log.AddE($"현재위치노드가 없습니다");
|
||||||
@@ -81,7 +81,7 @@ namespace Project
|
|||||||
|
|
||||||
//시작노드값이 없다면 현재위치를 노드로 결정한다
|
//시작노드값이 없다면 현재위치를 노드로 결정한다
|
||||||
if (PUB._virtualAGV.StartNode == null)
|
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)
|
if (PUB._virtualAGV.StartNode == null)
|
||||||
@@ -102,7 +102,7 @@ namespace Project
|
|||||||
//경로 생성(경로정보가 없거나 현재노드가 경로에 없는경우)
|
//경로 생성(경로정보가 없거나 현재노드가 경로에 없는경우)
|
||||||
if (PUB._virtualAGV.CurrentPath == null ||
|
if (PUB._virtualAGV.CurrentPath == null ||
|
||||||
PUB._virtualAGV.CurrentPath.DetailedPath.Any() == false ||
|
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)
|
if (PUB.AGV.system1.agv_run)
|
||||||
{
|
{
|
||||||
@@ -145,7 +145,7 @@ namespace Project
|
|||||||
$"현재 상태: {PUB._virtualAGV.CurrentState}\n" +
|
$"현재 상태: {PUB._virtualAGV.CurrentState}\n" +
|
||||||
$"현재 방향: {PUB._virtualAGV.CurrentDirection}\n" +
|
$"현재 방향: {PUB._virtualAGV.CurrentDirection}\n" +
|
||||||
$"위치 확정: {PUB._virtualAGV.IsPositionConfirmed} (RFID {PUB._virtualAGV.DetectedRfidCount}개)\n" +
|
$"위치 확정: {PUB._virtualAGV.IsPositionConfirmed} (RFID {PUB._virtualAGV.DetectedRfidCount}개)\n" +
|
||||||
$"현재 노드: {PUB._virtualAGV.CurrentNodeId ?? "없음"}";
|
$"현재 노드: {PUB._virtualAGV.CurrentNode.Id ?? "없음"}";
|
||||||
|
|
||||||
//모터에서 정지를 요청했다
|
//모터에서 정지를 요청했다
|
||||||
if (nextAction.Motor == AGVNavigationCore.Models.MotorCommand.Stop)
|
if (nextAction.Motor == AGVNavigationCore.Models.MotorCommand.Stop)
|
||||||
@@ -169,11 +169,11 @@ namespace Project
|
|||||||
// 현재 노드가 타겟 노드와 같고, 위치가 확정된 상태라면 도착으로 간주
|
// 현재 노드가 타겟 노드와 같고, 위치가 확정된 상태라면 도착으로 간주
|
||||||
// 단, AGV가 실제로 멈췄는지 확인 (agv_run == false)
|
// 단, AGV가 실제로 멈췄는지 확인 (agv_run == false)
|
||||||
if (PUB._virtualAGV.IsPositionConfirmed &&
|
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)
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -251,7 +251,7 @@ namespace Project
|
|||||||
else
|
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)
|
VAR.BOOL[eVarBool.MARK_SENSOR] == true)
|
||||||
{
|
{
|
||||||
PUB.log.AddI("충전위치 검색 완료");
|
PUB.log.AddI("충전위치 검색 완료");
|
||||||
|
|||||||
@@ -175,13 +175,10 @@ namespace Project
|
|||||||
var newNodeId = $"AUTO_{PUB.Result.LastTAG}";
|
var newNodeId = $"AUTO_{PUB.Result.LastTAG}";
|
||||||
var newNode = new MapNode
|
var newNode = new MapNode
|
||||||
{
|
{
|
||||||
NodeId = newNodeId,
|
Id = newNodeId,
|
||||||
RfidId = PUB.Result.LastTAG,
|
RfidId = PUB.Result.LastTAG,
|
||||||
Name = $"자동추가_{PUB.Result.LastTAG}",
|
|
||||||
Type = NodeType.Normal,
|
|
||||||
Position = new Point(100, 100), // 기본 위치
|
Position = new Point(100, 100), // 기본 위치
|
||||||
IsActive = true,
|
IsActive = true,
|
||||||
DisplayColor = Color.Orange, // 자동 추가된 노드는 오렌지색으로 표시
|
|
||||||
CreatedDate = DateTime.Now,
|
CreatedDate = DateTime.Now,
|
||||||
ModifiedDate = DateTime.Now
|
ModifiedDate = DateTime.Now
|
||||||
};
|
};
|
||||||
@@ -232,7 +229,7 @@ namespace Project
|
|||||||
$"현재 상태: {PUB._virtualAGV.CurrentState}\n" +
|
$"현재 상태: {PUB._virtualAGV.CurrentState}\n" +
|
||||||
$"현재 방향: {PUB._virtualAGV.CurrentDirection}\n" +
|
$"현재 방향: {PUB._virtualAGV.CurrentDirection}\n" +
|
||||||
$"위치 확정: {PUB._virtualAGV.IsPositionConfirmed} (RFID {PUB._virtualAGV.DetectedRfidCount}개)\n" +
|
$"위치 확정: {PUB._virtualAGV.IsPositionConfirmed} (RFID {PUB._virtualAGV.DetectedRfidCount}개)\n" +
|
||||||
$"현재 노드: {PUB._virtualAGV.CurrentNodeId ?? "없음"}";
|
$"현재 노드: {PUB._virtualAGV.CurrentNode.Id ?? "없음"}";
|
||||||
|
|
||||||
PUB._mapCanvas.PredictMessage = message;
|
PUB._mapCanvas.PredictMessage = message;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ using AGVNavigationCore.Utils;
|
|||||||
using AR;
|
using AR;
|
||||||
using arDev;
|
using arDev;
|
||||||
using COMM;
|
using COMM;
|
||||||
|
using Project.StateMachine;
|
||||||
|
|
||||||
namespace Project
|
namespace Project
|
||||||
{
|
{
|
||||||
@@ -56,7 +57,7 @@ namespace Project
|
|||||||
}
|
}
|
||||||
else
|
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);
|
PUB._mapCanvas.SetAGVPosition(PUB.setting.MCID, node, PUB._virtualAGV.CurrentDirection);
|
||||||
@@ -69,25 +70,25 @@ namespace Project
|
|||||||
case ENIGProtocol.AGVCommandHE.PickOff: // 111
|
case ENIGProtocol.AGVCommandHE.PickOff: // 111
|
||||||
{
|
{
|
||||||
PUB.log.AddI($"XBEE:작업명령수신:{cmd}");
|
PUB.log.AddI($"XBEE:작업명령수신:{cmd}");
|
||||||
|
|
||||||
// 현재 위치 확인 (TargetNode가 아닌 CurrentNode 기준)
|
// 현재 위치 확인 (TargetNode가 아닌 CurrentNode 기준)
|
||||||
var currNode = PUB._virtualAGV.CurrentNode;
|
var currNode = PUB._virtualAGV.CurrentNode;
|
||||||
if (currNode == null)
|
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");
|
PUB.XBE.SendError(ENIGProtocol.AGVErrorCode.EmptyNode, "Unknown Node");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
PUB.NextWorkCmd = cmd;
|
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 StationType.Loader: nextStep = ERunStep.LOADER_IN; break;
|
||||||
case NodeType.UnLoader: nextStep = ERunStep.UNLOADER_IN; break;
|
case StationType.UnLoader: nextStep = ERunStep.UNLOADER_IN; break;
|
||||||
case NodeType.Buffer: nextStep = ERunStep.BUFFER_IN; break;
|
case StationType.Buffer: nextStep = ERunStep.BUFFER_IN; break;
|
||||||
case NodeType.Clearner: nextStep = ERunStep.CLEANER_IN; break;
|
case StationType.Clearner: nextStep = ERunStep.CLEANER_IN; break;
|
||||||
default:
|
default:
|
||||||
PUB.log.AddE($"[{logPrefix}-{cmd}] 해당 노드타입({currNode.Type})은 작업을 지원하지 않습니다.");
|
PUB.log.AddE($"[{logPrefix}-{cmd}] 해당 노드타입({currNode.Type})은 작업을 지원하지 않습니다.");
|
||||||
return;
|
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;
|
PUB._virtualAGV.StartNode = startNode;
|
||||||
if (startNode == null)
|
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)
|
if (startNode != null)
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ namespace Project.ViewForm
|
|||||||
//PUB._mapCanvas.NodeAdded += OnNodeAdded;
|
//PUB._mapCanvas.NodeAdded += OnNodeAdded;
|
||||||
// 이벤트 연결
|
// 이벤트 연결
|
||||||
//PUB._mapCanvas.NodeAdded += OnNodeAdded;
|
//PUB._mapCanvas.NodeAdded += OnNodeAdded;
|
||||||
PUB._mapCanvas.NodesSelected += OnNodeSelected;
|
PUB._mapCanvas.NodeSelect += OnNodeSelected;
|
||||||
//PUB._mapCanvas.NodeMoved += OnNodeMoved;
|
//PUB._mapCanvas.NodeMoved += OnNodeMoved;
|
||||||
//PUB._mapCanvas.NodeDeleted += OnNodeDeleted;
|
//PUB._mapCanvas.NodeDeleted += OnNodeDeleted;
|
||||||
//PUB._mapCanvas.ConnectionDeleted += OnConnectionDeleted;
|
//PUB._mapCanvas.ConnectionDeleted += OnConnectionDeleted;
|
||||||
@@ -62,40 +62,40 @@ namespace Project.ViewForm
|
|||||||
panel1.Controls.Add(PUB._mapCanvas);
|
panel1.Controls.Add(PUB._mapCanvas);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnNodeSelected(object sender, List<MapNode> nodes, MouseEventArgs e)
|
|
||||||
|
private void OnNodeSelected(object sender, NodeBase node, MouseEventArgs e)
|
||||||
{
|
{
|
||||||
if (e.Button != MouseButtons.Right) return;
|
if (e.Button != MouseButtons.Right) return;
|
||||||
var node = nodes.FirstOrDefault();
|
if (node == null) return;
|
||||||
if (nodes == null) return;
|
if ((node is MapNode mapnode) == false) return;
|
||||||
|
|
||||||
// 도킹 가능한 노드인지 또는 작업 노드인지 확인
|
// 도킹 가능한 노드인지 또는 작업 노드인지 확인
|
||||||
bool isDockingNode = node.Type == NodeType.Loader || node.Type == NodeType.UnLoader
|
if (mapnode.isDockingNode == false) return;
|
||||||
|| node.Type == NodeType.Buffer || node.Type == NodeType.Clearner
|
|
||||||
|| node.Type == NodeType.Charging;
|
|
||||||
|
|
||||||
if (!isDockingNode) return;
|
|
||||||
|
|
||||||
ContextMenuStrip menu = new ContextMenuStrip();
|
ContextMenuStrip menu = new ContextMenuStrip();
|
||||||
|
|
||||||
// PickOn
|
// PickOn
|
||||||
var pickOn = new ToolStripMenuItem("Pick On (Move & Pick)");
|
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);
|
menu.Items.Add(pickOn);
|
||||||
|
|
||||||
// PickOff
|
// PickOff
|
||||||
var pickOff = new ToolStripMenuItem("Pick Off (Move & Drop)");
|
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);
|
menu.Items.Add(pickOff);
|
||||||
|
|
||||||
// Charge
|
// Charge
|
||||||
if (node.Type == NodeType.Charging)
|
if (mapnode.StationType == StationType.Charger)
|
||||||
{
|
{
|
||||||
var charge = new ToolStripMenuItem("Charge (Move & Charge)");
|
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.Items.Add(charge);
|
||||||
}
|
}
|
||||||
|
|
||||||
menu.Show(Cursor.Position);
|
menu.Show(Cursor.Position);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ExecuteManualCommand(MapNode targetNode, ENIGProtocol.AGVCommandHE cmd)
|
private void ExecuteManualCommand(MapNode targetNode, ENIGProtocol.AGVCommandHE cmd)
|
||||||
@@ -105,13 +105,19 @@ namespace Project.ViewForm
|
|||||||
MessageBox.Show("AGV의 현재 위치를 알 수 없습니다.");
|
MessageBox.Show("AGV의 현재 위치를 알 수 없습니다.");
|
||||||
return;
|
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 (MessageBox.Show("현재 대기상태가 아닙니다. 강제로 실행하시겠습니까?", "Warning", MessageBoxButtons.YesNo) == DialogResult.No) return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (targetNode.isDockingNode == false)
|
||||||
|
{
|
||||||
|
UTIL.MsgE("이동 가능한 노드가 아닙니다");
|
||||||
|
}
|
||||||
|
|
||||||
// 1. 경로 생성
|
// 1. 경로 생성
|
||||||
var pathFinder = new AGVNavigationCore.PathFinding.Planning.AGVPathfinder(PUB._mapNodes);
|
var pathFinder = new AGVNavigationCore.PathFinding.Planning.AGVPathfinder(PUB._mapNodes);
|
||||||
|
|
||||||
// 현재위치에서 목표위치까지
|
// 현재위치에서 목표위치까지
|
||||||
var result = pathFinder.FindPath(PUB._virtualAGV.CurrentNode, targetNode);
|
var result = pathFinder.FindPath(PUB._virtualAGV.CurrentNode, targetNode);
|
||||||
|
|
||||||
@@ -122,29 +128,22 @@ namespace Project.ViewForm
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 2. 상태 설정
|
// 2. 상태 설정
|
||||||
PUB.log.AddI($"[Manual Command] {cmd} to {targetNode.Name}({targetNode.NodeId})");
|
|
||||||
|
|
||||||
// Path 변환 (Node 리스트 -> AGVPathResult)
|
// 2. 상태 설정
|
||||||
// VirtualAGV.SetPath가 필요할 수 있음. 혹은 Goto 로직을 수동으로 구성.
|
if (targetNode is MapNode mapno)
|
||||||
// _SM_RUN_GOTO에서는 PUB._virtualAGV.CurrentPath를 사용함.
|
PUB.log.AddI($"[Manual Command] {cmd} to {mapno.RfidId}({targetNode.Id})");
|
||||||
// AGVPathResult 생성 필요.
|
else
|
||||||
|
PUB.log.AddI($"[Manual Command] {cmd} to ({targetNode.Id})");
|
||||||
|
|
||||||
var detailedPath = AGVNavigationCore.PathFinding.Planning.AGVPathfinder.MakeDetailData(result.Path, PUB._mapNodes);
|
// FindPathResult contains DetailedPath already.
|
||||||
PUB._virtualAGV.SetPath(result.Path, detailedPath);
|
PUB._virtualAGV.SetPath(result);
|
||||||
PUB._virtualAGV.TargetNode = targetNode;
|
PUB._virtualAGV.TargetNode = targetNode as MapNode;
|
||||||
|
|
||||||
// 3. 작업 설정
|
// 3. 작업 설정
|
||||||
PUB.NextWorkCmd = cmd;
|
PUB.NextWorkCmd = cmd;
|
||||||
|
|
||||||
// 4. 실행
|
// 4. 실행
|
||||||
PUB.sm.SetNewRunStep(ERunStep.GOTO); // GOTO -> Arrive -> _IN sequence execution
|
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<AGVNavigationCore.Controls.IAGV> { PUB._virtualAGV };
|
var agvList = new System.Collections.Generic.List<AGVNavigationCore.Controls.IAGV> { PUB._virtualAGV };
|
||||||
PUB._mapCanvas.AGVList = agvList;
|
PUB._mapCanvas.AGVList = agvList;
|
||||||
|
|
||||||
PUB.log.Add($"가상 AGV 생성: {startNode.NodeId} 위치");
|
PUB.log.Add($"가상 AGV 생성: {startNode.Id} 위치");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (PUB._virtualAGV != null)
|
else if (PUB._virtualAGV != null)
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ namespace Project.ViewForm
|
|||||||
|
|
||||||
// [Manual Safety] Clear previous auto-task state
|
// [Manual Safety] Clear previous auto-task state
|
||||||
PUB._virtualAGV.TargetNode = null;
|
PUB._virtualAGV.TargetNode = null;
|
||||||
PUB._virtualAGV.CurrentPath = null;
|
PUB._virtualAGV.StopPath();
|
||||||
PUB.NextWorkCmd = ENIGProtocol.AGVCommandHE.Stop; // Clear ACS Command
|
PUB.NextWorkCmd = ENIGProtocol.AGVCommandHE.Stop; // Clear ACS Command
|
||||||
PUB.sm.ClearRunStep(); // Clear RunStep sequence
|
PUB.sm.ClearRunStep(); // Clear RunStep sequence
|
||||||
}
|
}
|
||||||
@@ -117,7 +117,7 @@ namespace Project.ViewForm
|
|||||||
|
|
||||||
// [Manual Safety] Clear previous auto-task state
|
// [Manual Safety] Clear previous auto-task state
|
||||||
PUB._virtualAGV.TargetNode = null;
|
PUB._virtualAGV.TargetNode = null;
|
||||||
PUB._virtualAGV.CurrentPath = null;
|
PUB._virtualAGV.StopPath();
|
||||||
PUB.NextWorkCmd = ENIGProtocol.AGVCommandHE.Stop;
|
PUB.NextWorkCmd = ENIGProtocol.AGVCommandHE.Stop;
|
||||||
PUB.sm.ClearRunStep();
|
PUB.sm.ClearRunStep();
|
||||||
}
|
}
|
||||||
@@ -146,7 +146,7 @@ namespace Project.ViewForm
|
|||||||
|
|
||||||
// [Manual Safety] Clear previous auto-task state
|
// [Manual Safety] Clear previous auto-task state
|
||||||
PUB._virtualAGV.TargetNode = null;
|
PUB._virtualAGV.TargetNode = null;
|
||||||
PUB._virtualAGV.CurrentPath = null;
|
PUB._virtualAGV.StopPath();
|
||||||
PUB.NextWorkCmd = ENIGProtocol.AGVCommandHE.Stop;
|
PUB.NextWorkCmd = ENIGProtocol.AGVCommandHE.Stop;
|
||||||
PUB.sm.ClearRunStep();
|
PUB.sm.ClearRunStep();
|
||||||
}
|
}
|
||||||
@@ -175,7 +175,7 @@ namespace Project.ViewForm
|
|||||||
|
|
||||||
// [Manual Safety] Clear previous auto-task state
|
// [Manual Safety] Clear previous auto-task state
|
||||||
PUB._virtualAGV.TargetNode = null;
|
PUB._virtualAGV.TargetNode = null;
|
||||||
PUB._virtualAGV.CurrentPath = null;
|
PUB._virtualAGV.StopPath();
|
||||||
PUB.NextWorkCmd = ENIGProtocol.AGVCommandHE.Stop;
|
PUB.NextWorkCmd = ENIGProtocol.AGVCommandHE.Stop;
|
||||||
PUB.sm.ClearRunStep();
|
PUB.sm.ClearRunStep();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -806,6 +806,10 @@ namespace Project
|
|||||||
|
|
||||||
// 맵 캔버스에 데이터 설정
|
// 맵 캔버스에 데이터 설정
|
||||||
_mapCanvas.Nodes = result.Nodes;
|
_mapCanvas.Nodes = result.Nodes;
|
||||||
|
_mapCanvas.Labels = result.Labels;
|
||||||
|
_mapCanvas.Images = result.Images;
|
||||||
|
_mapCanvas.Marks = result.Marks;
|
||||||
|
_mapCanvas.Magnets = result.Magnets;
|
||||||
// RfidMappings 제거됨 - MapNode에 통합
|
// RfidMappings 제거됨 - MapNode에 통합
|
||||||
|
|
||||||
// 🔥 맵 설정 적용 (배경색, 그리드 표시)
|
// 🔥 맵 설정 적용 (배경색, 그리드 표시)
|
||||||
@@ -865,7 +869,7 @@ namespace Project
|
|||||||
ShowGrid = _mapCanvas.ShowGrid
|
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;
|
PUB.setting.LastMapFile = filePath;
|
||||||
|
|||||||
1
Cs_HMI/TestProject/mcpServers
Submodule
1
Cs_HMI/TestProject/mcpServers
Submodule
Submodule Cs_HMI/TestProject/mcpServers added at 792c47442d
@@ -1,13 +1,14 @@
|
|||||||
@echo off
|
|
||||||
|
|
||||||
echo Building AGV C# HMI Project...
|
echo Building AGV C# HMI Project...
|
||||||
|
|
||||||
REM Set MSBuild path
|
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 (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)
|
REM Rebuild Debug x86 configuration (VS-style Rebuild)
|
||||||
%MSBUILD% AGVCSharp.sln -property:Configuration=Debug -property:Platform=x86 -verbosity:quiet -nologo -t:Rebuild
|
%MSBUILD% "S:\Source\Amkor\ENIG\Cs_HMI\AGVCSharp.sln" -property:Configuration=Debug -property:Platform=x86 -verbosity:quiet -nologo -t:Rebuild
|
||||||
|
|
||||||
pause
|
|
||||||
Reference in New Issue
Block a user