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

@@ -17,7 +17,7 @@
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<OutputPath>..\..\..\..\..\..\Amkor\AGV4\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>

View File

@@ -111,6 +111,7 @@ namespace AGVMapEditor.Forms
// 이벤트 연결
_mapCanvas.NodeAdded += OnNodeAdded;
_mapCanvas.NodeSelected += OnNodeSelected;
_mapCanvas.NodesSelected += OnNodesSelected; // 다중 선택 이벤트
_mapCanvas.NodeMoved += OnNodeMoved;
_mapCanvas.NodeDeleted += OnNodeDeleted;
_mapCanvas.ConnectionDeleted += OnConnectionDeleted;
@@ -184,8 +185,46 @@ namespace AGVMapEditor.Forms
private void OnNodeSelected(object sender, MapNode node)
{
_selectedNode = node;
UpdateNodeProperties();
UpdateImageEditButton(); // 이미지 노드 선택 시 이미지 편집 버튼 활성화
if (node == null)
{
// 빈 공간 클릭 시 캔버스 속성 표시
ShowCanvasProperties();
}
else
{
// 노드 클릭 시 노드 속성 표시
UpdateNodeProperties();
UpdateImageEditButton(); // 이미지 노드 선택 시 이미지 편집 버튼 활성화
}
}
private void OnNodesSelected(object sender, List<MapNode> nodes)
{
// 다중 선택 시 처리
if (nodes == null || nodes.Count == 0)
{
ShowCanvasProperties();
return;
}
if (nodes.Count == 1)
{
// 단일 선택은 기존 방식 사용
_selectedNode = nodes[0];
UpdateNodeProperties();
UpdateImageEditButton();
}
else
{
// 다중 선택: 상태바에 선택 개수 표시
toolStripStatusLabel1.Text = $"{nodes.Count}개 노드 선택됨 - PropertyGrid에서 공통 속성 일괄 변경 가능";
// 다중 선택 PropertyWrapper 표시
var multiWrapper = new MultiNodePropertyWrapper(nodes);
_propertyGrid.SelectedObject = multiWrapper;
_propertyGrid.Focus();
}
}
private void OnNodeMoved(object sender, MapNode node)
@@ -589,6 +628,13 @@ namespace AGVMapEditor.Forms
_mapCanvas.Nodes = _mapNodes;
// RfidMappings 제거됨 - MapNode에 통합
// 🔥 맵 설정 적용 (배경색, 그리드 표시)
if (result.Settings != null)
{
_mapCanvas.BackColor = System.Drawing.Color.FromArgb(result.Settings.BackgroundColorArgb);
_mapCanvas.ShowGrid = result.Settings.ShowGrid;
}
// 현재 파일 경로 업데이트
_currentMapFile = filePath;
_hasChanges = false;
@@ -614,7 +660,38 @@ namespace AGVMapEditor.Forms
private void SaveMapToFile(string filePath)
{
if (MapLoader.SaveMapToFile(filePath, _mapNodes))
// 🔥 백업 파일 생성 (기존 파일이 있을 경우)
if (File.Exists(filePath))
{
try
{
// 날짜시간 포함 백업 파일명 생성
var directory = Path.GetDirectoryName(filePath);
var fileNameWithoutExt = Path.GetFileNameWithoutExtension(filePath);
var extension = Path.GetExtension(filePath);
var timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
var backupFileName = $"{fileNameWithoutExt}_{timestamp}{extension}.bak";
var backupFilePath = Path.Combine(directory, backupFileName);
// 기존 파일을 백업 파일로 복사
File.Copy(filePath, backupFilePath, true);
}
catch (Exception ex)
{
// 백업 파일 생성 실패 시 경고만 표시하고 계속 진행
MessageBox.Show($"백업 파일 생성 실패: {ex.Message}\n원본 파일은 계속 저장됩니다.", "백업 경고",
MessageBoxButtons.OK, MessageBoxIcon.Warning);
}
}
// 🔥 현재 캔버스 설정을 맵 파일에 저장
var settings = new MapLoader.MapSettings
{
BackgroundColorArgb = _mapCanvas.BackColor.ToArgb(),
ShowGrid = _mapCanvas.ShowGrid
};
if (MapLoader.SaveMapToFile(filePath, _mapNodes, settings))
{
// 현재 파일 경로 업데이트
_currentMapFile = filePath;
@@ -892,7 +969,7 @@ namespace AGVMapEditor.Forms
{
if (_selectedNode == null)
{
ClearNodeProperties();
ShowCanvasProperties();
return;
}
@@ -905,6 +982,16 @@ namespace AGVMapEditor.Forms
UpdateImageEditButton();
}
/// <summary>
/// 캔버스 속성 표시 (배경색 등)
/// </summary>
private void ShowCanvasProperties()
{
var canvasWrapper = new CanvasPropertyWrapper(_mapCanvas);
_propertyGrid.SelectedObject = canvasWrapper;
DisableImageEditButton();
}
private void ClearNodeProperties()
{
_propertyGrid.SelectedObject = null;
@@ -1010,6 +1097,9 @@ namespace AGVMapEditor.Forms
private void PropertyGrid_PropertyValueChanged(object s, PropertyValueChangedEventArgs e)
{
// 변경된 속성명 디버그 출력
System.Diagnostics.Debug.WriteLine($"[PropertyGrid] 속성 변경됨: {e.ChangedItem.PropertyDescriptor.Name}");
// RFID 값 변경시 중복 검사
if (e.ChangedItem.PropertyDescriptor.Name == "RFID")
{
@@ -1031,14 +1121,39 @@ namespace AGVMapEditor.Forms
_hasChanges = true;
UpdateTitle();
// 현재 선택된 노드를 기억
// 🔥 다중 선택 여부 확인 및 선택된 노드들 저장
bool isMultiSelect = _propertyGrid.SelectedObject is MultiNodePropertyWrapper;
List<MapNode> selectedNodes = null;
if (isMultiSelect)
{
// 캔버스에서 현재 선택된 노드들 가져오기
selectedNodes = new List<MapNode>(_mapCanvas.SelectedNodes);
System.Diagnostics.Debug.WriteLine($"[PropertyGrid] 다중 선택 노드 수: {selectedNodes.Count}");
}
// 현재 선택된 노드를 기억 (단일 선택인 경우만)
var currentSelectedNode = _selectedNode;
RefreshNodeList();
RefreshMapCanvas();
// 선택된 노드를 다시 선택
if (currentSelectedNode != null)
// 🔥 캔버스 강제 갱신 (bool 타입 속성 변경 시 특히 필요)
_mapCanvas.Invalidate();
_mapCanvas.Update();
// 🔥 다중 선택인 경우 MultiNodePropertyWrapper를 다시 생성하여 바인딩
if (isMultiSelect && selectedNodes != null && selectedNodes.Count > 0)
{
System.Diagnostics.Debug.WriteLine($"[PropertyGrid] MultiNodePropertyWrapper 재생성: {selectedNodes.Count}개");
var multiWrapper = new MultiNodePropertyWrapper(selectedNodes);
_propertyGrid.SelectedObject = multiWrapper;
}
// PropertyGrid 새로고침
_propertyGrid.Refresh();
// 🔥 단일 선택인 경우에만 노드를 다시 선택 (다중 선택은 캔버스에서 유지)
if (!isMultiSelect && currentSelectedNode != null)
{
var nodeIndex = _mapNodes.IndexOf(currentSelectedNode);
if (nodeIndex >= 0)
@@ -1162,5 +1277,96 @@ namespace AGVMapEditor.Forms
}
}
#region Multi-Node Selection
private void ShowMultiNodeContextMenu(List<MapNode> nodes)
{
// 다중 선택 시 간단한 다이얼로그 표시
var result = MessageBox.Show(
$"{nodes.Count}개의 노드가 선택되었습니다.\n\n" +
"일괄 속성 변경을 하시겠습니까?\n\n" +
"예: 글자색 변경\n" +
"아니오: 배경색 변경\n" +
"취소: 닫기",
"다중 노드 속성 변경",
MessageBoxButtons.YesNoCancel,
MessageBoxIcon.Question);
if (result == DialogResult.Yes)
{
BatchChangeForeColor();
}
else if (result == DialogResult.No)
{
BatchChangeBackColor();
}
}
/// <summary>
/// 선택된 노드들의 글자색 일괄 변경
/// </summary>
public void BatchChangeForeColor()
{
var selectedNodes = _mapCanvas.SelectedNodes;
if (selectedNodes == null || selectedNodes.Count == 0)
{
MessageBox.Show("선택된 노드가 없습니다.", "알림", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
using (var colorDialog = new ColorDialog())
{
colorDialog.Color = selectedNodes[0].ForeColor;
if (colorDialog.ShowDialog() == DialogResult.OK)
{
foreach (var node in selectedNodes)
{
node.ForeColor = colorDialog.Color;
node.ModifiedDate = DateTime.Now;
}
_hasChanges = true;
UpdateTitle();
RefreshMapCanvas();
MessageBox.Show($"{selectedNodes.Count}개 노드의 글자색이 변경되었습니다.", "완료", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
}
}
/// <summary>
/// 선택된 노드들의 배경색 일괄 변경
/// </summary>
public void BatchChangeBackColor()
{
var selectedNodes = _mapCanvas.SelectedNodes;
if (selectedNodes == null || selectedNodes.Count == 0)
{
MessageBox.Show("선택된 노드가 없습니다.", "알림", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
using (var colorDialog = new ColorDialog())
{
colorDialog.Color = selectedNodes[0].DisplayColor;
if (colorDialog.ShowDialog() == DialogResult.OK)
{
foreach (var node in selectedNodes)
{
node.DisplayColor = colorDialog.Color;
node.ModifiedDate = DateTime.Now;
}
_hasChanges = true;
UpdateTitle();
RefreshMapCanvas();
MessageBox.Show($"{selectedNodes.Count}개 노드의 배경색이 변경되었습니다.", "완료", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
}
}
#endregion
}
}

View File

@@ -4,6 +4,7 @@ using System.ComponentModel;
using System.Drawing;
using System.Drawing.Design;
using AGVNavigationCore.Models;
using AGVNavigationCore.Controls;
namespace AGVMapEditor.Models
{
@@ -521,6 +522,84 @@ namespace AGVMapEditor.Models
}
}
[Category("표시")]
[DisplayName("노드 배경색")]
[Description("노드 배경 색상")]
public Color DisplayColor
{
get => _node.DisplayColor;
set
{
_node.DisplayColor = value;
_node.ModifiedDate = DateTime.Now;
}
}
[Category("표시")]
[DisplayName("글자 색상")]
[Description("노드 텍스트 색상 (NodeId, Name 등)")]
public Color ForeColor
{
get => _node.ForeColor;
set
{
_node.ForeColor = value;
_node.ModifiedDate = DateTime.Now;
}
}
[Category("표시")]
[DisplayName("글자 크기")]
[Description("노드 텍스트 크기 (픽셀)")]
public float TextFontSize
{
get => _node.TextFontSize;
set
{
_node.TextFontSize = Math.Max(5.0f, Math.Min(20.0f, value));
_node.ModifiedDate = DateTime.Now;
}
}
[Category("표시")]
[DisplayName("글자 굵게")]
[Description("노드 텍스트 볼드 표시")]
public bool TextFontBold
{
get => _node.TextFontBold;
set
{
_node.TextFontBold = value;
_node.ModifiedDate = DateTime.Now;
}
}
[Category("표시")]
[DisplayName("이름 말풍선 배경색")]
[Description("노드 이름 말풍선(하단 표시) 배경색")]
public Color NameBubbleBackColor
{
get => _node.NameBubbleBackColor;
set
{
_node.NameBubbleBackColor = value;
_node.ModifiedDate = DateTime.Now;
}
}
[Category("표시")]
[DisplayName("이름 말풍선 글자색")]
[Description("노드 이름 말풍선(하단 표시) 글자색")]
public Color NameBubbleForeColor
{
get => _node.NameBubbleForeColor;
set
{
_node.NameBubbleForeColor = value;
_node.ModifiedDate = DateTime.Now;
}
}
[Category("정보")]
[DisplayName("생성 일시")]
[Description("노드가 생성된 일시")]
@@ -540,5 +619,207 @@ namespace AGVMapEditor.Models
}
}
/// <summary>
/// 다중 노드 선택 시 공통 속성 편집용 래퍼
/// </summary>
public class MultiNodePropertyWrapper
{
private List<MapNode> _nodes;
public MultiNodePropertyWrapper(List<MapNode> nodes)
{
_nodes = nodes ?? new List<MapNode>();
}
[Category("다중 선택")]
[DisplayName("선택된 노드 수")]
[Description("현재 선택된 노드의 개수")]
[ReadOnly(true)]
public int SelectedCount => _nodes.Count;
[Category("표시")]
[DisplayName("노드 배경색")]
[Description("선택된 모든 노드의 배경색을 일괄 변경")]
public Color DisplayColor
{
get => _nodes.Count > 0 ? _nodes[0].DisplayColor : Color.Blue;
set
{
foreach (var node in _nodes)
{
node.DisplayColor = value;
node.ModifiedDate = DateTime.Now;
}
}
}
[Category("표시")]
[DisplayName("글자 색상")]
[Description("선택된 모든 노드의 글자 색상을 일괄 변경")]
public Color ForeColor
{
get => _nodes.Count > 0 ? _nodes[0].ForeColor : Color.Black;
set
{
foreach (var node in _nodes)
{
node.ForeColor = value;
node.ModifiedDate = DateTime.Now;
}
}
}
[Category("표시")]
[DisplayName("글자 크기")]
[Description("선택된 모든 노드의 글자 크기를 일괄 변경 (5~20 픽셀)")]
public float TextFontSize
{
get => _nodes.Count > 0 ? _nodes[0].TextFontSize : 7.0f;
set
{
var validValue = Math.Max(5.0f, Math.Min(20.0f, value));
System.Diagnostics.Debug.WriteLine($"[MultiNode] TextFontSize 변경 시작: {validValue}, 노드 수: {_nodes.Count}");
foreach (var node in _nodes)
{
System.Diagnostics.Debug.WriteLine($" - 노드 {node.NodeId}: {node.TextFontSize} → {validValue}");
node.TextFontSize = validValue;
node.ModifiedDate = DateTime.Now;
System.Diagnostics.Debug.WriteLine($" - 변경 후: {node.TextFontSize}");
}
System.Diagnostics.Debug.WriteLine($"[MultiNode] TextFontSize 변경 완료");
}
}
[Category("표시")]
[DisplayName("글자 굵게")]
[Description("선택된 모든 노드의 글자 굵기를 일괄 변경")]
public bool TextFontBold
{
get
{
var result = _nodes.Count > 0 ? _nodes[0].TextFontBold : true;
System.Diagnostics.Debug.WriteLine($"[MultiNode] TextFontBold GET: {result}");
return result;
}
set
{
System.Diagnostics.Debug.WriteLine($"[MultiNode] TextFontBold 변경 시작: {value}, 노드 수: {_nodes.Count}");
foreach (var node in _nodes)
{
System.Diagnostics.Debug.WriteLine($" - 노드 {node.NodeId}: {node.TextFontBold} → {value}");
node.TextFontBold = value;
node.ModifiedDate = DateTime.Now;
System.Diagnostics.Debug.WriteLine($" - 변경 후: {node.TextFontBold}");
}
System.Diagnostics.Debug.WriteLine($"[MultiNode] TextFontBold 변경 완료");
}
}
[Category("표시")]
[DisplayName("이름 말풍선 배경색")]
[Description("선택된 모든 노드의 이름 말풍선(하단 표시) 배경색을 일괄 변경")]
public Color NameBubbleBackColor
{
get => _nodes.Count > 0 ? _nodes[0].NameBubbleBackColor : Color.Gold;
set
{
foreach (var node in _nodes)
{
node.NameBubbleBackColor = value;
node.ModifiedDate = DateTime.Now;
}
}
}
[Category("표시")]
[DisplayName("이름 말풍선 글자색")]
[Description("선택된 모든 노드의 이름 말풍선(하단 표시) 글자색을 일괄 변경")]
public Color NameBubbleForeColor
{
get => _nodes.Count > 0 ? _nodes[0].NameBubbleForeColor : Color.Black;
set
{
foreach (var node in _nodes)
{
node.NameBubbleForeColor = value;
node.ModifiedDate = DateTime.Now;
}
}
}
[Category("고급")]
[DisplayName("활성화")]
[Description("선택된 모든 노드의 활성화 상태를 일괄 변경")]
public bool IsActive
{
get => _nodes.Count > 0 ? _nodes[0].IsActive : true;
set
{
foreach (var node in _nodes)
{
node.IsActive = value;
node.ModifiedDate = DateTime.Now;
}
}
}
[Category("정보")]
[DisplayName("안내")]
[Description("다중 선택 시 공통 속성만 변경할 수 있습니다")]
[ReadOnly(true)]
public string HelpText => "위 속성을 변경하면 선택된 모든 노드에 일괄 적용됩니다.";
}
/// <summary>
/// 캔버스 속성 래퍼 (배경색 등 캔버스 전체 속성 편집)
/// </summary>
public class CanvasPropertyWrapper
{
private UnifiedAGVCanvas _canvas;
public CanvasPropertyWrapper(UnifiedAGVCanvas canvas)
{
_canvas = canvas;
}
[Category("배경")]
[DisplayName("배경색")]
[Description("맵 캔버스 배경색")]
public Color BackgroundColor
{
get => _canvas.BackColor;
set
{
_canvas.BackColor = value;
_canvas.Invalidate();
}
}
[Category("그리드")]
[DisplayName("그리드 표시")]
[Description("그리드 표시 여부")]
public bool ShowGrid
{
get => _canvas.ShowGrid;
set
{
_canvas.ShowGrid = value;
_canvas.Invalidate();
}
}
[Category("정보")]
[DisplayName("설명")]
[Description("맵 캔버스 전체 속성")]
[ReadOnly(true)]
public string Description
{
get => "빈 공간을 클릭하면 캔버스 전체 속성을 편집할 수 있습니다.";
}
}
}

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

View File

@@ -17,7 +17,7 @@
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<OutputPath>..\..\..\..\..\..\Amkor\AGV4\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>

View File

@@ -51,6 +51,8 @@ namespace AGVSimulator.Forms
this.fileToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.openMapToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.reloadMapToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.SToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.ToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.toolStripSeparator1 = new System.Windows.Forms.ToolStripSeparator();
this.launchMapEditorToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.toolStripSeparator4 = new System.Windows.Forms.ToolStripSeparator();
@@ -65,7 +67,6 @@ namespace AGVSimulator.Forms
this.helpToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.aboutToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this._toolStrip = new System.Windows.Forms.ToolStrip();
this.openMapToolStripButton = new System.Windows.Forms.ToolStripButton();
this.reloadMapToolStripButton = new System.Windows.Forms.ToolStripButton();
this.launchMapEditorToolStripButton = new System.Windows.Forms.ToolStripButton();
this.toolStripSeparator2 = new System.Windows.Forms.ToolStripSeparator();
@@ -79,6 +80,7 @@ namespace AGVSimulator.Forms
this.toolStripSeparator5 = new System.Windows.Forms.ToolStripSeparator();
this.toolStripButton1 = new System.Windows.Forms.ToolStripButton();
this.btPredict = new System.Windows.Forms.ToolStripButton();
this.btMakeMap = new System.Windows.Forms.ToolStripButton();
this._statusStrip = new System.Windows.Forms.StatusStrip();
this._statusLabel = new System.Windows.Forms.ToolStripStatusLabel();
this._coordLabel = new System.Windows.Forms.ToolStripStatusLabel();
@@ -109,12 +111,12 @@ namespace AGVSimulator.Forms
this._addAgvButton = new System.Windows.Forms.Button();
this._agvListCombo = new System.Windows.Forms.ComboBox();
this._canvasPanel = new System.Windows.Forms.Panel();
this.lbPredict = new System.Windows.Forms.RichTextBox();
this._agvInfoPanel = new System.Windows.Forms.Panel();
this._pathDebugLabel = new System.Windows.Forms.TextBox();
this._agvInfoTitleLabel = new System.Windows.Forms.Label();
this._liftDirectionLabel = new System.Windows.Forms.Label();
this._motorDirectionLabel = new System.Windows.Forms.Label();
this.lbPredict = new System.Windows.Forms.RichTextBox();
this.timer1 = new System.Windows.Forms.Timer(this.components);
this._menuStrip.SuspendLayout();
this._toolStrip.SuspendLayout();
@@ -145,6 +147,8 @@ namespace AGVSimulator.Forms
this.fileToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.openMapToolStripMenuItem,
this.reloadMapToolStripMenuItem,
this.SToolStripMenuItem,
this.ToolStripMenuItem,
this.toolStripSeparator1,
this.launchMapEditorToolStripMenuItem,
this.toolStripSeparator4,
@@ -169,6 +173,20 @@ namespace AGVSimulator.Forms
this.reloadMapToolStripMenuItem.Text = "맵 다시열기(&R)";
this.reloadMapToolStripMenuItem.Click += new System.EventHandler(this.OnReloadMap_Click);
//
// 맵저장SToolStripMenuItem
//
this.SToolStripMenuItem.Name = "맵저장SToolStripMenuItem";
this.SToolStripMenuItem.Size = new System.Drawing.Size(221, 22);
this.SToolStripMenuItem.Text = "맵 저장(&S)";
this.SToolStripMenuItem.Click += new System.EventHandler(this.SToolStripMenuItem_Click);
//
// 맵다른이름으로저장ToolStripMenuItem
//
this.ToolStripMenuItem.Name = "맵다른이름으로저장ToolStripMenuItem";
this.ToolStripMenuItem.Size = new System.Drawing.Size(221, 22);
this.ToolStripMenuItem.Text = "맵 다른 이름으로 저장";
this.ToolStripMenuItem.Click += new System.EventHandler(this.btMapSaveAs_Click);
//
// toolStripSeparator1
//
this.toolStripSeparator1.Name = "toolStripSeparator1";
@@ -272,7 +290,6 @@ namespace AGVSimulator.Forms
// _toolStrip
//
this._toolStrip.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.openMapToolStripButton,
this.reloadMapToolStripButton,
this.launchMapEditorToolStripButton,
this.toolStripSeparator2,
@@ -285,22 +302,14 @@ namespace AGVSimulator.Forms
this.resetZoomToolStripButton,
this.toolStripSeparator5,
this.toolStripButton1,
this.btPredict});
this.btPredict,
this.btMakeMap});
this._toolStrip.Location = new System.Drawing.Point(0, 24);
this._toolStrip.Name = "_toolStrip";
this._toolStrip.Size = new System.Drawing.Size(1034, 25);
this._toolStrip.TabIndex = 1;
this._toolStrip.Text = "toolStrip";
//
// openMapToolStripButton
//
this.openMapToolStripButton.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
this.openMapToolStripButton.Name = "openMapToolStripButton";
this.openMapToolStripButton.Size = new System.Drawing.Size(51, 22);
this.openMapToolStripButton.Text = "맵 열기";
this.openMapToolStripButton.ToolTipText = "맵 파일을 엽니다";
this.openMapToolStripButton.Click += new System.EventHandler(this.OnOpenMap_Click);
//
// reloadMapToolStripButton
//
this.reloadMapToolStripButton.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
@@ -406,6 +415,15 @@ namespace AGVSimulator.Forms
this.btPredict.Text = "다음 행동 예측";
this.btPredict.Click += new System.EventHandler(this.btPredict_Click);
//
// btMakeMap
//
this.btMakeMap.Image = ((System.Drawing.Image)(resources.GetObject("btMakeMap.Image")));
this.btMakeMap.ImageTransparentColor = System.Drawing.Color.Magenta;
this.btMakeMap.Name = "btMakeMap";
this.btMakeMap.Size = new System.Drawing.Size(63, 22);
this.btMakeMap.Text = "맵기록";
this.btMakeMap.Click += new System.EventHandler(this.btMakeMap_Click);
//
// _statusStrip
//
this._statusStrip.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
@@ -701,6 +719,15 @@ namespace AGVSimulator.Forms
this._canvasPanel.Size = new System.Drawing.Size(801, 560);
this._canvasPanel.TabIndex = 4;
//
// lbPredict
//
this.lbPredict.Dock = System.Windows.Forms.DockStyle.Bottom;
this.lbPredict.Location = new System.Drawing.Point(0, 513);
this.lbPredict.Name = "lbPredict";
this.lbPredict.Size = new System.Drawing.Size(801, 47);
this.lbPredict.TabIndex = 0;
this.lbPredict.Text = "";
//
// _agvInfoPanel
//
this._agvInfoPanel.BackColor = System.Drawing.Color.LightBlue;
@@ -757,15 +784,6 @@ namespace AGVSimulator.Forms
this._motorDirectionLabel.TabIndex = 2;
this._motorDirectionLabel.Text = "모터 방향: -";
//
// lbPredict
//
this.lbPredict.Dock = System.Windows.Forms.DockStyle.Bottom;
this.lbPredict.Location = new System.Drawing.Point(0, 513);
this.lbPredict.Name = "lbPredict";
this.lbPredict.Size = new System.Drawing.Size(801, 47);
this.lbPredict.TabIndex = 0;
this.lbPredict.Text = "";
//
// timer1
//
this.timer1.Interval = 500;
@@ -825,7 +843,6 @@ namespace AGVSimulator.Forms
private System.Windows.Forms.ToolStripMenuItem helpToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem aboutToolStripMenuItem;
private System.Windows.Forms.ToolStrip _toolStrip;
private System.Windows.Forms.ToolStripButton openMapToolStripButton;
private System.Windows.Forms.ToolStripSeparator toolStripSeparator2;
private System.Windows.Forms.ToolStripButton startSimulationToolStripButton;
private System.Windows.Forms.ToolStripButton stopSimulationToolStripButton;
@@ -879,5 +896,8 @@ namespace AGVSimulator.Forms
private System.Windows.Forms.ToolStripButton btPredict;
private System.Windows.Forms.RichTextBox lbPredict;
private System.Windows.Forms.Timer timer1;
private System.Windows.Forms.ToolStripButton btMakeMap;
private System.Windows.Forms.ToolStripMenuItem SToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem ToolStripMenuItem;
}
}

