diff --git a/Cs_HMI/AGVNavigationCore/AGVNavigationCore.csproj b/Cs_HMI/AGVNavigationCore/AGVNavigationCore.csproj
index 6def2d0..17ddb4e 100644
--- a/Cs_HMI/AGVNavigationCore/AGVNavigationCore.csproj
+++ b/Cs_HMI/AGVNavigationCore/AGVNavigationCore.csproj
@@ -71,6 +71,7 @@
+
@@ -87,11 +88,9 @@
UnifiedAGVCanvas.cs
UserControl
+
-
-
-
diff --git a/Cs_HMI/AGVNavigationCore/Controls/UnifiedAGVCanvas.Events.cs b/Cs_HMI/AGVNavigationCore/Controls/UnifiedAGVCanvas.Events.cs
index d042028..7b3d513 100644
--- a/Cs_HMI/AGVNavigationCore/Controls/UnifiedAGVCanvas.Events.cs
+++ b/Cs_HMI/AGVNavigationCore/Controls/UnifiedAGVCanvas.Events.cs
@@ -150,10 +150,14 @@ namespace AGVNavigationCore.Controls
}
}
- // 현재 선택된 경로 그리기
+ // 현재 선택된 경로 그리기 (AGVPathResult 활용)
if (_currentPath != null)
{
DrawPath(g, _currentPath, Color.Purple);
+
+ // AGVPathResult의 모터방향 정보가 있다면 향상된 경로 그리기
+ // 현재는 기본 PathResult를 사용하므로 향후 AGVPathResult로 업그레이드 시 활성화
+ // TODO: AGVPathfinder 사용시 AGVPathResult로 업그레이드
}
}
@@ -191,6 +195,87 @@ namespace AGVNavigationCore.Controls
pathPen.Dispose();
}
+ ///
+ /// AGV 경로 및 모터방향 정보를 시각화
+ ///
+ /// Graphics 객체
+ /// AGV 경로 계산 결과
+ 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);
+ }
+ }
+ }
+
+ ///
+ /// 모터방향 레이블 표시
+ ///
+ /// Graphics 객체
+ /// 노드 위치
+ /// 모터방향
+ 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 DrawNodes(Graphics g)
{
if (_nodes == null) return;
@@ -254,6 +339,17 @@ namespace AGVNavigationCore.Controls
g.DrawEllipse(_selectedNodePen, rect);
}
+ // 목적지 노드 강조
+ if (node == _destinationNode)
+ {
+ // 금색 테두리로 목적지 강조
+ g.DrawEllipse(_destinationNodePen, rect);
+
+ // 펄싱 효과를 위한 추가 원 그리기
+ var pulseRect = new Rectangle(rect.X - 3, rect.Y - 3, rect.Width + 6, rect.Height + 6);
+ g.DrawEllipse(new Pen(Color.Gold, 2) { DashStyle = DashStyle.Dash }, pulseRect);
+ }
+
// 호버된 노드 강조
if (node == _hoveredNode)
{
@@ -290,6 +386,25 @@ namespace AGVNavigationCore.Controls
g.DrawPolygon(_selectedNodePen, points);
}
+ // 목적지 노드 강조
+ if (node == _destinationNode)
+ {
+ // 금색 테두리로 목적지 강조
+ g.DrawPolygon(_destinationNodePen, points);
+
+ // 펄싱 효과를 위한 추가 오각형 그리기
+ var pulsePoints = new Point[5];
+ for (int i = 0; i < 5; i++)
+ {
+ var angle = (Math.PI * 2 * i / 5) - Math.PI / 2;
+ pulsePoints[i] = new Point(
+ (int)(center.X + (radius + 4) * Math.Cos(angle)),
+ (int)(center.Y + (radius + 4) * Math.Sin(angle))
+ );
+ }
+ g.DrawPolygon(new Pen(Color.Gold, 2) { DashStyle = DashStyle.Dash }, pulsePoints);
+ }
+
// 호버된 노드 강조
if (node == _hoveredNode)
{
@@ -335,6 +450,25 @@ namespace AGVNavigationCore.Controls
g.DrawPolygon(_selectedNodePen, points);
}
+ // 목적지 노드 강조
+ if (node == _destinationNode)
+ {
+ // 금색 테두리로 목적지 강조
+ g.DrawPolygon(_destinationNodePen, points);
+
+ // 펄싱 효과를 위한 추가 삼각형 그리기
+ var pulsePoints = new Point[3];
+ for (int i = 0; i < 3; i++)
+ {
+ var angle = (Math.PI * 2 * i / 3) - Math.PI / 2;
+ pulsePoints[i] = new Point(
+ (int)(center.X + (radius + 4) * Math.Cos(angle)),
+ (int)(center.Y + (radius + 4) * Math.Sin(angle))
+ );
+ }
+ g.DrawPolygon(new Pen(Color.Gold, 2) { DashStyle = DashStyle.Dash }, pulsePoints);
+ }
+
// 호버된 노드 강조
if (node == _hoveredNode)
{
@@ -643,7 +777,7 @@ namespace AGVNavigationCore.Controls
// AGV 색상 결정
var brush = GetAGVBrush(state);
- // AGV 사각형 그리기
+ // AGV 개선된 원형 디자인 그리기
var rect = new Rectangle(
position.X - AGV_SIZE / 2,
position.Y - AGV_SIZE / 2,
@@ -651,11 +785,82 @@ namespace AGVNavigationCore.Controls
AGV_SIZE
);
- g.FillRectangle(brush, rect);
- g.DrawRectangle(_agvPen, rect);
+ // 방사형 그라디언트 효과를 위한 브러시 생성
+ using (var gradientBrush = new System.Drawing.Drawing2D.PathGradientBrush(
+ new Point[] {
+ new Point(rect.Left, rect.Top),
+ new Point(rect.Right, rect.Top),
+ new Point(rect.Right, rect.Bottom),
+ new Point(rect.Left, rect.Bottom)
+ }))
+ {
+ gradientBrush.CenterPoint = new PointF(position.X, position.Y);
+ gradientBrush.CenterColor = GetAGVCenterColor(state);
+ gradientBrush.SurroundColors = new Color[] {
+ GetAGVOuterColor(state), GetAGVOuterColor(state),
+ GetAGVOuterColor(state), GetAGVOuterColor(state)
+ };
+
+ // 원형으로 AGV 본체 그리기 (3D 효과)
+ g.FillEllipse(gradientBrush, rect);
+
+ // 외곽선 (두께 조절)
+ using (var outerPen = new Pen(Color.DarkGray, 3))
+ {
+ g.DrawEllipse(outerPen, rect);
+ }
+
+ // 내부 링 (입체감)
+ var innerSize = AGV_SIZE - 8;
+ var innerRect = new Rectangle(
+ position.X - innerSize / 2,
+ position.Y - innerSize / 2,
+ innerSize,
+ innerSize
+ );
+ using (var innerPen = new Pen(GetAGVInnerRingColor(state), 2))
+ {
+ g.DrawEllipse(innerPen, innerRect);
+ }
+
+ // 중앙 상태 표시등 (더 크고 화려하게)
+ var indicatorSize = 10;
+ var indicatorRect = new Rectangle(
+ position.X - indicatorSize / 2,
+ position.Y - indicatorSize / 2,
+ indicatorSize,
+ indicatorSize
+ );
+
+ // 표시등 글로우 효과
+ using (var glowBrush = new System.Drawing.Drawing2D.PathGradientBrush(
+ new Point[] {
+ new Point(indicatorRect.Left, indicatorRect.Top),
+ new Point(indicatorRect.Right, indicatorRect.Top),
+ new Point(indicatorRect.Right, indicatorRect.Bottom),
+ new Point(indicatorRect.Left, indicatorRect.Bottom)
+ }))
+ {
+ glowBrush.CenterPoint = new PointF(position.X, position.Y);
+ glowBrush.CenterColor = Color.White;
+ glowBrush.SurroundColors = new Color[] {
+ GetStatusIndicatorColor(state), GetStatusIndicatorColor(state),
+ GetStatusIndicatorColor(state), GetStatusIndicatorColor(state)
+ };
+
+ g.FillEllipse(glowBrush, indicatorRect);
+ g.DrawEllipse(new Pen(Color.DarkGray, 1), indicatorRect);
+ }
+
+ // 원형 센서 패턴 (방사형 배치)
+ DrawCircularSensors(g, position, state);
+ }
- // 방향 표시 (화살표)
- DrawAGVDirection(g, position, direction);
+ // 리프트 그리기 (이동 경로 기반)
+ DrawAGVLiftAdvanced(g, agv);
+
+ // 모니터 그리기 (리프트 반대편)
+ DrawAGVMonitor(g, agv);
// AGV ID 표시
var font = new Font("Arial", 10, FontStyle.Bold);
@@ -700,35 +905,194 @@ namespace AGVNavigationCore.Controls
}
}
- private void DrawAGVDirection(Graphics g, Point position, AgvDirection direction)
- {
- var arrowSize = 10;
- Point[] arrowPoints = null;
- switch (direction)
+ ///
+ /// 이동 경로 기반 개선된 리프트 그리기 (각도 기반 동적 디자인)
+ ///
+ private void DrawAGVLiftAdvanced(Graphics g, IAGV agv)
+ {
+ const int liftLength = 28; // 리프트 길이 (더욱 크게)
+ const int liftWidth = 16; // 리프트 너비 (더욱 크게)
+ const int liftDistance = AGV_SIZE / 2 + 2; // AGV 본체 면에 바로 붙도록
+
+ var currentPos = agv.CurrentPosition;
+ var targetPos = agv.TargetPosition;
+ var dockingDirection = agv.DockingDirection;
+ var currentDirection = agv.CurrentDirection;
+
+ // 경로 예측 기반 LiftCalculator 사용
+ Point fallbackTarget = targetPos ?? new Point(currentPos.X + 1, currentPos.Y); // 기본 타겟
+ var liftInfo = AGVNavigationCore.Utils.LiftCalculator.CalculateLiftInfoWithPathPrediction(
+ currentPos, fallbackTarget, currentDirection, _nodes);
+
+ float liftAngle = (float)liftInfo.AngleRadians;
+
+ // 리프트 위치 계산 (각도 기반)
+ Point liftPosition = new Point(
+ (int)(currentPos.X + liftDistance * Math.Cos(liftAngle)),
+ (int)(currentPos.Y + liftDistance * Math.Sin(liftAngle))
+ );
+
+ // 방향을 알 수 있는지 확인
+ bool hasDirection = targetPos.HasValue && targetPos.Value != currentPos;
+
+ // 방향에 따른 리프트 색상 및 스타일 결정
+ Color liftColor;
+ Color borderColor;
+
+ // 모터 방향과 경로 상태에 따른 색상 결정 (더 눈에 띄게)
+ switch (currentDirection)
{
case AgvDirection.Forward:
- arrowPoints = new Point[]
- {
- new Point(position.X + arrowSize, position.Y),
- new Point(position.X - arrowSize/2, position.Y - arrowSize/2),
- new Point(position.X - arrowSize/2, position.Y + arrowSize/2)
- };
+ liftColor = Color.Yellow; // 전진 - 노란색 (잘 보임)
+ borderColor = Color.Orange;
break;
case AgvDirection.Backward:
- arrowPoints = new Point[]
- {
- new Point(position.X - arrowSize, position.Y),
- new Point(position.X + arrowSize/2, position.Y - arrowSize/2),
- new Point(position.X + arrowSize/2, position.Y + arrowSize/2)
- };
+ liftColor = Color.Cyan; // 후진 - 시안색 (잘 보임)
+ borderColor = Color.DarkCyan;
+ break;
+ default:
+ liftColor = Color.LightGray; // 방향 불명 - 회색
+ borderColor = Color.Gray;
break;
}
-
- if (arrowPoints != null)
+
+ // 경로 예측 결과에 따른 색상 조정
+ if (liftInfo.CalculationMethod.Contains("경로 예측"))
{
- g.FillPolygon(Brushes.White, arrowPoints);
- g.DrawPolygon(Pens.Black, arrowPoints);
+ // 경로 예측이 성공한 경우 더 선명한 색상
+ if (currentDirection == AgvDirection.Forward)
+ {
+ liftColor = Color.Gold; // 전진 예측 - 골드
+ borderColor = Color.DarkGoldenrod;
+ }
+ else if (currentDirection == AgvDirection.Backward)
+ {
+ liftColor = Color.DeepSkyBlue; // 후진 예측 - 딥 스카이 블루
+ borderColor = Color.Navy;
+ }
+ }
+ else if (liftInfo.CalculationMethod.Contains("갈래길"))
+ {
+ // 갈래길에서는 약간 흐린 색상
+ liftColor = Color.FromArgb(200, liftColor.R, liftColor.G, liftColor.B);
+ borderColor = Color.FromArgb(150, borderColor.R, borderColor.G, borderColor.B);
+ }
+
+ // Graphics 상태 저장
+ var oldTransform = g.Transform;
+
+ // 리프트 중심으로 회전 변환 적용
+ g.TranslateTransform(liftPosition.X, liftPosition.Y);
+ g.RotateTransform((float)(liftAngle * 180.0 / Math.PI));
+
+ // 회전된 좌표계에서 리프트 그리기 (중심이 원점)
+ var liftRect = new Rectangle(
+ -liftLength / 2, // 중심 기준 왼쪽
+ -liftWidth / 2, // 중심 기준 위쪽
+ liftLength,
+ liftWidth
+ );
+
+ using (var liftBrush = new SolidBrush(liftColor))
+ using (var liftPen = new Pen(borderColor, 3)) // 두껍게 테두리
+ {
+ // 둥근 사각형으로 리프트 그리기
+ DrawRoundedRectangle(g, liftBrush, liftPen, liftRect, 4);
+
+ // 추가 강조 테두리 (더 잘 보이게)
+ using (var emphasisPen = new Pen(Color.Black, 1))
+ {
+ DrawRoundedRectangle(g, null, emphasisPen, liftRect, 4);
+ }
+ }
+
+ if (hasDirection)
+ {
+ // 리프트 세부 표현 (방향 표시용 선들)
+ using (var detailPen = new Pen(Color.Gray, 1))
+ {
+ int lineSpacing = 4;
+ for (int i = -liftLength / 2 + 3; i < liftLength / 2 - 3; i += lineSpacing)
+ {
+ g.DrawLine(detailPen, i, -liftWidth / 2 + 2, i, liftWidth / 2 - 2);
+ }
+ }
+
+ // 이동 방향 표시 화살표 (리프트 끝쪽)
+ using (var arrowPen = new Pen(borderColor, 2))
+ {
+ var arrowSize = 3;
+ var arrowX = liftLength / 2 - 4;
+
+ // 화살표 모양
+ g.DrawLine(arrowPen, arrowX, 0, arrowX + arrowSize, -arrowSize);
+ g.DrawLine(arrowPen, arrowX, 0, arrowX + arrowSize, arrowSize);
+ }
+ }
+ else
+ {
+ // 방향을 모를 때 '?' 표시
+ using (var questionBrush = new SolidBrush(Color.DarkGray))
+ using (var questionFont = new Font("Arial", 9, FontStyle.Bold))
+ {
+ var questionSize = g.MeasureString("?", questionFont);
+ var questionPoint = new PointF(
+ -questionSize.Width / 2,
+ -questionSize.Height / 2
+ );
+ g.DrawString("?", questionFont, questionBrush, questionPoint);
+ }
+ }
+
+ // Graphics 상태 복원
+ g.Transform = oldTransform;
+ }
+
+ ///
+ /// 둥근 모서리 사각형 그리기
+ ///
+ private void DrawRoundedRectangle(Graphics g, Brush fillBrush, Pen borderPen, Rectangle rect, int cornerRadius)
+ {
+ if (cornerRadius <= 0)
+ {
+ if (fillBrush != null)
+ g.FillRectangle(fillBrush, rect);
+ if (borderPen != null)
+ g.DrawRectangle(borderPen, rect);
+ return;
+ }
+
+ using (var path = new System.Drawing.Drawing2D.GraphicsPath())
+ {
+ // 둥근 모서리 경로 생성
+ path.AddArc(rect.X, rect.Y, cornerRadius * 2, cornerRadius * 2, 180, 90);
+ path.AddArc(rect.Right - cornerRadius * 2, rect.Y, cornerRadius * 2, cornerRadius * 2, 270, 90);
+ path.AddArc(rect.Right - cornerRadius * 2, rect.Bottom - cornerRadius * 2, cornerRadius * 2, cornerRadius * 2, 0, 90);
+ path.AddArc(rect.X, rect.Bottom - cornerRadius * 2, cornerRadius * 2, cornerRadius * 2, 90, 90);
+ path.CloseFigure();
+
+ // 채우기와 테두리 그리기 (null 체크 추가)
+ if (fillBrush != null)
+ g.FillPath(fillBrush, path);
+ if (borderPen != null)
+ g.DrawPath(borderPen, path);
+ }
+ }
+
+ private void DrawAGVLiftDebugInfo(Graphics g, IAGV agv)
+ {
+ var currentPos = agv.CurrentPosition;
+ var targetPos = agv.TargetPosition;
+
+ // 디버그 정보 (개발용)
+ if (targetPos.HasValue)
+ {
+ // 이동 방향 벡터 그리기 (얇은 점선)
+ using (var debugPen = new Pen(Color.Red, 1) { DashStyle = System.Drawing.Drawing2D.DashStyle.Dot })
+ {
+ g.DrawLine(debugPen, currentPos, targetPos.Value);
+ }
}
}
@@ -740,6 +1104,289 @@ namespace AGVNavigationCore.Controls
}
}
+ ///
+ /// AGV 중앙 색상 (방사형 그라디언트)
+ ///
+ private Color GetAGVCenterColor(AGVState state)
+ {
+ switch (state)
+ {
+ case AGVState.Moving: return Color.White;
+ case AGVState.Charging: return Color.LightCyan;
+ case AGVState.Error: return Color.LightPink;
+ case AGVState.Docking: return Color.LightYellow;
+ default: return Color.WhiteSmoke;
+ }
+ }
+
+ ///
+ /// AGV 외곽 색상 (방사형 그라디언트)
+ ///
+ private Color GetAGVOuterColor(AGVState state)
+ {
+ switch (state)
+ {
+ case AGVState.Moving: return Color.DarkGreen;
+ case AGVState.Charging: return Color.DarkBlue;
+ case AGVState.Error: return Color.DarkRed;
+ case AGVState.Docking: return Color.DarkOrange;
+ default: return Color.DarkGray;
+ }
+ }
+
+ ///
+ /// AGV 내부 링 색상
+ ///
+ private Color GetAGVInnerRingColor(AGVState state)
+ {
+ switch (state)
+ {
+ case AGVState.Moving: return Color.Green;
+ case AGVState.Charging: return Color.Blue;
+ case AGVState.Error: return Color.Red;
+ case AGVState.Docking: return Color.Orange;
+ default: return Color.Gray;
+ }
+ }
+
+ ///
+ /// AGV 중앙 표시등 색상
+ ///
+ private Color GetStatusIndicatorColor(AGVState state)
+ {
+ switch (state)
+ {
+ case AGVState.Moving: return Color.Lime;
+ case AGVState.Charging: return Color.Blue;
+ case AGVState.Error: return Color.Red;
+ case AGVState.Docking: return Color.Yellow;
+ default: return Color.Gray;
+ }
+ }
+
+ ///
+ /// AGV 원형 센서 패턴 (방사형 배치)
+ ///
+ private void DrawCircularSensors(Graphics g, Point center, AGVState state)
+ {
+ var sensorSize = 3;
+ var sensorColor = state == AGVState.Error ? Color.Red : Color.Silver;
+ var radius = AGV_SIZE / 2 - 6;
+
+ // 8방향으로 센서 배치 (45도씩)
+ for (int i = 0; i < 8; i++)
+ {
+ double angle = i * Math.PI / 4; // 45도씩
+ int sensorX = (int)(center.X + radius * Math.Cos(angle));
+ int sensorY = (int)(center.Y + radius * Math.Sin(angle));
+
+ var sensorRect = new Rectangle(
+ sensorX - sensorSize / 2,
+ sensorY - sensorSize / 2,
+ sensorSize,
+ sensorSize
+ );
+
+ using (var sensorBrush = new SolidBrush(sensorColor))
+ {
+ g.FillEllipse(sensorBrush, sensorRect);
+ g.DrawEllipse(new Pen(Color.Black, 1), sensorRect);
+ }
+ }
+
+ // 내부 원형 패턴 (장식)
+ var innerRadius = AGV_SIZE / 2 - 12;
+ for (int i = 0; i < 4; i++)
+ {
+ double angle = i * Math.PI / 2 + Math.PI / 4; // 45도 오프셋된 4방향
+ int dotX = (int)(center.X + innerRadius * Math.Cos(angle));
+ int dotY = (int)(center.Y + innerRadius * Math.Sin(angle));
+
+ var dotRect = new Rectangle(dotX - 1, dotY - 1, 2, 2);
+ using (var dotBrush = new SolidBrush(GetAGVInnerRingColor(state)))
+ {
+ g.FillEllipse(dotBrush, dotRect);
+ }
+ }
+ }
+
+ ///
+ /// AGV 모니터 그리기 (리프트 반대편)
+ ///
+ private void DrawAGVMonitor(Graphics g, IAGV agv)
+ {
+ const int monitorWidth = 12; // 모니터 너비 (가로로 더 길게)
+ const int monitorHeight = 24; // 모니터 높이 (세로는 줄임)
+ const int monitorDistance = AGV_SIZE / 2 + 2; // AGV 본체에서 거리 (리프트와 동일)
+
+ var currentPos = agv.CurrentPosition;
+ var targetPos = agv.TargetPosition;
+ var dockingDirection = agv.DockingDirection;
+ var currentDirection = agv.CurrentDirection;
+
+ // 리프트 방향 계산 (모니터는 정반대)
+ Point fallbackTarget = targetPos ?? new Point(currentPos.X + 50, currentPos.Y);
+
+ var liftInfo = AGVNavigationCore.Utils.LiftCalculator.CalculateLiftInfoWithPathPrediction(
+ currentPos, fallbackTarget, currentDirection, _nodes);
+
+ bool hasDirection = liftInfo != null;
+ double monitorAngle = hasDirection ? liftInfo.AngleRadians + Math.PI : 0; // 리프트 반대 방향 (180도 회전)
+
+ // Graphics 변환 설정
+ var oldTransform = g.Transform;
+ var transform = oldTransform.Clone();
+ transform.Translate(currentPos.X, currentPos.Y);
+ transform.Rotate((float)(monitorAngle * 180.0 / Math.PI));
+ g.Transform = transform;
+
+ // 모니터 위치 (AGV 면에 붙도록)
+ var monitorRect = new Rectangle(
+ monitorDistance - monitorWidth / 2,
+ -monitorHeight / 2,
+ monitorWidth,
+ monitorHeight
+ );
+
+ if (hasDirection)
+ {
+ // 모니터 상태에 따른 색상
+ var monitorBackColor = GetMonitorBackColor(agv.CurrentState);
+ var monitorBorderColor = Color.DarkGray;
+
+ // 모니터 본체 (둥근 모서리)
+ using (var monitorBrush = new SolidBrush(monitorBackColor))
+ using (var borderPen = new Pen(monitorBorderColor, 2))
+ {
+ DrawRoundedRectangle(g, monitorBrush, borderPen, monitorRect, 2);
+ }
+
+ // 모니터 화면 (내부 사각형)
+ var screenRect = new Rectangle(
+ monitorRect.X + 2,
+ monitorRect.Y + 2,
+ monitorRect.Width - 4,
+ monitorRect.Height - 4
+ );
+
+ using (var screenBrush = new SolidBrush(GetMonitorScreenColor(agv.CurrentState)))
+ {
+ g.FillRectangle(screenBrush, screenRect);
+ }
+
+ // 화면 패턴 (상태별)
+ DrawMonitorScreen(g, screenRect, agv.CurrentState);
+
+ // 모니터 스탠드
+ var standRect = new Rectangle(
+ monitorDistance - 2,
+ monitorHeight / 2 - 1,
+ 4,
+ 3
+ );
+ using (var standBrush = new SolidBrush(Color.Gray))
+ {
+ g.FillRectangle(standBrush, standRect);
+ }
+ }
+ else
+ {
+ // 방향을 모를 때 기본 모니터 (스카이블루로)
+ using (var monitorBrush = new SolidBrush(Color.SkyBlue))
+ using (var borderPen = new Pen(Color.DarkGray, 2))
+ {
+ DrawRoundedRectangle(g, monitorBrush, borderPen, monitorRect, 2);
+ }
+ }
+
+ // Graphics 상태 복원
+ g.Transform = oldTransform;
+ }
+
+ ///
+ /// 모니터 배경 색상 (스카이블루 계통으로 통일)
+ ///
+ private Color GetMonitorBackColor(AGVState state)
+ {
+ // 모든 상태에서 스카이블루 계통으로 통일하여 AGV와 구분
+ return Color.SkyBlue;
+ }
+
+ ///
+ /// 모니터 화면 색상 (상태별로 구분)
+ ///
+ private Color GetMonitorScreenColor(AGVState state)
+ {
+ switch (state)
+ {
+ case AGVState.Moving: return Color.Navy; // 이동: 네이비
+ case AGVState.Charging: return Color.DarkBlue; // 충전: 다크블루
+ case AGVState.Error: return Color.DarkRed; // 에러: 다크레드
+ case AGVState.Docking: return Color.DarkGreen; // 도킹: 다크그린
+ default: return Color.DarkSlateGray; // 대기: 다크슬레이트그레이
+ }
+ }
+
+ ///
+ /// 모니터 화면 패턴 그리기
+ ///
+ private void DrawMonitorScreen(Graphics g, Rectangle screenRect, AGVState state)
+ {
+ using (var patternPen = new Pen(Color.White, 1))
+ {
+ switch (state)
+ {
+ case AGVState.Moving:
+ // 이동 중: 작은 점들 (데이터 표시)
+ for (int i = 1; i < screenRect.Width; i += 3)
+ {
+ for (int j = 1; j < screenRect.Height; j += 3)
+ {
+ g.DrawRectangle(patternPen, screenRect.X + i, screenRect.Y + j, 1, 1);
+ }
+ }
+ break;
+
+ case AGVState.Charging:
+ // 충전 중: 배터리 아이콘
+ var batteryRect = new Rectangle(
+ screenRect.X + screenRect.Width / 2 - 3,
+ screenRect.Y + screenRect.Height / 2 - 2,
+ 6,
+ 4
+ );
+ g.DrawRectangle(patternPen, batteryRect);
+ g.DrawLine(patternPen, batteryRect.Right, batteryRect.Y + 1, batteryRect.Right, batteryRect.Bottom - 1);
+ break;
+
+ case AGVState.Error:
+ // 에러: X 마크
+ g.DrawLine(patternPen, screenRect.X + 2, screenRect.Y + 2,
+ screenRect.Right - 2, screenRect.Bottom - 2);
+ g.DrawLine(patternPen, screenRect.Right - 2, screenRect.Y + 2,
+ screenRect.X + 2, screenRect.Bottom - 2);
+ break;
+
+ case AGVState.Docking:
+ // 도킹: 화살표
+ var centerX = screenRect.X + screenRect.Width / 2;
+ var centerY = screenRect.Y + screenRect.Height / 2;
+ g.DrawLine(patternPen, centerX - 4, centerY, centerX + 2, centerY);
+ g.DrawLine(patternPen, centerX, centerY - 2, centerX + 2, centerY);
+ g.DrawLine(patternPen, centerX, centerY + 2, centerX + 2, centerY);
+ break;
+
+ default:
+ // 대기: 중앙 점
+ g.DrawRectangle(patternPen,
+ screenRect.X + screenRect.Width / 2 - 1,
+ screenRect.Y + screenRect.Height / 2 - 1,
+ 2, 2);
+ break;
+ }
+ }
+ }
+
private void DrawUIInfo(Graphics g)
{
// 회사 로고
diff --git a/Cs_HMI/AGVNavigationCore/Controls/UnifiedAGVCanvas.cs b/Cs_HMI/AGVNavigationCore/Controls/UnifiedAGVCanvas.cs
index 91e0788..d9c30ac 100644
--- a/Cs_HMI/AGVNavigationCore/Controls/UnifiedAGVCanvas.cs
+++ b/Cs_HMI/AGVNavigationCore/Controls/UnifiedAGVCanvas.cs
@@ -22,7 +22,7 @@ namespace AGVNavigationCore.Controls
private const int GRID_SIZE = 20;
private const float CONNECTION_WIDTH = 2.0f;
private const int SNAP_DISTANCE = 10;
- private const int AGV_SIZE = 30;
+ private const int AGV_SIZE = 40;
private const int CONNECTION_ARROW_SIZE = 8;
#endregion
@@ -64,6 +64,7 @@ namespace AGVNavigationCore.Controls
private List _nodes;
private MapNode _selectedNode;
private MapNode _hoveredNode;
+ private MapNode _destinationNode;
// AGV 관련
private List _agvList;
@@ -105,6 +106,7 @@ namespace AGVNavigationCore.Controls
private Brush _chargingNodeBrush;
private Brush _selectedNodeBrush;
private Brush _hoveredNodeBrush;
+ private Brush _destinationNodeBrush;
private Brush _gridBrush;
private Brush _agvBrush;
private Brush _pathBrush;
@@ -113,6 +115,7 @@ namespace AGVNavigationCore.Controls
private Pen _gridPen;
private Pen _tempConnectionPen;
private Pen _selectedNodePen;
+ private Pen _destinationNodePen;
private Pen _pathPen;
private Pen _agvPen;
@@ -239,6 +242,7 @@ namespace AGVNavigationCore.Controls
set
{
_currentPath = value;
+ UpdateDestinationNode();
Invalidate();
}
}
@@ -324,6 +328,7 @@ namespace AGVNavigationCore.Controls
_chargingNodeBrush = new SolidBrush(Color.Green);
_selectedNodeBrush = new SolidBrush(Color.Red);
_hoveredNodeBrush = new SolidBrush(Color.LightCyan);
+ _destinationNodeBrush = new SolidBrush(Color.Gold);
// AGV 및 경로 브러쉬
_agvBrush = new SolidBrush(Color.Red);
@@ -339,6 +344,7 @@ namespace AGVNavigationCore.Controls
_gridPen = new Pen(Color.LightGray, 1);
_tempConnectionPen = new Pen(Color.Orange, 2) { DashStyle = DashStyle.Dash };
_selectedNodePen = new Pen(Color.Red, 3);
+ _destinationNodePen = new Pen(Color.Orange, 4);
_pathPen = new Pen(Color.Purple, 3);
_agvPen = new Pen(Color.Red, 3);
}
@@ -464,6 +470,20 @@ namespace AGVNavigationCore.Controls
Invalidate();
}
+ private void UpdateDestinationNode()
+ {
+ _destinationNode = null;
+
+ if (_currentPath != null && _currentPath.Success && _currentPath.Path != null && _currentPath.Path.Count > 0)
+ {
+ // 경로의 마지막 노드가 목적지
+ string destinationNodeId = _currentPath.Path[_currentPath.Path.Count - 1];
+
+ // 노드 목록에서 해당 노드 찾기
+ _destinationNode = _nodes?.FirstOrDefault(n => n.NodeId == destinationNodeId);
+ }
+ }
+
#endregion
#region Cleanup
@@ -480,6 +500,7 @@ namespace AGVNavigationCore.Controls
_chargingNodeBrush?.Dispose();
_selectedNodeBrush?.Dispose();
_hoveredNodeBrush?.Dispose();
+ _destinationNodeBrush?.Dispose();
_gridBrush?.Dispose();
_agvBrush?.Dispose();
_pathBrush?.Dispose();
@@ -489,6 +510,7 @@ namespace AGVNavigationCore.Controls
_gridPen?.Dispose();
_tempConnectionPen?.Dispose();
_selectedNodePen?.Dispose();
+ _destinationNodePen?.Dispose();
_pathPen?.Dispose();
_agvPen?.Dispose();
@@ -517,6 +539,12 @@ namespace AGVNavigationCore.Controls
AgvDirection CurrentDirection { get; }
AGVState CurrentState { get; }
float BatteryLevel { get; }
+
+ // 이동 경로 정보 추가
+ Point? TargetPosition { get; }
+ string CurrentNodeId { get; }
+ string TargetNodeId { get; }
+ DockingDirection DockingDirection { get; }
}
///
diff --git a/Cs_HMI/AGVNavigationCore/PathFinding/AGVPathResult.cs b/Cs_HMI/AGVNavigationCore/PathFinding/AGVPathResult.cs
index 33a5b0a..a65f01f 100644
--- a/Cs_HMI/AGVNavigationCore/PathFinding/AGVPathResult.cs
+++ b/Cs_HMI/AGVNavigationCore/PathFinding/AGVPathResult.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Linq;
using AGVNavigationCore.Models;
namespace AGVNavigationCore.PathFinding
@@ -24,6 +25,11 @@ namespace AGVNavigationCore.PathFinding
///
public List Commands { get; set; }
+ ///
+ /// 노드별 모터방향 정보 목록
+ ///
+ public List NodeMotorInfos { get; set; }
+
///
/// 총 거리
///
@@ -57,6 +63,7 @@ namespace AGVNavigationCore.PathFinding
Success = false;
Path = new List();
Commands = new List();
+ NodeMotorInfos = new List();
TotalDistance = 0;
CalculationTimeMs = 0;
EstimatedTimeSeconds = 0;
@@ -87,6 +94,31 @@ namespace AGVNavigationCore.PathFinding
return result;
}
+ ///
+ /// 성공 결과 생성 (노드별 모터방향 정보 포함)
+ ///
+ /// 경로
+ /// AGV 명령어 목록
+ /// 노드별 모터방향 정보
+ /// 총 거리
+ /// 계산 시간
+ /// 성공 결과
+ public static AGVPathResult CreateSuccess(List path, List commands, List nodeMotorInfos, float totalDistance, long calculationTimeMs)
+ {
+ var result = new AGVPathResult
+ {
+ Success = true,
+ Path = new List(path),
+ Commands = new List(commands),
+ NodeMotorInfos = new List(nodeMotorInfos),
+ TotalDistance = totalDistance,
+ CalculationTimeMs = calculationTimeMs
+ };
+
+ result.CalculateMetrics();
+ return result;
+ }
+
///
/// 실패 결과 생성
///
diff --git a/Cs_HMI/AGVNavigationCore/PathFinding/AGVPathfinder.cs b/Cs_HMI/AGVNavigationCore/PathFinding/AGVPathfinder.cs
index 9aa0342..70c5d30 100644
--- a/Cs_HMI/AGVNavigationCore/PathFinding/AGVPathfinder.cs
+++ b/Cs_HMI/AGVNavigationCore/PathFinding/AGVPathfinder.cs
@@ -159,7 +159,8 @@ namespace AGVNavigationCore.PathFinding
}
var agvCommands = GenerateAGVCommands(result.Path, targetDirection ?? AgvDirection.Forward);
- return AGVPathResult.CreateSuccess(result.Path, agvCommands, result.TotalDistance, stopwatch.ElapsedMilliseconds);
+ var nodeMotorInfos = GenerateNodeMotorInfos(result.Path);
+ return AGVPathResult.CreateSuccess(result.Path, agvCommands, nodeMotorInfos, result.TotalDistance, stopwatch.ElapsedMilliseconds);
}
///
@@ -182,7 +183,8 @@ namespace AGVNavigationCore.PathFinding
}
var agvCommands = GenerateAGVCommands(result.Path, actualTargetDirection);
- return AGVPathResult.CreateSuccess(result.Path, agvCommands, result.TotalDistance, stopwatch.ElapsedMilliseconds);
+ var nodeMotorInfos = GenerateNodeMotorInfos(result.Path);
+ return AGVPathResult.CreateSuccess(result.Path, agvCommands, nodeMotorInfos, result.TotalDistance, stopwatch.ElapsedMilliseconds);
}
///
@@ -264,6 +266,92 @@ namespace AGVNavigationCore.PathFinding
return AgvDirection.Right;
}
+ ///
+ /// 노드별 모터방향 정보 생성
+ ///
+ /// 경로 노드 목록
+ /// 노드별 모터방향 정보 목록
+ private List GenerateNodeMotorInfos(List path)
+ {
+ var nodeMotorInfos = new List();
+ if (path.Count < 2) return nodeMotorInfos;
+
+ for (int i = 0; i < path.Count; i++)
+ {
+ var currentNodeId = path[i];
+ string nextNodeId = i < path.Count - 1 ? path[i + 1] : null;
+
+ AgvDirection motorDirection;
+
+ if (i == path.Count - 1)
+ {
+ // 마지막 노드: 도킹/충전 노드 타입에 따라 결정
+ if (_nodeMap.ContainsKey(currentNodeId))
+ {
+ var currentNode = _nodeMap[currentNodeId];
+ motorDirection = GetRequiredDirectionForNode(currentNode);
+ }
+ else
+ {
+ motorDirection = AgvDirection.Forward;
+ }
+ }
+ else
+ {
+ // 중간 노드: 다음 노드와의 관계를 고려한 모터방향 결정
+ motorDirection = CalculateMotorDirection(currentNodeId, nextNodeId);
+ }
+
+ nodeMotorInfos.Add(new NodeMotorInfo(currentNodeId, motorDirection, nextNodeId));
+ }
+
+ return nodeMotorInfos;
+ }
+
+ ///
+ /// 현재 노드에서 다음 노드로 이동할 때의 모터방향 계산
+ ///
+ /// 현재 노드 ID
+ /// 다음 노드 ID
+ /// 모터방향
+ private AgvDirection CalculateMotorDirection(string currentNodeId, string nextNodeId)
+ {
+ if (!_nodeMap.ContainsKey(currentNodeId) || !_nodeMap.ContainsKey(nextNodeId))
+ {
+ return AgvDirection.Forward;
+ }
+
+ var currentNode = _nodeMap[currentNodeId];
+ var nextNode = _nodeMap[nextNodeId];
+
+ // 현재 노드와 다음 노드의 위치를 기반으로 이동 방향 계산
+ var dx = nextNode.Position.X - currentNode.Position.X;
+ var dy = nextNode.Position.Y - currentNode.Position.Y;
+ var moveAngle = Math.Atan2(dy, dx);
+
+ // AGV의 구조: 리프트 ↔ AGV 몸체 ↔ 모니터
+ // 전진: 모니터 방향으로 이동 (리프트에서 멀어짐)
+ // 후진: 리프트 방향으로 이동 (리프트에 가까워짐)
+
+ // 다음 노드가 특수 노드인지 확인
+ if (nextNode.Type == NodeType.Charging)
+ {
+ // 충전기: 전진으로 도킹
+ return AgvDirection.Forward;
+ }
+ else if (nextNode.Type == NodeType.Docking)
+ {
+ // 도킹 스테이션: 후진으로 도킹
+ return AgvDirection.Backward;
+ }
+ else
+ {
+ // 일반 이동: 기본적으로 전진
+ // 향후 경로 패턴 분석을 통해 더 정확한 방향 결정 가능
+ return AgvDirection.Forward;
+ }
+ }
+
///
/// 경로 유효성 검증
///
diff --git a/Cs_HMI/AGVNavigationCore/PathFinding/NodeMotorInfo.cs b/Cs_HMI/AGVNavigationCore/PathFinding/NodeMotorInfo.cs
new file mode 100644
index 0000000..387de43
--- /dev/null
+++ b/Cs_HMI/AGVNavigationCore/PathFinding/NodeMotorInfo.cs
@@ -0,0 +1,32 @@
+using AGVNavigationCore.Models;
+
+namespace AGVNavigationCore.PathFinding
+{
+ ///
+ /// 노드별 모터방향 정보
+ ///
+ public class NodeMotorInfo
+ {
+ ///
+ /// 노드 ID
+ ///
+ public string NodeId { get; set; }
+
+ ///
+ /// 해당 노드에서의 모터방향
+ ///
+ public AgvDirection MotorDirection { get; set; }
+
+ ///
+ /// 다음 노드 ID (경로예측용)
+ ///
+ public string NextNodeId { get; set; }
+
+ public NodeMotorInfo(string nodeId, AgvDirection motorDirection, string nextNodeId = null)
+ {
+ NodeId = nodeId;
+ MotorDirection = motorDirection;
+ NextNodeId = nextNodeId;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Cs_HMI/AGVNavigationCore/Utils/LiftCalculator.cs b/Cs_HMI/AGVNavigationCore/Utils/LiftCalculator.cs
new file mode 100644
index 0000000..640b043
--- /dev/null
+++ b/Cs_HMI/AGVNavigationCore/Utils/LiftCalculator.cs
@@ -0,0 +1,277 @@
+using System;
+using System.Collections.Generic;
+using System.Drawing;
+using System.Linq;
+using AGVNavigationCore.Models;
+
+namespace AGVNavigationCore.Utils
+{
+ ///
+ /// AGV 리프트 방향 계산 유틸리티 클래스
+ /// 모든 리프트 방향 계산 로직을 중앙화하여 일관성 보장
+ ///
+ public static class LiftCalculator
+ {
+ ///
+ /// AGV 이동 방향과 모터 방향을 기반으로 리프트 각도 계산
+ ///
+ /// 현재 위치
+ /// 목표 위치
+ /// 모터 방향
+ /// 리프트 각도 (라디안)
+ public static double CalculateLiftAngleRadians(Point currentPos, Point targetPos, AgvDirection motorDirection)
+ {
+ // 모터 방향에 따른 리프트 위치 계산
+ if (motorDirection == AgvDirection.Forward)
+ {
+ // 전진 모터: AGV가 앞으로 가므로 리프트는 뒤쪽 (타겟 → 현재 방향)
+ var dx = currentPos.X - targetPos.X;
+ var dy = currentPos.Y - targetPos.Y;
+ return Math.Atan2(dy, dx);
+ }
+ else if (motorDirection == AgvDirection.Backward)
+ {
+ // 후진 모터: AGV가 리프트 쪽으로 이동 (현재 → 타겟 방향이 리프트 방향)
+ var dx = targetPos.X - currentPos.X;
+ var dy = targetPos.Y - currentPos.Y;
+ return Math.Atan2(dy, dx);
+ }
+ else
+ {
+ // 기본값: 전진 모터와 동일
+ var dx = currentPos.X - targetPos.X;
+ var dy = currentPos.Y - targetPos.Y;
+ return Math.Atan2(dy, dx);
+ }
+ }
+
+ ///
+ /// AGV 이동 방향과 모터 방향을 기반으로 리프트 각도 계산 (도 단위)
+ ///
+ /// 현재 위치
+ /// 목표 위치
+ /// 모터 방향
+ /// 리프트 각도 (도)
+ public static double CalculateLiftAngleDegrees(Point currentPos, Point targetPos, AgvDirection motorDirection)
+ {
+ var radians = CalculateLiftAngleRadians(currentPos, targetPos, motorDirection);
+ var degrees = radians * 180.0 / Math.PI;
+
+ // 0-360도 범위로 정규화
+ while (degrees < 0) degrees += 360;
+ while (degrees >= 360) degrees -= 360;
+
+ return degrees;
+ }
+
+ ///
+ /// 각도를 8방향 문자열로 변환 (화면 좌표계 기준)
+ /// 화면 좌표계: 0°=동쪽, 90°=남쪽, 180°=서쪽, 270°=북쪽
+ ///
+ /// 각도 (도)
+ /// 방향 문자열
+ public static string AngleToDirectionString(double angleDegrees)
+ {
+ // 0-360도 범위로 정규화
+ while (angleDegrees < 0) angleDegrees += 360;
+ while (angleDegrees >= 360) angleDegrees -= 360;
+
+ // 8방향으로 분류 (화면 좌표계)
+ if (angleDegrees >= 337.5 || angleDegrees < 22.5)
+ return "동쪽(→)";
+ else if (angleDegrees >= 22.5 && angleDegrees < 67.5)
+ return "남동쪽(↘)";
+ else if (angleDegrees >= 67.5 && angleDegrees < 112.5)
+ return "남쪽(↓)";
+ else if (angleDegrees >= 112.5 && angleDegrees < 157.5)
+ return "남서쪽(↙)";
+ else if (angleDegrees >= 157.5 && angleDegrees < 202.5)
+ return "서쪽(←)";
+ else if (angleDegrees >= 202.5 && angleDegrees < 247.5)
+ return "북서쪽(↖)";
+ else if (angleDegrees >= 247.5 && angleDegrees < 292.5)
+ return "북쪽(↑)";
+ else if (angleDegrees >= 292.5 && angleDegrees < 337.5)
+ return "북동쪽(↗)";
+ else
+ return "알 수 없음";
+ }
+
+ ///
+ /// 리프트 계산 결과 정보
+ ///
+ public class LiftCalculationResult
+ {
+ public double AngleRadians { get; set; }
+ public double AngleDegrees { get; set; }
+ public string DirectionString { get; set; }
+ public string CalculationMethod { get; set; }
+ public AgvDirection MotorDirection { get; set; }
+ }
+
+ ///
+ /// 종합적인 리프트 계산 (모든 정보 포함)
+ ///
+ /// 현재 위치
+ /// 목표 위치
+ /// 모터 방향
+ /// 리프트 계산 결과
+ public static LiftCalculationResult CalculateLiftInfo(Point currentPos, Point targetPos, AgvDirection motorDirection)
+ {
+ var angleRadians = CalculateLiftAngleRadians(currentPos, targetPos, motorDirection);
+ var angleDegrees = angleRadians * 180.0 / Math.PI;
+
+ // 0-360도 범위로 정규화
+ while (angleDegrees < 0) angleDegrees += 360;
+ while (angleDegrees >= 360) angleDegrees -= 360;
+
+ var directionString = AngleToDirectionString(angleDegrees);
+
+ string calculationMethod;
+ if (motorDirection == AgvDirection.Forward)
+ calculationMethod = "이동방향 + 180도 (전진모터)";
+ else if (motorDirection == AgvDirection.Backward)
+ calculationMethod = "이동방향과 동일 (후진모터)";
+ else
+ calculationMethod = "기본값 (전진모터)";
+
+ return new LiftCalculationResult
+ {
+ AngleRadians = angleRadians,
+ AngleDegrees = angleDegrees,
+ DirectionString = directionString,
+ CalculationMethod = calculationMethod,
+ MotorDirection = motorDirection
+ };
+ }
+
+ ///
+ /// 경로 예측 기반 리프트 방향 계산
+ /// 현재 노드에서 연결된 다음 노드들을 분석하여 리프트 방향 결정
+ ///
+ /// 현재 위치
+ /// 이전 위치
+ /// 모터 방향
+ /// 맵 노드 리스트 (경로 예측용)
+ /// 위치 허용 오차
+ /// 리프트 계산 결과
+ public static LiftCalculationResult CalculateLiftInfoWithPathPrediction(
+ Point currentPos, Point previousPos, AgvDirection motorDirection,
+ List mapNodes, int tolerance = 10)
+ {
+ if (mapNodes == null || mapNodes.Count == 0)
+ {
+ // 맵 노드 정보가 없으면 기존 방식 사용
+ return CalculateLiftInfo(previousPos, currentPos, motorDirection);
+ }
+
+ // 현재 위치에 해당하는 노드 찾기
+ var currentNode = FindNodeByPosition(mapNodes, currentPos, tolerance);
+
+ if (currentNode == null)
+ {
+ // 현재 노드를 찾을 수 없으면 기존 방식 사용
+ return CalculateLiftInfo(previousPos, currentPos, motorDirection);
+ }
+
+ // 이전 위치에 해당하는 노드 찾기
+ var previousNode = FindNodeByPosition(mapNodes, previousPos, tolerance);
+
+ Point targetPosition;
+ string calculationMethod;
+
+ // 모터 방향에 따른 예측 방향 결정
+ if (motorDirection == AgvDirection.Backward)
+ {
+ // 후진 모터: AGV가 리프트 쪽(목표 위치)으로 이동
+ // 경로 예측 없이 단순히 현재→목표 방향 사용
+ return CalculateLiftInfo(currentPos, previousPos, motorDirection);
+ }
+ else
+ {
+ // 전진 모터: 기존 로직 (다음 노드 예측)
+ var nextNodes = GetConnectedNodes(mapNodes, currentNode);
+
+ // 이전 노드 제외 (되돌아가는 방향 제외)
+ if (previousNode != null)
+ {
+ nextNodes = nextNodes.Where(n => n.NodeId != previousNode.NodeId).ToList();
+ }
+
+ if (nextNodes.Count == 1)
+ {
+ // 직선 경로: 다음 노드 방향으로 예측
+ targetPosition = nextNodes.First().Position;
+ calculationMethod = $"전진 경로 예측 ({currentNode.NodeId}→{nextNodes.First().NodeId})";
+ }
+ else if (nextNodes.Count > 1)
+ {
+ // 갈래길: 이전 위치 기반 계산 사용
+ var prevResult = CalculateLiftInfo(previousPos, currentPos, motorDirection);
+ prevResult.CalculationMethod += " (전진 갈래길)";
+ return prevResult;
+ }
+ else
+ {
+ // 연결된 노드가 없으면 기존 방식 사용
+ return CalculateLiftInfo(previousPos, currentPos, motorDirection);
+ }
+ }
+
+ // 리프트 각도 계산
+ var angleRadians = CalculateLiftAngleRadians(currentPos, targetPosition, motorDirection);
+ var angleDegrees = angleRadians * 180.0 / Math.PI;
+
+ // 0-360도 범위로 정규화
+ while (angleDegrees < 0) angleDegrees += 360;
+ while (angleDegrees >= 360) angleDegrees -= 360;
+
+ var directionString = AngleToDirectionString(angleDegrees);
+
+ return new LiftCalculationResult
+ {
+ AngleRadians = angleRadians,
+ AngleDegrees = angleDegrees,
+ DirectionString = directionString,
+ CalculationMethod = calculationMethod,
+ MotorDirection = motorDirection
+ };
+ }
+
+ ///
+ /// 위치 기반 노드 찾기
+ ///
+ /// 맵 노드 리스트
+ /// 찾을 위치
+ /// 허용 오차
+ /// 해당하는 노드 또는 null
+ private static MapNode FindNodeByPosition(List mapNodes, Point position, int tolerance)
+ {
+ return mapNodes.FirstOrDefault(node =>
+ Math.Abs(node.Position.X - position.X) <= tolerance &&
+ Math.Abs(node.Position.Y - position.Y) <= tolerance);
+ }
+
+ ///
+ /// 노드에서 연결된 다른 노드들 찾기
+ ///
+ /// 맵 노드 리스트
+ /// 현재 노드
+ /// 연결된 노드 리스트
+ private static List GetConnectedNodes(List mapNodes, MapNode currentNode)
+ {
+ var connectedNodes = new List();
+
+ foreach (var nodeId in currentNode.ConnectedNodes)
+ {
+ var connectedNode = mapNodes.FirstOrDefault(n => n.NodeId == nodeId);
+ if (connectedNode != null)
+ {
+ connectedNodes.Add(connectedNode);
+ }
+ }
+
+ return connectedNodes;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Cs_HMI/AGVSimulator/Forms/SimulatorForm.cs b/Cs_HMI/AGVSimulator/Forms/SimulatorForm.cs
index 299d7c1..9e70878 100644
--- a/Cs_HMI/AGVSimulator/Forms/SimulatorForm.cs
+++ b/Cs_HMI/AGVSimulator/Forms/SimulatorForm.cs
@@ -8,6 +8,7 @@ using System.Windows.Forms;
using AGVMapEditor.Models;
using AGVNavigationCore.Models;
using AGVNavigationCore.Controls;
+using AGVNavigationCore.PathFinding;
using AGVSimulator.Models;
using Newtonsoft.Json;
@@ -59,21 +60,24 @@ namespace AGVSimulator.Forms
{
// 설정 로드
_config = SimulatorConfig.Load();
-
+
// 데이터 초기화
_mapNodes = new List();
_agvList = new List();
_simulationState = new SimulationState();
_currentMapFilePath = string.Empty;
-
+
// 시뮬레이터 캔버스 생성 (중앙 패널에만)
CreateSimulatorCanvas();
-
+
// 타이머 초기화
_simulationTimer = new Timer();
_simulationTimer.Interval = 100; // 100ms 간격
_simulationTimer.Tick += OnSimulationTimer_Tick;
-
+
+ // 방향 콤보박스 초기화
+ InitializeDirectionCombo();
+
// 초기 상태 설정
UpdateUI();
}
@@ -85,7 +89,7 @@ namespace AGVSimulator.Forms
_simulatorCanvas = new UnifiedAGVCanvas();
_simulatorCanvas.Dock = DockStyle.Fill;
_simulatorCanvas.Mode = UnifiedAGVCanvas.CanvasMode.ViewOnly;
-
+
_canvasPanel.Controls.Add(_simulatorCanvas);
}
@@ -95,6 +99,24 @@ namespace AGVSimulator.Forms
_canvasPanel.BringToFront();
}
+ ///
+ /// 모터 구동방향 콤보박스 초기화
+ ///
+ private void InitializeDirectionCombo()
+ {
+ _directionCombo.Items.Clear();
+
+ // AgvDirection enum 값들을 콤보박스에 추가
+ _directionCombo.Items.Add(new DirectionItem(AgvDirection.Forward, "전진 (모니터쪽)"));
+ _directionCombo.Items.Add(new DirectionItem(AgvDirection.Backward, "후진 (리프트쪽)"));
+ _directionCombo.Items.Add(new DirectionItem(AgvDirection.Left, "좌회전"));
+ _directionCombo.Items.Add(new DirectionItem(AgvDirection.Right, "우회전"));
+ _directionCombo.Items.Add(new DirectionItem(AgvDirection.Stop, "정지"));
+
+ // 기본 선택: 전진
+ _directionCombo.SelectedIndex = 0;
+ }
+
#endregion
#region Event Handlers
@@ -105,7 +127,7 @@ namespace AGVSimulator.Forms
{
openDialog.Filter = "AGV Map Files (*.agvmap)|*.agvmap|모든 파일 (*.*)|*.*";
openDialog.Title = "맵 파일 열기";
-
+
if (openDialog.ShowDialog() == DialogResult.OK)
{
try
@@ -115,7 +137,7 @@ namespace AGVSimulator.Forms
}
catch (Exception ex)
{
- MessageBox.Show($"맵 파일을 로드할 수 없습니다:\n{ex.Message}", "오류",
+ MessageBox.Show($"맵 파일을 로드할 수 없습니다:\n{ex.Message}", "오류",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
@@ -131,10 +153,11 @@ namespace AGVSimulator.Forms
{
if (_simulationState.IsRunning)
return;
-
+
_simulationState.IsRunning = true;
_simulationTimer.Start();
_statusLabel.Text = "시뮬레이션 실행 중";
+ Console.WriteLine("시뮬레이션 실행");
UpdateUI();
}
@@ -142,16 +165,17 @@ namespace AGVSimulator.Forms
{
if (!_simulationState.IsRunning)
return;
-
+
_simulationState.IsRunning = false;
_simulationTimer.Stop();
_statusLabel.Text = "시뮬레이션 정지";
+ Console.WriteLine("시뮬레이션 정지");
UpdateUI();
}
private void OnReset_Click(object sender, EventArgs e)
{
-
+
}
private void OnFitToMap_Click(object sender, EventArgs e)
@@ -166,7 +190,7 @@ namespace AGVSimulator.Forms
private void OnAbout_Click(object sender, EventArgs e)
{
- MessageBox.Show("AGV 시뮬레이터 v1.0\n\nENIG AGV 시스템용 시뮬레이터", "정보",
+ MessageBox.Show("AGV 시뮬레이터 v1.0\n\nENIG AGV 시스템용 시뮬레이터", "정보",
MessageBoxButtons.OK, MessageBoxIcon.Information);
}
@@ -177,17 +201,26 @@ namespace AGVSimulator.Forms
MessageBox.Show("먼저 맵을 로드해주세요.", "알림", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
-
+
var agvId = $"AGV{_agvList.Count + 1:D2}";
var startPosition = _mapNodes.First().Position; // 첫 번째 노드에서 시작
-
+
var newAGV = new VirtualAGV(agvId, startPosition);
_agvList.Add(newAGV);
_simulatorCanvas.AGVList = new List(_agvList.Cast());
-
+
+ // 콘솔 출력
+
+ Program.WriteLine($"[SYSTEM] AGV 추가:");
+ Program.WriteLine($" AGV ID: {agvId}");
+ Program.WriteLine($" 시작 위치: ({startPosition.X}, {startPosition.Y})");
+ Program.WriteLine($" 총 AGV 수: {_agvList.Count}");
+ Program.WriteLine("");
+
+
UpdateAGVComboBox();
UpdateUI();
-
+
_statusLabel.Text = $"{agvId} 추가됨";
}
@@ -195,16 +228,23 @@ namespace AGVSimulator.Forms
{
if (_agvListCombo.SelectedItem == null)
return;
-
+
var selectedAGV = _agvListCombo.SelectedItem as VirtualAGV;
if (selectedAGV != null)
{
_agvList.Remove(selectedAGV);
_simulatorCanvas.AGVList = new List(_agvList.Cast());
-
+
+ // 콘솔 출력
+ Console.WriteLine($"[SYSTEM] AGV 제거:");
+ Console.WriteLine($" AGV ID: {selectedAGV.AgvId}");
+ Console.WriteLine($" 남은 AGV 수: {_agvList.Count}");
+ Console.WriteLine("");
+
+
UpdateAGVComboBox();
UpdateUI();
-
+
_statusLabel.Text = $"{selectedAGV.AgvId} 제거됨";
}
}
@@ -221,27 +261,41 @@ namespace AGVSimulator.Forms
MessageBox.Show("시작 RFID와 목표 RFID를 선택해주세요.", "알림", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
+
+ var startItem = _startNodeCombo.SelectedItem as ComboBoxItem;
+ var targetItem = _targetNodeCombo.SelectedItem as ComboBoxItem;
+ var startNode = startItem?.Value;
+ var targetNode = targetItem?.Value;
- var startNode = _startNodeCombo.SelectedItem as MapNode;
- var targetNode = _targetNodeCombo.SelectedItem as MapNode;
-
+ if (startNode == null || targetNode == null)
+ {
+ MessageBox.Show("선택한 노드 정보가 올바르지 않습니다.", "오류", MessageBoxButtons.OK, MessageBoxIcon.Error);
+ return;
+ }
+
if (_pathCalculator == null)
{
_pathCalculator = new PathCalculator();
_pathCalculator.SetMapData(_mapNodes);
}
-
+
var agvResult = _pathCalculator.FindAGVPath(startNode.NodeId, targetNode.NodeId);
-
+
if (agvResult.Success)
{
_simulatorCanvas.CurrentPath = agvResult.ToPathResult();
_pathLengthLabel.Text = $"경로 길이: {agvResult.TotalDistance:F1}";
_statusLabel.Text = $"경로 계산 완료 ({agvResult.CalculationTimeMs}ms)";
+
+ // 경로 디버깅 정보 표시
+ UpdatePathDebugInfo(agvResult);
}
else
{
- MessageBox.Show($"경로를 찾을 수 없습니다:\n{agvResult.ErrorMessage}", "경로 계산 실패",
+ // 경로 실패시 디버깅 정보 초기화
+ _pathDebugLabel.Text = $"경로: 실패 - {agvResult.ErrorMessage}";
+
+ MessageBox.Show($"경로를 찾을 수 없습니다:\n{agvResult.ErrorMessage}", "경로 계산 실패",
MessageBoxButtons.OK, MessageBoxIcon.Warning);
}
}
@@ -254,13 +308,13 @@ namespace AGVSimulator.Forms
MessageBox.Show("AGV를 선택해주세요.", "알림", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
-
+
if (_simulatorCanvas.CurrentPath == null || !_simulatorCanvas.CurrentPath.Success)
{
MessageBox.Show("먼저 경로를 계산해주세요.", "알림", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
-
+
selectedAGV.StartPath(_simulatorCanvas.CurrentPath, _mapNodes);
_statusLabel.Text = $"{selectedAGV.AgvId} 경로 시작";
}
@@ -275,6 +329,7 @@ namespace AGVSimulator.Forms
private void OnSetPosition_Click(object sender, EventArgs e)
{
SetAGVPositionByRfid();
+ _simulatorCanvas.FitToNodes();
}
private void OnRfidTextBox_KeyPress(object sender, KeyPressEventArgs e)
@@ -282,6 +337,7 @@ namespace AGVSimulator.Forms
if (e.KeyChar == (char)Keys.Enter)
{
SetAGVPositionByRfid();
+ _simulatorCanvas.FitToNodes();
e.Handled = true;
}
}
@@ -318,17 +374,53 @@ namespace AGVSimulator.Forms
var targetNode = _mapNodes?.FirstOrDefault(n => n.RfidId.Equals(rfidId, StringComparison.OrdinalIgnoreCase));
if (targetNode == null)
{
- MessageBox.Show($"RFID '{rfidId}'에 해당하는 노드를 찾을 수 없습니다.\n\n사용 가능한 RFID 목록:\n{GetAvailableRfidList()}",
+ MessageBox.Show($"RFID '{rfidId}'에 해당하는 노드를 찾을 수 없습니다.\n\n사용 가능한 RFID 목록:\n{GetAvailableRfidList()}",
"RFID 찾기 실패", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
- // AGV 위치 설정
+ // 선택된 방향 확인
+ var selectedDirectionItem = _directionCombo.SelectedItem as DirectionItem;
+ var selectedDirection = selectedDirectionItem?.Direction ?? AgvDirection.Forward;
+
+ // 콘솔 출력 (상세한 리프트 방향 계산 과정)
+
+ Program.WriteLine($"[AGV-{selectedAGV.AgvId}] 위치 설정:");
+ Program.WriteLine($" RFID: {rfidId} → 노드: {targetNode.NodeId}");
+ Program.WriteLine($" 새로운 위치: ({targetNode.Position.X}, {targetNode.Position.Y})");
+ Program.WriteLine($" 모터 방향: {selectedDirectionItem?.DisplayText ?? "전진"} ({selectedDirection})");
+
+ // SetPosition 호출 전 상태
+ var oldTargetPos = selectedAGV.TargetPosition;
+ var oldCurrentPos = selectedAGV.CurrentPosition;
+ Program.WriteLine($" [BEFORE] 현재 CurrentPosition: ({oldCurrentPos.X}, {oldCurrentPos.Y})");
+ Program.WriteLine($" [BEFORE] 이전 TargetPosition: {(oldTargetPos.HasValue ? $"({oldTargetPos.Value.X}, {oldTargetPos.Value.Y})" : "None")}");
+
+
+ // AGV 위치 및 방향 설정
_simulatorCanvas.SetAGVPosition(selectedAGV.AgvId, targetNode.Position);
-
- _statusLabel.Text = $"{selectedAGV.AgvId} 위치를 RFID '{rfidId}' (노드: {targetNode.NodeId})로 설정했습니다.";
+ _simulatorCanvas.UpdateAGVDirection(selectedAGV.AgvId, selectedDirection);
+
+ // VirtualAGV 객체의 위치와 방향 업데이트
+ selectedAGV.SetPosition(targetNode.Position); // 이전 위치 기억하도록
+ selectedAGV.SetDirection(selectedDirection);
+
+ // SetPosition 호출 후 상태 확인 및 리프트 계산
+
+ var newTargetPos = selectedAGV.TargetPosition;
+ var newCurrentPos = selectedAGV.CurrentPosition;
+ Program.WriteLine($" [AFTER] 새로운 CurrentPosition: ({newCurrentPos.X}, {newCurrentPos.Y})");
+ Program.WriteLine($" [AFTER] 새로운 TargetPosition: {(newTargetPos.HasValue ? $"({newTargetPos.Value.X}, {newTargetPos.Value.Y})" : "None")}");
+
+ // 리프트 방향 계산 과정 상세 출력
+ Program.WriteLine($" [LIFT] 리프트 방향 계산:");
+ CalculateLiftDirectionDetailed(selectedAGV);
+ Program.WriteLine("");
+
+
+ _statusLabel.Text = $"{selectedAGV.AgvId} 위치를 RFID '{rfidId}' (노드: {targetNode.NodeId}), 방향: {selectedDirectionItem?.DisplayText ?? "전진"}로 설정했습니다.";
_rfidTextBox.Text = ""; // 입력 필드 초기화
-
+
// 시뮬레이터 캔버스의 해당 노드로 이동
_simulatorCanvas.PanToNode(targetNode.NodeId);
}
@@ -345,10 +437,10 @@ namespace AGVSimulator.Forms
// 처음 10개의 RFID만 표시
var rfidList = nodesWithRfid.Take(10).Select(n => $"- {n.RfidId} → {n.NodeId}");
var result = string.Join("\n", rfidList);
-
+
if (nodesWithRfid.Count > 10)
result += $"\n... 외 {nodesWithRfid.Count - 10}개";
-
+
return result;
}
@@ -357,29 +449,31 @@ namespace AGVSimulator.Forms
try
{
var result = MapLoader.LoadMapFromFile(filePath);
-
+
if (result.Success)
{
+ Console.WriteLine($"Map File Load : {filePath}");
+
_mapNodes = result.Nodes;
_currentMapFilePath = filePath;
-
+
// RFID가 없는 노드들에 자동 할당
MapLoader.AssignAutoRfidIds(_mapNodes);
-
+
// 시뮬레이터 캔버스에 맵 설정
_simulatorCanvas.Nodes = _mapNodes;
-
+
// 설정에 마지막 맵 파일 경로 저장
_config.LastMapFilePath = filePath;
if (_config.AutoSave)
{
_config.Save();
}
-
+
// UI 업데이트
UpdateNodeComboBoxes();
UpdateUI();
-
+
// 맵에 맞춤
_simulatorCanvas.FitToNodes();
}
@@ -398,27 +492,31 @@ namespace AGVSimulator.Forms
{
_startNodeCombo.Items.Clear();
_targetNodeCombo.Items.Clear();
-
+
if (_mapNodes != null)
{
foreach (var node in _mapNodes)
{
if (node.IsActive && node.HasRfid())
{
- _startNodeCombo.Items.Add(node);
- _targetNodeCombo.Items.Add(node);
+ // {rfid} - [{node}] 형식으로 ComboBoxItem 생성
+ var displayText = $"{node.RfidId} - [{node.NodeId}]";
+ var item = new ComboBoxItem(node, displayText);
+
+ _startNodeCombo.Items.Add(item);
+ _targetNodeCombo.Items.Add(item);
}
}
}
-
- _startNodeCombo.DisplayMember = "RfidId";
- _targetNodeCombo.DisplayMember = "RfidId";
+
+ _startNodeCombo.DisplayMember = "DisplayText";
+ _targetNodeCombo.DisplayMember = "DisplayText";
}
private void UpdateAGVComboBox()
{
_agvListCombo.Items.Clear();
-
+
if (_agvList != null)
{
foreach (var agv in _agvList)
@@ -426,9 +524,9 @@ namespace AGVSimulator.Forms
_agvListCombo.Items.Add(agv);
}
}
-
+
_agvListCombo.DisplayMember = "AgvId";
-
+
if (_agvListCombo.Items.Count > 0)
{
_agvListCombo.SelectedIndex = 0;
@@ -439,47 +537,260 @@ namespace AGVSimulator.Forms
{
// 시뮬레이션 상태
_simulationStatusLabel.Text = _simulationState.IsRunning ? "시뮬레이션: 실행 중" : "시뮬레이션: 정지";
-
+
// AGV 수
_agvCountLabel.Text = $"AGV 수: {_agvList?.Count ?? 0}";
-
+
// 버튼 상태
_startSimulationButton.Enabled = !_simulationState.IsRunning && _agvList?.Count > 0;
_stopSimulationButton.Enabled = _simulationState.IsRunning;
-
+
_removeAgvButton.Enabled = _agvListCombo.SelectedItem != null;
- _startPathButton.Enabled = _agvListCombo.SelectedItem != null &&
- _simulatorCanvas.CurrentPath != null &&
+ _startPathButton.Enabled = _agvListCombo.SelectedItem != null &&
+ _simulatorCanvas.CurrentPath != null &&
_simulatorCanvas.CurrentPath.Success;
-
- _calculatePathButton.Enabled = _startNodeCombo.SelectedItem != null &&
+
+ _calculatePathButton.Enabled = _startNodeCombo.SelectedItem != null &&
_targetNodeCombo.SelectedItem != null;
-
+
// RFID 위치 설정 관련
var hasSelectedAGV = _agvListCombo.SelectedItem != null;
var hasRfidNodes = _mapNodes != null && _mapNodes.Any(n => n.HasRfid());
-
+
_setPositionButton.Enabled = hasSelectedAGV && hasRfidNodes;
_rfidTextBox.Enabled = hasSelectedAGV && hasRfidNodes;
-
+
+ // AGV 정보 패널 업데이트
+ UpdateAGVInfoPanel();
+
// 맵 다시열기 버튼
var hasCurrentMap = !string.IsNullOrEmpty(_currentMapFilePath);
reloadMapToolStripMenuItem.Enabled = hasCurrentMap;
reloadMapToolStripButton.Enabled = hasCurrentMap;
}
+ ///
+ /// AGV 정보 패널 업데이트
+ ///
+ private void UpdateAGVInfoPanel()
+ {
+ var selectedAGV = _agvListCombo.SelectedItem as VirtualAGV;
+
+ if (selectedAGV == null)
+ {
+ _liftDirectionLabel.Text = "리프트 방향: -";
+ _motorDirectionLabel.Text = "모터 방향: -";
+ _agvInfoTitleLabel.Text = "AGV 상태 정보: (AGV 선택 안됨)";
+ return;
+ }
+
+ // AGV 선택됨
+ _agvInfoTitleLabel.Text = $"AGV 상태 정보: {selectedAGV.AgvId}";
+
+ // 리프트 방향 계산
+ var liftDirection = CalculateLiftDirection(selectedAGV);
+ _liftDirectionLabel.Text = $"리프트 방향: {liftDirection}";
+
+ // 모터 방향
+ var motorDirection = GetMotorDirectionString(selectedAGV.CurrentDirection);
+ _motorDirectionLabel.Text = $"모터 방향: {motorDirection}";
+ }
+
+ ///
+ /// AGV의 리프트 방향 계산 (상세 출력 버전)
+ ///
+ private void CalculateLiftDirectionDetailed(VirtualAGV agv)
+ {
+ var currentPos = agv.CurrentPosition;
+ var targetPos = agv.TargetPosition;
+ var dockingDirection = agv.DockingDirection;
+
+ Program.WriteLine($" 입력값: CurrentPos=({currentPos.X}, {currentPos.Y})");
+ Program.WriteLine($" 입력값: TargetPos={(!targetPos.HasValue ? "None" : $"({targetPos.Value.X}, {targetPos.Value.Y})")}");
+ Program.WriteLine($" 입력값: DockingDirection={dockingDirection}");
+
+ if (!targetPos.HasValue || targetPos.Value == currentPos)
+ {
+ Program.WriteLine($" 결과: 방향을 알 수 없음 (TargetPos 없음 또는 같은 위치)");
+ return;
+ }
+
+ // 이동 방향 계산 (이전 → 현재 = TargetPos → CurrentPos)
+ var dx = currentPos.X - targetPos.Value.X;
+ var dy = currentPos.Y - targetPos.Value.Y;
+ Program.WriteLine($" 이동 벡터: dx={dx}, dy={dy}");
+
+ if (Math.Abs(dx) < 1 && Math.Abs(dy) < 1)
+ {
+ Program.WriteLine($" 결과: 정지 상태 (이동거리 < 1픽셀)");
+ return;
+ }
+
+ // 경로 예측 기반 LiftCalculator를 사용하여 리프트 방향 계산
+ var liftInfo = AGVNavigationCore.Utils.LiftCalculator.CalculateLiftInfoWithPathPrediction(
+ currentPos, targetPos.Value, agv.CurrentDirection, _mapNodes);
+
+ // 이동 각도 계산 (표시용)
+ var moveAngleRad = Math.Atan2(dy, dx);
+ var moveAngleDeg = moveAngleRad * 180.0 / Math.PI;
+ while (moveAngleDeg < 0) moveAngleDeg += 360;
+ while (moveAngleDeg >= 360) moveAngleDeg -= 360;
+
+ Program.WriteLine($" 이동 각도: {moveAngleDeg:F1}도 (라디안: {moveAngleRad:F3})");
+ Program.WriteLine($" 모터 방향: {GetMotorDirectionString(liftInfo.MotorDirection)}");
+ Program.WriteLine($" 리프트 각도: {liftInfo.AngleDegrees:F1}도 ({liftInfo.CalculationMethod})");
+
+ // 도킹 방향 정보 추가
+ string dockingInfo = dockingDirection == DockingDirection.Forward ? "전진도킹" : "후진도킹";
+
+ Program.WriteLine($" 최종 결과: {liftInfo.DirectionString} ({liftInfo.AngleDegrees:F0}°) [{dockingInfo}]");
+ }
+
+ ///
+ /// AGV의 리프트 방향 계산 (간단한 버전)
+ ///
+ private string CalculateLiftDirection(VirtualAGV agv)
+ {
+ var currentPos = agv.CurrentPosition;
+ var targetPos = agv.TargetPosition;
+ var dockingDirection = agv.DockingDirection;
+
+ if (!targetPos.HasValue || targetPos.Value == currentPos)
+ {
+ // 방향을 알 수 없는 경우
+ return "알 수 없음 (?)";
+ }
+
+ // 이동 방향 계산 (현재 → 타겟, 실제로는 이전 → 현재)
+ var dx = currentPos.X - targetPos.Value.X;
+ var dy = currentPos.Y - targetPos.Value.Y;
+
+ if (Math.Abs(dx) < 1 && Math.Abs(dy) < 1)
+ {
+ return "정지 상태";
+ }
+
+ // 경로 예측 기반 LiftCalculator를 사용하여 리프트 방향 계산
+ var liftInfo = AGVNavigationCore.Utils.LiftCalculator.CalculateLiftInfoWithPathPrediction(
+ currentPos, targetPos.Value, agv.CurrentDirection, _mapNodes);
+
+ // 도킹 방향 정보 추가
+ string dockingInfo = dockingDirection == DockingDirection.Forward ? "전진도킹" : "후진도킹";
+
+ return $"{liftInfo.DirectionString} ({liftInfo.AngleDegrees:F0}°) [{dockingInfo}]";
+ }
+
+ ///
+ /// 모터 방향을 문자열로 변환
+ ///
+ private string GetMotorDirectionString(AgvDirection direction)
+ {
+ switch (direction)
+ {
+ case AgvDirection.Forward:
+ return "전진 (F)";
+ case AgvDirection.Backward:
+ return "후진 (B)";
+ case AgvDirection.Left:
+ return "좌회전 (L)";
+ case AgvDirection.Right:
+ return "우회전 (R)";
+ default:
+ return "알 수 없음";
+ }
+ }
+
+ ///
+ /// 노드 ID를 RFID 값으로 변환 (NodeResolver 사용)
+ ///
+ private string GetRfidByNodeId(string nodeId)
+ {
+ var node = _mapNodes?.FirstOrDefault(n => n.NodeId == nodeId);
+ return node?.HasRfid() == true ? node.RfidId : nodeId;
+ }
+
+ ///
+ /// 경로 디버깅 정보 업데이트 (RFID 값 표시, 모터방향 정보 포함)
+ ///
+ private void UpdatePathDebugInfo(AGVPathResult agvResult)
+ {
+ if (agvResult == null || !agvResult.Success)
+ {
+ _pathDebugLabel.Text = "경로: 설정되지 않음";
+ return;
+ }
+
+ // 노드 ID를 RFID로 변환한 경로 생성
+ var pathWithRfid = agvResult.Path.Select(nodeId => GetRfidByNodeId(nodeId)).ToList();
+
+ // 콘솔 디버그 정보 출력 (RFID 기준)
+ Program.WriteLine($"[DEBUG] 경로 계산 완료:");
+ Program.WriteLine($" 전체 경로 (RFID): [{string.Join(" → ", pathWithRfid)}]");
+ Program.WriteLine($" 전체 경로 (NodeID): [{string.Join(" → ", agvResult.Path)}]");
+ Program.WriteLine($" 경로 노드 수: {agvResult.Path.Count}");
+ if (agvResult.NodeMotorInfos != null)
+ {
+ Program.WriteLine($" 모터정보 수: {agvResult.NodeMotorInfos.Count}");
+ for (int i = 0; i < agvResult.NodeMotorInfos.Count; i++)
+ {
+ var info = agvResult.NodeMotorInfos[i];
+ var rfidId = GetRfidByNodeId(info.NodeId);
+ var nextRfidId = info.NextNodeId != null ? GetRfidByNodeId(info.NextNodeId) : "END";
+ Program.WriteLine($" {i}: {rfidId}({info.NodeId}) → {info.MotorDirection} → {nextRfidId}");
+ }
+ }
+
+ // 모터방향 정보가 있다면 이를 포함하여 경로 문자열 구성 (RFID 기준)
+ string pathString;
+ if (agvResult.NodeMotorInfos != null && agvResult.NodeMotorInfos.Count > 0)
+ {
+ // RFID별 모터방향 정보를 포함한 경로 문자열 생성
+ var pathWithMotorInfo = new List();
+ for (int i = 0; i < agvResult.NodeMotorInfos.Count; i++)
+ {
+ var motorInfo = agvResult.NodeMotorInfos[i];
+ var rfidId = GetRfidByNodeId(motorInfo.NodeId);
+ string motorSymbol = motorInfo.MotorDirection == AgvDirection.Forward ? "[전진]" : "[후진]";
+ pathWithMotorInfo.Add($"{rfidId}{motorSymbol}");
+ }
+ pathString = string.Join(" → ", pathWithMotorInfo);
+ }
+ else
+ {
+ // 기본 경로 정보만 표시 (RFID 기준)
+ pathString = string.Join(" → ", pathWithRfid);
+ }
+
+ // UI에 표시 (길이 제한)
+ if (pathString.Length > 120)
+ {
+ pathString = pathString.Substring(0, 117) + "...";
+ }
+
+ // 모터방향 통계 추가
+ string motorStats = "";
+ if (agvResult.NodeMotorInfos != null && agvResult.NodeMotorInfos.Count > 0)
+ {
+ var forwardCount = agvResult.NodeMotorInfos.Count(m => m.MotorDirection == AgvDirection.Forward);
+ var backwardCount = agvResult.NodeMotorInfos.Count(m => m.MotorDirection == AgvDirection.Backward);
+ motorStats = $", 전진: {forwardCount}, 후진: {backwardCount}";
+ }
+
+ _pathDebugLabel.Text = $"경로: {pathString} (총 {agvResult.Path.Count}개 노드, {agvResult.TotalDistance:F1}px{motorStats})";
+ }
+
private void OnReloadMap_Click(object sender, EventArgs e)
{
if (string.IsNullOrEmpty(_currentMapFilePath))
{
- MessageBox.Show("다시 로드할 맵 파일이 없습니다. 먼저 맵을 열어주세요.", "알림",
+ MessageBox.Show("다시 로드할 맵 파일이 없습니다. 먼저 맵을 열어주세요.", "알림",
MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
if (!File.Exists(_currentMapFilePath))
{
- MessageBox.Show($"맵 파일을 찾을 수 없습니다:\n{_currentMapFilePath}", "오류",
+ MessageBox.Show($"맵 파일을 찾을 수 없습니다:\n{_currentMapFilePath}", "오류",
MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
@@ -491,7 +802,7 @@ namespace AGVSimulator.Forms
}
catch (Exception ex)
{
- MessageBox.Show($"맵 파일을 다시 로드할 수 없습니다:\n{ex.Message}", "오류",
+ MessageBox.Show($"맵 파일을 다시 로드할 수 없습니다:\n{ex.Message}", "오류",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
@@ -515,7 +826,7 @@ namespace AGVSimulator.Forms
if (openDialog.ShowDialog() == DialogResult.OK)
{
mapEditorPath = openDialog.FileName;
-
+
// 설정에 저장
_config.MapEditorExecutablePath = mapEditorPath;
if (_config.AutoSave)
@@ -548,7 +859,7 @@ namespace AGVSimulator.Forms
}
catch (Exception ex)
{
- MessageBox.Show($"MapEditor를 실행할 수 없습니다:\n{ex.Message}", "오류",
+ MessageBox.Show($"MapEditor를 실행할 수 없습니다:\n{ex.Message}", "오류",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
@@ -578,4 +889,45 @@ namespace AGVSimulator.Forms
_statusLabel.Text = "초기화 완료";
}
}
+
+ ///
+ /// 방향 콤보박스용 아이템 클래스
+ ///
+ public class DirectionItem
+ {
+ public AgvDirection Direction { get; }
+ public string DisplayText { get; }
+
+ public DirectionItem(AgvDirection direction, string displayText)
+ {
+ Direction = direction;
+ DisplayText = displayText;
+ }
+
+ public override string ToString()
+ {
+ return DisplayText;
+ }
+ }
+
+ ///
+ /// 제네릭 콤보박스 아이템 클래스
+ ///
+ /// 값의 타입
+ public class ComboBoxItem
+ {
+ public T Value { get; }
+ public string DisplayText { get; }
+
+ public ComboBoxItem(T value, string displayText)
+ {
+ Value = value;
+ DisplayText = displayText;
+ }
+
+ public override string ToString()
+ {
+ return DisplayText;
+ }
+ }
}
\ No newline at end of file
diff --git a/Cs_HMI/AGVSimulator/Models/VirtualAGV.cs b/Cs_HMI/AGVSimulator/Models/VirtualAGV.cs
index 1648b09..64318b7 100644
--- a/Cs_HMI/AGVSimulator/Models/VirtualAGV.cs
+++ b/Cs_HMI/AGVSimulator/Models/VirtualAGV.cs
@@ -59,6 +59,7 @@ namespace AGVSimulator.Models
private List _remainingNodes;
private int _currentNodeIndex;
private string _currentNodeId;
+ private string _targetNodeId;
// 이동 관련
private System.Windows.Forms.Timer _moveTimer;
@@ -67,6 +68,9 @@ namespace AGVSimulator.Models
private Point _moveTargetPosition;
private float _moveProgress;
+ // 도킹 관련
+ private DockingDirection _dockingDirection;
+
// 시뮬레이션 설정
private readonly float _moveSpeed = 50.0f; // 픽셀/초
private readonly float _rotationSpeed = 90.0f; // 도/초
@@ -114,13 +118,23 @@ namespace AGVSimulator.Models
///
/// 목표 위치
///
- public Point TargetPosition => _targetPosition;
+ public Point? TargetPosition => _targetPosition;
///
/// 배터리 레벨 (시뮬레이션)
///
public float BatteryLevel { get; set; } = 100.0f;
+ ///
+ /// 목표 노드 ID
+ ///
+ public string TargetNodeId => _targetNodeId;
+
+ ///
+ /// 도킹 방향
+ ///
+ public DockingDirection DockingDirection => _dockingDirection;
+
#endregion
#region Constructor
@@ -138,6 +152,9 @@ namespace AGVSimulator.Models
_currentDirection = startDirection;
_currentState = AGVState.Idle;
_currentSpeed = 0;
+ _dockingDirection = DockingDirection.Forward; // 기본값: 전진 도킹
+ _currentNodeId = string.Empty;
+ _targetNodeId = string.Empty;
InitializeTimer();
}
@@ -175,13 +192,27 @@ namespace AGVSimulator.Models
_remainingNodes = new List(path.Path);
_currentNodeIndex = 0;
- // 시작 노드 위치로 이동
+ // 시작 노드와 목표 노드 설정
if (_remainingNodes.Count > 0)
{
var startNode = mapNodes.FirstOrDefault(n => n.NodeId == _remainingNodes[0]);
if (startNode != null)
{
_currentNodeId = startNode.NodeId;
+
+ // 목표 노드 설정 (경로의 마지막 노드)
+ if (_remainingNodes.Count > 1)
+ {
+ _targetNodeId = _remainingNodes[_remainingNodes.Count - 1];
+ var targetNode = mapNodes.FirstOrDefault(n => n.NodeId == _targetNodeId);
+
+ // 목표 노드의 타입에 따라 도킹 방향 결정
+ if (targetNode != null)
+ {
+ _dockingDirection = GetDockingDirection(targetNode.Type);
+ }
+ }
+
StartMovement();
}
else
@@ -245,6 +276,35 @@ namespace AGVSimulator.Models
SetState(AGVState.Idle);
}
+ ///
+ /// AGV 방향 직접 설정 (시뮬레이터용)
+ ///
+ /// 설정할 방향
+ public void SetDirection(AgvDirection direction)
+ {
+ _currentDirection = direction;
+ }
+
+ ///
+ /// AGV 위치 직접 설정 (시뮬레이터용)
+ /// 이전 위치를 TargetPosition으로 저장하여 리프트 방향 계산이 가능하도록 함
+ ///
+ /// 새로운 위치
+ public void SetPosition(Point newPosition)
+ {
+ // 현재 위치를 이전 위치로 저장 (리프트 방향 계산용)
+ if (_currentPosition != Point.Empty)
+ {
+ _targetPosition = _currentPosition;
+ }
+
+ // 새로운 위치 설정
+ _currentPosition = newPosition;
+
+ // 위치 변경 이벤트 발생
+ PositionChanged?.Invoke(this, _currentPosition);
+ }
+
///
/// 충전 시작 (시뮬레이션)
///
@@ -449,6 +509,19 @@ namespace AGVSimulator.Models
}
}
+ private DockingDirection GetDockingDirection(NodeType nodeType)
+ {
+ switch (nodeType)
+ {
+ case NodeType.Charging:
+ return DockingDirection.Forward; // 충전기: 전진 도킹
+ case NodeType.Docking:
+ return DockingDirection.Backward; // 장비 (로더, 클리너, 오프로더, 버퍼): 후진 도킹
+ default:
+ return DockingDirection.Forward; // 기본값: 전진
+ }
+ }
+
private void OnError(string message)
{
SetState(AGVState.Error);