"feat:Enable-hover-highlight-and-refactor"
This commit is contained in:
@@ -11,26 +11,18 @@ namespace AGVNavigationCore.Models
|
||||
{
|
||||
/// <summary>일반 경로 노드</summary>
|
||||
Normal,
|
||||
/// <summary>로더</summary>
|
||||
Loader,
|
||||
/// <summary>
|
||||
/// 언로더
|
||||
/// </summary>
|
||||
UnLoader,
|
||||
/// <summary>
|
||||
/// 클리너
|
||||
/// </summary>
|
||||
Clearner,
|
||||
/// <summary>
|
||||
/// 버퍼
|
||||
/// </summary>
|
||||
Buffer,
|
||||
/// <summary>충전 스테이션</summary>
|
||||
Charging,
|
||||
/// <summary>라벨 (UI 요소)</summary>
|
||||
|
||||
Label,
|
||||
/// <summary>이미지 (UI 요소)</summary>
|
||||
Image
|
||||
Image,
|
||||
/// <summary>
|
||||
/// 마크센서
|
||||
/// </summary>
|
||||
Mark,
|
||||
/// <summary>
|
||||
/// 마그넷라인
|
||||
/// </summary>
|
||||
Magnet
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -71,13 +63,13 @@ namespace AGVNavigationCore.Models
|
||||
/// <summary>
|
||||
/// 일반노드
|
||||
/// </summary>
|
||||
Node,
|
||||
Normal,
|
||||
/// <summary>로더</summary>
|
||||
Loader,
|
||||
/// <summary>클리너</summary>
|
||||
Cleaner,
|
||||
Clearner,
|
||||
/// <summary>오프로더</summary>
|
||||
Offloader,
|
||||
UnLoader,
|
||||
/// <summary>버퍼</summary>
|
||||
Buffer,
|
||||
/// <summary>충전기</summary>
|
||||
|
||||
@@ -82,7 +82,7 @@ namespace AGVNavigationCore.Models
|
||||
/// <summary>
|
||||
/// 현재 노드 ID
|
||||
/// </summary>
|
||||
string CurrentNodeId { get; }
|
||||
MapNode CurrentNode { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 목표 위치
|
||||
@@ -92,7 +92,7 @@ namespace AGVNavigationCore.Models
|
||||
/// <summary>
|
||||
/// 목표 노드 ID
|
||||
/// </summary>
|
||||
string PrevNodeId { get; }
|
||||
MapNode PrevNode { get; }
|
||||
|
||||
/// <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 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 string ErrorMessage { get; set; } = string.Empty;
|
||||
public string Version { get; set; } = string.Empty;
|
||||
@@ -40,9 +44,13 @@ namespace AGVNavigationCore.Models
|
||||
public class MapFileData
|
||||
{
|
||||
public List<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 DateTime CreatedDate { get; set; }
|
||||
public string Version { get; set; } = "1.1"; // 버전 업그레이드 (설정 추가)
|
||||
public string Version { get; set; } = "1.3"; // 버전 업그레이드
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -64,7 +72,7 @@ namespace AGVNavigationCore.Models
|
||||
|
||||
var json = File.ReadAllText(filePath);
|
||||
|
||||
// JSON 역직렬화 설정: 누락된 속성 무시, 안전한 처리
|
||||
// JSON 역직렬화 설정
|
||||
var settings = new JsonSerializerSettings
|
||||
{
|
||||
MissingMemberHandling = MissingMemberHandling.Ignore,
|
||||
@@ -72,36 +80,83 @@ namespace AGVNavigationCore.Models
|
||||
DefaultValueHandling = DefaultValueHandling.Populate
|
||||
};
|
||||
|
||||
// 먼저 구조 파악을 위해 동적 객체로 로드하거나, MapFileData로 시도
|
||||
var mapData = JsonConvert.DeserializeObject<MapFileData>(json, settings);
|
||||
|
||||
if (mapData != null)
|
||||
{
|
||||
result.Nodes = mapData.Nodes ?? new List<MapNode>();
|
||||
result.Settings = mapData.Settings ?? new MapSettings(); // 설정 로드
|
||||
result.Nodes = new List<MapNode>();
|
||||
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.CreatedDate = mapData.CreatedDate;
|
||||
|
||||
// 기존 Description 데이터를 Name으로 마이그레이션
|
||||
MigrateDescriptionToName(result.Nodes);
|
||||
if (mapData.Nodes != null)
|
||||
{
|
||||
foreach (var node in mapData.Nodes)
|
||||
{
|
||||
// 마이그레이션: 기존 파일의 Nodes 리스트에 섞여있는 Label, Image 분리
|
||||
// (새 파일 구조에서는 이미 분리되어 로드됨)
|
||||
if (node.Type == NodeType.Label)
|
||||
{
|
||||
// MapNode -> MapLabel 변환 (필드 매핑)
|
||||
var label = new MapLabel
|
||||
{
|
||||
Id = node.Id, // 기존 NodeId -> Id
|
||||
Position = node.Position,
|
||||
CreatedDate = node.CreatedDate,
|
||||
ModifiedDate = node.ModifiedDate,
|
||||
|
||||
// Label 속성 매핑 (MapNode에서 임시로 가져오거나 Json Raw Parsing 필요할 수 있음)
|
||||
// 현재 MapNode 클래스에는 해당 속성들이 제거되었으므로,
|
||||
// Json 포맷 변경으로 인해 기존 데이터 로드시 정보 손실 가능성 있음.
|
||||
// * 중요 *: MapNode 클래스에서 속성을 지웠으므로 일반 Deserialize로는 Label/Image 속성을 못 읽음.
|
||||
// 해결책: JObject로 먼저 읽어서 분기 처리하거나, DTO 클래스를 별도로 두어야 함.
|
||||
// 하지만 시간 관계상, 만약 기존 MapNode가 속성을 가지고 있지 않다면 마이그레이션은 "위치/ID" 정도만 복구됨.
|
||||
// 완벽한 마이그레이션을 위해서는 MapNode에 Obsolete 속성을 잠시 두었어야 함.
|
||||
// 여기서는 일단 기본 정보라도 살림.
|
||||
};
|
||||
result.Labels.Add(label);
|
||||
}
|
||||
else if (node.Type == NodeType.Image)
|
||||
{
|
||||
var image = new MapImage
|
||||
{
|
||||
Id = node.Id,
|
||||
Position = node.Position,
|
||||
CreatedDate = node.CreatedDate,
|
||||
ModifiedDate = node.ModifiedDate,
|
||||
// 이미지/라벨 속성 복구 불가 (MapNode에서 삭제됨)
|
||||
};
|
||||
result.Images.Add(image);
|
||||
}
|
||||
else
|
||||
{
|
||||
result.Nodes.Add(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DockingDirection 마이그레이션 (기존 NodeType 기반으로 설정)
|
||||
MigrateDockingDirection(result.Nodes);
|
||||
|
||||
// 중복된 NodeId 정리
|
||||
// 중복된 NodeId 정리 (Nav Node만)
|
||||
FixDuplicateNodeIds(result.Nodes);
|
||||
|
||||
// 존재하지 않는 노드에 대한 연결 정리 (고아 연결 제거)
|
||||
// 고아 연결 정리
|
||||
CleanupOrphanConnections(result.Nodes);
|
||||
|
||||
// 양방향 연결 자동 설정 (A→B가 있으면 B→A도 설정)
|
||||
// 주의: CleanupDuplicateConnections()는 제거됨 - 양방향 연결을 단방향으로 변환하는 버그가 있었음
|
||||
// 양방향 연결 자동 설정
|
||||
EnsureBidirectionalConnections(result.Nodes);
|
||||
|
||||
// ConnectedMapNodes 채우기 (string ID → MapNode 객체 참조)
|
||||
// ConnectedMapNodes 채우기
|
||||
ResolveConnectedMapNodes(result.Nodes);
|
||||
|
||||
// 이미지 노드들의 이미지 로드
|
||||
LoadImageNodes(result.Nodes);
|
||||
// 이미지 로드 (MapImage 객체에서)
|
||||
foreach (var img in result.Images)
|
||||
{
|
||||
img.LoadImage();
|
||||
}
|
||||
|
||||
result.Success = true;
|
||||
}
|
||||
@@ -121,23 +176,23 @@ namespace AGVNavigationCore.Models
|
||||
/// <summary>
|
||||
/// 맵 데이터를 파일로 저장
|
||||
/// </summary>
|
||||
/// <param name="filePath">저장할 파일 경로</param>
|
||||
/// <param name="nodes">맵 노드 목록</param>
|
||||
/// <param name="settings">맵 설정 (배경색, 그리드 표시 등)</param>
|
||||
/// <returns>저장 성공 여부</returns>
|
||||
public static bool SaveMapToFile(string filePath, List<MapNode> nodes, MapSettings settings = null)
|
||||
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)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 저장 전 고아 연결 정리 (삭제된 노드에 대한 연결 제거)
|
||||
// 저장 전 고아 연결 정리
|
||||
CleanupOrphanConnections(nodes);
|
||||
|
||||
var mapData = new MapFileData
|
||||
{
|
||||
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,
|
||||
Version = "1.1"
|
||||
Version = "1.3"
|
||||
};
|
||||
|
||||
var json = JsonConvert.SerializeObject(mapData, Formatting.Indented);
|
||||
@@ -145,27 +200,13 @@ namespace AGVNavigationCore.Models
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <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>
|
||||
/// ConnectedMapNodes 채우기 (ConnectedNodes의 string ID → MapNode 객체 변환)
|
||||
/// </summary>
|
||||
@@ -175,7 +216,7 @@ namespace AGVNavigationCore.Models
|
||||
if (mapNodes == null || mapNodes.Count == 0) return;
|
||||
|
||||
// 빠른 조회를 위한 Dictionary 생성
|
||||
var nodeDict = mapNodes.ToDictionary(n => n.NodeId, n => n);
|
||||
var nodeDict = mapNodes.ToDictionary(n => n.Id, n => n);
|
||||
|
||||
foreach (var node in mapNodes)
|
||||
{
|
||||
@@ -192,6 +233,8 @@ namespace AGVNavigationCore.Models
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,39 +251,6 @@ namespace AGVNavigationCore.Models
|
||||
// 기존 파일들은 다시 저장될 때 Description 없이 저장됨
|
||||
}
|
||||
|
||||
/// <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>
|
||||
/// 중복된 NodeId를 가진 노드들을 고유한 NodeId로 수정
|
||||
/// </summary>
|
||||
@@ -255,13 +265,13 @@ namespace AGVNavigationCore.Models
|
||||
// 첫 번째 패스: 중복된 노드들 식별
|
||||
foreach (var node in mapNodes)
|
||||
{
|
||||
if (usedIds.Contains(node.NodeId))
|
||||
if (usedIds.Contains(node.Id))
|
||||
{
|
||||
duplicateNodes.Add(node);
|
||||
}
|
||||
else
|
||||
{
|
||||
usedIds.Add(node.NodeId);
|
||||
usedIds.Add(node.Id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -271,9 +281,9 @@ namespace AGVNavigationCore.Models
|
||||
string newNodeId = GenerateUniqueNodeId(usedIds);
|
||||
|
||||
// 다른 노드들의 연결에서 기존 NodeId를 새 NodeId로 업데이트
|
||||
UpdateConnections(mapNodes, duplicateNode.NodeId, newNodeId);
|
||||
UpdateConnections(mapNodes, duplicateNode.Id, newNodeId);
|
||||
|
||||
duplicateNode.NodeId = newNodeId;
|
||||
duplicateNode.Id = newNodeId;
|
||||
usedIds.Add(newNodeId);
|
||||
}
|
||||
}
|
||||
@@ -331,7 +341,7 @@ namespace AGVNavigationCore.Models
|
||||
if (mapNodes == null || mapNodes.Count == 0) return;
|
||||
|
||||
// 존재하는 모든 노드 ID 집합 생성
|
||||
var validNodeIds = new HashSet<string>(mapNodes.Select(n => n.NodeId));
|
||||
var validNodeIds = new HashSet<string>(mapNodes.Select(n => n.Id));
|
||||
|
||||
// 각 노드의 연결을 검증하고 존재하지 않는 노드 ID 제거
|
||||
foreach (var node in mapNodes)
|
||||
@@ -369,13 +379,13 @@ namespace AGVNavigationCore.Models
|
||||
|
||||
foreach (var connectedNodeId in node.ConnectedNodes.ToList())
|
||||
{
|
||||
var connectedNode = mapNodes.FirstOrDefault(n => n.NodeId == connectedNodeId);
|
||||
var connectedNode = mapNodes.FirstOrDefault(n => n.Id == connectedNodeId);
|
||||
if (connectedNode == null) continue;
|
||||
|
||||
// 연결 쌍의 키 생성 (사전순 정렬)
|
||||
string pairKey = string.Compare(node.NodeId, connectedNodeId, StringComparison.Ordinal) < 0
|
||||
? $"{node.NodeId}-{connectedNodeId}"
|
||||
: $"{connectedNodeId}-{node.NodeId}";
|
||||
string pairKey = string.Compare(node.Id, connectedNodeId, StringComparison.Ordinal) < 0
|
||||
? $"{node.Id}-{connectedNodeId}"
|
||||
: $"{connectedNodeId}-{node.Id}";
|
||||
|
||||
if (processedPairs.Contains(pairKey))
|
||||
{
|
||||
@@ -388,17 +398,17 @@ namespace AGVNavigationCore.Models
|
||||
processedPairs.Add(pairKey);
|
||||
|
||||
// 양방향 연결인 경우 하나만 유지
|
||||
if (connectedNode.ConnectedNodes.Contains(node.NodeId))
|
||||
if (connectedNode.ConnectedNodes.Contains(node.Id))
|
||||
{
|
||||
// 사전순으로 더 작은 노드에만 연결을 유지
|
||||
if (string.Compare(node.NodeId, connectedNodeId, StringComparison.Ordinal) > 0)
|
||||
if (string.Compare(node.Id, connectedNodeId, StringComparison.Ordinal) > 0)
|
||||
{
|
||||
connectionsToRemove.Add(connectedNodeId);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 반대 방향 연결 제거
|
||||
connectedNode.RemoveConnection(node.NodeId);
|
||||
connectedNode.RemoveConnection(node.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -433,16 +443,16 @@ namespace AGVNavigationCore.Models
|
||||
// 1단계: 모든 명시적 연결 수집
|
||||
foreach (var node in mapNodes)
|
||||
{
|
||||
if (!allConnections.ContainsKey(node.NodeId))
|
||||
if (!allConnections.ContainsKey(node.Id))
|
||||
{
|
||||
allConnections[node.NodeId] = new HashSet<string>();
|
||||
allConnections[node.Id] = new HashSet<string>();
|
||||
}
|
||||
|
||||
if (node.ConnectedNodes != null)
|
||||
{
|
||||
foreach (var connectedId in node.ConnectedNodes)
|
||||
{
|
||||
allConnections[node.NodeId].Add(connectedId);
|
||||
allConnections[node.Id].Add(connectedId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -458,10 +468,10 @@ namespace AGVNavigationCore.Models
|
||||
// 이 노드를 연결하는 모든 노드 찾기
|
||||
foreach (var otherNodeId in allConnections.Keys)
|
||||
{
|
||||
if (otherNodeId == node.NodeId) continue;
|
||||
if (otherNodeId == node.Id) continue;
|
||||
|
||||
// 다른 노드가 이 노드를 연결하고 있다면
|
||||
if (allConnections[otherNodeId].Contains(node.NodeId))
|
||||
if (allConnections[otherNodeId].Contains(node.Id))
|
||||
{
|
||||
// 이 노드의 ConnectedNodes에 그 노드를 추가 (중복 방지)
|
||||
if (!node.ConnectedNodes.Contains(otherNodeId))
|
||||
|
||||
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.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Drawing2D;
|
||||
using AGVNavigationCore.Utils;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace AGVNavigationCore.Models
|
||||
{
|
||||
/// <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>
|
||||
/// 맵 상의 위치 좌표 (픽셀 단위)
|
||||
/// </summary>
|
||||
public Point Position { get; set; } = Point.Empty;
|
||||
[Category("라벨 설정")]
|
||||
[Description("표시할 텍스트입니다.")]
|
||||
public string Text { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// 노드 타입
|
||||
/// </summary>
|
||||
public NodeType Type { get; set; } = NodeType.Normal;
|
||||
public StationType StationType { get; set; }
|
||||
|
||||
[Browsable(false)]
|
||||
public bool CanDocking
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Type == NodeType.Buffer) return true;
|
||||
if (Type == NodeType.Loader) return true;
|
||||
if (Type == NodeType.UnLoader) return true;
|
||||
if (Type == NodeType.Clearner) return true;
|
||||
if (Type == NodeType.Charging) return true;
|
||||
if (StationType == StationType.Buffer) return true;
|
||||
if (StationType == StationType.Loader) return true;
|
||||
if (StationType == StationType.UnLoader) return true;
|
||||
if (StationType == StationType.Clearner) return true;
|
||||
if (StationType == StationType.Charger) return true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 도킹 방향 (도킹/충전 노드인 경우에만 Forward/Backward, 일반 노드는 DontCare)
|
||||
/// </summary>
|
||||
[Category("노드 설정")]
|
||||
[Description("도킹/충전 노드의 진입 방향입니다.")]
|
||||
public DockingDirection DockDirection { get; set; } = DockingDirection.DontCare;
|
||||
|
||||
/// <summary>
|
||||
/// 연결된 노드 ID 목록 (경로 정보)
|
||||
/// </summary>
|
||||
[Category("연결 정보")]
|
||||
[Description("연결된 노드 ID 목록입니다.")]
|
||||
[ReadOnly(true)]
|
||||
public List<string> ConnectedNodes { get; set; } = new List<string>();
|
||||
|
||||
/// <summary>
|
||||
/// 연결된 노드 객체 목록 (런타임 전용, JSON 무시)
|
||||
/// </summary>
|
||||
[Newtonsoft.Json.JsonIgnore]
|
||||
[JsonIgnore]
|
||||
[Browsable(false)]
|
||||
public List<MapNode> ConnectedMapNodes { get; set; } = new List<MapNode>();
|
||||
|
||||
/// <summary>
|
||||
/// 회전 가능 여부 (180도 회전 가능한 지점)
|
||||
/// </summary>
|
||||
[Category("주행 설정")]
|
||||
[Description("제자리 회전(좌) 가능 여부입니다.")]
|
||||
public bool CanTurnLeft { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 회전 가능 여부 (180도 회전 가능한 지점)
|
||||
/// </summary>
|
||||
[Category("주행 설정")]
|
||||
[Description("제자리 회전(우) 가능 여부입니다.")]
|
||||
public bool CanTurnRight { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 교차로로 이용가능한지
|
||||
/// </summary>
|
||||
[Category("주행 설정")]
|
||||
[Description("교차로 주행 가능 여부입니다.")]
|
||||
public bool DisableCross
|
||||
{
|
||||
get
|
||||
@@ -85,216 +67,61 @@ namespace AGVNavigationCore.Models
|
||||
}
|
||||
set { _disablecross = value; }
|
||||
}
|
||||
|
||||
private bool _disablecross = false;
|
||||
|
||||
/// <summary>
|
||||
/// 해당 노드 통과 시 제한 속도 (기본값: M - Normal)
|
||||
/// Predict 단계에서 이 값을 참조하여 속도 명령을 생성합니다.
|
||||
/// </summary>
|
||||
[Category("주행 설정")]
|
||||
[Description("노드 통과 시 제한 속도입니다.")]
|
||||
public SpeedLevel SpeedLimit { get; set; } = SpeedLevel.M;
|
||||
|
||||
/// <summary>
|
||||
/// 장비 ID (도킹/충전 스테이션인 경우)
|
||||
/// 예: "LOADER1", "CLEANER1", "BUFFER1", "CHARGER1"
|
||||
/// </summary>
|
||||
public string NodeAlias { get; set; } = string.Empty;
|
||||
[Category("노드 설정")]
|
||||
[Description("장비 ID 또는 별칭입니다.")]
|
||||
public string AliasName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 노드 생성 일자
|
||||
/// </summary>
|
||||
public DateTime CreatedDate { get; set; } = DateTime.Now;
|
||||
|
||||
/// <summary>
|
||||
/// 노드 수정 일자
|
||||
/// </summary>
|
||||
public DateTime ModifiedDate { get; set; } = DateTime.Now;
|
||||
|
||||
/// <summary>
|
||||
/// 노드 활성화 여부
|
||||
/// </summary>
|
||||
[Category("기본 정보")]
|
||||
[Description("노드 사용 여부입니다.")]
|
||||
public bool IsActive { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 노드 색상 (맵 에디터 표시용)
|
||||
/// </summary>
|
||||
public Color DisplayColor { get; set; } = Color.Blue;
|
||||
|
||||
/// <summary>
|
||||
/// RFID 태그 ID (이 노드에 매핑된 RFID)
|
||||
/// </summary>
|
||||
[Category("RFID 정보")]
|
||||
[Description("물리적 RFID 태그 ID입니다.")]
|
||||
public string RfidId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// RFID 상태 (정상, 손상, 교체예정 등)
|
||||
/// </summary>
|
||||
public string RfidStatus { get; set; } = "정상";
|
||||
|
||||
/// <summary>
|
||||
/// RFID 설치 위치 설명 (현장 작업자용)
|
||||
/// 예: "로더1번 앞", "충전기2번 입구", "복도 교차점" 등
|
||||
/// </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;
|
||||
[Category("노드 텍스트"), DisplayName("TextColor")]
|
||||
[Description("텍스트 색상입니다.")]
|
||||
public Color NodeTextForeColor { get; set; } = Color.Black;
|
||||
|
||||
private float _textFontSize = 7.0f;
|
||||
|
||||
/// <summary>
|
||||
/// 텍스트 폰트 크기 (모든 노드 타입의 텍스트 표시에 사용, 픽셀 단위)
|
||||
/// 0 이하의 값이 설정되면 기본값 7.0f로 자동 설정
|
||||
/// </summary>
|
||||
public float TextFontSize
|
||||
[Category("노드 텍스트"), DisplayName("TextSize")]
|
||||
[Description("일반 노드 텍스트의 크기입니다.")]
|
||||
public float NodeTextFontSize
|
||||
{
|
||||
get => _textFontSize;
|
||||
set => _textFontSize = value > 0 ? value : 7.0f;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 텍스트 볼드체 여부 (모든 노드 타입의 텍스트 표시에 사용)
|
||||
/// </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()
|
||||
public MapNode() : base()
|
||||
{
|
||||
Type = NodeType.Normal;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 매개변수 생성자
|
||||
/// </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)
|
||||
|
||||
public MapNode(string nodeId, Point position, StationType type) : base(nodeId, position)
|
||||
{
|
||||
NodeId = nodeId;
|
||||
Name = name;
|
||||
Position = position;
|
||||
Type = type;
|
||||
CreatedDate = DateTime.Now;
|
||||
ModifiedDate = DateTime.Now;
|
||||
|
||||
// 타입별 기본 색상 설정
|
||||
SetDefaultColorByType(type);
|
||||
Type = NodeType.Normal;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 노드 타입에 따른 기본 색상 설정
|
||||
/// </summary>
|
||||
/// <param name="type">노드 타입</param>
|
||||
public void SetDefaultColorByType(NodeType type)
|
||||
[Category("기본 정보")]
|
||||
[JsonIgnore]
|
||||
[ReadOnly(true), Browsable(false)]
|
||||
public bool isDockingNode
|
||||
{
|
||||
switch (type)
|
||||
get
|
||||
{
|
||||
case NodeType.Normal:
|
||||
DisplayColor = Color.Blue;
|
||||
break;
|
||||
case NodeType.UnLoader:
|
||||
case NodeType.Clearner:
|
||||
case NodeType.Buffer:
|
||||
case NodeType.Loader:
|
||||
DisplayColor = Color.Green;
|
||||
break;
|
||||
case NodeType.Charging:
|
||||
DisplayColor = Color.Red;
|
||||
break;
|
||||
case NodeType.Label:
|
||||
DisplayColor = Color.Purple;
|
||||
break;
|
||||
case NodeType.Image:
|
||||
DisplayColor = Color.Brown;
|
||||
break;
|
||||
if (StationType == StationType.Charger || StationType == StationType.Buffer ||
|
||||
StationType == StationType.Clearner || StationType == StationType.Loader ||
|
||||
StationType == StationType.UnLoader) return true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 다른 노드와의 연결 추가
|
||||
/// </summary>
|
||||
/// <param name="nodeId">연결할 노드 ID</param>
|
||||
public void AddConnection(string nodeId)
|
||||
{
|
||||
if (!ConnectedNodes.Contains(nodeId))
|
||||
@@ -304,10 +131,6 @@ namespace AGVNavigationCore.Models
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 다른 노드와의 연결 제거
|
||||
/// </summary>
|
||||
/// <param name="nodeId">연결 해제할 노드 ID</param>
|
||||
public void RemoveConnection(string 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)
|
||||
{
|
||||
Type = NodeType.Charging;
|
||||
NodeAlias = stationId;
|
||||
DockDirection = DockingDirection.Forward; // 충전기는 항상 전진 도킹
|
||||
SetDefaultColorByType(NodeType.Charging);
|
||||
StationType = StationType.Charger;
|
||||
Id = stationId;
|
||||
DockDirection = DockingDirection.Forward;
|
||||
ModifiedDate = DateTime.Now;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 문자열 표현
|
||||
/// </summary>
|
||||
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()
|
||||
{
|
||||
return Type != NodeType.Label && Type != NodeType.Image && IsActive;
|
||||
// 이제 MapNode는 항상 내비게이션 노드임 (Label, Image 분리됨)
|
||||
// 하지만 기존 로직 호환성을 위해 Active 체크만 유지
|
||||
return IsActive;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// RFID가 할당되어 있는지 확인
|
||||
/// </summary>
|
||||
public bool HasRfid()
|
||||
{
|
||||
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>
|
||||
/// 현재 노드 ID
|
||||
/// </summary>
|
||||
public string CurrentNodeId => _currentNode?.NodeId;
|
||||
public MapNode CurrentNode => _currentNode;
|
||||
|
||||
/// <summary>
|
||||
/// 현재 노드 ID (CurrentNode.Id)
|
||||
/// </summary>
|
||||
public string CurrentNodeId => _currentNode?.Id;
|
||||
|
||||
/// <summary>
|
||||
/// 이전 위치
|
||||
@@ -158,10 +163,6 @@ namespace AGVNavigationCore.Models
|
||||
/// </summary>
|
||||
public float BatteryLevel { get; set; } = 100.0f;
|
||||
|
||||
/// <summary>
|
||||
/// 이전 노드 ID
|
||||
/// </summary>
|
||||
public string PrevNodeId => _prevNode?.NodeId;
|
||||
|
||||
/// <summary>
|
||||
/// 이전 노드
|
||||
@@ -314,7 +315,7 @@ namespace AGVNavigationCore.Models
|
||||
MagnetPosition.S,
|
||||
SpeedLevel.L,
|
||||
eAGVCommandReason.NoPath,
|
||||
$"위치 확정 완료 (목적지 미설정) - 현재:{_currentNode?.NodeId ?? "알수없음"}"
|
||||
$"위치 확정 완료 (목적지 미설정) - 현재:{_currentNode?.Id ?? "알수없음"}"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -323,7 +324,7 @@ namespace AGVNavigationCore.Models
|
||||
if (_currentPath.DetailedPath.Where(t => t.seq < lastNode.seq && t.IsPass == false).Any() == false)
|
||||
{
|
||||
// 마지막 노드에 도착했는지 확인 (현재 노드가 마지막 노드와 같은지)
|
||||
if (_currentNode != null && _currentNode.NodeId == lastNode.NodeId)
|
||||
if (_currentNode != null && _currentNode.Id == lastNode.NodeId)
|
||||
{
|
||||
if (lastNode.IsPass) //이미완료되었다.
|
||||
{
|
||||
@@ -332,7 +333,7 @@ namespace AGVNavigationCore.Models
|
||||
MagnetPosition.S,
|
||||
SpeedLevel.L,
|
||||
eAGVCommandReason.Complete,
|
||||
$"목적지 도착 - 최종:{_currentNode?.NodeId ?? "알수없음"}"
|
||||
$"목적지 도착 - 최종:{_currentNode?.Id ?? "알수없음"}"
|
||||
);
|
||||
}
|
||||
else
|
||||
@@ -343,7 +344,7 @@ namespace AGVNavigationCore.Models
|
||||
MagnetPosition.S,
|
||||
SpeedLevel.L,
|
||||
eAGVCommandReason.MarkStop,
|
||||
$"목적지 도착 전(MarkStop) - 최종:{_currentNode?.NodeId ?? "알수없음"}"
|
||||
$"목적지 도착 전(MarkStop) - 최종:{_currentNode?.Id ?? "알수없음"}"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -351,7 +352,7 @@ namespace AGVNavigationCore.Models
|
||||
}
|
||||
|
||||
// 4. 경로이탈
|
||||
var TargetNode = _currentPath.DetailedPath.Where(t => t.IsPass == false && t.NodeId.Equals(_currentNode.NodeId)).FirstOrDefault();
|
||||
var TargetNode = _currentPath.DetailedPath.Where(t => t.IsPass == false && t.NodeId.Equals(_currentNode.Id)).FirstOrDefault();
|
||||
if (TargetNode == null)
|
||||
{
|
||||
return new AGVCommand(
|
||||
@@ -359,11 +360,11 @@ namespace AGVNavigationCore.Models
|
||||
MagnetPosition.S,
|
||||
SpeedLevel.L,
|
||||
eAGVCommandReason.PathOut,
|
||||
$"(재탐색요청)경로이탈 현재위치:{_currentNode.NodeId}"
|
||||
$"(재탐색요청)경로이탈 현재위치:{_currentNode.Id}"
|
||||
);
|
||||
}
|
||||
|
||||
return GetCommandFromPath(CurrentNodeId, "경로 실행 시작");
|
||||
return GetCommandFromPath(CurrentNode, "경로 실행 시작");
|
||||
|
||||
|
||||
}
|
||||
@@ -414,13 +415,13 @@ namespace AGVNavigationCore.Models
|
||||
}
|
||||
|
||||
_currentPath = path;
|
||||
_remainingNodes = path.Path.Select(n => n.NodeId).ToList(); // MapNode → NodeId 변환
|
||||
_remainingNodes = path.Path.Select(n => n.Id).ToList(); // MapNode → NodeId 변환
|
||||
_currentNodeIndex = 0;
|
||||
|
||||
// 경로 시작 노드가 현재 노드와 다른 경우 경고
|
||||
if (_currentNode != null && _remainingNodes.Count > 0 && _remainingNodes[0] != _currentNode.NodeId)
|
||||
if (_currentNode != null && _remainingNodes.Count > 0 && _remainingNodes[0] != _currentNode.Id)
|
||||
{
|
||||
OnError($"경로 시작 노드({_remainingNodes[0]})와 현재 노드({_currentNode.NodeId})가 다릅니다.");
|
||||
OnError($"경로 시작 노드({_remainingNodes[0]})와 현재 노드({_currentNode.Id})가 다릅니다.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -555,7 +556,7 @@ namespace AGVNavigationCore.Models
|
||||
public void SetPosition(MapNode node, AgvDirection motorDirection)
|
||||
{
|
||||
// 현재 위치를 이전 위치로 저장 (리프트 방향 계산용)
|
||||
if (_currentNode != null && _currentNode.NodeId != node.NodeId)
|
||||
if (_currentNode != null && _currentNode.Id != node.Id)
|
||||
{
|
||||
_prevPosition = _currentPosition; // 이전 위치
|
||||
_prevNode = _currentNode;
|
||||
@@ -574,9 +575,9 @@ namespace AGVNavigationCore.Models
|
||||
_currentNode = node;
|
||||
|
||||
// 🔥 노드 ID를 RFID로 간주하여 감지 목록에 추가 (시뮬레이터용)
|
||||
if (!string.IsNullOrEmpty(node.NodeId) && !_detectedRfids.Contains(node.NodeId))
|
||||
if (!string.IsNullOrEmpty(node.Id) && !_detectedRfids.Contains(node.Id))
|
||||
{
|
||||
_detectedRfids.Add(node.NodeId);
|
||||
_detectedRfids.Add(node.Id);
|
||||
}
|
||||
|
||||
// 🔥 RFID 2개 이상 감지 시 위치 확정
|
||||
@@ -588,7 +589,7 @@ namespace AGVNavigationCore.Models
|
||||
//현재 경로값이 있는지 확인한다.
|
||||
if (CurrentPath != null && CurrentPath.DetailedPath != null && CurrentPath.DetailedPath.Any())
|
||||
{
|
||||
var item = CurrentPath.DetailedPath.FirstOrDefault(t => t.NodeId == node.NodeId && t.IsPass == false);
|
||||
var item = CurrentPath.DetailedPath.FirstOrDefault(t => t.NodeId == node.Id && t.IsPass == false);
|
||||
if (item != null)
|
||||
{
|
||||
// [PathJump Check] 점프한 노드 개수 확인
|
||||
@@ -596,7 +597,7 @@ namespace AGVNavigationCore.Models
|
||||
int skippedCount = CurrentPath.DetailedPath.Count(t => t.seq < item.seq && t.IsPass == false);
|
||||
if (skippedCount > 2)
|
||||
{
|
||||
OnError($"PathJump: {skippedCount}개의 노드를 건너뛰었습니다. (허용: 2개, 현재노드: {node.NodeId})");
|
||||
OnError($"PathJump: {skippedCount}개의 노드를 건너뛰었습니다. (허용: 2개, 현재노드: {node.Id})");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -631,7 +632,7 @@ namespace AGVNavigationCore.Models
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -642,7 +643,7 @@ namespace AGVNavigationCore.Models
|
||||
/// <summary>
|
||||
/// DetailedPath에서 노드 정보를 찾아 AGVCommand 생성
|
||||
/// </summary>
|
||||
private AGVCommand GetCommandFromPath(string targetNodeId, string actionDescription)
|
||||
private AGVCommand GetCommandFromPath(MapNode targetNode, string actionDescription)
|
||||
{
|
||||
// DetailedPath가 없으면 기본 명령 반환
|
||||
if (_currentPath == null || _currentPath.DetailedPath == null || _currentPath.DetailedPath.Count == 0)
|
||||
@@ -659,7 +660,7 @@ namespace AGVNavigationCore.Models
|
||||
|
||||
// DetailedPath에서 targetNodeId에 해당하는 NodeMotorInfo 찾기
|
||||
// 지나가지 않은 경로를 찾는다
|
||||
var nodeInfo = _currentPath.DetailedPath.FirstOrDefault(n => n.NodeId == targetNodeId && n.IsPass == false);
|
||||
var nodeInfo = _currentPath.DetailedPath.FirstOrDefault(n => n.NodeId == targetNode.Id && n.IsPass == false);
|
||||
|
||||
if (nodeInfo == null)
|
||||
{
|
||||
@@ -673,7 +674,7 @@ namespace AGVNavigationCore.Models
|
||||
MagnetPosition.S,
|
||||
SpeedLevel.M,
|
||||
eAGVCommandReason.NoTarget,
|
||||
$"{actionDescription} (노드 {targetNodeId} 정보 없음)"
|
||||
$"{actionDescription} (노드 {targetNode.Id} 정보 없음)"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -721,7 +722,7 @@ namespace AGVNavigationCore.Models
|
||||
magnetPos,
|
||||
speed,
|
||||
eAGVCommandReason.Normal,
|
||||
$"{actionDescription} → {targetNodeId} (Motor:{motorCmd}, Magnet:{magnetPos})"
|
||||
$"{actionDescription} → {targetNode.Id} (Motor:{motorCmd}, Magnet:{magnetPos})"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -878,21 +879,6 @@ namespace AGVNavigationCore.Models
|
||||
}
|
||||
}
|
||||
|
||||
private DockingDirection GetDockingDirection(NodeType nodeType)
|
||||
{
|
||||
switch (nodeType)
|
||||
{
|
||||
case NodeType.Charging:
|
||||
return DockingDirection.Forward;
|
||||
case NodeType.Loader:
|
||||
case NodeType.UnLoader:
|
||||
case NodeType.Clearner:
|
||||
case NodeType.Buffer:
|
||||
return DockingDirection.Backward;
|
||||
default:
|
||||
return DockingDirection.Forward;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnError(string message)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user