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:
@@ -49,6 +49,7 @@
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="Models\EditorSettings.cs" />
|
||||
<Compile Include="Models\MapData.cs" />
|
||||
<Compile Include="Models\MapImage.cs" />
|
||||
<Compile Include="Models\MapLabel.cs" />
|
||||
@@ -63,11 +64,20 @@
|
||||
</Compile>
|
||||
<Compile Include="Program.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="Properties\Resources.Designer.cs">
|
||||
<AutoGen>True</AutoGen>
|
||||
<DesignTime>True</DesignTime>
|
||||
<DependentUpon>Resources.resx</DependentUpon>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Forms\MainForm.resx">
|
||||
<DependentUpon>MainForm.cs</DependentUpon>
|
||||
</EmbeddedResource>
|
||||
<EmbeddedResource Include="Properties\Resources.resx">
|
||||
<Generator>ResXFileCodeGenerator</Generator>
|
||||
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="build.bat" />
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
namespace AGVMapEditor.Controls
|
||||
{
|
||||
partial class MapCanvasInteractive
|
||||
{
|
||||
/// <summary>
|
||||
/// 필수 디자이너 변수입니다.
|
||||
/// </summary>
|
||||
private System.ComponentModel.IContainer components = null;
|
||||
|
||||
|
||||
#region 구성 요소 디자이너에서 생성한 코드
|
||||
|
||||
/// <summary>
|
||||
/// 디자이너 지원에 필요한 메서드입니다.
|
||||
/// 이 메서드의 내용을 코드 편집기로 수정하지 마세요.
|
||||
/// </summary>
|
||||
private void InitializeComponent()
|
||||
{
|
||||
this.SuspendLayout();
|
||||
//
|
||||
// MapCanvasInteractive
|
||||
//
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 12F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.BackColor = System.Drawing.Color.White;
|
||||
this.Name = "MapCanvasInteractive";
|
||||
this.Size = new System.Drawing.Size(800, 600);
|
||||
this.ResumeLayout(false);
|
||||
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
289
Cs_HMI/AGVMapEditor/Forms/MainForm.Designer.cs
generated
289
Cs_HMI/AGVMapEditor/Forms/MainForm.Designer.cs
generated
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
162
Cs_HMI/AGVMapEditor/Models/EditorSettings.cs
Normal file
162
Cs_HMI/AGVMapEditor/Models/EditorSettings.cs
Normal file
@@ -0,0 +1,162 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace AGVMapEditor.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// AGV 맵 에디터의 환경설정을 관리하는 클래스
|
||||
/// </summary>
|
||||
public class EditorSettings
|
||||
{
|
||||
#region Properties
|
||||
|
||||
/// <summary>
|
||||
/// 마지막으로 열었던 맵 파일의 경로
|
||||
/// </summary>
|
||||
public string LastMapFilePath { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 프로그램 시작시 마지막 맵 파일을 자동으로 로드할지 여부
|
||||
/// </summary>
|
||||
public bool AutoLoadLastMapFile { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 설정이 마지막으로 저장된 시간
|
||||
/// </summary>
|
||||
public DateTime LastSaved { get; set; } = DateTime.Now;
|
||||
|
||||
/// <summary>
|
||||
/// 기본 맵 파일 저장 디렉토리
|
||||
/// </summary>
|
||||
public string DefaultMapDirectory { get; set; } = string.Empty;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Constants
|
||||
|
||||
private static readonly string SettingsFileName = "EditorSettings.json";
|
||||
private static readonly string SettingsDirectory = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
||||
"AGVMapEditor");
|
||||
|
||||
private static readonly string SettingsFilePath = Path.Combine(SettingsDirectory, SettingsFileName);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Static Instance
|
||||
|
||||
private static EditorSettings _instance;
|
||||
private static readonly object _lock = new object();
|
||||
|
||||
/// <summary>
|
||||
/// 싱글톤 인스턴스
|
||||
/// </summary>
|
||||
public static EditorSettings Instance
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_instance == null)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_instance == null)
|
||||
{
|
||||
_instance = LoadSettings();
|
||||
}
|
||||
}
|
||||
}
|
||||
return _instance;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Methods
|
||||
|
||||
/// <summary>
|
||||
/// 설정을 파일에서 로드
|
||||
/// </summary>
|
||||
private static EditorSettings LoadSettings()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (File.Exists(SettingsFilePath))
|
||||
{
|
||||
string jsonContent = File.ReadAllText(SettingsFilePath);
|
||||
var settings = JsonConvert.DeserializeObject<EditorSettings>(jsonContent);
|
||||
return settings ?? new EditorSettings();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 설정 로드 실패시 기본 설정 사용
|
||||
System.Diagnostics.Debug.WriteLine($"설정 로드 실패: {ex.Message}");
|
||||
}
|
||||
|
||||
return new EditorSettings();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 설정을 파일에 저장
|
||||
/// </summary>
|
||||
public void Save()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 디렉토리가 없으면 생성
|
||||
if (!Directory.Exists(SettingsDirectory))
|
||||
{
|
||||
Directory.CreateDirectory(SettingsDirectory);
|
||||
}
|
||||
|
||||
LastSaved = DateTime.Now;
|
||||
string jsonContent = JsonConvert.SerializeObject(this, Formatting.Indented);
|
||||
File.WriteAllText(SettingsFilePath, jsonContent);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"설정 저장 실패: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 마지막 맵 파일 경로 업데이트
|
||||
/// </summary>
|
||||
/// <param name="filePath">맵 파일 경로</param>
|
||||
public void UpdateLastMapFile(string filePath)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(filePath) && File.Exists(filePath))
|
||||
{
|
||||
LastMapFilePath = filePath;
|
||||
|
||||
// 기본 디렉토리도 업데이트
|
||||
DefaultMapDirectory = Path.GetDirectoryName(filePath);
|
||||
|
||||
Save();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 마지막 맵 파일이 존재하는지 확인
|
||||
/// </summary>
|
||||
public bool HasValidLastMapFile()
|
||||
{
|
||||
return !string.IsNullOrEmpty(LastMapFilePath) && File.Exists(LastMapFilePath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 설정 초기화
|
||||
/// </summary>
|
||||
public void Reset()
|
||||
{
|
||||
LastMapFilePath = string.Empty;
|
||||
AutoLoadLastMapFile = true;
|
||||
DefaultMapDirectory = string.Empty;
|
||||
LastSaved = DateTime.Now;
|
||||
Save();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace AGVMapEditor.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// 노드 타입 열거형
|
||||
/// </summary>
|
||||
public enum NodeType
|
||||
{
|
||||
/// <summary>일반 경로 노드</summary>
|
||||
Normal,
|
||||
/// <summary>회전 가능 지점</summary>
|
||||
Rotation,
|
||||
/// <summary>도킹 스테이션</summary>
|
||||
Docking,
|
||||
/// <summary>충전 스테이션</summary>
|
||||
Charging,
|
||||
/// <summary>라벨 (UI 요소)</summary>
|
||||
Label,
|
||||
/// <summary>이미지 (UI 요소)</summary>
|
||||
Image
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 도킹 방향 열거형
|
||||
/// </summary>
|
||||
public enum DockingDirection
|
||||
{
|
||||
/// <summary>전진 도킹 (충전기)</summary>
|
||||
Forward,
|
||||
/// <summary>후진 도킹 (로더, 클리너, 오프로더, 버퍼)</summary>
|
||||
Backward
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// AGV 이동 방향 열거형
|
||||
/// </summary>
|
||||
public enum AgvDirection
|
||||
{
|
||||
/// <summary>전진 (모니터 방향)</summary>
|
||||
Forward,
|
||||
/// <summary>후진 (리프트 방향)</summary>
|
||||
Backward,
|
||||
/// <summary>좌회전</summary>
|
||||
Left,
|
||||
/// <summary>우회전</summary>
|
||||
Right,
|
||||
/// <summary>정지</summary>
|
||||
Stop
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 장비 타입 열거형
|
||||
/// </summary>
|
||||
public enum StationType
|
||||
{
|
||||
/// <summary>로더</summary>
|
||||
Loader,
|
||||
/// <summary>클리너</summary>
|
||||
Cleaner,
|
||||
/// <summary>오프로더</summary>
|
||||
Offloader,
|
||||
/// <summary>버퍼</summary>
|
||||
Buffer,
|
||||
/// <summary>충전기</summary>
|
||||
Charger
|
||||
}
|
||||
}
|
||||
@@ -1,422 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
|
||||
namespace AGVMapEditor.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// 맵 노드 정보를 관리하는 클래스
|
||||
/// 논리적 노드로서 실제 맵의 위치와 속성을 정의
|
||||
/// </summary>
|
||||
public class MapNode
|
||||
{
|
||||
/// <summary>
|
||||
/// 논리적 노드 ID (맵 에디터에서 관리하는 고유 ID)
|
||||
/// 예: "N001", "N002", "LOADER1", "CHARGER1"
|
||||
/// </summary>
|
||||
public string NodeId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 노드 표시 이름 (사용자 친화적)
|
||||
/// 예: "로더1", "충전기1", "교차점A", "회전지점1"
|
||||
/// </summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 맵 상의 위치 좌표 (픽셀 단위)
|
||||
/// </summary>
|
||||
public Point Position { get; set; } = Point.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 노드 타입
|
||||
/// </summary>
|
||||
public NodeType Type { get; set; } = NodeType.Normal;
|
||||
|
||||
/// <summary>
|
||||
/// 도킹 방향 (도킹/충전 노드인 경우만 사용)
|
||||
/// </summary>
|
||||
public DockingDirection? DockDirection { get; set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// 연결된 노드 ID 목록 (경로 정보)
|
||||
/// </summary>
|
||||
public List<string> ConnectedNodes { get; set; } = new List<string>();
|
||||
|
||||
/// <summary>
|
||||
/// 회전 가능 여부 (180도 회전 가능한 지점)
|
||||
/// </summary>
|
||||
public bool CanRotate { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// 장비 ID (도킹/충전 스테이션인 경우)
|
||||
/// 예: "LOADER1", "CLEANER1", "BUFFER1", "CHARGER1"
|
||||
/// </summary>
|
||||
public string StationId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 장비 타입 (도킹/충전 스테이션인 경우)
|
||||
/// </summary>
|
||||
public StationType? StationType { get; set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// 노드 생성 일자
|
||||
/// </summary>
|
||||
public DateTime CreatedDate { get; set; } = DateTime.Now;
|
||||
|
||||
/// <summary>
|
||||
/// 노드 수정 일자
|
||||
/// </summary>
|
||||
public DateTime ModifiedDate { get; set; } = DateTime.Now;
|
||||
|
||||
/// <summary>
|
||||
/// 노드 설명 (추가 정보)
|
||||
/// </summary>
|
||||
public string Description { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 노드 활성화 여부
|
||||
/// </summary>
|
||||
public bool IsActive { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 노드 색상 (맵 에디터 표시용)
|
||||
/// </summary>
|
||||
public Color DisplayColor { get; set; } = Color.Blue;
|
||||
|
||||
/// <summary>
|
||||
/// RFID 태그 ID (이 노드에 매핑된 RFID)
|
||||
/// </summary>
|
||||
public string RfidId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 라벨 텍스트 (NodeType.Label인 경우 사용)
|
||||
/// </summary>
|
||||
public string LabelText { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 라벨 폰트 패밀리 (NodeType.Label인 경우 사용)
|
||||
/// </summary>
|
||||
public string FontFamily { get; set; } = "Arial";
|
||||
|
||||
/// <summary>
|
||||
/// 라벨 폰트 크기 (NodeType.Label인 경우 사용)
|
||||
/// </summary>
|
||||
public float FontSize { get; set; } = 12.0f;
|
||||
|
||||
/// <summary>
|
||||
/// 라벨 폰트 스타일 (NodeType.Label인 경우 사용)
|
||||
/// </summary>
|
||||
public FontStyle FontStyle { get; set; } = FontStyle.Regular;
|
||||
|
||||
/// <summary>
|
||||
/// 라벨 전경색 (NodeType.Label인 경우 사용)
|
||||
/// </summary>
|
||||
public Color ForeColor { get; set; } = Color.Black;
|
||||
|
||||
/// <summary>
|
||||
/// 라벨 배경색 (NodeType.Label인 경우 사용)
|
||||
/// </summary>
|
||||
public Color BackColor { get; set; } = Color.Transparent;
|
||||
|
||||
/// <summary>
|
||||
/// 라벨 배경 표시 여부 (NodeType.Label인 경우 사용)
|
||||
/// </summary>
|
||||
public bool ShowBackground { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// 이미지 파일 경로 (NodeType.Image인 경우 사용)
|
||||
/// </summary>
|
||||
public string ImagePath { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 이미지 크기 배율 (NodeType.Image인 경우 사용)
|
||||
/// </summary>
|
||||
public SizeF Scale { get; set; } = new SizeF(1.0f, 1.0f);
|
||||
|
||||
/// <summary>
|
||||
/// 이미지 투명도 (NodeType.Image인 경우 사용, 0.0~1.0)
|
||||
/// </summary>
|
||||
public float Opacity { get; set; } = 1.0f;
|
||||
|
||||
/// <summary>
|
||||
/// 이미지 회전 각도 (NodeType.Image인 경우 사용, 도 단위)
|
||||
/// </summary>
|
||||
public float Rotation { get; set; } = 0.0f;
|
||||
|
||||
/// <summary>
|
||||
/// 로딩된 이미지 (런타임에서만 사용, JSON 직렬화 제외)
|
||||
/// </summary>
|
||||
[Newtonsoft.Json.JsonIgnore]
|
||||
public Image LoadedImage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 기본 생성자
|
||||
/// </summary>
|
||||
public MapNode()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 매개변수 생성자
|
||||
/// </summary>
|
||||
/// <param name="nodeId">노드 ID</param>
|
||||
/// <param name="name">노드 이름</param>
|
||||
/// <param name="position">위치</param>
|
||||
/// <param name="type">노드 타입</param>
|
||||
public MapNode(string nodeId, string name, Point position, NodeType type)
|
||||
{
|
||||
NodeId = nodeId;
|
||||
Name = name;
|
||||
Position = position;
|
||||
Type = type;
|
||||
CreatedDate = DateTime.Now;
|
||||
ModifiedDate = DateTime.Now;
|
||||
|
||||
// 타입별 기본 색상 설정
|
||||
SetDefaultColorByType(type);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 노드 타입에 따른 기본 색상 설정
|
||||
/// </summary>
|
||||
/// <param name="type">노드 타입</param>
|
||||
public void SetDefaultColorByType(NodeType type)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case NodeType.Normal:
|
||||
DisplayColor = Color.Blue;
|
||||
break;
|
||||
case NodeType.Rotation:
|
||||
DisplayColor = Color.Orange;
|
||||
break;
|
||||
case NodeType.Docking:
|
||||
DisplayColor = Color.Green;
|
||||
break;
|
||||
case NodeType.Charging:
|
||||
DisplayColor = Color.Red;
|
||||
break;
|
||||
case NodeType.Label:
|
||||
DisplayColor = Color.Purple;
|
||||
break;
|
||||
case NodeType.Image:
|
||||
DisplayColor = Color.Brown;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 다른 노드와의 연결 추가
|
||||
/// </summary>
|
||||
/// <param name="nodeId">연결할 노드 ID</param>
|
||||
public void AddConnection(string nodeId)
|
||||
{
|
||||
if (!ConnectedNodes.Contains(nodeId))
|
||||
{
|
||||
ConnectedNodes.Add(nodeId);
|
||||
ModifiedDate = DateTime.Now;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 다른 노드와의 연결 제거
|
||||
/// </summary>
|
||||
/// <param name="nodeId">연결 해제할 노드 ID</param>
|
||||
public void RemoveConnection(string nodeId)
|
||||
{
|
||||
if (ConnectedNodes.Remove(nodeId))
|
||||
{
|
||||
ModifiedDate = DateTime.Now;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 도킹 스테이션 설정
|
||||
/// </summary>
|
||||
/// <param name="stationId">장비 ID</param>
|
||||
/// <param name="stationType">장비 타입</param>
|
||||
/// <param name="dockDirection">도킹 방향</param>
|
||||
public void SetDockingStation(string stationId, StationType stationType, DockingDirection dockDirection)
|
||||
{
|
||||
Type = NodeType.Docking;
|
||||
StationId = stationId;
|
||||
StationType = stationType;
|
||||
DockDirection = dockDirection;
|
||||
SetDefaultColorByType(NodeType.Docking);
|
||||
ModifiedDate = DateTime.Now;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 충전 스테이션 설정
|
||||
/// </summary>
|
||||
/// <param name="stationId">충전기 ID</param>
|
||||
public void SetChargingStation(string stationId)
|
||||
{
|
||||
Type = NodeType.Charging;
|
||||
StationId = stationId;
|
||||
StationType = Models.StationType.Charger;
|
||||
DockDirection = DockingDirection.Forward; // 충전기는 항상 전진 도킹
|
||||
SetDefaultColorByType(NodeType.Charging);
|
||||
ModifiedDate = DateTime.Now;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 문자열 표현
|
||||
/// </summary>
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{NodeId}: {Name} ({Type}) at ({Position.X}, {Position.Y})";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 리스트박스 표시용 텍스트 (노드ID - 설명 - RFID 순서)
|
||||
/// </summary>
|
||||
public string DisplayText
|
||||
{
|
||||
get
|
||||
{
|
||||
var displayText = NodeId;
|
||||
|
||||
if (!string.IsNullOrEmpty(Description))
|
||||
{
|
||||
displayText += $" - {Description}";
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(RfidId))
|
||||
{
|
||||
displayText += $" - [{RfidId}]";
|
||||
}
|
||||
|
||||
return displayText;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 노드 복사
|
||||
/// </summary>
|
||||
/// <returns>복사된 노드</returns>
|
||||
public MapNode Clone()
|
||||
{
|
||||
var clone = new MapNode
|
||||
{
|
||||
NodeId = NodeId,
|
||||
Name = Name,
|
||||
Position = Position,
|
||||
Type = Type,
|
||||
DockDirection = DockDirection,
|
||||
ConnectedNodes = new List<string>(ConnectedNodes),
|
||||
CanRotate = CanRotate,
|
||||
StationId = StationId,
|
||||
StationType = StationType,
|
||||
CreatedDate = CreatedDate,
|
||||
ModifiedDate = ModifiedDate,
|
||||
Description = Description,
|
||||
IsActive = IsActive,
|
||||
DisplayColor = DisplayColor,
|
||||
RfidId = RfidId,
|
||||
LabelText = LabelText,
|
||||
FontFamily = FontFamily,
|
||||
FontSize = FontSize,
|
||||
FontStyle = FontStyle,
|
||||
ForeColor = ForeColor,
|
||||
BackColor = BackColor,
|
||||
ShowBackground = ShowBackground,
|
||||
ImagePath = ImagePath,
|
||||
Scale = Scale,
|
||||
Opacity = Opacity,
|
||||
Rotation = Rotation
|
||||
};
|
||||
return clone;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 이미지 로드 (256x256 이상일 경우 자동 리사이즈)
|
||||
/// </summary>
|
||||
/// <returns>로드 성공 여부</returns>
|
||||
public bool LoadImage()
|
||||
{
|
||||
if (Type != NodeType.Image) return false;
|
||||
|
||||
try
|
||||
{
|
||||
if (!string.IsNullOrEmpty(ImagePath) && System.IO.File.Exists(ImagePath))
|
||||
{
|
||||
LoadedImage?.Dispose();
|
||||
var originalImage = Image.FromFile(ImagePath);
|
||||
|
||||
// 이미지 크기 체크 및 리사이즈
|
||||
if (originalImage.Width > 256 || originalImage.Height > 256)
|
||||
{
|
||||
LoadedImage = ResizeImage(originalImage, 256, 256);
|
||||
originalImage.Dispose();
|
||||
}
|
||||
else
|
||||
{
|
||||
LoadedImage = originalImage;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// 이미지 로드 실패
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 이미지 리사이즈 (비율 유지)
|
||||
/// </summary>
|
||||
/// <param name="image">원본 이미지</param>
|
||||
/// <param name="maxWidth">최대 너비</param>
|
||||
/// <param name="maxHeight">최대 높이</param>
|
||||
/// <returns>리사이즈된 이미지</returns>
|
||||
private Image ResizeImage(Image image, int maxWidth, int maxHeight)
|
||||
{
|
||||
// 비율 계산
|
||||
double ratioX = (double)maxWidth / image.Width;
|
||||
double ratioY = (double)maxHeight / image.Height;
|
||||
double ratio = Math.Min(ratioX, ratioY);
|
||||
|
||||
// 새로운 크기 계산
|
||||
int newWidth = (int)(image.Width * ratio);
|
||||
int newHeight = (int)(image.Height * ratio);
|
||||
|
||||
// 리사이즈된 이미지 생성
|
||||
var resizedImage = new Bitmap(newWidth, newHeight);
|
||||
using (var graphics = Graphics.FromImage(resizedImage))
|
||||
{
|
||||
graphics.CompositingQuality = System.Drawing.Drawing2D.CompositingQuality.HighQuality;
|
||||
graphics.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
|
||||
graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;
|
||||
graphics.DrawImage(image, 0, 0, newWidth, newHeight);
|
||||
}
|
||||
|
||||
return resizedImage;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 실제 표시될 크기 계산 (이미지 노드인 경우)
|
||||
/// </summary>
|
||||
/// <returns>실제 크기</returns>
|
||||
public Size GetDisplaySize()
|
||||
{
|
||||
if (Type != NodeType.Image || LoadedImage == null) return Size.Empty;
|
||||
|
||||
return new Size(
|
||||
(int)(LoadedImage.Width * Scale.Width),
|
||||
(int)(LoadedImage.Height * Scale.Height)
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 리소스 정리
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
LoadedImage?.Dispose();
|
||||
LoadedImage = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -39,6 +39,11 @@ namespace AGVMapEditor.Models
|
||||
_mapNodes = mapNodes;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 래핑된 MapNode 인스턴스 접근
|
||||
/// </summary>
|
||||
public MapNode WrappedNode => _node;
|
||||
|
||||
[Category("기본 정보")]
|
||||
[DisplayName("노드 ID")]
|
||||
[Description("노드의 고유 식별자")]
|
||||
@@ -207,6 +212,11 @@ namespace AGVMapEditor.Models
|
||||
_mapNodes = mapNodes;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 래핑된 MapNode 인스턴스 접근
|
||||
/// </summary>
|
||||
public MapNode WrappedNode => _node;
|
||||
|
||||
[Category("기본 정보")]
|
||||
[DisplayName("노드 ID")]
|
||||
[Description("노드의 고유 식별자")]
|
||||
@@ -350,6 +360,11 @@ namespace AGVMapEditor.Models
|
||||
_mapNodes = mapNodes;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 래핑된 MapNode 인스턴스 접근
|
||||
/// </summary>
|
||||
public MapNode WrappedNode => _node;
|
||||
|
||||
[Category("기본 정보")]
|
||||
[DisplayName("노드 ID")]
|
||||
[Description("노드의 고유 식별자")]
|
||||
@@ -450,18 +465,6 @@ namespace AGVMapEditor.Models
|
||||
|
||||
|
||||
|
||||
[Category("고급")]
|
||||
[DisplayName("설명")]
|
||||
[Description("노드에 대한 추가 설명")]
|
||||
public string Description
|
||||
{
|
||||
get => _node.Description;
|
||||
set
|
||||
{
|
||||
_node.Description = value ?? "";
|
||||
_node.ModifiedDate = DateTime.Now;
|
||||
}
|
||||
}
|
||||
|
||||
[Category("고급")]
|
||||
[DisplayName("활성화")]
|
||||
|
||||
@@ -49,6 +49,8 @@ namespace AGVMapEditor.Models
|
||||
_astarPathfinder.SetMapNodes(mapNodes);
|
||||
// RfidPathfinder는 MapNode의 RFID 정보를 직접 사용
|
||||
_rfidPathfinder.SetMapNodes(mapNodes);
|
||||
// 도킹 조건 검색용 내부 노드 목록 업데이트
|
||||
UpdateInternalMapNodes(mapNodes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -63,6 +65,32 @@ namespace AGVMapEditor.Models
|
||||
return _agvPathfinder.FindAGVPath(startNodeId, endNodeId, targetDirection);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// AGV 경로 계산 (옵션 지정 가능)
|
||||
/// </summary>
|
||||
/// <param name="startNodeId">시작 노드 ID</param>
|
||||
/// <param name="endNodeId">목적지 노드 ID</param>
|
||||
/// <param name="targetDirection">목적지 도착 방향</param>
|
||||
/// <param name="options">경로 탐색 옵션</param>
|
||||
/// <returns>AGV 경로 계산 결과</returns>
|
||||
public AGVPathResult FindAGVPath(string startNodeId, string endNodeId, AgvDirection? targetDirection, PathfindingOptions options)
|
||||
{
|
||||
return _agvPathfinder.FindAGVPath(startNodeId, endNodeId, targetDirection, options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// AGV 경로 계산 (현재 방향 및 PathfindingOptions 지원)
|
||||
/// </summary>
|
||||
/// <param name="startNodeId">시작 노드 ID</param>
|
||||
/// <param name="endNodeId">목적지 노드 ID</param>
|
||||
/// <param name="currentDirection">현재 AGV 방향</param>
|
||||
/// <param name="targetDirection">목적지 도착 방향</param>
|
||||
/// <param name="options">경로 탐색 옵션</param>
|
||||
/// <returns>AGV 경로 계산 결과</returns>
|
||||
public AGVPathResult FindAGVPath(string startNodeId, string endNodeId, AgvDirection? currentDirection, AgvDirection? targetDirection, PathfindingOptions options)
|
||||
{
|
||||
return _agvPathfinder.FindAGVPath(startNodeId, endNodeId, currentDirection, targetDirection, options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 충전 스테이션으로의 경로 찾기
|
||||
@@ -264,5 +292,141 @@ namespace AGVMapEditor.Models
|
||||
{
|
||||
_rfidPathfinder.RotationCostWeight = weight;
|
||||
}
|
||||
|
||||
#region 도킹 조건 검색 기능
|
||||
|
||||
// 내부 노드 목록 저장
|
||||
private List<MapNode> _mapNodes;
|
||||
|
||||
/// <summary>
|
||||
/// 맵 노드 설정 (도킹 조건 검색용)
|
||||
/// </summary>
|
||||
private void UpdateInternalMapNodes(List<MapNode> mapNodes)
|
||||
{
|
||||
_mapNodes = mapNodes;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 도킹 방향 기반 노드 검색
|
||||
/// </summary>
|
||||
/// <param name="dockingDirection">도킹 방향</param>
|
||||
/// <returns>해당 도킹 방향의 노드 목록</returns>
|
||||
public List<MapNode> GetNodesByDockingDirection(DockingDirection dockingDirection)
|
||||
{
|
||||
if (_mapNodes == null) return new List<MapNode>();
|
||||
|
||||
var result = new List<MapNode>();
|
||||
|
||||
foreach (var node in _mapNodes)
|
||||
{
|
||||
if (!node.IsActive) continue;
|
||||
|
||||
var nodeDockingDirection = GetNodeDockingDirection(node);
|
||||
if (nodeDockingDirection == dockingDirection)
|
||||
{
|
||||
result.Add(node);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 노드의 도킹 방향 결정
|
||||
/// </summary>
|
||||
/// <param name="node">노드</param>
|
||||
/// <returns>도킹 방향</returns>
|
||||
public DockingDirection GetNodeDockingDirection(MapNode node)
|
||||
{
|
||||
switch (node.Type)
|
||||
{
|
||||
case NodeType.Charging:
|
||||
return DockingDirection.Forward; // 충전기: 전진 도킹
|
||||
case NodeType.Docking:
|
||||
return DockingDirection.Backward; // 장비: 후진 도킹
|
||||
default:
|
||||
return DockingDirection.Forward; // 기본값: 전진
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 도킹 방향과 장비 타입에 맞는 노드들로의 경로 검색
|
||||
/// </summary>
|
||||
/// <param name="startRfidId">시작 RFID</param>
|
||||
/// <param name="dockingDirection">필요한 도킹 방향</param>
|
||||
/// <param name="stationType">장비 타입 (선택사항)</param>
|
||||
/// <returns>경로 계산 결과 목록 (거리 순 정렬)</returns>
|
||||
public List<RfidPathResult> FindPathsByDockingCondition(string startRfidId, DockingDirection dockingDirection, StationType? stationType = null)
|
||||
{
|
||||
var targetNodes = GetNodesByDockingDirection(dockingDirection);
|
||||
var results = new List<RfidPathResult>();
|
||||
|
||||
// 장비 타입 필터링 (필요시)
|
||||
if (stationType.HasValue && dockingDirection == DockingDirection.Backward)
|
||||
{
|
||||
// 후진 도킹이면서 특정 장비 타입이 지정된 경우
|
||||
// 이 부분은 추후 StationMapping 정보가 있을 때 구현
|
||||
// 현재는 모든 도킹 노드를 대상으로 함
|
||||
}
|
||||
|
||||
foreach (var targetNode in targetNodes)
|
||||
{
|
||||
if (!targetNode.HasRfid()) continue;
|
||||
|
||||
try
|
||||
{
|
||||
var pathResult = _rfidPathfinder.FindAGVPath(startRfidId, targetNode.RfidId);
|
||||
if (pathResult.Success)
|
||||
{
|
||||
results.Add(pathResult);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 개별 경로 계산 실패는 무시하고 계속 진행
|
||||
System.Diagnostics.Debug.WriteLine($"Path calculation failed from {startRfidId} to {targetNode.RfidId}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// 거리 순으로 정렬
|
||||
return results.OrderBy(r => r.TotalDistance).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 가장 가까운 충전기 경로 찾기 (전진 도킹)
|
||||
/// </summary>
|
||||
/// <param name="startRfidId">시작 RFID</param>
|
||||
/// <returns>가장 가까운 충전기로의 경로</returns>
|
||||
public RfidPathResult FindNearestChargingStationPath(string startRfidId)
|
||||
{
|
||||
var chargingPaths = FindPathsByDockingCondition(startRfidId, DockingDirection.Forward);
|
||||
var chargingNodes = chargingPaths.Where(p => p.Success).ToList();
|
||||
|
||||
return chargingNodes.FirstOrDefault() ?? new RfidPathResult
|
||||
{
|
||||
Success = false,
|
||||
ErrorMessage = "충전 가능한 충전기를 찾을 수 없습니다."
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 가장 가까운 장비 도킹 경로 찾기 (후진 도킹)
|
||||
/// </summary>
|
||||
/// <param name="startRfidId">시작 RFID</param>
|
||||
/// <param name="stationType">장비 타입 (선택사항)</param>
|
||||
/// <returns>가장 가까운 장비로의 경로</returns>
|
||||
public RfidPathResult FindNearestEquipmentPath(string startRfidId, StationType? stationType = null)
|
||||
{
|
||||
var equipmentPaths = FindPathsByDockingCondition(startRfidId, DockingDirection.Backward, stationType);
|
||||
var equipmentNodes = equipmentPaths.Where(p => p.Success).ToList();
|
||||
|
||||
return equipmentNodes.FirstOrDefault() ?? new RfidPathResult
|
||||
{
|
||||
Success = false,
|
||||
ErrorMessage = $"도킹 가능한 장비를 찾을 수 없습니다. ({stationType?.ToString() ?? "모든 타입"})"
|
||||
};
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -1,144 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace AGVMapEditor.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// A* 알고리즘에서 사용되는 경로 노드
|
||||
/// </summary>
|
||||
public class PathNode : IComparable<PathNode>
|
||||
{
|
||||
/// <summary>
|
||||
/// 맵 노드 ID
|
||||
/// </summary>
|
||||
public string NodeId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// AGV의 현재 방향 (이 노드에 도달했을 때의 방향)
|
||||
/// </summary>
|
||||
public AgvDirection Direction { get; set; } = AgvDirection.Forward;
|
||||
|
||||
/// <summary>
|
||||
/// 시작점에서 이 노드까지의 실제 비용 (G)
|
||||
/// </summary>
|
||||
public float GCost { get; set; } = float.MaxValue;
|
||||
|
||||
/// <summary>
|
||||
/// 이 노드에서 목표까지의 추정 비용 (H)
|
||||
/// </summary>
|
||||
public float HCost { get; set; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// 총 비용 (F = G + H)
|
||||
/// </summary>
|
||||
public float FCost => GCost + HCost;
|
||||
|
||||
/// <summary>
|
||||
/// 이전 노드 (경로 추적용)
|
||||
/// </summary>
|
||||
public PathNode Parent { get; set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// 회전 횟수 (방향 전환 비용 계산용)
|
||||
/// </summary>
|
||||
public int RotationCount { get; set; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// 이 노드에 도달하기 위한 이동 명령 시퀀스
|
||||
/// </summary>
|
||||
public List<AgvDirection> MovementSequence { get; set; } = new List<AgvDirection>();
|
||||
|
||||
/// <summary>
|
||||
/// 기본 생성자
|
||||
/// </summary>
|
||||
public PathNode()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 매개변수 생성자
|
||||
/// </summary>
|
||||
/// <param name="nodeId">노드 ID</param>
|
||||
/// <param name="direction">AGV 방향</param>
|
||||
public PathNode(string nodeId, AgvDirection direction)
|
||||
{
|
||||
NodeId = nodeId;
|
||||
Direction = direction;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 우선순위 큐를 위한 비교 (FCost 기준)
|
||||
/// </summary>
|
||||
public int CompareTo(PathNode other)
|
||||
{
|
||||
if (other == null) return 1;
|
||||
|
||||
int compare = FCost.CompareTo(other.FCost);
|
||||
if (compare == 0)
|
||||
{
|
||||
// FCost가 같으면 HCost가 낮은 것을 우선
|
||||
compare = HCost.CompareTo(other.HCost);
|
||||
}
|
||||
if (compare == 0)
|
||||
{
|
||||
// 그것도 같으면 회전 횟수가 적은 것을 우선
|
||||
compare = RotationCount.CompareTo(other.RotationCount);
|
||||
}
|
||||
|
||||
return compare;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 노드 상태 복사
|
||||
/// </summary>
|
||||
public PathNode Clone()
|
||||
{
|
||||
return new PathNode
|
||||
{
|
||||
NodeId = NodeId,
|
||||
Direction = Direction,
|
||||
GCost = GCost,
|
||||
HCost = HCost,
|
||||
Parent = Parent,
|
||||
RotationCount = RotationCount,
|
||||
MovementSequence = new List<AgvDirection>(MovementSequence)
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 고유 키 생성 (노드ID + 방향)
|
||||
/// </summary>
|
||||
public string GetKey()
|
||||
{
|
||||
return $"{NodeId}_{Direction}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 문자열 표현
|
||||
/// </summary>
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{NodeId}({Direction}) F:{FCost:F1} G:{GCost:F1} H:{HCost:F1} R:{RotationCount}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 해시코드 (딕셔너리 키용)
|
||||
/// </summary>
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return GetKey().GetHashCode();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 동등성 비교
|
||||
/// </summary>
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
if (obj is PathNode other)
|
||||
{
|
||||
return NodeId == other.NodeId && Direction == other.Direction;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,277 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace AGVMapEditor.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// 경로 계산 결과
|
||||
/// </summary>
|
||||
public class PathResult
|
||||
{
|
||||
/// <summary>
|
||||
/// 경로 계산 성공 여부
|
||||
/// </summary>
|
||||
public bool Success { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// 경로상의 노드 ID 시퀀스
|
||||
/// </summary>
|
||||
public List<string> NodeSequence { get; set; } = new List<string>();
|
||||
|
||||
/// <summary>
|
||||
/// AGV 이동 명령 시퀀스
|
||||
/// </summary>
|
||||
public List<AgvDirection> MovementSequence { get; set; } = new List<AgvDirection>();
|
||||
|
||||
/// <summary>
|
||||
/// 총 이동 거리 (비용)
|
||||
/// </summary>
|
||||
public float TotalDistance { get; set; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// 총 회전 횟수
|
||||
/// </summary>
|
||||
public int TotalRotations { get; set; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// 예상 소요 시간 (초)
|
||||
/// </summary>
|
||||
public float EstimatedTime { get; set; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// 시작 노드 ID
|
||||
/// </summary>
|
||||
public string StartNodeId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 목표 노드 ID
|
||||
/// </summary>
|
||||
public string TargetNodeId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 시작시 AGV 방향
|
||||
/// </summary>
|
||||
public AgvDirection StartDirection { get; set; } = AgvDirection.Forward;
|
||||
|
||||
/// <summary>
|
||||
/// 도착시 AGV 방향
|
||||
/// </summary>
|
||||
public AgvDirection EndDirection { get; set; } = AgvDirection.Forward;
|
||||
|
||||
/// <summary>
|
||||
/// 경로 계산에 걸린 시간 (밀리초)
|
||||
/// </summary>
|
||||
public long CalculationTime { get; set; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// 오류 메시지 (실패시)
|
||||
/// </summary>
|
||||
public string ErrorMessage { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 경로상의 상세 정보 (디버깅용)
|
||||
/// </summary>
|
||||
public List<PathNode> DetailedPath { get; set; } = new List<PathNode>();
|
||||
|
||||
/// <summary>
|
||||
/// 회전이 발생하는 노드들
|
||||
/// </summary>
|
||||
public List<string> RotationNodes { get; set; } = new List<string>();
|
||||
|
||||
/// <summary>
|
||||
/// 기본 생성자
|
||||
/// </summary>
|
||||
public PathResult()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 성공 결과 생성자
|
||||
/// </summary>
|
||||
public PathResult(List<PathNode> path, string startNodeId, string targetNodeId, AgvDirection startDirection)
|
||||
{
|
||||
if (path == null || path.Count == 0)
|
||||
{
|
||||
Success = false;
|
||||
ErrorMessage = "빈 경로입니다.";
|
||||
return;
|
||||
}
|
||||
|
||||
Success = true;
|
||||
StartNodeId = startNodeId;
|
||||
TargetNodeId = targetNodeId;
|
||||
StartDirection = startDirection;
|
||||
DetailedPath = new List<PathNode>(path);
|
||||
|
||||
// 노드 시퀀스 구성
|
||||
NodeSequence = path.Select(p => p.NodeId).ToList();
|
||||
|
||||
// 이동 명령 시퀀스 구성
|
||||
MovementSequence = new List<AgvDirection>();
|
||||
for (int i = 0; i < path.Count; i++)
|
||||
{
|
||||
MovementSequence.AddRange(path[i].MovementSequence);
|
||||
}
|
||||
|
||||
// 통계 계산
|
||||
if (path.Count > 0)
|
||||
{
|
||||
TotalDistance = path[path.Count - 1].GCost;
|
||||
EndDirection = path[path.Count - 1].Direction;
|
||||
}
|
||||
|
||||
TotalRotations = MovementSequence.Count(cmd =>
|
||||
cmd == AgvDirection.Left || cmd == AgvDirection.Right);
|
||||
|
||||
// 회전 노드 추출
|
||||
var previousDirection = startDirection;
|
||||
for (int i = 0; i < path.Count; i++)
|
||||
{
|
||||
if (path[i].Direction != previousDirection)
|
||||
{
|
||||
RotationNodes.Add(path[i].NodeId);
|
||||
}
|
||||
previousDirection = path[i].Direction;
|
||||
}
|
||||
|
||||
// 예상 소요 시간 계산 (단순 추정)
|
||||
EstimatedTime = CalculateEstimatedTime();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 실패 결과 생성자
|
||||
/// </summary>
|
||||
public PathResult(string errorMessage)
|
||||
{
|
||||
Success = false;
|
||||
ErrorMessage = errorMessage;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 예상 소요 시간 계산
|
||||
/// </summary>
|
||||
private float CalculateEstimatedTime()
|
||||
{
|
||||
// 기본 이동 속도 및 회전 시간 가정
|
||||
const float MOVE_SPEED = 1.0f; // 단위/초
|
||||
const float ROTATION_TIME = 2.0f; // 초/회전
|
||||
|
||||
float moveTime = TotalDistance / MOVE_SPEED;
|
||||
float rotationTime = TotalRotations * ROTATION_TIME;
|
||||
|
||||
return moveTime + rotationTime;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 경로 요약 정보
|
||||
/// </summary>
|
||||
public string GetSummary()
|
||||
{
|
||||
if (!Success)
|
||||
{
|
||||
return $"경로 계산 실패: {ErrorMessage}";
|
||||
}
|
||||
|
||||
return $"경로: {NodeSequence.Count}개 노드, " +
|
||||
$"거리: {TotalDistance:F1}, " +
|
||||
$"회전: {TotalRotations}회, " +
|
||||
$"예상시간: {EstimatedTime:F1}초";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 상세 경로 정보
|
||||
/// </summary>
|
||||
public List<string> GetDetailedSteps()
|
||||
{
|
||||
var steps = new List<string>();
|
||||
|
||||
if (!Success)
|
||||
{
|
||||
steps.Add($"경로 계산 실패: {ErrorMessage}");
|
||||
return steps;
|
||||
}
|
||||
|
||||
steps.Add($"시작: {StartNodeId} (방향: {StartDirection})");
|
||||
|
||||
for (int i = 0; i < DetailedPath.Count; i++)
|
||||
{
|
||||
var node = DetailedPath[i];
|
||||
var step = $"{i + 1}. {node.NodeId}";
|
||||
|
||||
if (node.MovementSequence.Count > 0)
|
||||
{
|
||||
step += $" [명령: {string.Join(",", node.MovementSequence)}]";
|
||||
}
|
||||
|
||||
step += $" (F:{node.FCost:F1}, 방향:{node.Direction})";
|
||||
steps.Add(step);
|
||||
}
|
||||
|
||||
steps.Add($"도착: {TargetNodeId} (최종 방향: {EndDirection})");
|
||||
|
||||
return steps;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// RFID 시퀀스 추출 (실제 AGV 제어용)
|
||||
/// </summary>
|
||||
public List<string> GetRfidSequence(NodeResolver nodeResolver)
|
||||
{
|
||||
var rfidSequence = new List<string>();
|
||||
|
||||
foreach (var nodeId in NodeSequence)
|
||||
{
|
||||
var rfidId = nodeResolver.GetRfidByNodeId(nodeId);
|
||||
if (!string.IsNullOrEmpty(rfidId))
|
||||
{
|
||||
rfidSequence.Add(rfidId);
|
||||
}
|
||||
}
|
||||
|
||||
return rfidSequence;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 경로 유효성 검증
|
||||
/// </summary>
|
||||
public bool ValidatePath(List<MapNode> mapNodes)
|
||||
{
|
||||
if (!Success || NodeSequence.Count == 0)
|
||||
return false;
|
||||
|
||||
// 모든 노드가 존재하는지 확인
|
||||
foreach (var nodeId in NodeSequence)
|
||||
{
|
||||
if (!mapNodes.Any(n => n.NodeId == nodeId))
|
||||
{
|
||||
ErrorMessage = $"존재하지 않는 노드: {nodeId}";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 연결성 확인
|
||||
for (int i = 0; i < NodeSequence.Count - 1; i++)
|
||||
{
|
||||
var currentNode = mapNodes.FirstOrDefault(n => n.NodeId == NodeSequence[i]);
|
||||
var nextNodeId = NodeSequence[i + 1];
|
||||
|
||||
if (currentNode != null && !currentNode.ConnectedNodes.Contains(nextNodeId))
|
||||
{
|
||||
ErrorMessage = $"연결되지 않은 노드: {currentNode.NodeId} → {nextNodeId}";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// JSON 직렬화를 위한 문자열 변환
|
||||
/// </summary>
|
||||
public override string ToString()
|
||||
{
|
||||
return GetSummary();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
using System;
|
||||
using AGVNavigationCore.Models;
|
||||
|
||||
namespace AGVMapEditor.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// RFID와 논리적 노드 ID를 매핑하는 클래스
|
||||
/// 물리적 RFID는 의미없는 고유값, 논리적 노드는 맵 에디터에서 관리
|
||||
/// </summary>
|
||||
public class RfidMapping
|
||||
{
|
||||
/// <summary>
|
||||
/// 물리적 RFID 값 (의미 없는 고유 식별자)
|
||||
/// 예: "1234567890", "ABCDEF1234" 등
|
||||
/// </summary>
|
||||
public string RfidId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 논리적 노드 ID (맵 에디터에서 관리)
|
||||
/// 예: "N001", "N002", "LOADER1", "CHARGER1" 등
|
||||
/// </summary>
|
||||
public string LogicalNodeId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 매핑 생성 일자
|
||||
/// </summary>
|
||||
public DateTime CreatedDate { get; set; } = DateTime.Now;
|
||||
|
||||
/// <summary>
|
||||
/// 마지막 수정 일자
|
||||
/// </summary>
|
||||
public DateTime ModifiedDate { get; set; } = DateTime.Now;
|
||||
|
||||
/// <summary>
|
||||
/// 설치 위치 설명 (현장 작업자용)
|
||||
/// 예: "로더1번 앞", "충전기2번 입구", "복도 교차점" 등
|
||||
/// </summary>
|
||||
public string Description { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// RFID 상태 (정상, 손상, 교체예정 등)
|
||||
/// </summary>
|
||||
public string Status { get; set; } = "정상";
|
||||
|
||||
/// <summary>
|
||||
/// 매핑 활성화 여부
|
||||
/// </summary>
|
||||
public bool IsActive { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 기본 생성자
|
||||
/// </summary>
|
||||
public RfidMapping()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 매개변수 생성자
|
||||
/// </summary>
|
||||
/// <param name="rfidId">물리적 RFID ID</param>
|
||||
/// <param name="logicalNodeId">논리적 노드 ID</param>
|
||||
/// <param name="description">설치 위치 설명</param>
|
||||
public RfidMapping(string rfidId, string logicalNodeId, string description = "")
|
||||
{
|
||||
RfidId = rfidId;
|
||||
LogicalNodeId = logicalNodeId;
|
||||
Description = description;
|
||||
CreatedDate = DateTime.Now;
|
||||
ModifiedDate = DateTime.Now;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 문자열 표현
|
||||
/// </summary>
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{RfidId} → {LogicalNodeId} ({Description})";
|
||||
}
|
||||
}
|
||||
}
|
||||
63
Cs_HMI/AGVMapEditor/Properties/Resources.Designer.cs
generated
Normal file
63
Cs_HMI/AGVMapEditor/Properties/Resources.Designer.cs
generated
Normal file
@@ -0,0 +1,63 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// 이 코드는 도구를 사용하여 생성되었습니다.
|
||||
// 런타임 버전:4.0.30319.42000
|
||||
//
|
||||
// 파일 내용을 변경하면 잘못된 동작이 발생할 수 있으며, 코드를 다시 생성하면
|
||||
// 이러한 변경 내용이 손실됩니다.
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace AGVMapEditor.Properties {
|
||||
using System;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 지역화된 문자열 등을 찾기 위한 강력한 형식의 리소스 클래스입니다.
|
||||
/// </summary>
|
||||
// 이 클래스는 ResGen 또는 Visual Studio와 같은 도구를 통해 StronglyTypedResourceBuilder
|
||||
// 클래스에서 자동으로 생성되었습니다.
|
||||
// 멤버를 추가하거나 제거하려면 .ResX 파일을 편집한 다음 /str 옵션을 사용하여 ResGen을
|
||||
// 다시 실행하거나 VS 프로젝트를 다시 빌드하십시오.
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
||||
internal class Resources {
|
||||
|
||||
private static global::System.Resources.ResourceManager resourceMan;
|
||||
|
||||
private static global::System.Globalization.CultureInfo resourceCulture;
|
||||
|
||||
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
|
||||
internal Resources() {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 이 클래스에서 사용하는 캐시된 ResourceManager 인스턴스를 반환합니다.
|
||||
/// </summary>
|
||||
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||
internal static global::System.Resources.ResourceManager ResourceManager {
|
||||
get {
|
||||
if (object.ReferenceEquals(resourceMan, null)) {
|
||||
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("AGVMapEditor.Properties.Resources", typeof(Resources).Assembly);
|
||||
resourceMan = temp;
|
||||
}
|
||||
return resourceMan;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 이 강력한 형식의 리소스 클래스를 사용하여 모든 리소스 조회에 대해 현재 스레드의 CurrentUICulture 속성을
|
||||
/// 재정의합니다.
|
||||
/// </summary>
|
||||
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||
internal static global::System.Globalization.CultureInfo Culture {
|
||||
get {
|
||||
return resourceCulture;
|
||||
}
|
||||
set {
|
||||
resourceCulture = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
120
Cs_HMI/AGVMapEditor/Properties/Resources.resx
Normal file
120
Cs_HMI/AGVMapEditor/Properties/Resources.resx
Normal file
@@ -0,0 +1,120 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
</root>
|
||||
Reference in New Issue
Block a user