fix: Add motor direction parameter to magnet direction calculation in pathfinding

- Fixed critical issue in ConvertToDetailedPath where motor direction was not passed to GetRequiredMagnetDirection
- Motor direction is essential for backward movement as Left/Right directions must be inverted
- Modified AGVPathfinder.cs line 280 to pass currentDirection parameter
- Ensures backward motor direction properly inverts magnet sensor directions

feat: Add waypoint support to pathfinding system

- Added FindPath overload with params string[] waypointNodeIds in AStarPathfinder
- Supports sequential traversal through multiple intermediate nodes
- Validates waypoints and prevents duplicates in sequence
- Returns combined path result with aggregated metrics

feat: Implement path result merging with DetailedPath preservation

- Added CombineResults method in AStarPathfinder for intelligent path merging
- Automatically deduplicates nodes when last of previous path equals first of current
- Preserves DetailedPath information including motor and magnet directions
- Essential for multi-segment path operations

feat: Integrate magnet direction with motor direction awareness

- Modified JunctionAnalyzer.GetRequiredMagnetDirection to accept AgvDirection parameter
- Inverts Left/Right magnet directions when moving Backward
- Properly handles motor direction context throughout pathfinding

feat: Add automatic start node selection in simulator

- Added SetStartNodeToCombo method to SimulatorForm
- Automatically selects start node combo box when AGV position is set via RFID
- Improves UI usability and workflow efficiency

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
backuppc
2025-10-24 15:46:16 +09:00
parent 3ddecf63ed
commit d932b8d332
47 changed files with 7473 additions and 1088 deletions

View File

@@ -17,9 +17,9 @@ namespace AGVNavigationCore.Controls
float BatteryLevel { get; }
// 이동 경로 정보 추가
Point? TargetPosition { get; }
Point? PrevPosition { get; }
string CurrentNodeId { get; }
string TargetNodeId { get; }
string PrevNodeId { get; }
DockingDirection DockingDirection { get; }
}

View File

