fix: RFID duplicate validation and correct magnet direction calculation

- Add real-time RFID duplicate validation in map editor with automatic rollback
- Remove RFID auto-assignment to maintain data consistency between editor and simulator
- Fix magnet direction calculation to use actual forward direction angles instead of arbitrary assignment
- Add node names to simulator combo boxes for better identification
- Improve UI layout by drawing connection lines before text for better visibility

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
ChiKyun Kim
2025-09-15 16:31:40 +09:00
parent 1add9ed59a
commit 7f48253770
41 changed files with 4827 additions and 3649 deletions

View File

@@ -22,7 +22,7 @@ namespace AGVNavigationCore.Controls
switch (_editMode)
{
case EditMode.Select:
HandleSelectClick(hitNode);
HandleSelectClick(hitNode, worldPoint);
break;
case EditMode.AddNode:
@@ -36,6 +36,10 @@ namespace AGVNavigationCore.Controls
case EditMode.Delete:
HandleDeleteClick(hitNode);
break;
case EditMode.DeleteConnection:
HandleDeleteConnectionClick(worldPoint);
break;
}
}
@@ -250,7 +254,10 @@ namespace AGVNavigationCore.Controls
private bool IsPointInCircle(Point point, MapNode node)
{
var hitRadius = Math.Max(NODE_RADIUS, 10 / _zoomFactor);
// 화면에서 최소 20픽셀 정도의 히트 영역을 확보하되, 노드 크기보다 작아지지 않게 함
var minHitRadiusInScreen = 20;
var hitRadius = Math.Max(NODE_RADIUS, minHitRadiusInScreen / _zoomFactor);
var distance = Math.Sqrt(
Math.Pow(node.Position.X - point.X, 2) +
Math.Pow(node.Position.Y - point.Y, 2)
@@ -260,7 +267,9 @@ namespace AGVNavigationCore.Controls
private bool IsPointInPentagon(Point point, MapNode node)
{
var radius = NODE_RADIUS;
// 화면에서 최소 20픽셀 정도의 히트 영역을 확보
var minHitRadiusInScreen = 20;
var radius = Math.Max(NODE_RADIUS, minHitRadiusInScreen / _zoomFactor);
var center = node.Position;
// 5각형 꼭짓점 계산
@@ -279,7 +288,9 @@ namespace AGVNavigationCore.Controls
private bool IsPointInTriangle(Point point, MapNode node)
{
var radius = NODE_RADIUS;
// 화면에서 최소 20픽셀 정도의 히트 영역을 확보하되, 노드 크기보다 작아지지 않게 함
var minHitRadiusInScreen = 20;
var radius = Math.Max(NODE_RADIUS, minHitRadiusInScreen / _zoomFactor);
var center = node.Position;
// 삼각형 꼭짓점 계산
@@ -378,13 +389,63 @@ namespace AGVNavigationCore.Controls
});
}
private void HandleSelectClick(MapNode hitNode)
private void HandleSelectClick(MapNode hitNode, Point worldPoint)
{
if (hitNode != _selectedNode)
if (hitNode != null)
{
_selectedNode = hitNode;
NodeSelected?.Invoke(this, hitNode);
Invalidate();
// 노드 선택
if (hitNode != _selectedNode)
{
_selectedNode = hitNode;
NodeSelected?.Invoke(this, hitNode);
Invalidate();
}
}
else
{
// 노드가 없으면 연결선 체크
var connection = GetConnectionAt(worldPoint);
if (connection != null)
{
// 연결선을 클릭했을 때 삭제 확인
var (fromNode, toNode) = connection.Value;
string fromDisplay = !string.IsNullOrEmpty(fromNode.RfidId) ? fromNode.RfidId : fromNode.NodeId;
string toDisplay = !string.IsNullOrEmpty(toNode.RfidId) ? toNode.RfidId : toNode.NodeId;
var result = MessageBox.Show(
$"연결을 삭제하시겠습니까?\n\n{fromDisplay} ↔ {toDisplay}",
"연결 삭제 확인",
MessageBoxButtons.YesNo,
MessageBoxIcon.Question);
if (result == DialogResult.Yes)
{
// 단일 연결 삭제 (어느 방향에 저장되어 있는지 확인 후 삭제)
if (fromNode.ConnectedNodes.Contains(toNode.NodeId))
{
fromNode.RemoveConnection(toNode.NodeId);
}
else if (toNode.ConnectedNodes.Contains(fromNode.NodeId))
{
toNode.RemoveConnection(fromNode.NodeId);
}
// 이벤트 발생
ConnectionDeleted?.Invoke(this, (fromNode, toNode));
MapChanged?.Invoke(this, EventArgs.Empty);
Invalidate();
}
}
else
{
// 빈 공간 클릭 시 선택 해제
if (_selectedNode != null)
{
_selectedNode = null;
NodeSelected?.Invoke(this, null);
Invalidate();
}
}
}
}
@@ -397,14 +458,16 @@ namespace AGVNavigationCore.Controls
worldPoint.Y = (worldPoint.Y / GRID_SIZE) * GRID_SIZE;
}
// 고유한 NodeId 생성
string newNodeId = GenerateUniqueNodeId();
var newNode = new MapNode
{
NodeId = $"N{_nodeCounter:D3}",
NodeId = newNodeId,
Position = worldPoint,
Type = NodeType.Normal
};
_nodeCounter++;
_nodes.Add(newNode);
NodeAdded?.Invoke(this, newNode);
@@ -412,6 +475,25 @@ namespace AGVNavigationCore.Controls
Invalidate();
}
/// <summary>
/// 중복되지 않는 고유한 NodeId 생성
/// </summary>
private string GenerateUniqueNodeId()
{
string nodeId;
int counter = _nodeCounter;
do
{
nodeId = $"N{counter:D3}";
counter++;
}
while (_nodes.Any(n => n.NodeId == nodeId));
_nodeCounter = counter;
return nodeId;
}
private void HandleConnectClick(MapNode hitNode)
{
if (hitNode == null) return;
@@ -458,11 +540,21 @@ namespace AGVNavigationCore.Controls
private void CreateConnection(MapNode fromNode, MapNode toNode)
{
// 중복 연결 체크
if (fromNode.ConnectedNodes.Contains(toNode.NodeId))
// 중복 연결 체크 (양방향)
if (fromNode.ConnectedNodes.Contains(toNode.NodeId) ||
toNode.ConnectedNodes.Contains(fromNode.NodeId))
return;
fromNode.AddConnection(toNode.NodeId);
// 단일 연결 생성 (사전순으로 정렬하여 일관성 유지)
if (string.Compare(fromNode.NodeId, toNode.NodeId, StringComparison.Ordinal) < 0)
{
fromNode.AddConnection(toNode.NodeId);
}
else
{
toNode.AddConnection(fromNode.NodeId);
}
MapChanged?.Invoke(this, EventArgs.Empty);
}
@@ -496,6 +588,91 @@ namespace AGVNavigationCore.Controls
_contextMenu.Show(this, location);
}
private void HandleDeleteConnectionClick(Point worldPoint)
{
// 클릭한 위치 근처의 연결선을 찾아 삭제
var connection = GetConnectionAt(worldPoint);
if (connection != null)
{
var (fromNode, toNode) = connection.Value;
// 단일 연결 삭제 (어느 방향에 저장되어 있는지 확인 후 삭제)
if (fromNode.ConnectedNodes.Contains(toNode.NodeId))
{
fromNode.RemoveConnection(toNode.NodeId);
}
else if (toNode.ConnectedNodes.Contains(fromNode.NodeId))
{
toNode.RemoveConnection(fromNode.NodeId);
}
// 이벤트 발생
ConnectionDeleted?.Invoke(this, (fromNode, toNode));
MapChanged?.Invoke(this, EventArgs.Empty);
Invalidate();
}
}
private (MapNode From, MapNode To)? GetConnectionAt(Point worldPoint)
{
const int CONNECTION_HIT_TOLERANCE = 10;
// 모든 연결선을 확인하여 클릭한 위치와 가장 가까운 연결선 찾기
foreach (var fromNode in _nodes)
{
foreach (var toNodeId in fromNode.ConnectedNodes)
{
var toNode = _nodes.FirstOrDefault(n => n.NodeId == toNodeId);
if (toNode != null)
{
// 연결선과 클릭 위치 간의 거리 계산
var distance = CalculatePointToLineDistance(worldPoint, fromNode.Position, toNode.Position);
if (distance <= CONNECTION_HIT_TOLERANCE / _zoomFactor)
{
return (fromNode, toNode);
}
}
}
}
return null;
}
private float CalculatePointToLineDistance(Point point, Point lineStart, Point lineEnd)
{
// 점에서 선분까지의 거리 계산
var A = point.X - lineStart.X;
var B = point.Y - lineStart.Y;
var C = lineEnd.X - lineStart.X;
var D = lineEnd.Y - lineStart.Y;
var dot = A * C + B * D;
var lenSq = C * C + D * D;
if (lenSq == 0) return CalculateDistance(point, lineStart);
var param = dot / lenSq;
Point xx, yy;
if (param < 0)
{
xx = lineStart;
yy = lineStart;
}
else if (param > 1)
{
xx = lineEnd;
yy = lineEnd;
}
else
{
xx = new Point((int)(lineStart.X + param * C), (int)(lineStart.Y + param * D));
yy = xx;
}
return CalculateDistance(point, xx);
}
private void UpdateTooltip(Point worldPoint)
{
string tooltipText = "";