..
This commit is contained in:
@@ -374,11 +374,26 @@ namespace AGVNavigationCore.Controls
|
||||
foreach (var targetNode in node.ConnectedMapNodes)
|
||||
{
|
||||
if (targetNode == null) continue;
|
||||
|
||||
// 강조된 연결은 나중에 그리기 위해 건너뜀
|
||||
if (IsConnectionHighlighted(node.Id, targetNode.Id)) continue;
|
||||
|
||||
DrawConnection(g, node, targetNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 1.1 강조된 연결 그리기 (항상 위에 표시되도록)
|
||||
if (_highlightedConnection.HasValue)
|
||||
{
|
||||
var n1 = _nodes.FirstOrDefault(n => n.Id == _highlightedConnection.Value.FromNodeId);
|
||||
var n2 = _nodes.FirstOrDefault(n => n.Id == _highlightedConnection.Value.ToNodeId);
|
||||
if (n1 != null && n2 != null)
|
||||
{
|
||||
DrawConnection(g, n1, n2);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 마그넷 그리기 (별도 리스트 사용)
|
||||
if (_magnets != null)
|
||||
{
|
||||
@@ -442,10 +457,17 @@ namespace AGVNavigationCore.Controls
|
||||
g.DrawLine(_magnetPen, startPoint, endPoint);
|
||||
}
|
||||
|
||||
// 호버된 마그넷 강조
|
||||
if (magnet == _hoveredNode)
|
||||
// 호버되거나 선택된 마그넷 강조 (선택: Red, 호버: Orange)
|
||||
bool isHovered = (magnet == _hoveredNode);
|
||||
bool isSelected = (magnet == _selectedNode);
|
||||
|
||||
if (isHovered || isSelected)
|
||||
{
|
||||
using (var highlightPen = new Pen(Color.Orange, 19))
|
||||
Color highlightColor = isSelected ? Color.Red : Color.Orange;
|
||||
// 선택된 상태에서 호버되면? -> 선택 색상 우선 (Red) 또는 명확한 구분 필요
|
||||
// 여기서는 선택이 더 중요하므로 Red 유지
|
||||
|
||||
using (var highlightPen = new Pen(highlightColor, 19) { StartCap = LineCap.Round, EndCap = LineCap.Round })
|
||||
{
|
||||
if (magnet.ControlPoint != null)
|
||||
{
|
||||
@@ -470,15 +492,6 @@ namespace AGVNavigationCore.Controls
|
||||
g.DrawLine(highlightPen, startPoint, endPoint);
|
||||
}
|
||||
}
|
||||
// Redraw normal to keep it on top? No, highlight is usually outer.
|
||||
// If I draw highlight AFTER, it covers.
|
||||
// But DrawMagnet is void. If I draw highlight after, it's fine if I want it to glow.
|
||||
// Actually _magnetPen is Width 15, very thick.
|
||||
// If I draw highlight Width 19 *before* normal, it acts as border.
|
||||
// But this method draws normal first.
|
||||
// So I should refactor to calculate path first, then draw?
|
||||
// Or just draw highlight on top with alpha?
|
||||
// Let's draw highlight on top for now, maybe with alpha.
|
||||
}
|
||||
|
||||
// 선택된 마그넷 핸들 그리기
|
||||
@@ -859,7 +872,7 @@ namespace AGVNavigationCore.Controls
|
||||
{
|
||||
case NodeType.Normal:
|
||||
var item = _selectedNode as MapNode;
|
||||
if ( item.StationType == StationType.Charger1 || item.StationType == StationType.Charger2)
|
||||
if ( item.StationType == StationType.Charger)
|
||||
DrawTriangleGhost(g, ghostBrush);
|
||||
else
|
||||
DrawPentagonGhost(g, ghostBrush);
|
||||
@@ -1049,13 +1062,12 @@ namespace AGVNavigationCore.Controls
|
||||
switch (node.StationType)
|
||||
{
|
||||
case StationType.Loader:
|
||||
case StationType.UnLoader:
|
||||
case StationType.Clearner:
|
||||
case StationType.Cleaner:
|
||||
case StationType.Plating:
|
||||
case StationType.Buffer:
|
||||
DrawPentagonNodeShape(g, node, brush);
|
||||
break;
|
||||
case StationType.Charger1:
|
||||
case StationType.Charger2:
|
||||
case StationType.Charger:
|
||||
DrawTriangleNodeShape(g, node, brush);
|
||||
break;
|
||||
case StationType.Limit:
|
||||
@@ -1470,7 +1482,7 @@ namespace AGVNavigationCore.Controls
|
||||
|
||||
// 🔥 노드의 폰트 설정 사용 (0 이하일 경우 기본값 7.0f 사용)
|
||||
var topFont = new Font("Arial", 9, FontStyle.Bold);
|
||||
var btmFont = new Font("Arial", 12, FontStyle.Bold);
|
||||
var btmFont = new Font("Arial", node.NodeTextFontSize, FontStyle.Bold);
|
||||
|
||||
// 메인 텍스트 크기 측정
|
||||
var TopSize = g.MeasureString(TopIDText, topFont);
|
||||
@@ -1496,8 +1508,7 @@ namespace AGVNavigationCore.Controls
|
||||
Color bgColor = Color.White;
|
||||
switch (node.StationType)
|
||||
{
|
||||
case StationType.Charger1:
|
||||
case StationType.Charger2:
|
||||
case StationType.Charger:
|
||||
fgColor = Color.White;
|
||||
bgColor = Color.Tomato;
|
||||
break;
|
||||
@@ -1505,12 +1516,12 @@ namespace AGVNavigationCore.Controls
|
||||
fgColor = Color.Black;
|
||||
bgColor = Color.White;
|
||||
break;
|
||||
case StationType.Clearner:
|
||||
case StationType.Plating:
|
||||
fgColor = Color.Black;
|
||||
bgColor = Color.DeepSkyBlue;
|
||||
break;
|
||||
case StationType.Loader:
|
||||
case StationType.UnLoader:
|
||||
case StationType.Cleaner:
|
||||
fgColor = Color.Black;
|
||||
bgColor = Color.Gold;
|
||||
break;
|
||||
@@ -1519,6 +1530,8 @@ namespace AGVNavigationCore.Controls
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
var rectpaddingx = 4;
|
||||
@@ -1541,7 +1554,8 @@ namespace AGVNavigationCore.Controls
|
||||
}
|
||||
|
||||
|
||||
using (var descBrush = new SolidBrush(fgColor))
|
||||
|
||||
using (var descBrush = new SolidBrush(node.NodeTextForeColor))
|
||||
{
|
||||
g.DrawString(BottomLabelText, btmFont, descBrush, roundRect, new StringFormat
|
||||
{
|
||||
@@ -1793,12 +1807,16 @@ namespace AGVNavigationCore.Controls
|
||||
|
||||
switch (node.StationType)
|
||||
{
|
||||
case StationType.Normal: bgColor = Color.DeepSkyBlue; break;
|
||||
case StationType.Charger1: bgColor = Color.Tomato; break;
|
||||
case StationType.Charger2: bgColor = Color.Tomato; break;
|
||||
case StationType.Normal:
|
||||
if(node.CanTurnLeft || node.CanTurnRight)
|
||||
bgColor = Color.Violet;
|
||||
else
|
||||
bgColor = Color.DeepSkyBlue;
|
||||
break;
|
||||
case StationType.Charger: bgColor = Color.Tomato; break;
|
||||
case StationType.Loader:
|
||||
case StationType.UnLoader: bgColor = Color.Gold; break;
|
||||
case StationType.Clearner: bgColor = Color.DeepSkyBlue; break;
|
||||
case StationType.Cleaner: bgColor = Color.Gold; break;
|
||||
case StationType.Plating: bgColor = Color.DeepSkyBlue; break;
|
||||
case StationType.Buffer: bgColor = Color.WhiteSmoke; break;
|
||||
case StationType.Limit: bgColor = Color.Red; break;
|
||||
default: bgColor = Color.White; break;
|
||||
|
||||
@@ -104,6 +104,10 @@ namespace AGVNavigationCore.Controls
|
||||
HandleConnectClick(hitNode as MapNode);
|
||||
break;
|
||||
|
||||
case EditMode.ConnectDirection:
|
||||
HandleConnectDirectionClick(hitNode as MapNode);
|
||||
break;
|
||||
|
||||
case EditMode.Delete:
|
||||
HandleDeleteClick(hitNode);
|
||||
break;
|
||||
@@ -227,6 +231,23 @@ namespace AGVNavigationCore.Controls
|
||||
if (handleIdx != -1)
|
||||
{
|
||||
_dragHandleIndex = handleIdx;
|
||||
|
||||
// 핸들 드래그 시 초기 오프셋 설정 (점프 현상 방지)
|
||||
if (_selectedNode is MapMagnet magnet)
|
||||
{
|
||||
Point handlePos = Point.Empty;
|
||||
if (handleIdx == 0) handlePos = magnet.StartPoint;
|
||||
else if (handleIdx == 1) handlePos = magnet.EndPoint;
|
||||
else if (handleIdx == 2 && magnet.ControlPoint != null)
|
||||
handlePos = new Point((int)magnet.ControlPoint.X, (int)magnet.ControlPoint.Y);
|
||||
|
||||
_dragOffset = new Point(worldPoint.X - handlePos.X, worldPoint.Y - handlePos.Y);
|
||||
}
|
||||
else
|
||||
{
|
||||
_dragOffset = Point.Empty; // Mark 등은 오프셋 없이 마우스 포인터 기준 계산
|
||||
}
|
||||
|
||||
_isDragging = true;
|
||||
_isPanning = false;
|
||||
Capture = true;
|
||||
@@ -562,12 +583,11 @@ namespace AGVNavigationCore.Controls
|
||||
switch (node.StationType)
|
||||
{
|
||||
case StationType.Loader:
|
||||
case StationType.UnLoader:
|
||||
case StationType.Clearner:
|
||||
case StationType.Cleaner:
|
||||
case StationType.Plating:
|
||||
case StationType.Buffer:
|
||||
return IsPointInPentagon(point, node);
|
||||
case StationType.Charger2:
|
||||
case StationType.Charger1:
|
||||
case StationType.Charger:
|
||||
return IsPointInTriangle(point, node);
|
||||
default:
|
||||
return IsPointInCircle(point, node);
|
||||
@@ -887,7 +907,9 @@ namespace AGVNavigationCore.Controls
|
||||
var newNode = new MapNode
|
||||
{
|
||||
Id = newNodeId,
|
||||
Position = worldPoint
|
||||
Position = worldPoint,
|
||||
CanTurnLeft=false,
|
||||
CanTurnRight= false,
|
||||
};
|
||||
|
||||
_nodes.Add(newNode);
|
||||
@@ -998,6 +1020,31 @@ namespace AGVNavigationCore.Controls
|
||||
Invalidate();
|
||||
}
|
||||
|
||||
private void HandleConnectDirectionClick(MapNode hitNode)
|
||||
{
|
||||
if (hitNode == null) return;
|
||||
|
||||
if (!_isConnectionMode)
|
||||
{
|
||||
// 연결 시작 (방향 설정)
|
||||
_isConnectionMode = true;
|
||||
_connectionStartNode = hitNode;
|
||||
_selectedNode = hitNode;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 연결 완료
|
||||
if (_connectionStartNode != null && _connectionStartNode != hitNode)
|
||||
{
|
||||
// 기본값 S (Straight)로 방향 설정
|
||||
SetMagnetDirection(_connectionStartNode, hitNode, MagnetPosition.S);
|
||||
}
|
||||
CancelConnection();
|
||||
}
|
||||
|
||||
Invalidate();
|
||||
}
|
||||
|
||||
private void HandleDeleteClick(MapNode hitNode)
|
||||
{
|
||||
if (hitNode == null) return;
|
||||
@@ -1025,10 +1072,46 @@ namespace AGVNavigationCore.Controls
|
||||
toNode.ConnectedNodes.Contains(fromNode.Id))
|
||||
return;
|
||||
|
||||
// 양방향 연결 생성 (AGV가 양쪽 방향으로 이동 가능하도록)
|
||||
// 양방향 연결 생성 (AGV가 양쪽 방향으로 이동 가능하도록)
|
||||
fromNode.AddConnection(toNode.Id);
|
||||
toNode.AddConnection(fromNode.Id);
|
||||
|
||||
// 🔥 화면 표시용 ConnectedMapNodes 리스트도 즉시 갱신해야 함
|
||||
if (!fromNode.ConnectedMapNodes.Contains(toNode))
|
||||
fromNode.ConnectedMapNodes.Add(toNode);
|
||||
|
||||
if (!toNode.ConnectedMapNodes.Contains(fromNode))
|
||||
toNode.ConnectedMapNodes.Add(fromNode);
|
||||
|
||||
ConnectionCreated?.Invoke(this, (fromNode, toNode));
|
||||
MapChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
public void SetMagnetDirection(MapNode fromNode, MapNode toNode, MagnetPosition direction)
|
||||
{
|
||||
if (fromNode == null || toNode == null) return;
|
||||
|
||||
// 이미 연결된 노드인지 확인 (연결되어 있어야 방향 설정 가능)
|
||||
// 이미 연결된 노드인지 확인 (연결되어 있어야 방향 설정 가능)
|
||||
if (!fromNode.ConnectedNodes.Contains(toNode.Id))
|
||||
{
|
||||
// 연결되어 있지 않으면 자동 연결
|
||||
CreateConnection(fromNode, toNode);
|
||||
}
|
||||
|
||||
if (fromNode.MagnetDirections == null)
|
||||
fromNode.MagnetDirections = new Dictionary<string, MagnetPosition>();
|
||||
|
||||
if (fromNode.MagnetDirections.ContainsKey(toNode.Id))
|
||||
{
|
||||
fromNode.MagnetDirections[toNode.Id] = direction;
|
||||
}
|
||||
else
|
||||
{
|
||||
fromNode.MagnetDirections.Add(toNode.Id, direction);
|
||||
}
|
||||
|
||||
MapChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ namespace AGVNavigationCore.Controls
|
||||
{
|
||||
#region Constants
|
||||
|
||||
private const int NODE_SIZE = 24;
|
||||
private const int NODE_SIZE = 18;
|
||||
private const int NODE_RADIUS = NODE_SIZE / 2;
|
||||
private const int GRID_SIZE = 20;
|
||||
private const float CONNECTION_WIDTH = 1.0f;
|
||||
@@ -56,6 +56,7 @@ namespace AGVNavigationCore.Controls
|
||||
DeleteConnection, // 연결 삭제 모드
|
||||
AddLabel, // 라벨 추가 모드
|
||||
AddImage, // 이미지 추가 모드
|
||||
ConnectDirection, // 방향 연결 모드
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -139,7 +140,7 @@ namespace AGVNavigationCore.Controls
|
||||
|
||||
string _alertmesage = "";
|
||||
bool showalert = false;
|
||||
|
||||
|
||||
// 깜박임 효과를 위한 타이머 및 상태
|
||||
private Timer _alertBlinkTimer;
|
||||
private bool _isAlertBlinkOn = true;
|
||||
@@ -176,7 +177,7 @@ namespace AGVNavigationCore.Controls
|
||||
//_isAlertBlinkOn = !_isAlertBlinkOn;
|
||||
//Invalidate();
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 브러쉬 및 펜
|
||||
private Brush _normalNodeBrush;
|
||||
@@ -220,6 +221,7 @@ namespace AGVNavigationCore.Controls
|
||||
public event EventHandler<List<NodeBase>> NodesSelected; // 다중 선택 이벤트
|
||||
public event EventHandler<NodeBase> NodeDeleted;
|
||||
public event EventHandler<NodeBase> NodeMoved;
|
||||
public event EventHandler<(MapNode From, MapNode To)> ConnectionCreated; // 연결 생성 이벤트 추가
|
||||
public event EventHandler<(MapNode From, MapNode To)> ConnectionDeleted;
|
||||
public event EventHandler<MapImage> ImageDoubleClicked;
|
||||
public event EventHandler<MapLabel> LabelDoubleClicked;
|
||||
@@ -266,6 +268,18 @@ namespace AGVNavigationCore.Controls
|
||||
{
|
||||
if (_nodes != null && _nodes.Contains(node))
|
||||
{
|
||||
// 🔥 삭제되는 노드와 연결된 다른 노드들의 연결 정보도 삭제
|
||||
foreach (var otherNode in _nodes.ToList()) // ToList()로 복사본 순회 (안전장치)
|
||||
{
|
||||
if (otherNode == node) continue;
|
||||
|
||||
// 다른 노드 -> 삭제되는 노드 연결 제거
|
||||
if (otherNode.ConnectedNodes.Contains(node.Id))
|
||||
{
|
||||
otherNode.RemoveConnection(node.Id);
|
||||
}
|
||||
}
|
||||
|
||||
_nodes.Remove(node);
|
||||
Invalidate();
|
||||
}
|
||||
@@ -336,10 +350,10 @@ namespace AGVNavigationCore.Controls
|
||||
{
|
||||
// Zoom 값 범위 제한
|
||||
float newZoom = Math.Max(0.1f, Math.Min(5.0f, zoom));
|
||||
|
||||
|
||||
_panOffset = new PointF(panX, panY);
|
||||
_zoomFactor = newZoom;
|
||||
|
||||
|
||||
Invalidate();
|
||||
}
|
||||
|
||||
@@ -408,12 +422,12 @@ namespace AGVNavigationCore.Controls
|
||||
/// <summary>
|
||||
/// 선택된 노드 (단일)
|
||||
/// </summary>
|
||||
public MapNode SelectedNode
|
||||
public NodeBase SelectedNode
|
||||
{
|
||||
get { return this._selectedNode as MapNode; }
|
||||
set
|
||||
{
|
||||
_selectedNode = value;
|
||||
get { return this._selectedNode; }
|
||||
set
|
||||
{
|
||||
_selectedNode = value;
|
||||
if (value != null)
|
||||
{
|
||||
_selectedNodes.Clear();
|
||||
@@ -423,7 +437,7 @@ namespace AGVNavigationCore.Controls
|
||||
{
|
||||
_selectedNodes.Clear();
|
||||
}
|
||||
Invalidate();
|
||||
Invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -746,8 +760,8 @@ namespace AGVNavigationCore.Controls
|
||||
_destinationNodePen = new Pen(Color.Orange, 4);
|
||||
_pathPen = new Pen(Color.Purple, 3);
|
||||
_agvPen = new Pen(Color.Red, 3);
|
||||
_highlightedConnectionPen = new Pen(Color.Red, 4) { DashStyle = DashStyle.Solid };
|
||||
_magnetPen = new Pen(Color.FromArgb(100, Color.LightSkyBlue), 15) { DashStyle = DashStyle.Solid };
|
||||
_highlightedConnectionPen = new Pen(Color.Red, 6) { DashStyle = DashStyle.Solid };
|
||||
_magnetPen = new Pen(Color.FromArgb(100, Color.LightSkyBlue), 15) { DashStyle = DashStyle.Solid, StartCap = LineCap.Round, EndCap = LineCap.Round };
|
||||
_markPen = new Pen(Color.White, 3); // 마크는 흰색 선으로 표시
|
||||
}
|
||||
|
||||
|
||||
@@ -67,15 +67,13 @@ namespace AGVNavigationCore.Models
|
||||
/// <summary>로더</summary>
|
||||
Loader,
|
||||
/// <summary>클리너</summary>
|
||||
Clearner,
|
||||
Plating,
|
||||
/// <summary>오프로더</summary>
|
||||
UnLoader,
|
||||
Cleaner,
|
||||
/// <summary>버퍼</summary>
|
||||
Buffer,
|
||||
/// <summary>충전기1</summary>
|
||||
Charger1,
|
||||
/// <summary>충전기2</summary>
|
||||
Charger2,
|
||||
Charger,
|
||||
|
||||
/// <summary>
|
||||
/// 끝점(더이상 이동불가)
|
||||
|
||||
@@ -76,5 +76,17 @@ namespace AGVNavigationCore.Models
|
||||
get => new Point((int)P2.X, (int)P2.Y);
|
||||
set { P2.X = value.X; P2.Y = value.Y; }
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
if (ControlPoint == null)
|
||||
{
|
||||
return $"[LINE] ({P1.X:F0},{P1.Y:F0}) -> ({P2.X:F0},{P2.Y:F0})";
|
||||
}
|
||||
else
|
||||
{
|
||||
return $"[CURVE] ({P1.X:F0},{P1.Y:F0}) -> ({P2.X:F0},{P2.Y:F0})";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,9 +15,7 @@ namespace AGVNavigationCore.Models
|
||||
{
|
||||
|
||||
|
||||
[Category("라벨 설정")]
|
||||
[Description("표시할 텍스트입니다.")]
|
||||
public string Text { get; set; } = "";
|
||||
|
||||
|
||||
public StationType StationType { get; set; }
|
||||
|
||||
@@ -28,10 +26,9 @@ namespace AGVNavigationCore.Models
|
||||
{
|
||||
if (StationType == StationType.Buffer) return true;
|
||||
if (StationType == StationType.Loader) return true;
|
||||
if (StationType == StationType.UnLoader) return true;
|
||||
if (StationType == StationType.Clearner) return true;
|
||||
if (StationType == StationType.Charger1) return true;
|
||||
if (StationType == StationType.Charger2) return true;
|
||||
if (StationType == StationType.Cleaner) return true;
|
||||
if (StationType == StationType.Plating) return true;
|
||||
if (StationType == StationType.Charger) return true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -56,11 +53,11 @@ namespace AGVNavigationCore.Models
|
||||
|
||||
[Category("주행 설정")]
|
||||
[Description("제자리 회전(좌) 가능 여부입니다.")]
|
||||
public bool CanTurnLeft { get; set; } = true;
|
||||
public bool CanTurnLeft { get; set; } = false;
|
||||
|
||||
[Category("주행 설정")]
|
||||
[Description("제자리 회전(우) 가능 여부입니다.")]
|
||||
public bool CanTurnRight { get; set; } = true;
|
||||
public bool CanTurnRight { get; set; } = false;
|
||||
|
||||
[Category("주행 설정")]
|
||||
[Description("교차로 주행 가능 여부입니다.")]
|
||||
@@ -73,7 +70,7 @@ namespace AGVNavigationCore.Models
|
||||
}
|
||||
set { _disablecross = value; }
|
||||
}
|
||||
private bool _disablecross = false;
|
||||
private bool _disablecross = true;
|
||||
|
||||
[Category("주행 설정")]
|
||||
[Description("노드 통과 시 제한 속도입니다.")]
|
||||
@@ -105,6 +102,10 @@ namespace AGVNavigationCore.Models
|
||||
set => _textFontSize = value > 0 ? value : 7.0f;
|
||||
}
|
||||
|
||||
[Category("노드 텍스트")]
|
||||
[Description("표시할 텍스트입니다.")]
|
||||
public string Text { get; set; } = "";
|
||||
|
||||
public MapNode() : base()
|
||||
{
|
||||
Type = NodeType.Normal;
|
||||
@@ -121,9 +122,9 @@ namespace AGVNavigationCore.Models
|
||||
{
|
||||
get
|
||||
{
|
||||
if (StationType == StationType.Charger1 || StationType == StationType.Charger2 || StationType == StationType.Buffer ||
|
||||
StationType == StationType.Clearner || StationType == StationType.Loader ||
|
||||
StationType == StationType.UnLoader) return true;
|
||||
if (StationType == StationType.Charger || StationType == StationType.Buffer ||
|
||||
StationType == StationType.Plating || StationType == StationType.Loader ||
|
||||
StationType == StationType.Cleaner) return true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -141,6 +142,15 @@ namespace AGVNavigationCore.Models
|
||||
{
|
||||
if (ConnectedNodes.Remove(nodeId))
|
||||
{
|
||||
if (MagnetDirections != null && MagnetDirections.ContainsKey(nodeId))
|
||||
{
|
||||
MagnetDirections.Remove(nodeId);
|
||||
}
|
||||
|
||||
// 🔥 ConnectedMapNodes에서도 제거 (화면 갱신용)
|
||||
var target = ConnectedMapNodes.Find(n => n.Id == nodeId);
|
||||
if (target != null) ConnectedMapNodes.Remove(target);
|
||||
|
||||
ModifiedDate = DateTime.Now;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -240,17 +240,17 @@ namespace AGVNavigationCore.PathFinding.Planning
|
||||
|
||||
// 2. Buffer-to-Buffer 예외 처리
|
||||
// 05~31 구간 체크
|
||||
var node05 = _mapNodes.FirstOrDefault(n => n.RfidId == 5);
|
||||
var node31 = _mapNodes.FirstOrDefault(n => n.RfidId == 31);
|
||||
var node_buff_sta = _mapNodes.Where(t => t.StationType == StationType.Buffer).OrderBy(s => s.RfidId).FirstOrDefault();// (n => n.RfidId == 5);
|
||||
var node_buff_end = _mapNodes.Where(t => t.StationType == StationType.Buffer).OrderByDescending(s => s.RfidId).FirstOrDefault();//
|
||||
|
||||
bool fixpath = false;
|
||||
Retval = null;
|
||||
MapNode gatewayNode = null;
|
||||
|
||||
if (node05 != null && node31 != null)
|
||||
if (node_buff_sta != null && node_buff_end != null)
|
||||
{
|
||||
// 버퍼 구간 경로 테스트
|
||||
var rlt = this.FindPathAStar(node05, node31);
|
||||
var rlt = this.FindPathAStar(node_buff_sta, node_buff_end);
|
||||
if (rlt.Success)
|
||||
{
|
||||
// 버퍼구간내에 시작과 종료가 모두 포함되어있다
|
||||
@@ -265,53 +265,89 @@ namespace AGVNavigationCore.PathFinding.Planning
|
||||
|
||||
if (!fixpath)
|
||||
{
|
||||
// 3. 목적지별 Gateway 및 진입 조건 확인
|
||||
gatewayNode = GetGatewayNode(targetNode);
|
||||
|
||||
|
||||
if (gatewayNode == null)
|
||||
if (targetNode.StationType == StationType.Limit || targetNode.StationType == StationType.Normal)
|
||||
{
|
||||
// 게이트웨이가 없는 경우라면(일반 노드 등), Gateway 로직 없이 기본 경로 탐색
|
||||
//일반노드라면 방향 무관하게 그냥 이동하게한다.
|
||||
Retval = this.FindBasicPath(startNode, targetNode, prevNode, prevDir);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Gateway Node 찾음
|
||||
// 4. Start -> Gateway 경로 계산 (A*)
|
||||
var pathToGateway = this.FindBasicPath(startNode, gatewayNode, prevNode, prevDir);
|
||||
if (pathToGateway.Success == false)
|
||||
return AGVPathResult.CreateFailure($"Gateway({gatewayNode.ID2})까지 경로 실패: {pathToGateway.Message}");
|
||||
|
||||
//목적지까지 그대로 방향을 계산한다.
|
||||
var pathToTaget = this.FindBasicPath(startNode, targetNode, prevNode, prevDir);
|
||||
if (pathToTaget.Success == false)
|
||||
return AGVPathResult.CreateFailure($"Target({targetNode.ID2})까지 경로 실패: {pathToTaget.Message}");
|
||||
|
||||
// 방향을 확인하여, 왔던 방향으로 되돌아가야 한다면 방향 반전
|
||||
if (pathToGateway.Path.Count > 1)
|
||||
//다음이동방향이 이전노드와 동일하다면? 되돌아가야한다는것이다
|
||||
var predictNext = pathToTaget.Path[1];
|
||||
if (predictNext.Id == prevNode.Id)
|
||||
{
|
||||
var predictNext = pathToGateway.Path[1];
|
||||
if (predictNext.Id == prevNode.Id)
|
||||
var reverseDir = prevDir == AgvDirection.Backward ? AgvDirection.Forward : AgvDirection.Backward;
|
||||
foreach (var item in pathToTaget.DetailedPath)
|
||||
item.MotorDirection = reverseDir;
|
||||
}
|
||||
|
||||
if (targetNode.DockDirection != DockingDirection.DontCare)
|
||||
{
|
||||
Retval = pathToTaget;
|
||||
}
|
||||
//현재 진행방향과 목적지의 도킹방향이 일치하는지 확인한다.
|
||||
else if ((pathToTaget.DetailedPath.Last().MotorDirection == AgvDirection.Backward && targetNode.DockDirection == DockingDirection.Backward) ||
|
||||
(pathToTaget.DetailedPath.Last().MotorDirection == AgvDirection.Forward && targetNode.DockDirection == DockingDirection.Forward))
|
||||
{
|
||||
//일치하는 경우 그대로 사용하낟.
|
||||
Retval = pathToTaget;
|
||||
}
|
||||
else
|
||||
{
|
||||
//불일치하는경우라면 Turn 가능노드를 찾아서 이동한 후 턴을 한다.
|
||||
|
||||
// 3. 목적지별 Turn 및 진입 조건 확인
|
||||
gatewayNode = GetTurnNode(targetNode);
|
||||
|
||||
|
||||
// Gateway Node 찾음
|
||||
// 4. Start -> Gateway 경로 계산 (A*)
|
||||
var pathToGateway = this.FindBasicPath(startNode, gatewayNode, prevNode, prevDir);
|
||||
if (pathToGateway.Success == false)
|
||||
return AGVPathResult.CreateFailure($"Gateway({gatewayNode.ID2})까지 경로 실패: {pathToGateway.Message}");
|
||||
|
||||
// 방향을 확인하여, 왔던 방향으로 되돌아가야 한다면 방향 반전
|
||||
if (pathToGateway.Path.Count > 1)
|
||||
{
|
||||
var reverseDir = prevDir == AgvDirection.Backward ? AgvDirection.Forward : AgvDirection.Backward;
|
||||
foreach (var item in pathToGateway.DetailedPath)
|
||||
item.MotorDirection = reverseDir;
|
||||
//다음이동방향이 이전노드와 동일하다면? 되돌아가야한다는것이다
|
||||
predictNext = pathToGateway.Path[1];
|
||||
if (predictNext.Id == prevNode.Id)
|
||||
{
|
||||
var reverseDir = prevDir == AgvDirection.Backward ? AgvDirection.Forward : AgvDirection.Backward;
|
||||
foreach (var item in pathToGateway.DetailedPath)
|
||||
item.MotorDirection = reverseDir;
|
||||
}
|
||||
}
|
||||
|
||||
// 마지막 경로는 게이트웨이이므로 제거 (Gateway 진입 후 처리는 GetPathFromGateway에서 담당)
|
||||
if (pathToGateway.Path.Count > 0 && pathToGateway.Path.Last().Id == gatewayNode.Id)
|
||||
{
|
||||
var idx = pathToGateway.Path.Count - 1;
|
||||
pathToGateway.Path.RemoveAt(idx);
|
||||
pathToGateway.DetailedPath.RemoveAt(idx);
|
||||
}
|
||||
|
||||
// 5. Gateway -> Target 경로 계산 (회차 패턴 및 최종 진입 포함)
|
||||
MapNode GateprevNode = pathToGateway.Path.LastOrDefault() ?? prevNode;
|
||||
NodeMotorInfo GatePrevDetail = pathToGateway.DetailedPath.LastOrDefault();
|
||||
|
||||
var arrivalOrientation = GatePrevDetail?.MotorDirection ?? prevDir;
|
||||
var gatewayPathResult = GetPathFromGateway(gatewayNode, targetNode, GateprevNode, arrivalOrientation);
|
||||
|
||||
if (!gatewayPathResult.Success)
|
||||
return AGVPathResult.CreateFailure($"{gatewayPathResult.Message}");
|
||||
|
||||
Retval = CombinePaths(pathToGateway, gatewayPathResult);
|
||||
}
|
||||
|
||||
// 마지막 경로는 게이트웨이이므로 제거 (Gateway 진입 후 처리는 GetPathFromGateway에서 담당)
|
||||
if (pathToGateway.Path.Count > 0 && pathToGateway.Path.Last().Id == gatewayNode.Id)
|
||||
{
|
||||
var idx = pathToGateway.Path.Count - 1;
|
||||
pathToGateway.Path.RemoveAt(idx);
|
||||
pathToGateway.DetailedPath.RemoveAt(idx);
|
||||
}
|
||||
|
||||
// 5. Gateway -> Target 경로 계산 (회차 패턴 및 최종 진입 포함)
|
||||
MapNode GateprevNode = pathToGateway.Path.LastOrDefault() ?? prevNode;
|
||||
NodeMotorInfo GatePrevDetail = pathToGateway.DetailedPath.LastOrDefault();
|
||||
|
||||
var arrivalOrientation = GatePrevDetail?.MotorDirection ?? prevDir;
|
||||
var gatewayPathResult = GetPathFromGateway(gatewayNode, targetNode, GateprevNode, arrivalOrientation);
|
||||
|
||||
if (!gatewayPathResult.Success)
|
||||
return AGVPathResult.CreateFailure($"{gatewayPathResult.Message}");
|
||||
|
||||
Retval = CombinePaths(pathToGateway, gatewayPathResult);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -457,13 +493,13 @@ namespace AGVNavigationCore.PathFinding.Planning
|
||||
var firstnode = Retval.Path.FirstOrDefault();
|
||||
var firstDet = Retval.DetailedPath.First();
|
||||
var failmessage = $"[{firstnode.ID2}] 노드의 시작모터 방향({firstDet.MotorDirection})이 올바르지 않습니다";
|
||||
if (firstnode.StationType == StationType.Charger1 && firstDet.MotorDirection != AgvDirection.Forward)
|
||||
if (firstnode.StationType == StationType.Charger && firstDet.MotorDirection != AgvDirection.Forward)
|
||||
return AGVPathResult.CreateFailure(failmessage);
|
||||
else if (firstnode.StationType == StationType.Loader && firstDet.MotorDirection != AgvDirection.Backward)
|
||||
return AGVPathResult.CreateFailure(failmessage);
|
||||
else if (firstnode.StationType == StationType.UnLoader && firstDet.MotorDirection != AgvDirection.Backward)
|
||||
else if (firstnode.StationType == StationType.Cleaner && firstDet.MotorDirection != AgvDirection.Backward)
|
||||
return AGVPathResult.CreateFailure(failmessage);
|
||||
else if (firstnode.StationType == StationType.Clearner && firstDet.MotorDirection != AgvDirection.Backward)
|
||||
else if (firstnode.StationType == StationType.Plating && firstDet.MotorDirection != AgvDirection.Backward)
|
||||
return AGVPathResult.CreateFailure(failmessage);
|
||||
else if (firstnode.StationType == StationType.Buffer)
|
||||
{
|
||||
@@ -621,7 +657,7 @@ namespace AGVNavigationCore.PathFinding.Planning
|
||||
if (deltaX > 0) isMonitorLeft = PrevDirection == AgvDirection.Backward;
|
||||
else isMonitorLeft = PrevDirection == AgvDirection.Forward;
|
||||
|
||||
if (targetNode.StationType == StationType.Loader || targetNode.StationType == StationType.Charger2)
|
||||
if (targetNode.StationType == StationType.Loader)
|
||||
{
|
||||
deltaX = GTNode.Position.Y - PrevNode.Position.Y;
|
||||
if (deltaX < 0) isMonitorLeft = PrevDirection == AgvDirection.Backward;
|
||||
@@ -631,14 +667,14 @@ namespace AGVNavigationCore.PathFinding.Planning
|
||||
switch (targetNode.StationType)
|
||||
{
|
||||
case StationType.Loader:
|
||||
case StationType.Charger2:
|
||||
case StationType.Charger1:
|
||||
case StationType.UnLoader:
|
||||
case StationType.Clearner:
|
||||
case StationType.Charger:
|
||||
case StationType.Cleaner:
|
||||
case StationType.Plating:
|
||||
case StationType.Buffer:
|
||||
var rlt1 = new AGVPathResult();
|
||||
rlt1.Success = true;
|
||||
|
||||
//단순 경로를 찾는다
|
||||
var motdir = targetNode.DockDirection == DockingDirection.Backward ? AgvDirection.Backward : AgvDirection.Forward;
|
||||
var pathtarget = this.FindBasicPath(GTNode, targetNode, PrevNode, motdir);
|
||||
|
||||
@@ -682,15 +718,14 @@ namespace AGVNavigationCore.PathFinding.Planning
|
||||
}
|
||||
}
|
||||
|
||||
private MapNode GetGatewayNode(MapNode node)
|
||||
private MapNode GetTurnNode(MapNode node)
|
||||
{
|
||||
var rfid = 0;
|
||||
if (node.StationType == StationType.UnLoader) rfid = 10;
|
||||
else if (node.StationType == StationType.Charger1) rfid = 9;
|
||||
else if (node.StationType == StationType.Clearner) rfid = 6;
|
||||
else if (node.StationType == StationType.Charger2) rfid = 13;
|
||||
else if (node.StationType == StationType.Loader) rfid = 13;
|
||||
else if (node.StationType == StationType.Buffer) rfid = 6;
|
||||
if (node.StationType == StationType.Cleaner) rfid = 3;
|
||||
else if (node.StationType == StationType.Charger) rfid = 3;
|
||||
else if (node.StationType == StationType.Plating) rfid = 3;
|
||||
else if (node.StationType == StationType.Loader) rfid = 3;
|
||||
else if (node.StationType == StationType.Buffer) rfid = 3;
|
||||
|
||||
if (rfid == 0) return null;
|
||||
return _mapNodes.FirstOrDefault(t => t.RfidId == rfid);
|
||||
|
||||
Reference in New Issue
Block a user