This commit is contained in:
backuppc
2025-11-10 14:43:47 +09:00
parent 68745f23bb
commit 6e54633c08
57 changed files with 4432 additions and 1018 deletions

View File

@@ -26,6 +26,10 @@ namespace AGVNavigationCore.Controls
}
var g = e.Graphics;
// 🔥 배경색 그리기 (변환 행렬 적용 전에 전체 화면을 배경색으로 채움)
g.Clear(this.BackColor);
g.SmoothingMode = SmoothingMode.AntiAlias;
g.InterpolationMode = InterpolationMode.High;
@@ -77,7 +81,8 @@ namespace AGVNavigationCore.Controls
}
// UI 정보 그리기 (변환 없이)
DrawUIInfo(g);
if (_showGrid)
DrawUIInfo(g);
}
private void DrawGrid(Graphics g)
@@ -689,8 +694,8 @@ namespace AGVNavigationCore.Controls
var pulseRect = new Rectangle(rect.X - 4, rect.Y - 4, rect.Width + 8, rect.Height + 8);
g.DrawEllipse(new Pen(Color.FromArgb(150, 0, 255, 255), 2) { DashStyle = DashStyle.Dash }, pulseRect);
}
// 선택된 노드 강조
else if (node == _selectedNode)
// 선택된 노드 강조 (단일 또는 다중)
else if (node == _selectedNode || (_selectedNodes != null && _selectedNodes.Contains(node)))
{
g.DrawEllipse(_selectedNodePen, rect);
}
@@ -938,19 +943,22 @@ namespace AGVNavigationCore.Controls
// 아래쪽에 표시할 값 (RFID 우선, 없으면 노드ID)
if (node.HasRfid())
{
// RFID가 있는 경우: 순수 RFID 값만 표시 (진한 색상)
// RFID가 있는 경우: 순수 RFID 값만 표시 (노드 전경색 사용)
displayText = node.RfidId;
textColor = Color.Black;
textColor = node.ForeColor;
}
else
{
// RFID가 없는 경우: 노드 ID 표시 (연한 색상)
// RFID가 없는 경우: 노드 ID 표시 (노드 전경색의 50% 투명도)
displayText = node.NodeId;
textColor = Color.Gray;
textColor = Color.FromArgb(128, node.ForeColor);
}
var font = new Font("Arial", 7, FontStyle.Bold);
var descFont = new Font("Arial", 8, FontStyle.Bold);
// 🔥 노드의 폰트 설정 사용 (0 이하일 경우 기본값 7.0f 사용)
var fontStyle = node.TextFontBold ? FontStyle.Bold : FontStyle.Regular;
var fontSize = node.TextFontSize > 0 ? node.TextFontSize : 7.0f;
var font = new Font("Arial", fontSize, fontStyle);
var descFont = new Font("Arial", fontSize + 1, fontStyle);
// 메인 텍스트 크기 측정
var textSize = g.MeasureString(displayText, font);
@@ -971,12 +979,8 @@ namespace AGVNavigationCore.Controls
// 설명 텍스트 그리기 (설명이 있는 경우에만)
if (!string.IsNullOrEmpty(descriptionText))
{
// 노드 이름 입력 여부에 따라 색상 구분
// 입력된 경우: 진한 색상 (잘 보이게)
// 기본값인 경우: 흐린 색상 (현재처럼)
Color descColor = string.IsNullOrEmpty(node.Name)
? Color.FromArgb(120, Color.Black) // 입력 안됨: 흐린 색상
: Color.FromArgb(200, Color.Black); // 입력됨: 진한 색상
// 🔥 노드의 말풍선 글자색 사용 (NameBubbleForeColor)
Color descColor = node.NameBubbleForeColor;
var rectpaddingx = 4;
var rectpaddingy = 2;
@@ -985,10 +989,10 @@ namespace AGVNavigationCore.Controls
(int)descSize.Width + rectpaddingx * 2,
(int)descSize.Height + rectpaddingy * 2);
// 라운드 사각형 그리기 (빨간 배경)
using (var backgroundBrush = new SolidBrush(Color.Gold))
// 라운드 사각형 그리기 (노드 이름 말풍선 배경색 사용)
using (var backgroundBrush = new SolidBrush(node.NameBubbleBackColor))
{
DrawRoundedRectangle(g, backgroundBrush, roundRect, 3); // 모서리 반지름 6px
DrawRoundedRectangle(g, backgroundBrush, roundRect, 3); // 모서리 반지름 3px
}
// 라운드 사각형 테두리 그리기 (진한 빨간색)
@@ -1287,26 +1291,19 @@ namespace AGVNavigationCore.Controls
private Brush GetNodeBrush(MapNode node)
{
// RFID가 없는 노드는 회색 계통으로 표시
// 🔥 노드의 DisplayColor를 배경색으로 사용
// RFID가 없는 노드는 DisplayColor를 50% 투명도로 표시
bool hasRfid = node.HasRfid();
switch (node.Type)
Color bgColor = node.DisplayColor;
// RFID가 없는 경우 투명도 50%
if (!hasRfid)
{
case NodeType.Normal:
return hasRfid ? _normalNodeBrush : new SolidBrush(Color.LightGray);
case NodeType.Rotation:
return hasRfid ? _rotationNodeBrush : new SolidBrush(Color.DarkGray);
case NodeType.Docking:
return hasRfid ? _dockingNodeBrush : new SolidBrush(Color.Gray);
case NodeType.Charging:
return hasRfid ? _chargingNodeBrush : new SolidBrush(Color.Silver);
case NodeType.Label:
return new SolidBrush(Color.Purple);
case NodeType.Image:
return new SolidBrush(Color.Brown);
default:
return hasRfid ? _normalNodeBrush : new SolidBrush(Color.LightGray);
bgColor = Color.FromArgb(128, bgColor);
}
return new SolidBrush(bgColor);
}
private void DrawAGVs(Graphics g)