View File

@@ -35,6 +35,13 @@ namespace AGVSimulator.Forms
private string _currentMapFilePath;
private bool _isTargetCalcMode; // 타겟계산 모드 상태
// 맵 스캔 모드 관련
private bool _isMapScanMode; // 맵 스캔 모드 상태
private DateTime _lastNodeAddTime; // 마지막 노드 추가 시간
private MapNode _lastScannedNode; // 마지막으로 스캔된 노드
private int _scanNodeCounter; // 스캔 노드 카운터
private AgvDirection _lastScanDirection; // 마지막 스캔 방향
// UI Controls - Designer에서 생성됨
#endregion
@@ -238,7 +245,8 @@ namespace AGVSimulator.Forms
UpdateAGVComboBox();
UpdateUI();
_statusLabel.Text = $"{agvId} 추가됨";
_statusLabel.Text = $"{agvId} 추가됨";
_simulatorCanvas.FitToNodes();
}
private void OnRemoveAGV_Click(object sender, EventArgs e)
@@ -508,6 +516,165 @@ namespace AGVSimulator.Forms
return closestNode;
}
/// <summary>
/// 방향을 기호로 변환
/// </summary>
private string GetDirectionSymbol(AgvDirection direction)
{
switch (direction)
{
case AgvDirection.Forward: return "→";
case AgvDirection.Backward: return "←";
case AgvDirection.Left: return "↺";
case AgvDirection.Right: return "↻";
default: return "-";
}
}
/// <summary>
/// 맵 스캔 모드에서 RFID로부터 노드 생성
/// </summary>
private void CreateNodeFromRfidScan(string rfidId, VirtualAGV selectedAGV)
{
try
{
// 현재 선택된 방향 확인 (최상단에서 먼저 확인)
var directionItem = _directionCombo.SelectedItem as DirectionItem;
var currentDirection = directionItem?.Direction ?? AgvDirection.Forward;
// 중복 RFID 확인
var existingNode = _mapNodes?.FirstOrDefault(n => n.RfidId.Equals(rfidId, StringComparison.OrdinalIgnoreCase));
if (existingNode != null)
{
// 이미 존재하는 노드로 이동
Program.WriteLine($"[맵 스캔] RFID '{rfidId}'는 이미 존재합니다 (노드: {existingNode.NodeId})");
// 기존 노드로 AGV 위치 설정
_simulatorCanvas.SetAGVPosition(selectedAGV.AgvId, existingNode, currentDirection);
selectedAGV.SetPosition(existingNode, currentDirection);
_lastScannedNode = existingNode;
_lastNodeAddTime = DateTime.Now;
_lastScanDirection = currentDirection; // 방향 업데이트
_statusLabel.Text = $"기존 노드로 이동: {existingNode.NodeId} [{GetDirectionSymbol(currentDirection)}]";
_rfidTextBox.Text = "";
return;
}
// 새 노드 생성 위치 계산
int newX = 100; // 기본 시작 X 위치
int newY = 300; // 기본 시작 Y 위치
if (_lastScannedNode != null)
{
// 시간차 기반 X축 거리 계산
var timeDiff = (DateTime.Now - _lastNodeAddTime).TotalSeconds;
// 10초당 10px, 최소 50px, 최대 100px
int distanceX = Math.Max(50, Math.Min(100, (int)(timeDiff * 10)));
// 방향 전환 확인
bool directionChanged = (_lastScanDirection != currentDirection);
if (directionChanged)
{
// 방향이 바뀌면 Y축을 50px 증가시켜서 겹치지 않게 함
newY = _lastScannedNode.Position.Y + 50;
newX = _lastScannedNode.Position.X; // X는 같은 위치에서 시작
Program.WriteLine($"[맵 스캔] 방향 전환: {_lastScanDirection} → {currentDirection}, Y축 +50px");
}
else
{
// 방향이 같으면 Y축 유지
newY = _lastScannedNode.Position.Y;
// 모터 방향에 따라 X축 증가/감소
if (currentDirection == AgvDirection.Forward)
{
// 전진: X축 증가
newX = _lastScannedNode.Position.X + distanceX;
Program.WriteLine($"[맵 스캔] 전진 모드: X축 +{distanceX}px");
}
else if (currentDirection == AgvDirection.Backward)
{
// 후진: X축 감소
newX = _lastScannedNode.Position.X - distanceX;
Program.WriteLine($"[맵 스캔] 후진 모드: X축 -{distanceX}px");
}
else
{
// 그 외(회전 등): 기본적으로 전진 방향 사용
newX = _lastScannedNode.Position.X + distanceX;
Program.WriteLine($"[맵 스캔] 기타 방향({currentDirection}): X축 +{distanceX}px");
}
}
Program.WriteLine($"[맵 스캔] 시간차: {timeDiff:F1}초 → 거리: {distanceX}px");
}
// 새 노드 생성
var newNodeId = $"{_scanNodeCounter:D3}";
var newNode = new MapNode
{
NodeId = newNodeId,
RfidId = rfidId,
Position = new Point(newX, newY),
Type = NodeType.Normal,
IsActive = true,
Name = $"N{_scanNodeCounter}"
};
// 맵에 추가
if (_mapNodes == null)
_mapNodes = new List<MapNode>();
_mapNodes.Add(newNode);
// 이전 노드와 연결 생성
if (_lastScannedNode != null)
{
// 양방향 연결 (ConnectedNodes에 추가 - JSON 저장됨)
_lastScannedNode.AddConnection(newNode.NodeId);
newNode.AddConnection(_lastScannedNode.NodeId);
Program.WriteLine($"[맵 스캔] 연결 생성: {_lastScannedNode.NodeId} ↔ {newNode.NodeId}");
}
// AGV 위치 설정
_simulatorCanvas.SetAGVPosition(selectedAGV.AgvId, newNode, currentDirection);
selectedAGV.SetPosition(newNode, currentDirection);
// 캔버스 업데이트
_simulatorCanvas.Nodes = _mapNodes;
// 화면을 새 노드 위치로 이동
_simulatorCanvas.PanToNode(newNode.NodeId);
_simulatorCanvas.Invalidate();
// 상태 업데이트
_lastScannedNode = newNode;
_lastNodeAddTime = DateTime.Now;
_lastScanDirection = currentDirection; // 현재 방향 저장
_scanNodeCounter++;
// UI 업데이트
UpdateNodeComboBoxes();
_statusLabel.Text = $"노드 생성: {newNode.NodeId} (RFID: {rfidId}) [{GetDirectionSymbol(currentDirection)}] - 총 {_mapNodes.Count}개";
_rfidTextBox.Text = "";
Program.WriteLine($"[맵 스캔] 노드 생성 완료: {newNode.NodeId} (RFID: {rfidId}) at ({newX}, {newY}), 방향: {currentDirection}");
}
catch (Exception ex)
{
MessageBox.Show($"노드 생성 중 오류 발생:\n{ex.Message}", "오류",
MessageBoxButtons.OK, MessageBoxIcon.Error);
Program.WriteLine($"[맵 스캔 오류] {ex.Message}");
}
}
private void OnSetPosition_Click(object sender, EventArgs e)
{
SetAGVPositionByRfid();
@@ -561,6 +728,19 @@ namespace AGVSimulator.Forms
MessageBox.Show("RFID 값을 입력해주세요.", "알림", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
// 선택된 방향 확인
var selectedDirectionItem = _directionCombo.SelectedItem as DirectionItem;
var selectedDirection = selectedDirectionItem?.Direction ?? AgvDirection.Forward;
// 맵 스캔 모드일 때: 노드 자동 생성
if (_isMapScanMode)
{
CreateNodeFromRfidScan(rfidId, selectedAGV);
this._simulatorCanvas.FitToNodes();
return;
}
// RFID에 해당하는 노드 직접 찾기
var targetNode = _mapNodes?.FirstOrDefault(n => n.RfidId.Equals(rfidId, StringComparison.OrdinalIgnoreCase));
@@ -571,10 +751,7 @@ namespace AGVSimulator.Forms
return;
}
// 선택된 방향 확인
var selectedDirectionItem = _directionCombo.SelectedItem as DirectionItem;
var selectedDirection = selectedDirectionItem?.Direction ?? AgvDirection.Forward;
//이전위치와 동일한지 체크한다.
if (selectedAGV.CurrentNodeId == targetNode.NodeId && selectedAGV.CurrentDirection == selectedDirection)
{
@@ -1625,6 +1802,184 @@ namespace AGVSimulator.Forms
var command = agv.Predict();
this.lbPredict.Text = $"Motor:{command.Motor},Magnet:{command.Magnet},Speed:{command.Speed} : {command.Reason}";
}
private void btMakeMap_Click(object sender, EventArgs e)
{
if (!_isMapScanMode)
{
// 스캔 모드 시작
var result = MessageBox.Show(
"맵 스캔 모드를 시작합니다.\n\n" +
"RFID를 입력하면 자동으로 맵 노드가 생성되고\n" +
"이전 노드와 연결됩니다.\n\n" +
"기존 맵 데이터를 삭제하고 시작하시겠습니까?\n\n" +
"예: 새 맵 시작\n" +
"아니오: 기존 맵에 추가",
"맵 스캔 모드",
MessageBoxButtons.YesNoCancel,
MessageBoxIcon.Question);
if (result == DialogResult.Cancel)
return;
if (result == DialogResult.Yes)
{
// 기존 맵 데이터 삭제
_mapNodes?.Clear();
_mapNodes = new List<MapNode>();
_simulatorCanvas.Nodes = _mapNodes;
_currentMapFilePath = string.Empty;
UpdateNodeComboBoxes();
_statusLabel.Text = "맵 초기화 완료 - 스캔 모드 시작";
}
// 스캔 모드 활성화
_isMapScanMode = true;
_lastNodeAddTime = DateTime.Now;
_lastScannedNode = null;
_scanNodeCounter = 1;
_lastScanDirection = AgvDirection.Forward; // 기본 방향은 전진
btMakeMap.Text = "스캔 중지";
btMakeMap.BackColor = Color.LightCoral;
_statusLabel.Text = "맵 스캔 모드: RFID를 입력하여 노드를 생성하세요";
Program.WriteLine("[맵 스캔] 스캔 모드 시작");
}
else
{
// 스캔 모드 종료
_isMapScanMode = false;
btMakeMap.Text = "맵 생성";
btMakeMap.BackColor = SystemColors.Control;
_statusLabel.Text = $"맵 스캔 완료 - {_mapNodes?.Count ?? 0}개 노드 생성됨";
Program.WriteLine($"[맵 스캔] 스캔 모드 종료 - 총 {_mapNodes?.Count ?? 0}개 노드");
// 맵 저장 권장
if (_mapNodes != null && _mapNodes.Count > 0)
{
var saveResult = MessageBox.Show(
$"맵 스캔이 완료되었습니다.\n\n" +
$"생성된 노드: {_mapNodes.Count}개\n\n" +
"맵을 저장하시겠습니까?",
"맵 저장",
MessageBoxButtons.YesNo,
MessageBoxIcon.Question);
if (saveResult == DialogResult.Yes)
{
btMapSaveAs_Click(sender, e);
}
}
}
}
/// <summary>
/// 맵 데이터를 파일에 저장 (MapLoader 공통 저장 로직 사용)
/// </summary>
private void SaveMapToFile(string filePath)
{
try
{
// MapLoader의 표준 저장 메서드 사용 (AGVMapEditor와 동일한 형식)
bool success = MapLoader.SaveMapToFile(filePath, _mapNodes);
if (success)
{
Program.WriteLine($"[맵 저장] 파일 저장 완료: {filePath} ({_mapNodes.Count}개 노드)");
}
else
{
throw new InvalidOperationException("맵 저장에 실패했습니다.");
}
}
catch (Exception ex)
{
Program.WriteLine($"[맵 저장 오류] {ex.Message}");
throw;
}
}
private void btMapSaveAs_Click(object sender, EventArgs e)
{
// 맵 데이터 확인
if (_mapNodes == null || _mapNodes.Count == 0)
{
MessageBox.Show("저장할 맵 데이터가 없습니다.", "알림",
MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
using (var saveDialog = new SaveFileDialog())
{
saveDialog.Filter = "AGV Map Files (*.agvmap)|*.agvmap|모든 파일 (*.*)|*.*";
saveDialog.Title = "맵 파일 저장";
saveDialog.DefaultExt = "agvmap";
// 현재 파일이 있으면 기본 파일명으로 설정
if (!string.IsNullOrEmpty(_currentMapFilePath))
{
saveDialog.FileName = Path.GetFileName(_currentMapFilePath);
saveDialog.InitialDirectory = Path.GetDirectoryName(_currentMapFilePath);
}
else
{
// 기본 파일명: 날짜_시간 형식
saveDialog.FileName = $"ScanMap_{DateTime.Now:yyyyMMdd_HHmmss}.agvmap";
}
if (saveDialog.ShowDialog() == DialogResult.OK)
{
try
{
SaveMapToFile(saveDialog.FileName);
_currentMapFilePath = saveDialog.FileName;
// 설정에 마지막 맵 파일 경로 저장
_config.LastMapFilePath = _currentMapFilePath;
if (_config.AutoSave)
{
_config.Save();
}
_statusLabel.Text = $"맵 저장 완료: {Path.GetFileName(_currentMapFilePath)}";
MessageBox.Show($"맵이 저장되었습니다.\n\n파일: {_currentMapFilePath}", "저장 완료",
MessageBoxButtons.OK, MessageBoxIcon.Information);
}
catch (Exception ex)
{
MessageBox.Show($"맵 저장 중 오류 발생:\n{ex.Message}", "저장 오류",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
}
}
private void SToolStripMenuItem_Click(object sender, EventArgs e)
{
// 현재 맵 파일 경로가 있는 경우 해당 파일에 저장
if (string.IsNullOrEmpty(_currentMapFilePath))
{
// 경로가 없으면 다른 이름으로 저장 다이얼로그 표시
btMapSaveAs_Click(sender, e);
return;
}
try
{
SaveMapToFile(_currentMapFilePath);
_statusLabel.Text = $"맵 저장 완료: {Path.GetFileName(_currentMapFilePath)}";
MessageBox.Show($"맵이 저장되었습니다.\n\n파일: {_currentMapFilePath}", "저장 완료",
MessageBoxButtons.OK, MessageBoxIcon.Information);
}
catch (Exception ex)
{
MessageBox.Show($"맵 저장 중 오류 발생:\n{ex.Message}", "저장 오류",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
}
/// <summary>

View File

@@ -152,6 +152,21 @@
HBUzHot52djqQ6HZhfR7IwK4mKpHtvEDMqvfCiQ6zaAAXM8x94aIWTNrLLG4kVUzgaTSPlzLtyJOZxbb
1wtfyg4Q+AfA3aZlButjSfxGcUJBk4g5tuP3haQKRKXcUQDOmbvNTpPOJeFFjordZmbWTNvMTHFUcpUC
nOccAdABIDXXE1nzAAAAAElFTkSuQmCC
</value>
</data>
<data name="btMakeMap.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>
iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8
YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAIFSURBVDhPpZLtS1NhGMbPPxJmmlYSgqHiKzGU1EDxg4iK
YKyG2WBogqMYJQOtCEVRFBGdTBCJfRnkS4VaaWNT5sqx1BUxRXxDHYxAJLvkusEeBaPAB+5z4Jzn+t3X
/aLhnEfjo8m+dCoa+7/C3O2Hqe0zDC+8KG+cRZHZhdzaaWTVTCLDMIY0vfM04Nfh77/G/sEhwpEDbO3t
I7TxE8urEVy99fT/AL5gWDLrTB/hnF4XsW0khCu5ln8DmJliT2AXrcNBsU1gj/MH4nMeKwBrPktM28xM
cX79DFKrHHD5d9D26hvicx4pABt2lpg10zYzU0zr7+e3xXGcrkEB2O2TNec9nJFwB3alZn5jZorfeDZh
6Q3g8s06BeCoKF4MRURoH1+BY2oNCbeb0TIclIYxOhzf8frTOuo7FxCbbVIAzpni0iceEc8vhzEwGkJD
lx83ymxifejdKjRNk/8PWnyIyTQqAJek0jqHwfEVscu31baIu8+90sTE4nY025dQ2/5FIPpnXlzKuK8A
HBUzHot52djqQ6HZhfR7IwK4mKpHtvEDMqvfCiQ6zaAAXM8x94aIWTNrLLG4kVUzgaTSPlzLtyJOZxbb
1wtfyg4Q+AfA3aZlButjSfxGcUJBk4g5tuP3haQKRKXcUQDOmbvNTpPOJeFFjordZmbWTNvMTHFUcpUC
nOccAdABIDXXE1nzAAAAAElFTkSuQmCC
</value>
</data>
<metadata name="_statusStrip.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">