@@ -76,27 +76,21 @@ namespace AGVNavigationCore.Controls
int startX = (bounds.Left / GRID_SIZE) * GRID_SIZE;
int startY = (bounds.Top / GRID_SIZE) * GRID_SIZE;
// 월드 좌표로 그리드 라인 계산
// 월드 좌표로 그리드 라인 계산 (Transform이 자동으로 적용됨)
for (int x = startX; x <= bounds.Right; x += GRID_SIZE)
{
// 월드 좌표를 스크린 좌표로 변환
int screenX = x * (int)_zoomFactor + _panOffset.X;
if (x % (GRID_SIZE * 5) == 0)
g.DrawLine(new Pen(Color.Gray, 1), screenX, 0, screenX, Height);
g.DrawLine(new Pen(Color.Gray, 1), x, bounds.Top, x, bounds.Bottom);
else
g.DrawLine(_gridPen, screenX, 0, screenX, Height);
g.DrawLine(_gridPen, x, bounds.Top, x, bounds.Bottom);
}
for (int y = startY; y <= bounds.Bottom; y += GRID_SIZE)
{
// 월드 좌표를 스크린 좌표로 변환
int screenY = y * (int)_zoomFactor + _panOffset.Y;
if (y % (GRID_SIZE * 5) == 0)
g.DrawLine(new Pen(Color.Gray, 1), 0, screenY, Width, screenY);
g.DrawLine(new Pen(Color.Gray, 1), bounds.Left, y, bounds.Right, y);
else
g.DrawLine(_gridPen, 0, screenY, Width, screenY);
g.DrawLine(_gridPen, bounds.Left, y, bounds.Right, y);
}
}
@@ -240,86 +234,8 @@ namespace AGVNavigationCore.Controls
pathPen.Dispose();
}
/// <summary>
/// AGV 경로 및 모터방향 정보를 시각화
/// </summary>
/// <param name="g">Graphics 객체</param>
/// <param name="agvResult">AGV 경로 계산 결과</param>
private void DrawAGVPath(Graphics g, AGVPathResult agvResult)
{
if (agvResult?.NodeMotorInfos == null || agvResult.NodeMotorInfos.Count == 0) return;
// 노드별 모터방향 정보를 기반으로 향상된 경로 표시
for (int i = 0; i < agvResult.NodeMotorInfos.Count - 1; i++)
{
var currentMotorInfo = agvResult.NodeMotorInfos[i];
var nextMotorInfo = agvResult.NodeMotorInfos[i + 1];
var currentNode = _nodes?.FirstOrDefault(n => n.NodeId == currentMotorInfo.NodeId);
var nextNode = _nodes?.FirstOrDefault(n => n.NodeId == nextMotorInfo.NodeId);
if (currentNode != null && nextNode != null)
{
// 모터방향에 따른 색상 결정
var motorDirection = currentMotorInfo.MotorDirection;
Color pathColor = motorDirection == AgvDirection.Forward ? Color.Green : Color.Orange;
// 강조된 경로 선 그리기
var enhancedPen = new Pen(pathColor, 6) { DashStyle = DashStyle.Solid };
g.DrawLine(enhancedPen, currentNode.Position, nextNode.Position);
// 중간점에 모터방향 화살표 표시
var midPoint = new Point(
(currentNode.Position.X + nextNode.Position.X) / 2,
(currentNode.Position.Y + nextNode.Position.Y) / 2
);
var angle = Math.Atan2(nextNode.Position.Y - currentNode.Position.Y,
nextNode.Position.X - currentNode.Position.X);
// 모터방향별 화살표 그리기
DrawDirectionArrow(g, midPoint, angle, motorDirection);
// 노드 옆에 모터방향 텍스트 표시
DrawMotorDirectionLabel(g, currentNode.Position, motorDirection);
enhancedPen.Dispose();
}
}
// 마지막 노드의 모터방향 표시
if (agvResult.NodeMotorInfos.Count > 0)
{
var lastMotorInfo = agvResult.NodeMotorInfos[agvResult.NodeMotorInfos.Count - 1];
var lastNode = _nodes?.FirstOrDefault(n => n.NodeId == lastMotorInfo.NodeId);
if (lastNode != null)
{
DrawMotorDirectionLabel(g, lastNode.Position, lastMotorInfo.MotorDirection);
}
}
}
/// <summary>
/// 모터방향 레이블 표시
/// </summary>
/// <param name="g">Graphics 객체</param>
/// <param name="nodePosition">노드 위치</param>
/// <param name="motorDirection">모터방향</param>
private void DrawMotorDirectionLabel(Graphics g, Point nodePosition, AgvDirection motorDirection)
{
string motorText = motorDirection == AgvDirection.Forward ? "전진" : "후진";
Color textColor = motorDirection == AgvDirection.Forward ? Color.DarkGreen : Color.DarkOrange;
var font = new Font("맑은 고딕", 8, FontStyle.Bold);
var brush = new SolidBrush(textColor);
// 노드 우측 상단에 모터방향 텍스트 표시
var textPosition = new Point(nodePosition.X + NODE_RADIUS + 2, nodePosition.Y - NODE_RADIUS - 2);
g.DrawString(motorText, font, brush, textPosition);
font.Dispose();
brush.Dispose();
}
private void DrawNodesOnly(Graphics g)
{
@@ -1027,7 +943,7 @@ namespace AGVNavigationCore.Controls
const int liftDistance = AGV_SIZE / 2 + 2; // AGV 본체 면에 바로 붙도록
var currentPos = agv.CurrentPosition;
var targetPos = agv.TargetPosition;
var targetPos = agv.PrevPosition;
var dockingDirection = agv.DockingDirection;
var currentDirection = agv.CurrentDirection;
@@ -1194,7 +1110,7 @@ namespace AGVNavigationCore.Controls
private void DrawAGVLiftDebugInfo(Graphics g, IAGV agv)
{
var currentPos = agv.CurrentPosition;
var targetPos = agv.TargetPosition;
var targetPos = agv.PrevPosition;
// 디버그 정보 (개발용)
if (targetPos.HasValue)
@@ -1331,7 +1247,7 @@ namespace AGVNavigationCore.Controls
const int monitorDistance = AGV_SIZE / 2 + 2; // AGV 본체에서 거리 (리프트와 동일)
var currentPos = agv.CurrentPosition;
var targetPos = agv.TargetPosition;
var targetPos = agv.PrevPosition;
var dockingDirection = agv.DockingDirection;
var currentDirection = agv.CurrentDirection;

View File

@@ -215,21 +215,18 @@ namespace AGVNavigationCore.Controls
private Point ScreenToWorld(Point screenPoint)
{
// 변환 행렬 생성 (렌더링과 동일)
var transform = new System.Drawing.Drawing2D.Matrix();
transform.Scale(_zoomFactor, _zoomFactor);
transform.Translate(_panOffset.X, _panOffset.Y);
// 스크린 좌표를 월드 좌표로 변환
// 역순으로: 팬 오프셋 제거 → 줌 적용
float worldX = (screenPoint.X - _panOffset.X) / _zoomFactor;
float worldY = (screenPoint.Y - _panOffset.Y) / _zoomFactor;
// 역변환 행렬로 화면 좌표를 월드 좌표로 변환
transform.Invert();
var points = new System.Drawing.PointF[] { new System.Drawing.PointF(screenPoint.X, screenPoint.Y) };
transform.TransformPoints(points);
return new Point((int)points[0].X, (int)points[0].Y);
return new Point((int)worldX, (int)worldY);
}
private Point WorldToScreen(Point worldPoint)
{
// 월드 좌표를 스크린 좌표로 변환
// 순서: 줌 적용 → 팬 오프셋 추가
return new Point(
(int)(worldPoint.X * _zoomFactor + _panOffset.X),
(int)(worldPoint.Y * _zoomFactor + _panOffset.Y)

View File

@@ -469,9 +469,10 @@ namespace AGVNavigationCore.Controls
/// </summary>
/// <param name="agvId">AGV ID</param>
/// <param name="position">새로운 위치</param>
public void SetAGVPosition(string agvId, Point position)
public void SetAGVPosition(string agvId, MapNode node, AgvDirection direction)
{
UpdateAGVPosition(agvId, position);
UpdateAGVPosition(agvId, node.Position);
UpdateAGVDirection(agvId, direction);
}
/// <summary>