fix: 맵 에디터 연결 버그 수정 및 기능 개선
주요 변경사항: - ConnectedMapNodes 속성 추가로 런타임 객체 참조 지원 - 이미지 에디터 UI 개선 (ImageEditorCanvas 추가) - 연결 생성 버그 수정: 양방향 연결 생성 - 연결 삭제 버그 수정: 양방향 모두 제거 - CleanupDuplicateConnections 비활성화 (단방향 변환 버그) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -101,11 +101,10 @@ namespace AGVNavigationCore.Controls
|
||||
|
||||
foreach (var node in _nodes)
|
||||
{
|
||||
if (node.ConnectedNodes == null) continue;
|
||||
if (node.ConnectedMapNodes == null) continue;
|
||||
|
||||
foreach (var connectedNodeId in node.ConnectedNodes)
|
||||
foreach (var targetNode in node.ConnectedMapNodes)
|
||||
{
|
||||
var targetNode = _nodes.FirstOrDefault(n => n.NodeId == connectedNodeId);
|
||||
if (targetNode == null) continue;
|
||||
|
||||
DrawConnection(g, node, targetNode);
|
||||
@@ -302,7 +301,7 @@ namespace AGVNavigationCore.Controls
|
||||
if (node == null) continue;
|
||||
|
||||
// 교차로 판정: 3개 이상의 노드가 연결된 경우
|
||||
if (node.ConnectedNodes != null && node.ConnectedNodes.Count >= JUNCTION_CONNECTIONS)
|
||||
if (node.ConnectedMapNodes != null && node.ConnectedMapNodes.Count >= JUNCTION_CONNECTIONS)
|
||||
{
|
||||
DrawJunctionHighlight(g, node);
|
||||
}
|
||||
|
||||
@@ -567,15 +567,9 @@ namespace AGVNavigationCore.Controls
|
||||
toNode.ConnectedNodes.Contains(fromNode.NodeId))
|
||||
return;
|
||||
|
||||
// 단일 연결 생성 (사전순으로 정렬하여 일관성 유지)
|
||||
if (string.Compare(fromNode.NodeId, toNode.NodeId, StringComparison.Ordinal) < 0)
|
||||
{
|
||||
fromNode.AddConnection(toNode.NodeId);
|
||||
}
|
||||
else
|
||||
{
|
||||
toNode.AddConnection(fromNode.NodeId);
|
||||
}
|
||||
// 양방향 연결 생성 (AGV가 양쪽 방향으로 이동 가능하도록)
|
||||
fromNode.AddConnection(toNode.NodeId);
|
||||
toNode.AddConnection(fromNode.NodeId);
|
||||
|
||||
MapChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
@@ -78,12 +78,13 @@ namespace AGVNavigationCore.Models
|
||||
// 중복된 NodeId 정리
|
||||
FixDuplicateNodeIds(result.Nodes);
|
||||
|
||||
// 중복 연결 정리 (양방향 중복 제거)
|
||||
CleanupDuplicateConnections(result.Nodes);
|
||||
|
||||
// 양방향 연결 자동 설정 (A→B가 있으면 B→A도 설정)
|
||||
// 주의: CleanupDuplicateConnections()는 제거됨 - 양방향 연결을 단방향으로 변환하는 버그가 있었음
|
||||
EnsureBidirectionalConnections(result.Nodes);
|
||||
|
||||
// ConnectedMapNodes 채우기 (string ID → MapNode 객체 참조)
|
||||
ResolveConnectedMapNodes(result.Nodes);
|
||||
|
||||
// 이미지 노드들의 이미지 로드
|
||||
LoadImageNodes(result.Nodes);
|
||||
|
||||
@@ -145,6 +146,35 @@ namespace AGVNavigationCore.Models
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ConnectedMapNodes 채우기 (ConnectedNodes의 string ID → MapNode 객체 변환)
|
||||
/// </summary>
|
||||
/// <param name="mapNodes">맵 노드 목록</param>
|
||||
private static void ResolveConnectedMapNodes(List<MapNode> mapNodes)
|
||||
{
|
||||
if (mapNodes == null || mapNodes.Count == 0) return;
|
||||
|
||||
// 빠른 조회를 위한 Dictionary 생성
|
||||
var nodeDict = mapNodes.ToDictionary(n => n.NodeId, n => n);
|
||||
|
||||
foreach (var node in mapNodes)
|
||||
{
|
||||
// ConnectedMapNodes 초기화
|
||||
node.ConnectedMapNodes.Clear();
|
||||
|
||||
if (node.ConnectedNodes != null && node.ConnectedNodes.Count > 0)
|
||||
{
|
||||
foreach (var connectedNodeId in node.ConnectedNodes)
|
||||
{
|
||||
if (nodeDict.TryGetValue(connectedNodeId, out var connectedNode))
|
||||
{
|
||||
node.ConnectedMapNodes.Add(connectedNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 기존 Description 데이터를 Name 필드로 마이그레이션
|
||||
/// JSON 파일에서 Description 필드가 있는 경우 Name으로 이동
|
||||
@@ -269,9 +299,12 @@ namespace AGVNavigationCore.Models
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 중복 연결을 정리합니다. 양방향 중복 연결을 단일 연결로 통합합니다.
|
||||
/// [사용 중지됨] 중복 연결을 정리합니다. 양방향 중복 연결을 단일 연결로 통합합니다.
|
||||
/// 주의: 이 함수는 버그가 있어 사용 중지됨 - 양방향 연결을 단방향으로 변환하여 경로 탐색 실패 발생
|
||||
/// AGV 시스템에서는 모든 연결이 양방향이어야 하므로 EnsureBidirectionalConnections()만 사용
|
||||
/// </summary>
|
||||
/// <param name="mapNodes">맵 노드 목록</param>
|
||||
[Obsolete("이 함수는 양방향 연결을 단방향으로 변환하는 버그가 있습니다. 사용하지 마세요.")]
|
||||
private static void CleanupDuplicateConnections(List<MapNode> mapNodes)
|
||||
{
|
||||
if (mapNodes == null || mapNodes.Count == 0) return;
|
||||
|
||||
@@ -44,6 +44,12 @@ namespace AGVNavigationCore.Models
|
||||
/// </summary>
|
||||
public List<string> ConnectedNodes { get; set; } = new List<string>();
|
||||
|
||||
/// <summary>
|
||||
/// 연결된 노드 객체 목록 (런타임 전용, JSON 무시)
|
||||
/// </summary>
|
||||
[Newtonsoft.Json.JsonIgnore]
|
||||
public List<MapNode> ConnectedMapNodes { get; set; } = new List<MapNode>();
|
||||
|
||||
/// <summary>
|
||||
/// 회전 가능 여부 (180도 회전 가능한 지점)
|
||||
/// </summary>
|
||||
|
||||
@@ -92,15 +92,18 @@ namespace AGVNavigationCore.PathFinding.Analysis
|
||||
var connected = new HashSet<string>();
|
||||
|
||||
// 직접 연결된 노드들
|
||||
foreach (var connectedId in node.ConnectedNodes)
|
||||
foreach (var connectedNode in node.ConnectedMapNodes)
|
||||
{
|
||||
connected.Add(connectedId);
|
||||
if (connectedNode != null)
|
||||
{
|
||||
connected.Add(connectedNode.NodeId);
|
||||
}
|
||||
}
|
||||
|
||||
// 역방향 연결된 노드들 (다른 노드에서 이 노드로 연결)
|
||||
foreach (var otherNode in _mapNodes)
|
||||
{
|
||||
if (otherNode.NodeId != node.NodeId && otherNode.ConnectedNodes.Contains(node.NodeId))
|
||||
if (otherNode.NodeId != node.NodeId && otherNode.ConnectedMapNodes.Any(n => n.NodeId == node.NodeId))
|
||||
{
|
||||
connected.Add(otherNode.NodeId);
|
||||
}
|
||||
|
||||
@@ -66,17 +66,17 @@ namespace AGVNavigationCore.PathFinding.Core
|
||||
{
|
||||
var pathNode = _nodeMap[mapNode.NodeId];
|
||||
|
||||
foreach (var connectedNode in mapNode.ConnectedNodes)
|
||||
foreach (var connectedNode in mapNode.ConnectedMapNodes)
|
||||
{
|
||||
if (_nodeMap.ContainsKey(connectedNode))
|
||||
if (connectedNode != null && _nodeMap.ContainsKey(connectedNode.NodeId))
|
||||
{
|
||||
// 양방향 연결 생성 (단일 연결이 양방향을 의미)
|
||||
if (!pathNode.ConnectedNodes.Contains(connectedNode))
|
||||
if (!pathNode.ConnectedNodes.Contains(connectedNode.NodeId))
|
||||
{
|
||||
pathNode.ConnectedNodes.Add(connectedNode);
|
||||
pathNode.ConnectedNodes.Add(connectedNode.NodeId);
|
||||
}
|
||||
|
||||
var connectedPathNode = _nodeMap[connectedNode];
|
||||
var connectedPathNode = _nodeMap[connectedNode.NodeId];
|
||||
if (!connectedPathNode.ConnectedNodes.Contains(mapNode.NodeId))
|
||||
{
|
||||
connectedPathNode.ConnectedNodes.Add(mapNode.NodeId);
|
||||
|
||||
@@ -22,7 +22,7 @@ namespace AGVNavigationCore.PathFinding.Planning
|
||||
private readonly JunctionAnalyzer _junctionAnalyzer;
|
||||
private readonly DirectionChangePlanner _directionChangePlanner;
|
||||
|
||||
|
||||
|
||||
|
||||
public AGVPathfinder(List<MapNode> mapNodes)
|
||||
{
|
||||
@@ -50,9 +50,13 @@ namespace AGVNavigationCore.PathFinding.Planning
|
||||
n.IsNavigationNode() &&
|
||||
n.ConnectedNodes != null &&
|
||||
n.ConnectedNodes.Count >= 3 &&
|
||||
n.ConnectedMapNodes.Where(t => t.Type == NodeType.Docking || t.Type == NodeType.Charging).Any() == false &&
|
||||
n.NodeId != startNode.NodeId
|
||||
).ToList();
|
||||
|
||||
// docking 포인트가 연결된 노드는 제거한다.
|
||||
|
||||
|
||||
if (junctions.Count == 0)
|
||||
return null;
|
||||
|
||||
@@ -96,7 +100,8 @@ namespace AGVNavigationCore.PathFinding.Planning
|
||||
pathNode.IsActive &&
|
||||
pathNode.IsNavigationNode() &&
|
||||
pathNode.ConnectedNodes != null &&
|
||||
pathNode.ConnectedNodes.Count >= 3)
|
||||
pathNode.ConnectedNodes.Count >= 3 &&
|
||||
pathNode.ConnectedMapNodes.Where(t => t.Type == NodeType.Docking || t.Type == NodeType.Charging).Any() == false)
|
||||
{
|
||||
if (pathNode.NodeId.Equals(StartNode.NodeId) == false)
|
||||
return pathNode;
|
||||
@@ -107,7 +112,7 @@ namespace AGVNavigationCore.PathFinding.Planning
|
||||
}
|
||||
|
||||
public AGVPathResult FindPath_test(MapNode startNode, MapNode targetNode,
|
||||
MapNode prevNode, AgvDirection prevDirection, AgvDirection currentDirection)
|
||||
MapNode prevNode, AgvDirection prevDirection, AgvDirection currentDirection, bool crossignore = false)
|
||||
{
|
||||
// 입력 검증
|
||||
if (startNode == null)
|
||||
@@ -117,7 +122,7 @@ namespace AGVNavigationCore.PathFinding.Planning
|
||||
if (prevNode == null)
|
||||
return AGVPathResult.CreateFailure("이전위치 노드가 null입니다.", 0, 0);
|
||||
if (startNode.NodeId == targetNode.NodeId && targetNode.DockDirection.MatchAGVDirection(prevDirection))
|
||||
return AGVPathResult.CreateFailure("목적지와 현재위치가 동일합니다.", 0, 0);
|
||||
return AGVPathResult.CreateSuccess(new List<MapNode> { startNode,startNode }, new List<AgvDirection>(), 0, 0);
|
||||
|
||||
var ReverseDirection = (currentDirection == AgvDirection.Forward ? AgvDirection.Backward : AgvDirection.Forward);
|
||||
|
||||
@@ -203,6 +208,39 @@ namespace AGVNavigationCore.PathFinding.Planning
|
||||
// return pathResult;
|
||||
//}
|
||||
|
||||
//현재 내 포인트가 교차로라면.. 무조건 왓던 방향 혹은 그 반대방향으로 이동해서 경로를 계산해야한다.
|
||||
//교차로에 멈춰있을때에는 바로 방향전환을 할 수없으니. 정/역(straight)로 이동해서 다시 계산을 해야한다
|
||||
if (crossignore == false && startNode.ConnectedNodes.Count > 2)
|
||||
{
|
||||
//진행방향으로 이동했을때 나오는 노드를 사용한다.
|
||||
if (nextNodeForward != null)
|
||||
{
|
||||
var Path0 = _basicPathfinder.FindPath(startNode.NodeId, nextNodeForward.NodeId);
|
||||
Path0.PrevNode = prevNode;
|
||||
Path0.PrevDirection = prevDirection;
|
||||
MakeDetailData(Path0, prevDirection);
|
||||
|
||||
var Path1 = FindPath_test(nextNodeForward, targetNode, startNode, prevDirection, currentDirection, true);
|
||||
Path1.PrevNode = startNode;
|
||||
Path1.PrevDirection = prevDirection;
|
||||
//MakeDetailData(Path1, ReverseDirection);
|
||||
|
||||
var combinedResult0 = Path0;
|
||||
combinedResult0 = _basicPathfinder.CombineResults(combinedResult0, Path1);
|
||||
MakeMagnetDirection(combinedResult0);
|
||||
return combinedResult0;
|
||||
}
|
||||
else if (nextNodeBackward != null)
|
||||
{
|
||||
return AGVPathResult.CreateFailure("backward 처리코드가 없습니다 오류", 0, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
return AGVPathResult.CreateFailure("교차로에서 시작하는 조건중 forward/backrad 노드 검색 실패", 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//3. 도킹방향이 일치하지 않으니 교차로에서 방향을 회전시켜야 한다
|
||||
//최단거리(=minpath)경로에 속하는 교차로가 있다면 그것을 사용하고 없다면 가장 가까운 교차로를 찾는다.
|
||||
var JunctionInPath = FindNearestJunctionOnPath(pathResult);
|
||||
@@ -224,19 +262,23 @@ namespace AGVNavigationCore.PathFinding.Planning
|
||||
path1.PrevNode = prevNode;
|
||||
path1.PrevDirection = prevDirection;
|
||||
|
||||
//다음좌표를 보고 교차로가 진행방향인지 반대방향인지 체크한다.(!모터의 정/역방향을 말하는것이 아님)
|
||||
//다음좌표를 보고 교차로가 진행방향인지 반대방향인지 체크한다.
|
||||
bool ReverseCheck = false;
|
||||
if (path1.Path.Count > 1 && nextNodeForward != null && nextNodeForward.NodeId.Equals(path1.Path[1].NodeId))
|
||||
{
|
||||
ReverseCheck = false; //현재 진행 방향으로 이동해야한다
|
||||
MakeDetailData(path1, currentDirection); // path1의 상세 경로 정보 채우기 (모터 방향 설정)
|
||||
}
|
||||
else if (path1.Path.Count > 1 && nextNodeBackward != null && nextNodeBackward.NodeId.Equals(path1.Path[1].NodeId))
|
||||
//if (path1.Path.Count > 1 && nextNodeForward != null && nextNodeForward.NodeId.Equals(path1.Path[1].NodeId))
|
||||
//{
|
||||
// ReverseCheck = false; //현재 진행 방향으로 이동해야한다
|
||||
// MakeDetailData(path1, currentDirection); // path1의 상세 경로 정보 채우기 (모터 방향 설정)
|
||||
//}
|
||||
if (path1.Path.Count > 1 && nextNodeBackward != null && nextNodeBackward.NodeId.Equals(path1.Path[1].NodeId))
|
||||
{
|
||||
ReverseCheck = true; //현재 방향의 반대방향으로 이동해야한다
|
||||
MakeDetailData(path1, ReverseDirection); // path1의 상세 경로 정보 채우기 (모터 방향 설정)
|
||||
}
|
||||
else return AGVPathResult.CreateFailure("교차로까지 계산된 경로에 현재 위치정보로 추측을 할 수 없습니다", 0, 0);
|
||||
else
|
||||
{
|
||||
ReverseCheck = false; //현재 진행 방향으로 이동해야한다
|
||||
MakeDetailData(path1, currentDirection); // path1의 상세 경로 정보 채우기 (모터 방향 설정)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -232,15 +232,18 @@ namespace AGVNavigationCore.PathFinding.Planning
|
||||
var connected = new HashSet<string>();
|
||||
|
||||
// 직접 연결
|
||||
foreach (var connectedId in node.ConnectedNodes)
|
||||
foreach (var connectedNode in node.ConnectedMapNodes)
|
||||
{
|
||||
connected.Add(connectedId);
|
||||
if (connectedNode != null)
|
||||
{
|
||||
connected.Add(connectedNode.NodeId);
|
||||
}
|
||||
}
|
||||
|
||||
// 역방향 연결
|
||||
foreach (var otherNode in _mapNodes)
|
||||
{
|
||||
if (otherNode.NodeId != nodeId && otherNode.ConnectedNodes.Contains(nodeId))
|
||||
if (otherNode.NodeId != nodeId && otherNode.ConnectedMapNodes.Any(n => n.NodeId == nodeId))
|
||||
{
|
||||
connected.Add(otherNode.NodeId);
|
||||
}
|
||||
@@ -728,9 +731,9 @@ namespace AGVNavigationCore.PathFinding.Planning
|
||||
string currentNode = path[i].NodeId;
|
||||
string nextNode = path[i + 1].NodeId;
|
||||
|
||||
// 두 노드간 직접 연결성 확인 (맵 노드의 ConnectedNodes 리스트 사용)
|
||||
// 두 노드간 직접 연결성 확인 (맵 노드의 ConnectedMapNodes 리스트 사용)
|
||||
var currentMapNode = _mapNodes.FirstOrDefault(n => n.NodeId == currentNode);
|
||||
if (currentMapNode == null || !currentMapNode.ConnectedNodes.Contains(nextNode))
|
||||
if (currentMapNode == null || !currentMapNode.ConnectedMapNodes.Any(n => n.NodeId == nextNode))
|
||||
{
|
||||
return PathValidationResult.CreateInvalid(currentNode, nextNode, $"노드 {currentNode}와 {nextNode} 사이에 연결이 없음");
|
||||
}
|
||||
|
||||
@@ -69,22 +69,18 @@ namespace AGVNavigationCore.Utils
|
||||
InitializeJunctionAnalyzer(allNodes);
|
||||
|
||||
// 현재 노드에 연결된 노드들 중 이전 노드가 아닌 노드들만 필터링
|
||||
var connectedNodeIds = currentNode.ConnectedNodes;
|
||||
if (connectedNodeIds == null || connectedNodeIds.Count == 0)
|
||||
var connectedMapNodes = currentNode.ConnectedMapNodes;
|
||||
if (connectedMapNodes == null || connectedMapNodes.Count == 0)
|
||||
return null;
|
||||
|
||||
List<MapNode> candidateNodes = new List<MapNode>();
|
||||
if (prevDirection == direction)
|
||||
{
|
||||
candidateNodes = allNodes.Where(n => n.NodeId != prevNode.NodeId &&
|
||||
connectedNodeIds.Contains(n.NodeId)
|
||||
).ToList();
|
||||
candidateNodes = connectedMapNodes.Where(n => n.NodeId != prevNode.NodeId).ToList();
|
||||
}
|
||||
else
|
||||
{
|
||||
candidateNodes = allNodes.Where(n =>
|
||||
connectedNodeIds.Contains(n.NodeId)
|
||||
).ToList();
|
||||
candidateNodes = connectedMapNodes.ToList();
|
||||
}
|
||||
|
||||
if (candidateNodes.Count == 0)
|
||||
@@ -375,13 +371,11 @@ namespace AGVNavigationCore.Utils
|
||||
if (currentNode == null || prevNode == null || allNodes == null)
|
||||
return (null, 0, "입력 파라미터가 null입니다");
|
||||
|
||||
var connectedNodeIds = currentNode.ConnectedNodes;
|
||||
if (connectedNodeIds == null || connectedNodeIds.Count == 0)
|
||||
var connectedMapNodes = currentNode.ConnectedMapNodes;
|
||||
if (connectedMapNodes == null || connectedMapNodes.Count == 0)
|
||||
return (null, 0, "연결된 노드가 없습니다");
|
||||
|
||||
var candidateNodes = allNodes.Where(n =>
|
||||
connectedNodeIds.Contains(n.NodeId)
|
||||
).ToList();
|
||||
var candidateNodes = connectedMapNodes.ToList();
|
||||
|
||||
if (candidateNodes.Count == 0)
|
||||
return (null, 0, "후보 노드가 없습니다");
|
||||
|
||||
@@ -28,6 +28,12 @@ namespace AGVNavigationCore.Utils
|
||||
System.Diagnostics.Debug.WriteLine($"[DockingValidator] 도킹 검증 불필요: 경로 없음");
|
||||
return DockingValidationResult.CreateNotRequired();
|
||||
}
|
||||
if (pathResult.DetailedPath.Any() == false && pathResult.Path.Any() && pathResult.Path.Count == 2 &&
|
||||
pathResult.Path[0].NodeId == pathResult.Path[1].NodeId)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"[DockingValidator] 도킹 검증 불필요: 동일포인트");
|
||||
return DockingValidationResult.CreateNotRequired();
|
||||
}
|
||||
|
||||
// 목적지 노드 가져오기 (Path는 이제 List<MapNode>)
|
||||
var LastNode = pathResult.Path[pathResult.Path.Count - 1];
|
||||
|
||||
Reference in New Issue
Block a user