fix: RFID duplicate validation and correct magnet direction calculation

- Add real-time RFID duplicate validation in map editor with automatic rollback
- Remove RFID auto-assignment to maintain data consistency between editor and simulator
- Fix magnet direction calculation to use actual forward direction angles instead of arbitrary assignment
- Add node names to simulator combo boxes for better identification
- Improve UI layout by drawing connection lines before text for better visibility

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
ChiKyun Kim
2025-09-15 16:31:40 +09:00
parent 1add9ed59a
commit 7f48253770
41 changed files with 4827 additions and 3649 deletions

View File

@@ -28,113 +28,38 @@ namespace AGVMapEditor.Forms
/// </summary>
private void InitializeComponent()
{
this.menuStrip1 = new System.Windows.Forms.MenuStrip();
this.fileToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.newToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.openToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.toolStripSeparator1 = new System.Windows.Forms.ToolStripSeparator();
this.saveToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.saveAsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.closeToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.toolStripSeparator2 = new System.Windows.Forms.ToolStripSeparator();
this.exitToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(MainForm));
this.statusStrip1 = new System.Windows.Forms.StatusStrip();
this.toolStripStatusLabel1 = new System.Windows.Forms.ToolStripStatusLabel();
this.splitContainer1 = new System.Windows.Forms.SplitContainer();
this.tabControl1 = new System.Windows.Forms.TabControl();
this.tabPageNodes = new System.Windows.Forms.TabPage();
this.listBoxNodes = new System.Windows.Forms.ListBox();
this._propertyGrid = new System.Windows.Forms.PropertyGrid();
this.label1 = new System.Windows.Forms.Label();
this.menuStrip1.SuspendLayout();
this.tabPage1 = new System.Windows.Forms.TabPage();
this.lstNodeConnection = new System.Windows.Forms.ListBox();
this.toolStrip1 = new System.Windows.Forms.ToolStrip();
this.btNodeRemove = new System.Windows.Forms.ToolStripButton();
this._propertyGrid = new System.Windows.Forms.PropertyGrid();
this.toolStrip2 = new System.Windows.Forms.ToolStrip();
this.btnNew = new System.Windows.Forms.ToolStripButton();
this.btnOpen = new System.Windows.Forms.ToolStripButton();
this.btnReopen = new System.Windows.Forms.ToolStripButton();
this.btnClose = new System.Windows.Forms.ToolStripButton();
this.toolStripSeparator3 = new System.Windows.Forms.ToolStripSeparator();
this.btnSave = new System.Windows.Forms.ToolStripButton();
this.btnSaveAs = new System.Windows.Forms.ToolStripButton();
this.statusStrip1.SuspendLayout();
((System.ComponentModel.ISupportInitialize)(this.splitContainer1)).BeginInit();
this.splitContainer1.Panel1.SuspendLayout();
this.splitContainer1.SuspendLayout();
this.tabControl1.SuspendLayout();
this.tabPageNodes.SuspendLayout();
this.tabPage1.SuspendLayout();
this.toolStrip1.SuspendLayout();
this.toolStrip2.SuspendLayout();
this.SuspendLayout();
//
// menuStrip1
//
this.menuStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.fileToolStripMenuItem});
this.menuStrip1.Location = new System.Drawing.Point(0, 0);
this.menuStrip1.Name = "menuStrip1";
this.menuStrip1.Size = new System.Drawing.Size(1200, 24);
this.menuStrip1.TabIndex = 0;
this.menuStrip1.Text = "menuStrip1";
//
// fileToolStripMenuItem
//
this.fileToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.newToolStripMenuItem,
this.openToolStripMenuItem,
this.closeToolStripMenuItem,
this.toolStripSeparator1,
this.saveToolStripMenuItem,
this.saveAsToolStripMenuItem,
this.toolStripSeparator2,
this.exitToolStripMenuItem});
this.fileToolStripMenuItem.Name = "fileToolStripMenuItem";
this.fileToolStripMenuItem.Size = new System.Drawing.Size(57, 20);
this.fileToolStripMenuItem.Text = "파일(&F)";
//
// newToolStripMenuItem
//
this.newToolStripMenuItem.Name = "newToolStripMenuItem";
this.newToolStripMenuItem.ShortcutKeys = ((System.Windows.Forms.Keys)((System.Windows.Forms.Keys.Control | System.Windows.Forms.Keys.N)));
this.newToolStripMenuItem.Size = new System.Drawing.Size(198, 22);
this.newToolStripMenuItem.Text = "새로 만들기(&N)";
this.newToolStripMenuItem.Click += new System.EventHandler(this.newToolStripMenuItem_Click);
//
// openToolStripMenuItem
//
this.openToolStripMenuItem.Name = "openToolStripMenuItem";
this.openToolStripMenuItem.ShortcutKeys = ((System.Windows.Forms.Keys)((System.Windows.Forms.Keys.Control | System.Windows.Forms.Keys.O)));
this.openToolStripMenuItem.Size = new System.Drawing.Size(198, 22);
this.openToolStripMenuItem.Text = "열기(&O)";
this.openToolStripMenuItem.Click += new System.EventHandler(this.openToolStripMenuItem_Click);
//
// toolStripSeparator1
//
this.toolStripSeparator1.Name = "toolStripSeparator1";
this.toolStripSeparator1.Size = new System.Drawing.Size(195, 6);
//
// saveToolStripMenuItem
//
this.saveToolStripMenuItem.Name = "saveToolStripMenuItem";
this.saveToolStripMenuItem.ShortcutKeys = ((System.Windows.Forms.Keys)((System.Windows.Forms.Keys.Control | System.Windows.Forms.Keys.S)));
this.saveToolStripMenuItem.Size = new System.Drawing.Size(198, 22);
this.saveToolStripMenuItem.Text = "저장(&S)";
this.saveToolStripMenuItem.Click += new System.EventHandler(this.saveToolStripMenuItem_Click);
//
// saveAsToolStripMenuItem
//
this.saveAsToolStripMenuItem.Name = "saveAsToolStripMenuItem";
this.saveAsToolStripMenuItem.Size = new System.Drawing.Size(198, 22);
this.saveAsToolStripMenuItem.Text = "다른 이름으로 저장(&A)";
this.saveAsToolStripMenuItem.Click += new System.EventHandler(this.saveAsToolStripMenuItem_Click);
//
// closeToolStripMenuItem
//
this.closeToolStripMenuItem.Name = "closeToolStripMenuItem";
this.closeToolStripMenuItem.Size = new System.Drawing.Size(198, 22);
this.closeToolStripMenuItem.Text = "닫기(&C)";
this.closeToolStripMenuItem.Click += new System.EventHandler(this.closeToolStripMenuItem_Click);
//
// toolStripSeparator2
//
this.toolStripSeparator2.Name = "toolStripSeparator2";
this.toolStripSeparator2.Size = new System.Drawing.Size(195, 6);
//
// exitToolStripMenuItem
//
this.exitToolStripMenuItem.Name = "exitToolStripMenuItem";
this.exitToolStripMenuItem.Size = new System.Drawing.Size(198, 22);
this.exitToolStripMenuItem.Text = "종료(&X)";
this.exitToolStripMenuItem.Click += new System.EventHandler(this.exitToolStripMenuItem_Click);
//
// statusStrip1
//
this.statusStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
@@ -154,36 +79,37 @@ namespace AGVMapEditor.Forms
// splitContainer1
//
this.splitContainer1.Dock = System.Windows.Forms.DockStyle.Fill;
this.splitContainer1.Location = new System.Drawing.Point(0, 24);
this.splitContainer1.Location = new System.Drawing.Point(0, 25);
this.splitContainer1.Name = "splitContainer1";
//
// splitContainer1.Panel1
//
this.splitContainer1.Panel1.Controls.Add(this.tabControl1);
this.splitContainer1.Panel1.Controls.Add(this._propertyGrid);
this.splitContainer1.Panel1MinSize = 300;
this.splitContainer1.Size = new System.Drawing.Size(1200, 727);
this.splitContainer1.Size = new System.Drawing.Size(1200, 726);
this.splitContainer1.SplitterDistance = 300;
this.splitContainer1.TabIndex = 2;
//
// tabControl1
//
this.tabControl1.Controls.Add(this.tabPageNodes);
this.tabControl1.Controls.Add(this.tabPage1);
this.tabControl1.Dock = System.Windows.Forms.DockStyle.Fill;
this.tabControl1.Location = new System.Drawing.Point(0, 0);
this.tabControl1.Name = "tabControl1";
this.tabControl1.SelectedIndex = 0;
this.tabControl1.Size = new System.Drawing.Size(300, 727);
this.tabControl1.Size = new System.Drawing.Size(300, 335);
this.tabControl1.TabIndex = 0;
//
// tabPageNodes
//
this.tabPageNodes.Controls.Add(this.listBoxNodes);
this.tabPageNodes.Controls.Add(this._propertyGrid);
this.tabPageNodes.Controls.Add(this.label1);
this.tabPageNodes.Location = new System.Drawing.Point(4, 22);
this.tabPageNodes.Name = "tabPageNodes";
this.tabPageNodes.Padding = new System.Windows.Forms.Padding(3);
this.tabPageNodes.Size = new System.Drawing.Size(292, 701);
this.tabPageNodes.Size = new System.Drawing.Size(292, 309);
this.tabPageNodes.TabIndex = 0;
this.tabPageNodes.Text = "노드 관리";
this.tabPageNodes.UseVisualStyleBackColor = true;
@@ -195,17 +121,9 @@ namespace AGVMapEditor.Forms
this.listBoxNodes.ItemHeight = 12;
this.listBoxNodes.Location = new System.Drawing.Point(3, 3);
this.listBoxNodes.Name = "listBoxNodes";
this.listBoxNodes.Size = new System.Drawing.Size(286, 245);
this.listBoxNodes.Size = new System.Drawing.Size(286, 303);
this.listBoxNodes.TabIndex = 1;
//
// _propertyGrid
//
this._propertyGrid.Dock = System.Windows.Forms.DockStyle.Bottom;
this._propertyGrid.Location = new System.Drawing.Point(3, 248);
this._propertyGrid.Name = "_propertyGrid";
this._propertyGrid.Size = new System.Drawing.Size(286, 450);
this._propertyGrid.TabIndex = 6;
//
// label1
//
this.label1.AutoSize = true;
@@ -215,6 +133,132 @@ namespace AGVMapEditor.Forms
this.label1.TabIndex = 0;
this.label1.Text = "노드 목록";
//
// tabPage1
//
this.tabPage1.Controls.Add(this.lstNodeConnection);
this.tabPage1.Controls.Add(this.toolStrip1);
this.tabPage1.Location = new System.Drawing.Point(4, 22);
this.tabPage1.Name = "tabPage1";
this.tabPage1.Padding = new System.Windows.Forms.Padding(3);
this.tabPage1.Size = new System.Drawing.Size(292, 310);
this.tabPage1.TabIndex = 1;
this.tabPage1.Text = "연결 관리";
this.tabPage1.UseVisualStyleBackColor = true;
//
// lstNodeConnection
//
this.lstNodeConnection.Dock = System.Windows.Forms.DockStyle.Fill;
this.lstNodeConnection.FormattingEnabled = true;
this.lstNodeConnection.ItemHeight = 12;
this.lstNodeConnection.Location = new System.Drawing.Point(3, 3);
this.lstNodeConnection.Name = "lstNodeConnection";
this.lstNodeConnection.Size = new System.Drawing.Size(286, 279);
this.lstNodeConnection.TabIndex = 2;
//
// toolStrip1
//
this.toolStrip1.Dock = System.Windows.Forms.DockStyle.Bottom;
this.toolStrip1.GripStyle = System.Windows.Forms.ToolStripGripStyle.Hidden;
this.toolStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.btNodeRemove});
this.toolStrip1.Location = new System.Drawing.Point(3, 282);
this.toolStrip1.Name = "toolStrip1";
this.toolStrip1.Size = new System.Drawing.Size(286, 25);
this.toolStrip1.TabIndex = 3;
this.toolStrip1.Text = "toolStrip1";
//
// btNodeRemove
//
this.btNodeRemove.Image = ((System.Drawing.Image)(resources.GetObject("btNodeRemove.Image")));
this.btNodeRemove.ImageTransparentColor = System.Drawing.Color.Magenta;
this.btNodeRemove.Name = "btNodeRemove";
this.btNodeRemove.Size = new System.Drawing.Size(70, 22);
this.btNodeRemove.Text = "Remove";
this.btNodeRemove.Click += new System.EventHandler(this.btNodeRemove_Click);
//
// _propertyGrid
//
this._propertyGrid.Dock = System.Windows.Forms.DockStyle.Bottom;
this._propertyGrid.Location = new System.Drawing.Point(0, 335);
this._propertyGrid.Name = "_propertyGrid";
this._propertyGrid.Size = new System.Drawing.Size(300, 391);
this._propertyGrid.TabIndex = 6;
//
// toolStrip2
//
this.toolStrip2.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.btnNew,
this.btnOpen,
this.btnReopen,
this.btnClose,
this.toolStripSeparator3,
this.btnSave,
this.btnSaveAs});
this.toolStrip2.Location = new System.Drawing.Point(0, 0);
this.toolStrip2.Name = "toolStrip2";
this.toolStrip2.Size = new System.Drawing.Size(1200, 25);
this.toolStrip2.TabIndex = 0;
this.toolStrip2.Text = "toolStrip2";
//
// btnNew
//
this.btnNew.Image = ((System.Drawing.Image)(resources.GetObject("btnNew.Image")));
this.btnNew.Name = "btnNew";
this.btnNew.Size = new System.Drawing.Size(104, 22);
this.btnNew.Text = "새로만들기(&N)";
this.btnNew.ToolTipText = "새로 만들기 (Ctrl+N)";
this.btnNew.Click += new System.EventHandler(this.btnNew_Click);
//
// btnOpen
//
this.btnOpen.Image = ((System.Drawing.Image)(resources.GetObject("btnOpen.Image")));
this.btnOpen.Name = "btnOpen";
this.btnOpen.Size = new System.Drawing.Size(68, 22);
this.btnOpen.Text = "열기(&O)";
this.btnOpen.ToolTipText = "열기 (Ctrl+O)";
this.btnOpen.Click += new System.EventHandler(this.btnOpen_Click);
//
// btnReopen
//
this.btnReopen.Image = ((System.Drawing.Image)(resources.GetObject("btnReopen.Image")));
this.btnReopen.Name = "btnReopen";
this.btnReopen.Size = new System.Drawing.Size(90, 22);
this.btnReopen.Text = "다시열기(&R)";
this.btnReopen.ToolTipText = "현재 파일 다시 열기";
this.btnReopen.Click += new System.EventHandler(this.btnReopen_Click);
//
// btnClose
//
this.btnClose.Image = ((System.Drawing.Image)(resources.GetObject("btnClose.Image")));
this.btnClose.Name = "btnClose";
this.btnClose.Size = new System.Drawing.Size(75, 22);
this.btnClose.Text = "파일닫기";
this.btnClose.ToolTipText = "닫기";
this.btnClose.Click += new System.EventHandler(this.btnClose_Click);
//
// toolStripSeparator3
//
this.toolStripSeparator3.Name = "toolStripSeparator3";
this.toolStripSeparator3.Size = new System.Drawing.Size(6, 25);
//
// btnSave
//
this.btnSave.Image = ((System.Drawing.Image)(resources.GetObject("btnSave.Image")));
this.btnSave.Name = "btnSave";
this.btnSave.Size = new System.Drawing.Size(66, 22);
this.btnSave.Text = "저장(&S)";
this.btnSave.ToolTipText = "저장 (Ctrl+S)";
this.btnSave.Click += new System.EventHandler(this.btnSave_Click);
//
// btnSaveAs
//
this.btnSaveAs.Image = ((System.Drawing.Image)(resources.GetObject("btnSaveAs.Image")));
this.btnSaveAs.Name = "btnSaveAs";
this.btnSaveAs.Size = new System.Drawing.Size(123, 22);
this.btnSaveAs.Text = "다른이름으로저장";
this.btnSaveAs.ToolTipText = "다른 이름으로 저장";
this.btnSaveAs.Click += new System.EventHandler(this.btnSaveAs_Click);
//
// MainForm
//
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 12F);
@@ -222,15 +266,12 @@ namespace AGVMapEditor.Forms
this.ClientSize = new System.Drawing.Size(1200, 773);
this.Controls.Add(this.splitContainer1);
this.Controls.Add(this.statusStrip1);
this.Controls.Add(this.menuStrip1);
this.MainMenuStrip = this.menuStrip1;
this.Controls.Add(this.toolStrip2);
this.Name = "MainForm";
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen;
this.Text = "AGV Map Editor";
this.FormClosing += new System.Windows.Forms.FormClosingEventHandler(this.MainForm_FormClosing);
this.Load += new System.EventHandler(this.MainForm_Load);
this.menuStrip1.ResumeLayout(false);
this.menuStrip1.PerformLayout();
this.statusStrip1.ResumeLayout(false);
this.statusStrip1.PerformLayout();
this.splitContainer1.Panel1.ResumeLayout(false);
@@ -239,6 +280,12 @@ namespace AGVMapEditor.Forms
this.tabControl1.ResumeLayout(false);
this.tabPageNodes.ResumeLayout(false);
this.tabPageNodes.PerformLayout();
this.tabPage1.ResumeLayout(false);
this.tabPage1.PerformLayout();
this.toolStrip1.ResumeLayout(false);
this.toolStrip1.PerformLayout();
this.toolStrip2.ResumeLayout(false);
this.toolStrip2.PerformLayout();
this.ResumeLayout(false);
this.PerformLayout();
@@ -246,16 +293,6 @@ namespace AGVMapEditor.Forms
#endregion
private System.Windows.Forms.MenuStrip menuStrip1;
private System.Windows.Forms.ToolStripMenuItem fileToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem newToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem openToolStripMenuItem;
private System.Windows.Forms.ToolStripSeparator toolStripSeparator1;
private System.Windows.Forms.ToolStripMenuItem saveToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem saveAsToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem closeToolStripMenuItem;
private System.Windows.Forms.ToolStripSeparator toolStripSeparator2;
private System.Windows.Forms.ToolStripMenuItem exitToolStripMenuItem;
private System.Windows.Forms.StatusStrip statusStrip1;
private System.Windows.Forms.ToolStripStatusLabel toolStripStatusLabel1;
private System.Windows.Forms.SplitContainer splitContainer1;
@@ -264,5 +301,17 @@ namespace AGVMapEditor.Forms
private System.Windows.Forms.ListBox listBoxNodes;
private System.Windows.Forms.Label label1;
private System.Windows.Forms.PropertyGrid _propertyGrid;
private System.Windows.Forms.TabPage tabPage1;
private System.Windows.Forms.ListBox lstNodeConnection;
private System.Windows.Forms.ToolStrip toolStrip1;
private System.Windows.Forms.ToolStripButton btNodeRemove;
private System.Windows.Forms.ToolStrip toolStrip2;
private System.Windows.Forms.ToolStripButton btnNew;
private System.Windows.Forms.ToolStripButton btnOpen;
private System.Windows.Forms.ToolStripButton btnReopen;
private System.Windows.Forms.ToolStripButton btnClose;
private System.Windows.Forms.ToolStripSeparator toolStripSeparator3;
private System.Windows.Forms.ToolStripButton btnSave;
private System.Windows.Forms.ToolStripButton btnSaveAs;
}
}