View File

@@ -17,6 +17,54 @@ namespace AGVNavigationCore.Controls
var worldPoint = ScreenToWorld(e.Location);
var hitNode = GetNodeAt(worldPoint);
// 🔥 어떤 모드에서든 노드/빈 공간 클릭 시 선택 이벤트 발생 (속성창 업데이트)
bool ctrlPressed = (ModifierKeys & Keys.Control) == Keys.Control;
if (hitNode != null)
{
// 노드 클릭
if (ctrlPressed && _editMode == EditMode.Select)
{
// Ctrl+클릭: 다중 선택 토글
if (_selectedNodes.Contains(hitNode))
{
_selectedNodes.Remove(hitNode);
}
else
{
_selectedNodes.Add(hitNode);
}
// 마지막 선택된 노드 업데이트 (단일 참조용)
_selectedNode = _selectedNodes.Count > 0 ? _selectedNodes[_selectedNodes.Count - 1] : null;
// 다중 선택 이벤트만 발생 (OnNodesSelected에서 단일/다중 구분 처리)
NodesSelected?.Invoke(this, _selectedNodes);
Invalidate();
}
else
{
// 일반 클릭: 단일 선택
_selectedNode = hitNode;
_selectedNodes.Clear();
_selectedNodes.Add(hitNode);
// NodesSelected 이벤트만 발생 (OnNodesSelected에서 단일/다중 구분 처리)
NodesSelected?.Invoke(this, _selectedNodes);
Invalidate();
}
}
else if (_editMode == EditMode.Select)
{
// 빈 공간 클릭 (Select 모드에서만) - 선택 해제
_selectedNode = null;
_selectedNodes.Clear();
// NodesSelected 이벤트만 발생 (OnNodesSelected에서 빈 리스트 처리)
NodesSelected?.Invoke(this, _selectedNodes);
Invalidate();
}
switch (_editMode)
{
case EditMode.Select:
@@ -76,7 +124,10 @@ namespace AGVNavigationCore.Controls
default:
// 기본 동작: 노드 선택 이벤트 발생
NodeSelected?.Invoke(this, hitNode);
_selectedNode = hitNode;
_selectedNodes.Clear();
_selectedNodes.Add(hitNode);
NodesSelected?.Invoke(this, _selectedNodes);
break;
}
}
@@ -98,8 +149,11 @@ namespace AGVNavigationCore.Controls
Invalidate();
}
// 노드 선택 이벤트도 발생 (속성창 업데이트)
NodeSelected?.Invoke(this, node);
// 더블클릭 시 해당 노드 선택 (다중 선택 해제)
_selectedNode = node;
_selectedNodes.Clear();
_selectedNodes.Add(node);
NodesSelected?.Invoke(this, _selectedNodes);
}
private void HandleLabelNodeDoubleClick(MapNode node)
@@ -118,14 +172,20 @@ namespace AGVNavigationCore.Controls
Invalidate();
}
// 노드 선택 이벤트도 발생 (속성창 업데이트)
NodeSelected?.Invoke(this, node);
// 더블클릭 시 해당 노드 선택 (다중 선택 해제)
_selectedNode = node;
_selectedNodes.Clear();
_selectedNodes.Add(node);
NodesSelected?.Invoke(this, _selectedNodes);
}
private void HandleImageNodeDoubleClick(MapNode node)
{
// 이미지 노드 선택 이벤트만 발생 (MainForm에서 이미지 편집 버튼 활성화됨)
NodeSelected?.Invoke(this, node);
// 더블클릭 시 해당 노드 선택 (다중 선택 해제)
_selectedNode = node;
_selectedNodes.Clear();
_selectedNodes.Add(node);
NodesSelected?.Invoke(this, _selectedNodes);
// 이미지 편집 이벤트 발생 (MainForm에서 처리)
ImageNodeDoubleClicked?.Invoke(this, node);
@@ -519,13 +579,8 @@ namespace AGVNavigationCore.Controls
{
if (hitNode != null)
{
// 노드 선택
if (hitNode != _selectedNode)
{
_selectedNode = hitNode;
NodeSelected?.Invoke(this, hitNode);
Invalidate();
}
// 노드 선택은 위쪽 MouseClick에서 이미 처리됨 (NodesSelected 이벤트 발생)
// 여기서는 추가 처리 없음
}
else
{
@@ -565,10 +620,11 @@ namespace AGVNavigationCore.Controls
else
{
// 빈 공간 클릭 시 선택 해제
if (_selectedNode != null)
if (_selectedNode != null || _selectedNodes.Count > 0)
{
_selectedNode = null;
NodeSelected?.Invoke(this, null);
_selectedNodes.Clear();
NodesSelected?.Invoke(this, _selectedNodes);
Invalidate();
}
}
@@ -745,7 +801,13 @@ namespace AGVNavigationCore.Controls
if (hitNode != null)
{
_contextMenu.Items.Add("노드 속성...", null, (s, e) => NodeSelected?.Invoke(this, hitNode));
_contextMenu.Items.Add("노드 속성...", null, (s, e) =>
{
_selectedNode = hitNode;
_selectedNodes.Clear();
_selectedNodes.Add(hitNode);
NodesSelected?.Invoke(this, _selectedNodes);
});
_contextMenu.Items.Add("노드 삭제", null, (s, e) => HandleDeleteClick(hitNode));
_contextMenu.Items.Add("-");
}

View File

@@ -64,6 +64,7 @@ namespace AGVNavigationCore.Controls
// 맵 데이터
private List<MapNode> _nodes;
private MapNode _selectedNode;
private List<MapNode> _selectedNodes; // 다중 선택
private MapNode _hoveredNode;
private MapNode _destinationNode;
@@ -95,6 +96,11 @@ namespace AGVNavigationCore.Controls
private Point _connectionEndPoint;
private int _mouseMoveCounter = 0; // 디버그용: MouseMove 실행 횟수
// 영역 선택 관련
private bool _isAreaSelecting;
private Point _areaSelectStart;
private Point _areaSelectEnd;
// 그리드 및 줌 관련
private bool _showGrid = true;
private float _zoomFactor = 1.0f;
@@ -141,6 +147,7 @@ namespace AGVNavigationCore.Controls
// 맵 편집 이벤트
public event EventHandler<MapNode> NodeAdded;
public event EventHandler<MapNode> NodeSelected;
public event EventHandler<List<MapNode>> NodesSelected; // 다중 선택 이벤트
public event EventHandler<MapNode> NodeDeleted;
public event EventHandler<MapNode> NodeMoved;
public event EventHandler<(MapNode From, MapNode To)> ConnectionDeleted;
@@ -212,10 +219,15 @@ namespace AGVNavigationCore.Controls
}
/// <summary>
/// 선택된 노드
/// 선택된 노드 (단일)
/// </summary>
public MapNode SelectedNode => _selectedNode;
/// <summary>
/// 선택된 노드들 (다중)
/// </summary>
public List<MapNode> SelectedNodes => _selectedNodes ?? new List<MapNode>();
/// <summary>
/// 노드 목록
/// </summary>
@@ -365,6 +377,7 @@ namespace AGVNavigationCore.Controls
ControlStyles.ResizeRedraw, true);
_nodes = new List<MapNode>();
_selectedNodes = new List<MapNode>(); // 다중 선택 리스트 초기화
_agvList = new List<IAGV>();
_agvPositions = new Dictionary<string, Point>();
_agvDirections = new Dictionary<string, AgvDirection>();

View File

@@ -12,6 +12,15 @@ namespace AGVNavigationCore.Models
/// </summary>
public static class MapLoader
{
/// <summary>
/// 맵 설정 정보 (배경색, 그리드 표시 등)
/// </summary>
public class MapSettings
{
public int BackgroundColorArgb { get; set; } = System.Drawing.Color.White.ToArgb();
public bool ShowGrid { get; set; } = true;
}
/// <summary>
/// 맵 파일 로딩 결과
/// </summary>
@@ -19,6 +28,7 @@ namespace AGVNavigationCore.Models
{
public bool Success { get; set; }
public List<MapNode> Nodes { get; set; } = new List<MapNode>();
public MapSettings Settings { get; set; } = new MapSettings();
public string ErrorMessage { get; set; } = string.Empty;
public string Version { get; set; } = string.Empty;
public DateTime CreatedDate { get; set; }
@@ -30,8 +40,9 @@ namespace AGVNavigationCore.Models
public class MapFileData
{
public List<MapNode> Nodes { get; set; } = new List<MapNode>();
public MapSettings Settings { get; set; } = new MapSettings();
public DateTime CreatedDate { get; set; }
public string Version { get; set; } = "1.0";
public string Version { get; set; } = "1.1"; // 버전 업그레이드 (설정 추가)
}
/// <summary>
@@ -66,6 +77,7 @@ namespace AGVNavigationCore.Models
if (mapData != null)
{
result.Nodes = mapData.Nodes ?? new List<MapNode>();
result.Settings = mapData.Settings ?? new MapSettings(); // 설정 로드
result.Version = mapData.Version ?? "1.0";
result.CreatedDate = mapData.CreatedDate;
@@ -111,8 +123,9 @@ namespace AGVNavigationCore.Models
/// </summary>
/// <param name="filePath">저장할 파일 경로</param>
/// <param name="nodes">맵 노드 목록</param>
/// <param name="settings">맵 설정 (배경색, 그리드 표시 등)</param>
/// <returns>저장 성공 여부</returns>
public static bool SaveMapToFile(string filePath, List<MapNode> nodes)
public static bool SaveMapToFile(string filePath, List<MapNode> nodes, MapSettings settings = null)
{
try
{
@@ -122,8 +135,9 @@ namespace AGVNavigationCore.Models
var mapData = new MapFileData
{
Nodes = nodes,
Settings = settings ?? new MapSettings(), // 설정 저장
CreatedDate = DateTime.Now,
Version = "1.0"
Version = "1.1"
};
var json = JsonConvert.SerializeObject(mapData, Formatting.Indented);

View File

@@ -124,7 +124,7 @@ namespace AGVNavigationCore.Models
public FontStyle FontStyle { get; set; } = FontStyle.Regular;
/// <summary>
/// 라벨 전경색 (NodeType.Label인 경우 사용)
/// 텍스트 전경색 (모든 노드 타입에서 사용)
/// </summary>
public Color ForeColor { get; set; } = Color.Black;
@@ -133,6 +133,33 @@ namespace AGVNavigationCore.Models
/// </summary>
public Color BackColor { get; set; } = Color.Transparent;
private float _textFontSize = 7.0f;
/// <summary>
/// 텍스트 폰트 크기 (모든 노드 타입의 텍스트 표시에 사용, 픽셀 단위)
/// 0 이하의 값이 설정되면 기본값 7.0f로 자동 설정
/// </summary>
public float TextFontSize
{
get => _textFontSize;
set => _textFontSize = value > 0 ? value : 7.0f;
}
/// <summary>
/// 텍스트 볼드체 여부 (모든 노드 타입의 텍스트 표시에 사용)
/// </summary>
public bool TextFontBold { get; set; } = true;
/// <summary>
/// 노드 이름 말풍선 배경색 (하단에 표시되는 노드 이름의 배경색)
/// </summary>
public Color NameBubbleBackColor { get; set; } = Color.Gold;
/// <summary>
/// 노드 이름 말풍선 글자색 (하단에 표시되는 노드 이름의 글자색)
/// </summary>
public Color NameBubbleForeColor { get; set; } = Color.Black;
/// <summary>
/// 라벨 배경 표시 여부 (NodeType.Label인 경우 사용)
/// </summary>
@@ -347,6 +374,10 @@ namespace AGVNavigationCore.Models
FontStyle = FontStyle,
ForeColor = ForeColor,
BackColor = BackColor,
TextFontSize = TextFontSize,
TextFontBold = TextFontBold,
NameBubbleBackColor = NameBubbleBackColor,
NameBubbleForeColor = NameBubbleForeColor,
ShowBackground = ShowBackground,
Padding = Padding,
ImagePath = ImagePath,

View File

@@ -306,15 +306,15 @@ namespace AGVNavigationCore.Models
}
// 5. 방향체크
if(CurrentDirection != TargetNode.MotorDirection)
{
return new AGVCommand(
MotorCommand.Stop,
MagnetPosition.S,
SpeedLevel.L,
$"(재탐색요청)모터방향 불일치 현재위치:{_currentNode.NodeId}"
);
}
//if(CurrentDirection != TargetNode.MotorDirection)
//{
// return new AGVCommand(
// MotorCommand.Stop,
// MagnetPosition.S,
// SpeedLevel.L,
// $"(재탐색요청)모터방향 불일치 현재위치:{_currentNode.NodeId}"
// );
//}
//this.CurrentNodeId