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:
@@ -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 = "";
|
||||
|
||||
Reference in New Issue
Block a user