View File

@@ -27,6 +27,35 @@ namespace AGVMapEditor.Forms
// 파일 경로
private string _currentMapFile = string.Empty;
private bool _hasChanges = false;
private bool _hasCommandLineArgs = false;
// 노드 연결 정보를 표현하는 클래스
public class NodeConnectionInfo
{
public string FromNodeId { get; set; }
public string FromNodeName { get; set; }
public string FromRfidId { get; set; }
public string ToNodeId { get; set; }
public string ToNodeName { get; set; }
public string ToRfidId { get; set; }
public string ConnectionType { get; set; }
public override string ToString()
{
// RFID가 있으면 RFID(노드이름), 없으면 NodeID(노드이름) 형태로 표시
string fromDisplay = !string.IsNullOrEmpty(FromRfidId)
? $"{FromRfidId}({FromNodeName})"
: $"---({FromNodeId})";
string toDisplay = !string.IsNullOrEmpty(ToRfidId)
? $"{ToRfidId}({ToNodeName})"
: $"---({ToNodeId})";
// 양방향 연결은 ↔ 기호 사용
string arrow = ConnectionType == "양방향" ? "↔" : "→";
return $"{fromDisplay} {arrow} {toDisplay}";
}
}
#endregion
@@ -47,6 +76,7 @@ namespace AGVMapEditor.Forms
// 명령줄 인수로 파일이 전달되었으면 자동으로 열기
if (args != null && args.Length > 0)
{
_hasCommandLineArgs = true;
string filePath = args[0];
if (System.IO.File.Exists(filePath))
{
@@ -54,14 +84,16 @@ namespace AGVMapEditor.Forms
}
else
{
MessageBox.Show($"지정된 파일을 찾을 수 없습니다: {filePath}", "파일 오류",
MessageBox.Show($"지정된 파일을 찾을 수 없습니다: {filePath}", "파일 오류",
MessageBoxButtons.OK, MessageBoxIcon.Warning);
}
}
// 명령줄 인수가 없는 경우는 Form_Load에서 마지막 맵 파일 자동 로드 확인
}
#endregion
#region Initialization
private void InitializeData()
@@ -82,6 +114,7 @@ namespace AGVMapEditor.Forms
_mapCanvas.NodeSelected += OnNodeSelected;
_mapCanvas.NodeMoved += OnNodeMoved;
_mapCanvas.NodeDeleted += OnNodeDeleted;
_mapCanvas.ConnectionDeleted += OnConnectionDeleted;
_mapCanvas.MapChanged += OnMapChanged;
// 스플리터 패널에 맵 캔버스 추가
@@ -148,31 +181,38 @@ namespace AGVMapEditor.Forms
btnDelete.Location = new Point(495, 3);
btnDelete.Click += (s, e) => _mapCanvas.CurrentEditMode = UnifiedAGVCanvas.EditMode.Delete;
// 연결 삭제 버튼
var btnDeleteConnection = new Button();
btnDeleteConnection.Text = "연결삭제 (X)";
btnDeleteConnection.Size = new Size(80, 28);
btnDeleteConnection.Location = new Point(570, 3);
btnDeleteConnection.Click += (s, e) => _mapCanvas.CurrentEditMode = UnifiedAGVCanvas.EditMode.DeleteConnection;
// 구분선
var separator1 = new Label();
separator1.Text = "|";
separator1.Size = new Size(10, 28);
separator1.Location = new Point(570, 3);
separator1.Location = new Point(655, 3);
separator1.TextAlign = ContentAlignment.MiddleCenter;
// 그리드 토글 버튼
var btnToggleGrid = new Button();
btnToggleGrid.Text = "그리드";
btnToggleGrid.Size = new Size(60, 28);
btnToggleGrid.Location = new Point(585, 3);
btnToggleGrid.Location = new Point(670, 3);
btnToggleGrid.Click += (s, e) => _mapCanvas.ShowGrid = !_mapCanvas.ShowGrid;
// 맵 맞춤 버튼
var btnFitMap = new Button();
btnFitMap.Text = "맵 맞춤";
btnFitMap.Size = new Size(70, 28);
btnFitMap.Location = new Point(650, 3);
btnFitMap.Location = new Point(735, 3);
btnFitMap.Click += (s, e) => _mapCanvas.FitToNodes();
// 툴바에 버튼들 추가
toolbarPanel.Controls.AddRange(new Control[]
{
btnSelect, btnMove, btnAddNode, btnAddLabel, btnAddImage, btnConnect, btnDelete, separator1, btnToggleGrid, btnFitMap
btnSelect, btnMove, btnAddNode, btnAddLabel, btnAddImage, btnConnect, btnDelete, btnDeleteConnection, separator1, btnToggleGrid, btnFitMap
});
// 스플리터 패널에 툴바 추가 (맨 위에)
@@ -186,9 +226,18 @@ namespace AGVMapEditor.Forms
private void MainForm_Load(object sender, EventArgs e)
{
RefreshNodeList();
// 속성 변경 시 이벤트 연결
_propertyGrid.PropertyValueChanged += PropertyGrid_PropertyValueChanged;
// 명령줄 인수가 없는 경우에만 마지막 맵 파일 자동 로드 확인
if (!_hasCommandLineArgs)
{
this.Show();
Application.DoEvents();
CheckAndLoadLastMapFile();
}
}
private void OnNodeAdded(object sender, MapNode node)
@@ -228,6 +277,14 @@ namespace AGVMapEditor.Forms
UpdateNodeProperties(); // 연결 정보 업데이트
}
private void OnConnectionDeleted(object sender, (MapNode From, MapNode To) connection)
{
_hasChanges = true;
UpdateTitle();
RefreshNodeConnectionList();
UpdateNodeProperties(); // 연결 정보 업데이트
}
private void OnMapChanged(object sender, EventArgs e)
{
_hasChanges = true;
@@ -242,9 +299,9 @@ namespace AGVMapEditor.Forms
#endregion
#region Menu Event Handlers
#region ToolStrip Button Event Handlers
private void newToolStripMenuItem_Click(object sender, EventArgs e)
private void btnNew_Click(object sender, EventArgs e)
{
if (CheckSaveChanges())
{
@@ -252,7 +309,7 @@ namespace AGVMapEditor.Forms
}
}
private void openToolStripMenuItem_Click(object sender, EventArgs e)
private void btnOpen_Click(object sender, EventArgs e)
{
if (CheckSaveChanges())
{
@@ -260,28 +317,70 @@ namespace AGVMapEditor.Forms
}
}
private void saveToolStripMenuItem_Click(object sender, EventArgs e)
private void btnReopen_Click(object sender, EventArgs e)
{
SaveMap();
if (string.IsNullOrEmpty(_currentMapFile))
{
MessageBox.Show("다시 열 파일이 없습니다.", "알림", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
if (!File.Exists(_currentMapFile))
{
MessageBox.Show($"파일을 찾을 수 없습니다: {_currentMapFile}", "오류", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
if (CheckSaveChanges())
{
LoadMapFromFile(_currentMapFile);
UpdateStatusBar($"파일을 다시 열었습니다: {Path.GetFileName(_currentMapFile)}");
}
}
private void saveAsToolStripMenuItem_Click(object sender, EventArgs e)
{
SaveAsMap();
}
private void closeToolStripMenuItem_Click(object sender, EventArgs e)
private void btnClose_Click(object sender, EventArgs e)
{
CloseMap();
}
private void exitToolStripMenuItem_Click(object sender, EventArgs e)
private void btnSave_Click(object sender, EventArgs e)
{
SaveMap();
}
private void btnSaveAs_Click(object sender, EventArgs e)
{
SaveAsMap();
}
private void btnExit_Click(object sender, EventArgs e)
{
this.Close();
}
#endregion
#region Keyboard Shortcuts
protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
{
switch (keyData)
{
case Keys.Control | Keys.N:
btnNew_Click(null, null);
return true;
case Keys.Control | Keys.O:
btnOpen_Click(null, null);
return true;
case Keys.Control | Keys.S:
btnSave_Click(null, null);
return true;
}
return base.ProcessCmdKey(ref msg, keyData);
}
#endregion
#region Button Event Handlers
private void btnAddNode_Click(object sender, EventArgs e)
@@ -529,27 +628,55 @@ namespace AGVMapEditor.Forms
private void LoadMapFromFile(string filePath)
{
var result = MapLoader.LoadMapFromFile(filePath);
if (result.Success)
{
_mapNodes = result.Nodes;
// 맵 캔버스에 데이터 설정
_mapCanvas.Nodes = _mapNodes;
// RfidMappings 제거됨 - MapNode에 통합
// 현재 파일 경로 업데이트
_currentMapFile = filePath;
_hasChanges = false;
// 설정에 마지막 맵 파일 경로 저장
EditorSettings.Instance.UpdateLastMapFile(filePath);
UpdateTitle();
UpdateNodeList();
RefreshNodeConnectionList();
// 맵 로드 후 자동으로 맵에 맞춤
_mapCanvas.FitToNodes();
UpdateStatusBar($"맵 파일을 성공적으로 로드했습니다: {Path.GetFileName(filePath)}");
}
else
{
MessageBox.Show($"맵 파일 로딩 실패: {result.ErrorMessage}", "오류",
MessageBox.Show($"맵 파일 로딩 실패: {result.ErrorMessage}", "오류",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
private void SaveMapToFile(string filePath)
{
if (!MapLoader.SaveMapToFile(filePath, _mapNodes))
if (MapLoader.SaveMapToFile(filePath, _mapNodes))
{
MessageBox.Show("맵 파일 저장 실패", "오류",
// 현재 파일 경로 업데이트
_currentMapFile = filePath;
_hasChanges = false;
// 설정에 마지막 맵 파일 경로 저장
EditorSettings.Instance.UpdateLastMapFile(filePath);
UpdateTitle();
UpdateStatusBar($"맵 파일을 성공적으로 저장했습니다: {Path.GetFileName(filePath)}");
}
else
{
MessageBox.Show("맵 파일 저장 실패", "오류",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
@@ -559,8 +686,8 @@ namespace AGVMapEditor.Forms
/// </summary>
private void UpdateRfidMappings()
{
// 네비게이션 노드들에 RFID 자동 할당
MapLoader.AssignAutoRfidIds(_mapNodes);
// RFID 자동 할당 제거 - 사용자가 직접 입력한 값 유지
// MapLoader.AssignAutoRfidIds(_mapNodes);
}
private bool CheckSaveChanges()
@@ -584,6 +711,29 @@ namespace AGVMapEditor.Forms
return true;
}
/// <summary>
/// 마지막 맵 파일이 있는지 확인하고 사용자에게 로드할지 물어봄
/// </summary>
private void CheckAndLoadLastMapFile()
{
var settings = EditorSettings.Instance;
if (settings.AutoLoadLastMapFile && settings.HasValidLastMapFile())
{
string fileName = Path.GetFileName(settings.LastMapFilePath);
var result = MessageBox.Show(
$"마지막으로 사용한 맵 파일을 찾았습니다:\n\n{fileName}\n\n이 파일을 열까요?",
"마지막 맵 파일 로드",
MessageBoxButtons.YesNo,
MessageBoxIcon.Question);
if (result == DialogResult.Yes)
{
LoadMapFromFile(settings.LastMapFilePath);
}
}
}
#endregion
#region UI Updates
@@ -591,6 +741,7 @@ namespace AGVMapEditor.Forms
private void RefreshAll()
{
RefreshNodeList();
RefreshNodeConnectionList();
RefreshMapCanvas();
ClearNodeProperties();
}
@@ -672,12 +823,12 @@ namespace AGVMapEditor.Forms
e.Graphics.FillRectangle(brush, e.Bounds);
}
// 텍스트 그리기 (노드ID - 명 - RFID 순서)
// 텍스트 그리기 (노드ID - 노드명 - RFID 순서)
var displayText = node.NodeId;
if (!string.IsNullOrEmpty(node.Description))
if (!string.IsNullOrEmpty(node.Name))
{
displayText += $" - {node.Description}";
displayText += $" - {node.Name}";
}
if (!string.IsNullOrEmpty(node.RfidId))
@@ -694,6 +845,92 @@ namespace AGVMapEditor.Forms
e.DrawFocusRectangle();
}
private void RefreshNodeConnectionList()
{
var connections = new List<NodeConnectionInfo>();
var processedPairs = new HashSet<string>();
// 모든 노드의 연결 정보를 수집 (중복 방지)
foreach (var fromNode in _mapNodes)
{
foreach (var toNodeId in fromNode.ConnectedNodes)
{
var toNode = _mapNodes.FirstOrDefault(n => n.NodeId == toNodeId);
if (toNode != null)
{
// 중복 체크 (단일 연결만 표시)
string pairKey1 = $"{fromNode.NodeId}-{toNode.NodeId}";
string pairKey2 = $"{toNode.NodeId}-{fromNode.NodeId}";
if (!processedPairs.Contains(pairKey1) && !processedPairs.Contains(pairKey2))
{
// 사전 순으로 정렬하여 일관성 있게 표시
var (firstNode, secondNode) = string.Compare(fromNode.NodeId, toNode.NodeId) < 0
? (fromNode, toNode)
: (toNode, fromNode);
connections.Add(new NodeConnectionInfo
{
FromNodeId = firstNode.NodeId,
FromNodeName = firstNode.Name,
FromRfidId = firstNode.RfidId,
ToNodeId = secondNode.NodeId,
ToNodeName = secondNode.Name,
ToRfidId = secondNode.RfidId,
ConnectionType = "양방향" // 모든 연결이 양방향
});
processedPairs.Add(pairKey1);
processedPairs.Add(pairKey2);
}
}
}
}
// 리스트박스에 표시
lstNodeConnection.DataSource = null;
lstNodeConnection.DataSource = connections;
lstNodeConnection.DisplayMember = "ToString";
// 리스트박스 클릭 이벤트 연결
lstNodeConnection.SelectedIndexChanged -= LstNodeConnection_SelectedIndexChanged;
lstNodeConnection.SelectedIndexChanged += LstNodeConnection_SelectedIndexChanged;
// 더블클릭 이벤트 연결 (연결 삭제)
lstNodeConnection.DoubleClick -= LstNodeConnection_DoubleClick;
lstNodeConnection.DoubleClick += LstNodeConnection_DoubleClick;
}
private void LstNodeConnection_SelectedIndexChanged(object sender, EventArgs e)
{
if (lstNodeConnection.SelectedItem is NodeConnectionInfo connectionInfo)
{
// 캔버스에서 해당 연결선 강조 표시
_mapCanvas?.HighlightConnection(connectionInfo.FromNodeId, connectionInfo.ToNodeId);
// 연결된 노드들을 맵에서 하이라이트 표시 (선택적)
var fromNode = _mapNodes.FirstOrDefault(n => n.NodeId == connectionInfo.FromNodeId);
if (fromNode != null)
{
_selectedNode = fromNode;
UpdateNodeProperties();
_mapCanvas?.Invalidate();
}
}
else
{
// 선택 해제 시 강조 표시 제거
_mapCanvas?.ClearHighlightedConnection();
}
}
private void LstNodeConnection_DoubleClick(object sender, EventArgs e)
{
// 더블클릭으로 연결 삭제
DeleteSelectedConnection();
}
private void RefreshMapCanvas()
{
_mapCanvas?.Invalidate();
@@ -735,6 +972,31 @@ namespace AGVMapEditor.Forms
this.Text = title;
}
/// <summary>
/// 노드 목록을 업데이트
/// </summary>
private void UpdateNodeList()
{
if (listBoxNodes != null)
{
listBoxNodes.DataSource = null;
listBoxNodes.DataSource = _mapNodes;
listBoxNodes.DisplayMember = "DisplayText";
}
}
/// <summary>
/// 상태바에 메시지 표시
/// </summary>
/// <param name="message">표시할 메시지</param>
private void UpdateStatusBar(string message)
{
if (toolStripStatusLabel1 != null)
{
toolStripStatusLabel1.Text = message;
}
}
#endregion
#region Form Events
@@ -754,10 +1016,27 @@ namespace AGVMapEditor.Forms
private void PropertyGrid_PropertyValueChanged(object s, PropertyValueChangedEventArgs e)
{
// RFID 값 변경시 중복 검사
if (e.ChangedItem.PropertyDescriptor.Name == "RFID")
{
string newRfidValue = e.ChangedItem.Value?.ToString();
if (!string.IsNullOrEmpty(newRfidValue) && CheckRfidDuplicate(newRfidValue))
{
// 중복된 RFID 값 발견
MessageBox.Show($"RFID 값 '{newRfidValue}'이(가) 이미 다른 노드에서 사용 중입니다.\n입력값을 되돌립니다.",
"RFID 중복 오류", MessageBoxButtons.OK, MessageBoxIcon.Warning);
// 원래 값으로 되돌리기 - PropertyGrid의 SelectedObject 사용
e.ChangedItem.PropertyDescriptor.SetValue(_propertyGrid.SelectedObject, e.OldValue);
_propertyGrid.Refresh();
return;
}
}
// 속성이 변경되었을 때 자동으로 변경사항 표시
_hasChanges = true;
UpdateTitle();
// 현재 선택된 노드를 기억
var currentSelectedNode = _selectedNode;
@@ -775,12 +1054,107 @@ namespace AGVMapEditor.Forms
}
}
/// <summary>
/// RFID 값 중복 검사
/// </summary>
/// <param name="rfidValue">검사할 RFID 값</param>
/// <returns>중복되면 true, 아니면 false</returns>
private bool CheckRfidDuplicate(string rfidValue)
{
if (string.IsNullOrEmpty(rfidValue) || _mapNodes == null)
return false;
// 현재 편집 중인 노드 제외하고 중복 검사
string currentNodeId = null;
var selectedObject = _propertyGrid.SelectedObject;
// 다양한 PropertyWrapper 타입 처리
if (selectedObject is NodePropertyWrapper nodeWrapper)
{
currentNodeId = nodeWrapper.WrappedNode?.NodeId;
}
else if (selectedObject is LabelNodePropertyWrapper labelWrapper)
{
currentNodeId = labelWrapper.WrappedNode?.NodeId;
}
else if (selectedObject is ImageNodePropertyWrapper imageWrapper)
{
currentNodeId = imageWrapper.WrappedNode?.NodeId;
}
int duplicateCount = 0;
foreach (var node in _mapNodes)
{
// 현재 편집 중인 노드는 제외
if (node.NodeId == currentNodeId)
continue;
// 같은 RFID 값을 가진 노드가 있는지 확인
if (!string.IsNullOrEmpty(node.RfidId) && node.RfidId.Equals(rfidValue, StringComparison.OrdinalIgnoreCase))
{
duplicateCount++;
break; // 하나라도 발견되면 중복
}
}
return duplicateCount > 0;
}
#endregion
#region Data Model for Serialization
#endregion
private void btNodeRemove_Click(object sender, EventArgs e)
{
DeleteSelectedConnection();
}
private void DeleteSelectedConnection()
{
if (lstNodeConnection.SelectedItem is NodeConnectionInfo connectionInfo)
{
var result = MessageBox.Show(
$"다음 연결을 삭제하시겠습니까?\n{connectionInfo}",
"연결 삭제 확인",
MessageBoxButtons.YesNo,
MessageBoxIcon.Question);
if (result == DialogResult.Yes)
{
// 단일 연결 삭제
var fromNode = _mapNodes.FirstOrDefault(n => n.NodeId == connectionInfo.FromNodeId);
var toNode = _mapNodes.FirstOrDefault(n => n.NodeId == connectionInfo.ToNodeId);
if (fromNode != null && toNode != null)
{
// 단일 연결 삭제 (어느 방향에 저장되어 있는지 확인 후 삭제)
if (fromNode.ConnectedNodes.Contains(toNode.NodeId))
{
fromNode.RemoveConnection(toNode.NodeId);
}
else if (toNode.ConnectedNodes.Contains(fromNode.NodeId))
{
toNode.RemoveConnection(fromNode.NodeId);
}
_hasChanges = true;
RefreshNodeConnectionList();
RefreshMapCanvas();
UpdateNodeProperties();
UpdateTitle();
toolStripStatusLabel1.Text = $"연결 삭제됨: {connectionInfo.FromNodeId} ↔ {connectionInfo.ToNodeId}";
}
}
}
else
{
MessageBox.Show("삭제할 연결을 선택하세요.", "알림", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
}
}
}

View File

@@ -117,10 +117,112 @@
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<metadata name="menuStrip1.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>17, 17</value>
</metadata>
<metadata name="statusStrip1.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>132, 17</value>
</metadata>
<metadata name="toolStrip1.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>249, 17</value>
</metadata>
<metadata name="toolStrip1.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>249, 17</value>
</metadata>
<assembly alias="System.Drawing" name="System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
<data name="btNodeRemove.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>
iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8
YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAIDSURBVDhPpZLrS5NhGMb3j4SWh0oRQVExD4gonkDpg4hG
YKxG6WBogkMZKgPNCEVJFBGdGETEvgwyO9DJE5syZw3PIlPEE9pgBCLZ5XvdMB8Ew8gXbl54nuf63dd9
0OGSnwCahxbPRNPAPMw9Xpg6ZmF46kZZ0xSKzJPIrhpDWsVnpBhGkKx3nAX8Pv7z1zg8OoY/cITdn4fw
bf/C0kYAN3Ma/w3gWfZL5kzTKBxjWyK2DftwI9tyMYCZKXbNHaD91bLYJrDXsYbrWfUKwJrPE9M2M1Oc
VzOOpHI7Jr376Hi9ogHqFIANO0/MmmmbmSmm9a8ze+I4MrNWAdjtoJgWcx+PSzg166yZZ8xM8XvXDix9
c4jIqFYAjoriBV9AhEPv1mH/sonogha0afbZMMZz+yreTGyhpusHwtNNCsA5U1zS4BLxzJIfg299qO32
Ir7UJtZfftyATqeT+8o2D8JSjQrAJblrncYL7ZJ2+bfaFnC/1S1NjL3diRat7qrO7wLRP3HjWsojBeCo
mDEo5mNjuweFGvjWg2EBhCbpkW78htSHHwRyNdmgAFzPEee2iFkzayy2OLXzT4gr6UdUnlXrullsxxQ+
kx0g8BTA3aZlButjSTyjODq/WcQcW/B/Je4OQhLvKQDnzN1mp0nnkvAhR8VuMzNrpm1mpjgkoVwB/v8D
TgDQASA1MVpwzwAAAABJRU5ErkJggg==
</value>
</data>
<metadata name="toolStrip2.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>356, 17</value>
</metadata>
<data name="btnNew.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>
R0lGODlhEAAQAIUoAOLp8ElVa0NLXIivyJXK/D5FVYm77N7n7ykxQ5rA1svM0YS8O4C4OJXJSInAP5fL
S362N4/ERJLHR5DFRdXb5JbKStXn8HqzM4vAQJ7O+1Jhe1dwkezx9nKsLeXt9XaXtKTR+7HX/PL2+sDf
/bvc/avU+7ba/P///////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH/C05FVFNDQVBFMi4wAwEBAAAh+QQBAAAoACwAAAAAEAAQ
AAAIswBPCBxIcCCKgycsnOjQ4USCAQM+SPxwAqHABw0GetjowcCGigsrSIjgoOGIkyIMeBTYYQKGBRBM
kiAhoqYGggwuDBxBwoSJECJuOlxokqfPECWCChxAcOZPpEkDLBVxsufPEiVAgPAg9cQHDlWvZgWRwYMA
gV+dQtWaIQOAs145WEXKlgCBAwXQiui5tq1dDnlPbKAggsNGAIgBHOCgAAHaDZA1aAgQQICAAgUQCC3I
mWBAADs=
</value>
</data>
<data name="btnOpen.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>
iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29m
dHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAILSURBVDhPpY9LaxNRGIa7cO/On+CqFdFmRHQpiCs3
ohtvIAW1WUiUQkXQLnoBIVAaRUQshVLBFotGBJfWRTW17dhEM5mkteklNMFm7pecubx+mWomQhBLYZ75
zjnwPu85bQD2RMvD3dDycDcEv9zbTr44c5mtfbzaxBWWf3dyWXjTfoDY/xfJgH0NgZjkbN/3aOnTV2dn
7VgVbMx219Znu+0/lOZ7rc3UnWLu9ZHzoWA6IvuuBXsjDmNlGMbqKMziFHxHIpcO39UIZWfvVAGP1cRp
LhMKXnKqxxQKJ6AXRqB+68N26iYqc1FI6X5ISw8g8XdRXewhYnSzMsRXnVoomIwwx9yClEmgujCE8ofb
MMufqVX93V6fcngDep44yVmhYOIYC97v2YQJuEYQdE0BdikBa70f5o8eGPnr0IWLcPUsxPFIk2CMq/mu
CVf9BEd6D/ZzCrWtZxR8CHP1HsxCFEbuEvTvZ6GlT8HVeIijzYKnnO0xFWw7iVp5nFofwVqj1pV6axf0
7AVomTNQv56AsnAYjvoF4pNmQSJi+Uyi8BjszWFYxT4Kx2CI1yh8jlpPQ+WPQ5k/BHnuIAlSqGcaglz8
aInJBVlffuHp+efQxMfQhDi07ADUzH2o6V4oSzEofBTK4g3YlRmfMnJDIAy03xKGOiaEwY4KTeufDAZT
ocxIQ7AXWh7+P2j7BY3RGzIVTOkAAAAAAElFTkSuQmCC
</value>
</data>
<data name="btnReopen.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>
R0lGODlhEAAQAIQfAJfL/OTs9HWVsW6aUqnT+6bnZldwkYiux7TZ/O3z+UlVa/P2+ZfTW36wWJDLV4m7
69nn78bi/qjL3qDP+VJhe4rAVa7S40NLXJ3bYJrA1ikxQz5FVdDU22OPRf///////yH/C05FVFNDQVBF
Mi4wAwEBAAAh+QQBAAAfACwAAAAAEAAQAAAIwQA9CBxIcOCHgx4gWLAgIUOGAwcESBTgAaEFCAEGaBwQ
IGOABwYqerCQsYBJBho7JHgAUqCEDjAxYGBQgYHKBAsoCMzQIUIEmA6CdkCAIOfOBT5/MnBQYSgBozCj
SoVJ4KkCDx1MFhhKFEFVAhMCXM1aAANMoh2qTgh7AWvZmQ6igp0AIEDbDg0aLA06YC4AABA2eBjgYcHG
vmv/Akgg2IMBDgsSdJwcAEICDhoECjDAmQIFBQouXNiwQYPOgqgLBgQAOw==
</value>
</data>
<data name="btnClose.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>
iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29m
dHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHlSURBVDhPpZJbSxtBGIb3VhQULVoqUunRtEpCEmmM
0UJi2ZiE1qYHg1ZKbUsP/9MLD+DfKGZDScJmd7M783RmY9LdpjelFy/DDPO837zfNwbwXxo/NAwrPL6W
iMv6835so7ZNHj+Ab+/g6yHyywHycwP58S3yw2uCh3fwDaMTZcbh70dglpDZLDKdRqRSiLU1gkKO4OgF
/fu3cfTdmEEUPmzA5SWyXkckk4jVVYJqFXlxgV8r4+/v4t1dwr42GRoMnqwqa5huF1otglqNoFIBS0Xv
dJBnZ/STj+i/LNIN0YhBmDeTQezthfDQJJSGlUl/ext3YQHv+VPaUQPdYflp/3de9eQhGErB3tYW7vw8
ztwcbrXwF4PjN4O8iQRBuRwz0NW9fB5ndpbe9DRuOTduIN7XCVZWCExzlFmvGqbdRl5d4ajJ2JOTuM/W
+Rk1ULNFqBH5T9LI8/NB1WYTb3MTd2MjhKUyEScn9JZu4pQycQNP/TA9X79RwTeLiNPTENZ5ezMzOLq5
CnbWUzg7WTqLN2gZxo+RgZb6HJaeb//VDl7i3iivPTWFPTGBvagaqCq3I3DMQEt9DstevoVXK+Du5nHN
XJjXKWVxiukxWCtmoKW6a+kOD6WzailQS40mfj+2+Xdh/ALnlbiDsb03NQAAAABJRU5ErkJggg==
</value>
</data>
<data name="btnSave.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>
R0lGODlhEAAQAIQAAJXD9Iasxm6MqnSn2lZtjVaRyEpXbYu767TX/2KZztvr/4Gy5KrT/3ut32+gzlFh
e+r0/0RNX9/u/9Ln+8Xg//n8/4e36CkxQz9GVkSCvKjL35/N/Je91K7T5bDS4////yH/C05FVFNDQVBF
Mi4wAwEBAAAh+QQAAAAAACwAAAAAEAAQAAAIuQA/CBxIsKDACRwScggQwIGAhwIICBDYQcEEgwg+bNjw
QKCHCQgkQBgpQcKBCg0AEBCoAaRIkhIsVBigUiAHCgwkKNjJU8GAAx0/3NwIAMABCwsaDHCwIGgAChuK
HjiQdMDSAQYEPpWKtKqDBA6yfgiAwGhXpUsTJIgg0AGCo0nRfi1QgO0HAQyQNpCrtkAGDAIFbKi69GsC
un8FEohqdEFavxkyXAhMoPKDBwYMRIiAAcOFoAZDCwwIADs=
</value>
</data>
<data name="btnSaveAs.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>
iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29m
dHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAMPSURBVDhPZZL9S1NhFMfvT/Vv9BeEUURkRIQWZSGh
URRRRsZMM0UqzbSyWdq0d0gtVmlZJJHkC7FJS92rtd5cmzbnu1PX5tzutuduu3ffzi69aQ98OOc+3Ps5
5zzP5dIqeo/sVpm1mXXmBQIEy6wn6iwsU2ViGbUGW+oZ/a1Nio4VALjlcDuq9f3eIPMuhATRxwv4jTeY
hMHhDqLi9SRy7zrDqYqOlf8JMmoMnmAkJprGY7BMxmEej8MwFkWvU8AbewhCHOgciqCifQLHbjsjm/M7
l0i4XbXGSCAchXVKJOJ4L0ui6BsRoLGHSSDB6hZhmYlB2T2JrWW61iWCjFojSwo+keDTdFIiYmAiBr0r
ip7hCMbmQ/IoPItj3h9GemUf9bRMsBiKYpCqDM5K+CJLkl3E5C601EWXjUfH1wAWQwK2VenZEsHOGiPz
k8Axm8A3EtjcEr6SrPCeDeVqG4obPqPqiR2K61bk3bTidNNnlDR+wVGVGasOvFzP7SCBjwRDcwk4CDuJ
Bmck5NZ/QLuVpyJAh2lKjv+uxzoPjtRawG1XGpmXj8IufyzJJDs5eMUE9Tu//HLhnQ9yTC46CgQEoO7V
NHKumcClVxnwg+48WfmPgMY4VGOmKn7ERAlxMQFJSkAkYnQrcdqrfu5C1oVecFvO65l7kWFvp4DsLoYs
ilmvBRphAI1vPKh86sLFVhfOtzhx7tEwyojSh0MoVQ/hkFIHblNpL5vyMRT2SVA7gPt2oIFijsqC211u
XHo2SUygomUU5Y9HUPbQibPqYZQ02bCvUgtuQ7EuPPYjnFDo6ODexnHsrSiTfUmPe90eeHge7oCP8MO9
6McM5b4wj7IH35BW1A5ubV7PqCcgBMe8TBr1RDDyiz0X+tHU7ccc74FStwcNllNotBRTno350DTKH7iw
Me8FuHUntPkpuZqeNce1vpTjGhCMntney0ZoPiZvwQXTwlU4Qs1wRJopr6O9cVQ3u7D5RNvfP2o5axUa
w35lPxSqQRTdmEBB/QhOqr6j8JoDBSor0k62YfXhVvwE3mQsoPunpBAAAAAASUVORK5CYII=
</value>
</data>
</root>