refactor: Move AGV development projects to separate AGVLogic folder
- Reorganized AGVMapEditor, AGVNavigationCore, AGVSimulator into AGVLogic folder - Removed deleted project files from root folder tracking - Updated CLAUDE.md with AGVLogic-specific development guidelines - Clean separation of independent project development from main codebase - Projects now ready for independent development and future integration 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
88
Cs_HMI/AGVLogic/AGVMapEditor/AGVMapEditor.csproj
Normal file
88
Cs_HMI/AGVLogic/AGVMapEditor/AGVMapEditor.csproj
Normal file
@@ -0,0 +1,88 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
|
||||
<PropertyGroup>
|
||||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||
<ProjectGuid>{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}</ProjectGuid>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<RootNamespace>AGVMapEditor</RootNamespace>
|
||||
<AssemblyName>AGVMapEditor</AssemblyName>
|
||||
<TargetFrameworkVersion>v4.8</TargetFrameworkVersion>
|
||||
<FileAlignment>512</FileAlignment>
|
||||
<UseWindowsForms>true</UseWindowsForms>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
<DebugType>full</DebugType>
|
||||
<Optimize>false</Optimize>
|
||||
<OutputPath>bin\Debug\</OutputPath>
|
||||
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||
<DebugType>pdbonly</DebugType>
|
||||
<Optimize>true</Optimize>
|
||||
<OutputPath>bin\Release\</OutputPath>
|
||||
<DefineConstants>TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="System" />
|
||||
<Reference Include="System.Core" />
|
||||
<Reference Include="System.Drawing" />
|
||||
<Reference Include="System.Windows.Forms" />
|
||||
<Reference Include="System.Xml" />
|
||||
<Reference Include="Newtonsoft.Json, Version=13.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed">
|
||||
<HintPath>..\packages\Newtonsoft.Json.13.0.3\lib\net45\Newtonsoft.Json.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Microsoft.VisualBasic" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\AGVNavigationCore\AGVNavigationCore.csproj">
|
||||
<Project>{C5F7A8B2-8D3E-4A1B-9C6E-7F4D5E2A9B1C}</Project>
|
||||
<Name>AGVNavigationCore</Name>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="Models\EditorSettings.cs" />
|
||||
<Compile Include="Models\MapImage.cs" />
|
||||
<Compile Include="Models\MapLabel.cs" />
|
||||
<Compile Include="Models\NodePropertyWrapper.cs" />
|
||||
<Compile Include="Forms\MainForm.cs">
|
||||
<SubType>Form</SubType>
|
||||
</Compile>
|
||||
<Compile Include="Forms\MainForm.Designer.cs">
|
||||
<DependentUpon>MainForm.cs</DependentUpon>
|
||||
</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" />
|
||||
<None Include="packages.config" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Folder Include="Data\" />
|
||||
<Folder Include="Utils\" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
</Project>
|
||||
317
Cs_HMI/AGVLogic/AGVMapEditor/Forms/MainForm.Designer.cs
generated
Normal file
317
Cs_HMI/AGVLogic/AGVMapEditor/Forms/MainForm.Designer.cs
generated
Normal file
@@ -0,0 +1,317 @@
|
||||
namespace AGVMapEditor.Forms
|
||||
{
|
||||
partial class MainForm
|
||||
{
|
||||
/// <summary>
|
||||
/// 필수 디자이너 변수입니다.
|
||||
/// </summary>
|
||||
private System.ComponentModel.IContainer components = null;
|
||||
|
||||
/// <summary>
|
||||
/// 사용 중인 모든 리소스를 정리합니다.
|
||||
/// </summary>
|
||||
/// <param name="disposing">관리되는 리소스를 삭제해야 하면 true이고, 그렇지 않으면 false입니다.</param>
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && (components != null))
|
||||
{
|
||||
components.Dispose();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
#region Windows Form 디자이너에서 생성한 코드
|
||||
|
||||
/// <summary>
|
||||
/// 디자이너 지원에 필요한 메서드입니다.
|
||||
/// 이 메서드의 내용을 코드 편집기로 수정하지 마세요.
|
||||
/// </summary>
|
||||
private void InitializeComponent()
|
||||
{
|
||||
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.label1 = new System.Windows.Forms.Label();
|
||||
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();
|
||||
//
|
||||
// statusStrip1
|
||||
//
|
||||
this.statusStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
|
||||
this.toolStripStatusLabel1});
|
||||
this.statusStrip1.Location = new System.Drawing.Point(0, 751);
|
||||
this.statusStrip1.Name = "statusStrip1";
|
||||
this.statusStrip1.Size = new System.Drawing.Size(1200, 22);
|
||||
this.statusStrip1.TabIndex = 1;
|
||||
this.statusStrip1.Text = "statusStrip1";
|
||||
//
|
||||
// toolStripStatusLabel1
|
||||
//
|
||||
this.toolStripStatusLabel1.Name = "toolStripStatusLabel1";
|
||||
this.toolStripStatusLabel1.Size = new System.Drawing.Size(39, 17);
|
||||
this.toolStripStatusLabel1.Text = "Ready";
|
||||
//
|
||||
// splitContainer1
|
||||
//
|
||||
this.splitContainer1.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
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, 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, 335);
|
||||
this.tabControl1.TabIndex = 0;
|
||||
//
|
||||
// tabPageNodes
|
||||
//
|
||||
this.tabPageNodes.Controls.Add(this.listBoxNodes);
|
||||
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, 309);
|
||||
this.tabPageNodes.TabIndex = 0;
|
||||
this.tabPageNodes.Text = "노드 관리";
|
||||
this.tabPageNodes.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// listBoxNodes
|
||||
//
|
||||
this.listBoxNodes.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
this.listBoxNodes.FormattingEnabled = true;
|
||||
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, 303);
|
||||
this.listBoxNodes.TabIndex = 1;
|
||||
//
|
||||
// label1
|
||||
//
|
||||
this.label1.AutoSize = true;
|
||||
this.label1.Location = new System.Drawing.Point(6, 6);
|
||||
this.label1.Name = "label1";
|
||||
this.label1.Size = new System.Drawing.Size(57, 12);
|
||||
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);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.ClientSize = new System.Drawing.Size(1200, 773);
|
||||
this.Controls.Add(this.splitContainer1);
|
||||
this.Controls.Add(this.statusStrip1);
|
||||
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.statusStrip1.ResumeLayout(false);
|
||||
this.statusStrip1.PerformLayout();
|
||||
this.splitContainer1.Panel1.ResumeLayout(false);
|
||||
((System.ComponentModel.ISupportInitialize)(this.splitContainer1)).EndInit();
|
||||
this.splitContainer1.ResumeLayout(false);
|
||||
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();
|
||||
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private System.Windows.Forms.StatusStrip statusStrip1;
|
||||
private System.Windows.Forms.ToolStripStatusLabel toolStripStatusLabel1;
|
||||
private System.Windows.Forms.SplitContainer splitContainer1;
|
||||
private System.Windows.Forms.TabControl tabControl1;
|
||||
private System.Windows.Forms.TabPage tabPageNodes;
|
||||
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;
|
||||
}
|
||||
}
|
||||
1158
Cs_HMI/AGVLogic/AGVMapEditor/Forms/MainForm.cs
Normal file
1158
Cs_HMI/AGVLogic/AGVMapEditor/Forms/MainForm.cs
Normal file
File diff suppressed because it is too large
Load Diff
228
Cs_HMI/AGVLogic/AGVMapEditor/Forms/MainForm.resx
Normal file
228
Cs_HMI/AGVLogic/AGVMapEditor/Forms/MainForm.resx
Normal file
@@ -0,0 +1,228 @@
|
||||
<?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>
|
||||
<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/AGVLogic/AGVMapEditor/Models/EditorSettings.cs
Normal file
162
Cs_HMI/AGVLogic/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
|
||||
}
|
||||
}
|
||||
210
Cs_HMI/AGVLogic/AGVMapEditor/Models/MapImage.cs
Normal file
210
Cs_HMI/AGVLogic/AGVMapEditor/Models/MapImage.cs
Normal file
@@ -0,0 +1,210 @@
|
||||
using System;
|
||||
using System.Drawing;
|
||||
|
||||
namespace AGVMapEditor.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// 맵 이미지 정보를 관리하는 클래스
|
||||
/// 디자인 요소용 이미지/비트맵 요소
|
||||
/// </summary>
|
||||
public class MapImage
|
||||
{
|
||||
/// <summary>
|
||||
/// 이미지 고유 ID
|
||||
/// </summary>
|
||||
public string ImageId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 이미지 파일 경로
|
||||
/// </summary>
|
||||
public string ImagePath { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 맵 상의 위치 좌표 (좌상단 기준)
|
||||
/// </summary>
|
||||
public Point Position { get; set; } = Point.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 이미지 크기 (원본 크기 기준 배율)
|
||||
/// </summary>
|
||||
public SizeF Scale { get; set; } = new SizeF(1.0f, 1.0f);
|
||||
|
||||
/// <summary>
|
||||
/// 이미지 투명도 (0.0 ~ 1.0)
|
||||
/// </summary>
|
||||
public float Opacity { get; set; } = 1.0f;
|
||||
|
||||
/// <summary>
|
||||
/// 이미지 회전 각도 (도 단위)
|
||||
/// </summary>
|
||||
public float Rotation { get; set; } = 0.0f;
|
||||
|
||||
/// <summary>
|
||||
/// 이미지 설명
|
||||
/// </summary>
|
||||
public string Description { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 이미지 생성 일자
|
||||
/// </summary>
|
||||
public DateTime CreatedDate { get; set; } = DateTime.Now;
|
||||
|
||||
/// <summary>
|
||||
/// 이미지 수정 일자
|
||||
/// </summary>
|
||||
public DateTime ModifiedDate { get; set; } = DateTime.Now;
|
||||
|
||||
/// <summary>
|
||||
/// 이미지 활성화 여부
|
||||
/// </summary>
|
||||
public bool IsActive { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 로딩된 이미지 (런타임에서만 사용, JSON 직렬화 제외)
|
||||
/// </summary>
|
||||
[Newtonsoft.Json.JsonIgnore]
|
||||
public Image LoadedImage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 기본 생성자
|
||||
/// </summary>
|
||||
public MapImage()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 매개변수 생성자
|
||||
/// </summary>
|
||||
/// <param name="imageId">이미지 ID</param>
|
||||
/// <param name="imagePath">이미지 파일 경로</param>
|
||||
/// <param name="position">위치</param>
|
||||
public MapImage(string imageId, string imagePath, Point position)
|
||||
{
|
||||
ImageId = imageId;
|
||||
ImagePath = imagePath;
|
||||
Position = position;
|
||||
CreatedDate = DateTime.Now;
|
||||
ModifiedDate = DateTime.Now;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 이미지 로드 (256x256 이상일 경우 자동 리사이즈)
|
||||
/// </summary>
|
||||
/// <returns>로드 성공 여부</returns>
|
||||
public bool LoadImage()
|
||||
{
|
||||
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 (LoadedImage == null) return Size.Empty;
|
||||
|
||||
return new Size(
|
||||
(int)(LoadedImage.Width * Scale.Width),
|
||||
(int)(LoadedImage.Height * Scale.Height)
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 문자열 표현
|
||||
/// </summary>
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{ImageId}: {System.IO.Path.GetFileName(ImagePath)} at ({Position.X}, {Position.Y})";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 이미지 복사
|
||||
/// </summary>
|
||||
/// <returns>복사된 이미지</returns>
|
||||
public MapImage Clone()
|
||||
{
|
||||
var clone = new MapImage
|
||||
{
|
||||
ImageId = ImageId,
|
||||
ImagePath = ImagePath,
|
||||
Position = Position,
|
||||
Scale = Scale,
|
||||
Opacity = Opacity,
|
||||
Rotation = Rotation,
|
||||
Description = Description,
|
||||
CreatedDate = CreatedDate,
|
||||
ModifiedDate = ModifiedDate,
|
||||
IsActive = IsActive
|
||||
};
|
||||
|
||||
// 이미지는 복사하지 않음 (필요시 LoadImage() 호출)
|
||||
return clone;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 리소스 정리
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
LoadedImage?.Dispose();
|
||||
LoadedImage = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
125
Cs_HMI/AGVLogic/AGVMapEditor/Models/MapLabel.cs
Normal file
125
Cs_HMI/AGVLogic/AGVMapEditor/Models/MapLabel.cs
Normal file
@@ -0,0 +1,125 @@
|
||||
using System;
|
||||
using System.Drawing;
|
||||
|
||||
namespace AGVMapEditor.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// 맵 라벨 정보를 관리하는 클래스
|
||||
/// 디자인 요소용 텍스트 라벨
|
||||
/// </summary>
|
||||
public class MapLabel
|
||||
{
|
||||
/// <summary>
|
||||
/// 라벨 고유 ID
|
||||
/// </summary>
|
||||
public string LabelId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 라벨 텍스트
|
||||
/// </summary>
|
||||
public string Text { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 맵 상의 위치 좌표
|
||||
/// </summary>
|
||||
public Point Position { get; set; } = Point.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 폰트 정보
|
||||
/// </summary>
|
||||
public string FontFamily { get; set; } = "Arial";
|
||||
|
||||
/// <summary>
|
||||
/// 폰트 크기
|
||||
/// </summary>
|
||||
public float FontSize { get; set; } = 12;
|
||||
|
||||
/// <summary>
|
||||
/// 폰트 스타일 (Bold, Italic 등)
|
||||
/// </summary>
|
||||
public FontStyle FontStyle { get; set; } = FontStyle.Regular;
|
||||
|
||||
/// <summary>
|
||||
/// 글자 색상
|
||||
/// </summary>
|
||||
public Color ForeColor { get; set; } = Color.Black;
|
||||
|
||||
/// <summary>
|
||||
/// 배경 색상
|
||||
/// </summary>
|
||||
public Color BackColor { get; set; } = Color.Transparent;
|
||||
|
||||
/// <summary>
|
||||
/// 배경 표시 여부
|
||||
/// </summary>
|
||||
public bool ShowBackground { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// 라벨 생성 일자
|
||||
/// </summary>
|
||||
public DateTime CreatedDate { get; set; } = DateTime.Now;
|
||||
|
||||
/// <summary>
|
||||
/// 라벨 수정 일자
|
||||
/// </summary>
|
||||
public DateTime ModifiedDate { get; set; } = DateTime.Now;
|
||||
|
||||
/// <summary>
|
||||
/// 라벨 활성화 여부
|
||||
/// </summary>
|
||||
public bool IsActive { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 기본 생성자
|
||||
/// </summary>
|
||||
public MapLabel()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 매개변수 생성자
|
||||
/// </summary>
|
||||
/// <param name="labelId">라벨 ID</param>
|
||||
/// <param name="text">라벨 텍스트</param>
|
||||
/// <param name="position">위치</param>
|
||||
public MapLabel(string labelId, string text, Point position)
|
||||
{
|
||||
LabelId = labelId;
|
||||
Text = text;
|
||||
Position = position;
|
||||
CreatedDate = DateTime.Now;
|
||||
ModifiedDate = DateTime.Now;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 문자열 표현
|
||||
/// </summary>
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{LabelId}: {Text} at ({Position.X}, {Position.Y})";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 라벨 복사
|
||||
/// </summary>
|
||||
/// <returns>복사된 라벨</returns>
|
||||
public MapLabel Clone()
|
||||
{
|
||||
return new MapLabel
|
||||
{
|
||||
LabelId = LabelId,
|
||||
Text = Text,
|
||||
Position = Position,
|
||||
FontFamily = FontFamily,
|
||||
FontSize = FontSize,
|
||||
FontStyle = FontStyle,
|
||||
ForeColor = ForeColor,
|
||||
BackColor = BackColor,
|
||||
ShowBackground = ShowBackground,
|
||||
CreatedDate = CreatedDate,
|
||||
ModifiedDate = ModifiedDate,
|
||||
IsActive = IsActive
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
515
Cs_HMI/AGVLogic/AGVMapEditor/Models/NodePropertyWrapper.cs
Normal file
515
Cs_HMI/AGVLogic/AGVMapEditor/Models/NodePropertyWrapper.cs
Normal file
@@ -0,0 +1,515 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Drawing;
|
||||
using AGVNavigationCore.Models;
|
||||
|
||||
namespace AGVMapEditor.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// 노드 타입에 따른 PropertyWrapper 팩토리
|
||||
/// </summary>
|
||||
public static class NodePropertyWrapperFactory
|
||||
{
|
||||
public static object CreateWrapper(MapNode node, List<MapNode> mapNodes)
|
||||
{
|
||||
switch (node.Type)
|
||||
{
|
||||
case NodeType.Label:
|
||||
return new LabelNodePropertyWrapper(node, mapNodes);
|
||||
case NodeType.Image:
|
||||
return new ImageNodePropertyWrapper(node, mapNodes);
|
||||
default:
|
||||
return new NodePropertyWrapper(node, mapNodes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 라벨 노드 전용 PropertyWrapper
|
||||
/// </summary>
|
||||
public class LabelNodePropertyWrapper
|
||||
{
|
||||
private MapNode _node;
|
||||
private List<MapNode> _mapNodes;
|
||||
|
||||
public LabelNodePropertyWrapper(MapNode node, List<MapNode> mapNodes)
|
||||
{
|
||||
_node = node;
|
||||
_mapNodes = mapNodes;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 래핑된 MapNode 인스턴스 접근
|
||||
/// </summary>
|
||||
public MapNode WrappedNode => _node;
|
||||
|
||||
[Category("기본 정보")]
|
||||
[DisplayName("노드 ID")]
|
||||
[Description("노드의 고유 식별자")]
|
||||
[ReadOnly(true)]
|
||||
public string NodeId
|
||||
{
|
||||
get => _node.NodeId;
|
||||
}
|
||||
|
||||
[Category("기본 정보")]
|
||||
[DisplayName("노드 타입")]
|
||||
[Description("노드의 타입")]
|
||||
[ReadOnly(true)]
|
||||
public NodeType Type
|
||||
{
|
||||
get => _node.Type;
|
||||
}
|
||||
|
||||
[Category("라벨")]
|
||||
[DisplayName("텍스트")]
|
||||
[Description("표시할 텍스트")]
|
||||
public string LabelText
|
||||
{
|
||||
get => _node.LabelText;
|
||||
set
|
||||
{
|
||||
_node.LabelText = value ?? "";
|
||||
_node.ModifiedDate = DateTime.Now;
|
||||
}
|
||||
}
|
||||
|
||||
[Category("라벨")]
|
||||
[DisplayName("폰트 패밀리")]
|
||||
[Description("폰트 패밀리")]
|
||||
public string FontFamily
|
||||
{
|
||||
get => _node.FontFamily;
|
||||
set
|
||||
{
|
||||
_node.FontFamily = value ?? "Arial";
|
||||
_node.ModifiedDate = DateTime.Now;
|
||||
}
|
||||
}
|
||||
|
||||
[Category("라벨")]
|
||||
[DisplayName("폰트 크기")]
|
||||
[Description("폰트 크기")]
|
||||
public float FontSize
|
||||
{
|
||||
get => _node.FontSize;
|
||||
set
|
||||
{
|
||||
_node.FontSize = Math.Max(6, Math.Min(72, value));
|
||||
_node.ModifiedDate = DateTime.Now;
|
||||
}
|
||||
}
|
||||
|
||||
[Category("라벨")]
|
||||
[DisplayName("폰트 스타일")]
|
||||
[Description("폰트 스타일")]
|
||||
public FontStyle FontStyle
|
||||
{
|
||||
get => _node.FontStyle;
|
||||
set
|
||||
{
|
||||
_node.FontStyle = value;
|
||||
_node.ModifiedDate = DateTime.Now;
|
||||
}
|
||||
}
|
||||
|
||||
[Category("라벨")]
|
||||
[DisplayName("전경색")]
|
||||
[Description("텍스트 색상")]
|
||||
public Color ForeColor
|
||||
{
|
||||
get => _node.ForeColor;
|
||||
set
|
||||
{
|
||||
_node.ForeColor = value;
|
||||
_node.ModifiedDate = DateTime.Now;
|
||||
}
|
||||
}
|
||||
|
||||
[Category("라벨")]
|
||||
[DisplayName("배경색")]
|
||||
[Description("배경 색상")]
|
||||
public Color BackColor
|
||||
{
|
||||
get => _node.BackColor;
|
||||
set
|
||||
{
|
||||
_node.BackColor = value;
|
||||
_node.ModifiedDate = DateTime.Now;
|
||||
}
|
||||
}
|
||||
|
||||
[Category("라벨")]
|
||||
[DisplayName("배경 표시")]
|
||||
[Description("배경을 표시할지 여부")]
|
||||
public bool ShowBackground
|
||||
{
|
||||
get => _node.ShowBackground;
|
||||
set
|
||||
{
|
||||
_node.ShowBackground = value;
|
||||
_node.ModifiedDate = DateTime.Now;
|
||||
}
|
||||
}
|
||||
|
||||
[Category("위치")]
|
||||
[DisplayName("X 좌표")]
|
||||
[Description("맵에서의 X 좌표")]
|
||||
public int PositionX
|
||||
{
|
||||
get => _node.Position.X;
|
||||
set
|
||||
{
|
||||
_node.Position = new Point(value, _node.Position.Y);
|
||||
_node.ModifiedDate = DateTime.Now;
|
||||
}
|
||||
}
|
||||
|
||||
[Category("위치")]
|
||||
[DisplayName("Y 좌표")]
|
||||
[Description("맵에서의 Y 좌표")]
|
||||
public int PositionY
|
||||
{
|
||||
get => _node.Position.Y;
|
||||
set
|
||||
{
|
||||
_node.Position = new Point(_node.Position.X, value);
|
||||
_node.ModifiedDate = DateTime.Now;
|
||||
}
|
||||
}
|
||||
|
||||
[Category("정보")]
|
||||
[DisplayName("생성 일시")]
|
||||
[Description("노드가 생성된 일시")]
|
||||
[ReadOnly(true)]
|
||||
public DateTime CreatedDate
|
||||
{
|
||||
get => _node.CreatedDate;
|
||||
}
|
||||
|
||||
[Category("정보")]
|
||||
[DisplayName("수정 일시")]
|
||||
[Description("노드가 마지막으로 수정된 일시")]
|
||||
[ReadOnly(true)]
|
||||
public DateTime ModifiedDate
|
||||
{
|
||||
get => _node.ModifiedDate;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 이미지 노드 전용 PropertyWrapper
|
||||
/// </summary>
|
||||
public class ImageNodePropertyWrapper
|
||||
{
|
||||
private MapNode _node;
|
||||
private List<MapNode> _mapNodes;
|
||||
|
||||
public ImageNodePropertyWrapper(MapNode node, List<MapNode> mapNodes)
|
||||
{
|
||||
_node = node;
|
||||
_mapNodes = mapNodes;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 래핑된 MapNode 인스턴스 접근
|
||||
/// </summary>
|
||||
public MapNode WrappedNode => _node;
|
||||
|
||||
[Category("기본 정보")]
|
||||
[DisplayName("노드 ID")]
|
||||
[Description("노드의 고유 식별자")]
|
||||
[ReadOnly(true)]
|
||||
public string NodeId
|
||||
{
|
||||
get => _node.NodeId;
|
||||
}
|
||||
|
||||
[Category("기본 정보")]
|
||||
[DisplayName("노드 타입")]
|
||||
[Description("노드의 타입")]
|
||||
[ReadOnly(true)]
|
||||
public NodeType Type
|
||||
{
|
||||
get => _node.Type;
|
||||
}
|
||||
|
||||
[Category("이미지")]
|
||||
[DisplayName("이미지 경로")]
|
||||
[Description("이미지 파일 경로")]
|
||||
// 파일 선택 에디터는 나중에 구현
|
||||
public string ImagePath
|
||||
{
|
||||
get => _node.ImagePath;
|
||||
set
|
||||
{
|
||||
_node.ImagePath = value ?? "";
|
||||
_node.LoadImage(); // 이미지 다시 로드
|
||||
_node.ModifiedDate = DateTime.Now;
|
||||
}
|
||||
}
|
||||
|
||||
[Category("이미지")]
|
||||
[DisplayName("가로 배율")]
|
||||
[Description("가로 배율 (1.0 = 원본 크기)")]
|
||||
public float ScaleWidth
|
||||
{
|
||||
get => _node.Scale.Width;
|
||||
set
|
||||
{
|
||||
_node.Scale = new SizeF(Math.Max(0.1f, Math.Min(5.0f, value)), _node.Scale.Height);
|
||||
_node.ModifiedDate = DateTime.Now;
|
||||
}
|
||||
}
|
||||
|
||||
[Category("이미지")]
|
||||
[DisplayName("세로 배율")]
|
||||
[Description("세로 배율 (1.0 = 원본 크기)")]
|
||||
public float ScaleHeight
|
||||
{
|
||||
get => _node.Scale.Height;
|
||||
set
|
||||
{
|
||||
_node.Scale = new SizeF(_node.Scale.Width, Math.Max(0.1f, Math.Min(5.0f, value)));
|
||||
_node.ModifiedDate = DateTime.Now;
|
||||
}
|
||||
}
|
||||
|
||||
[Category("이미지")]
|
||||
[DisplayName("투명도")]
|
||||
[Description("투명도 (0.0 = 투명, 1.0 = 불투명)")]
|
||||
public float Opacity
|
||||
{
|
||||
get => _node.Opacity;
|
||||
set
|
||||
{
|
||||
_node.Opacity = Math.Max(0.0f, Math.Min(1.0f, value));
|
||||
_node.ModifiedDate = DateTime.Now;
|
||||
}
|
||||
}
|
||||
|
||||
[Category("이미지")]
|
||||
[DisplayName("회전각도")]
|
||||
[Description("회전 각도 (도 단위)")]
|
||||
public float Rotation
|
||||
{
|
||||
get => _node.Rotation;
|
||||
set
|
||||
{
|
||||
_node.Rotation = value % 360;
|
||||
_node.ModifiedDate = DateTime.Now;
|
||||
}
|
||||
}
|
||||
|
||||
[Category("위치")]
|
||||
[DisplayName("X 좌표")]
|
||||
[Description("맵에서의 X 좌표")]
|
||||
public int PositionX
|
||||
{
|
||||
get => _node.Position.X;
|
||||
set
|
||||
{
|
||||
_node.Position = new Point(value, _node.Position.Y);
|
||||
_node.ModifiedDate = DateTime.Now;
|
||||
}
|
||||
}
|
||||
|
||||
[Category("위치")]
|
||||
[DisplayName("Y 좌표")]
|
||||
[Description("맵에서의 Y 좌표")]
|
||||
public int PositionY
|
||||
{
|
||||
get => _node.Position.Y;
|
||||
set
|
||||
{
|
||||
_node.Position = new Point(_node.Position.X, value);
|
||||
_node.ModifiedDate = DateTime.Now;
|
||||
}
|
||||
}
|
||||
|
||||
[Category("정보")]
|
||||
[DisplayName("생성 일시")]
|
||||
[Description("노드가 생성된 일시")]
|
||||
[ReadOnly(true)]
|
||||
public DateTime CreatedDate
|
||||
{
|
||||
get => _node.CreatedDate;
|
||||
}
|
||||
|
||||
[Category("정보")]
|
||||
[DisplayName("수정 일시")]
|
||||
[Description("노드가 마지막으로 수정된 일시")]
|
||||
[ReadOnly(true)]
|
||||
public DateTime ModifiedDate
|
||||
{
|
||||
get => _node.ModifiedDate;
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// PropertyGrid에서 사용할 노드 속성 래퍼 클래스
|
||||
/// </summary>
|
||||
public class NodePropertyWrapper
|
||||
{
|
||||
private MapNode _node;
|
||||
private List<MapNode> _mapNodes;
|
||||
|
||||
public NodePropertyWrapper(MapNode node, List<MapNode> mapNodes)
|
||||
{
|
||||
_node = node;
|
||||
_mapNodes = mapNodes;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 래핑된 MapNode 인스턴스 접근
|
||||
/// </summary>
|
||||
public MapNode WrappedNode => _node;
|
||||
|
||||
[Category("기본 정보")]
|
||||
[DisplayName("노드 ID")]
|
||||
[Description("노드의 고유 식별자")]
|
||||
[ReadOnly(true)]
|
||||
public string NodeId
|
||||
{
|
||||
get => _node.NodeId;
|
||||
set => _node.NodeId = value;
|
||||
}
|
||||
|
||||
[Category("기본 정보")]
|
||||
[DisplayName("노드 이름")]
|
||||
[Description("노드의 표시 이름")]
|
||||
public string Name
|
||||
{
|
||||
get => _node.Name;
|
||||
set
|
||||
{
|
||||
_node.Name = value;
|
||||
_node.ModifiedDate = DateTime.Now;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
[Category("기본 정보")]
|
||||
[DisplayName("노드 타입")]
|
||||
[Description("노드의 타입 (Normal, Rotation, Docking, Charging)")]
|
||||
public NodeType Type
|
||||
{
|
||||
get => _node.Type;
|
||||
set
|
||||
{
|
||||
_node.Type = value;
|
||||
_node.CanRotate = value == NodeType.Rotation;
|
||||
_node.SetDefaultColorByType(value);
|
||||
_node.ModifiedDate = DateTime.Now;
|
||||
}
|
||||
}
|
||||
|
||||
[Category("기본 정보")]
|
||||
[DisplayName("도킹 방향")]
|
||||
[Description("도킹이 필요한 노드의 경우 AGV 진입 방향 (DontCare: 방향 무관, Forward: 전진 도킹, Backward: 후진 도킹)")]
|
||||
public DockingDirection DockDirection
|
||||
{
|
||||
get => _node.DockDirection;
|
||||
set
|
||||
{
|
||||
_node.DockDirection = value;
|
||||
_node.ModifiedDate = DateTime.Now;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
[Category("위치")]
|
||||
[DisplayName("X 좌표")]
|
||||
[Description("맵에서의 X 좌표")]
|
||||
public int PositionX
|
||||
{
|
||||
get => _node.Position.X;
|
||||
set
|
||||
{
|
||||
_node.Position = new Point(value, _node.Position.Y);
|
||||
_node.ModifiedDate = DateTime.Now;
|
||||
}
|
||||
}
|
||||
|
||||
[Category("위치")]
|
||||
[DisplayName("Y 좌표")]
|
||||
[Description("맵에서의 Y 좌표")]
|
||||
public int PositionY
|
||||
{
|
||||
get => _node.Position.Y;
|
||||
set
|
||||
{
|
||||
_node.Position = new Point(_node.Position.X, value);
|
||||
_node.ModifiedDate = DateTime.Now;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
[Category("고급")]
|
||||
[DisplayName("회전 가능")]
|
||||
[Description("이 노드에서 AGV가 회전할 수 있는지 여부")]
|
||||
public bool CanRotate
|
||||
{
|
||||
get => _node.CanRotate;
|
||||
set
|
||||
{
|
||||
_node.CanRotate = value;
|
||||
_node.ModifiedDate = DateTime.Now;
|
||||
}
|
||||
}
|
||||
|
||||
[Category("고급")]
|
||||
[DisplayName("RFID")]
|
||||
[Description("RFID ID")]
|
||||
public string RFID
|
||||
{
|
||||
get => _node.RfidId;
|
||||
set
|
||||
{
|
||||
_node.RfidId = value;
|
||||
_node.ModifiedDate = DateTime.Now;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
[Category("고급")]
|
||||
[DisplayName("활성화")]
|
||||
[Description("노드 활성화 여부")]
|
||||
public bool IsActive
|
||||
{
|
||||
get => _node.IsActive;
|
||||
set
|
||||
{
|
||||
_node.IsActive = value;
|
||||
_node.ModifiedDate = DateTime.Now;
|
||||
}
|
||||
}
|
||||
|
||||
[Category("정보")]
|
||||
[DisplayName("생성 일시")]
|
||||
[Description("노드가 생성된 일시")]
|
||||
[ReadOnly(true)]
|
||||
public DateTime CreatedDate
|
||||
{
|
||||
get => _node.CreatedDate;
|
||||
}
|
||||
|
||||
[Category("정보")]
|
||||
[DisplayName("수정 일시")]
|
||||
[Description("노드가 마지막으로 수정된 일시")]
|
||||
[ReadOnly(true)]
|
||||
public DateTime ModifiedDate
|
||||
{
|
||||
get => _node.ModifiedDate;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
26
Cs_HMI/AGVLogic/AGVMapEditor/Program.cs
Normal file
26
Cs_HMI/AGVLogic/AGVMapEditor/Program.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
using System;
|
||||
using System.Windows.Forms;
|
||||
using AGVMapEditor.Forms;
|
||||
|
||||
namespace AGVMapEditor
|
||||
{
|
||||
/// <summary>
|
||||
/// 애플리케이션 진입점
|
||||
/// </summary>
|
||||
internal static class Program
|
||||
{
|
||||
/// <summary>
|
||||
/// 애플리케이션의 기본 진입점입니다.
|
||||
/// </summary>
|
||||
[STAThread]
|
||||
static void Main(string[] args)
|
||||
{
|
||||
// Windows Forms 애플리케이션 초기화
|
||||
Application.EnableVisualStyles();
|
||||
Application.SetCompatibleTextRenderingDefault(false);
|
||||
|
||||
// 메인 폼 실행 (명령줄 인수 전달)
|
||||
Application.Run(new MainForm(args));
|
||||
}
|
||||
}
|
||||
}
|
||||
36
Cs_HMI/AGVLogic/AGVMapEditor/Properties/AssemblyInfo.cs
Normal file
36
Cs_HMI/AGVLogic/AGVMapEditor/Properties/AssemblyInfo.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
// 어셈블리에 대한 일반 정보는 다음 특성 집합을 통해
|
||||
// 제어됩니다. 어셈블리와 관련된 정보를 수정하려면
|
||||
// 이러한 특성 값을 변경하세요.
|
||||
[assembly: AssemblyTitle("AGV Map Editor")]
|
||||
[assembly: AssemblyDescription("AGV Navigation Map Editor Tool")]
|
||||
[assembly: AssemblyConfiguration("")]
|
||||
[assembly: AssemblyCompany("ENIG AGV")]
|
||||
[assembly: AssemblyProduct("AGV Map Editor")]
|
||||
[assembly: AssemblyCopyright("Copyright © ENIG AGV 2025")]
|
||||
[assembly: AssemblyTrademark("")]
|
||||
[assembly: AssemblyCulture("")]
|
||||
|
||||
// ComVisible을 false로 설정하면 이 어셈블리의 형식이 COM 구성 요소에
|
||||
// 표시되지 않습니다. COM에서 이 어셈블리의 형식에 액세스하려면
|
||||
// 해당 형식에 대해 ComVisible 특성을 true로 설정하세요.
|
||||
[assembly: ComVisible(false)]
|
||||
|
||||
// 이 프로젝트가 COM에 노출되는 경우 다음 GUID는 typelib의 ID를 나타냅니다.
|
||||
[assembly: Guid("a1b2c3d4-e5f6-7890-abcd-ef1234567890")]
|
||||
|
||||
// 어셈블리의 버전 정보는 다음 네 개의 값으로 구성됩니다.
|
||||
//
|
||||
// 주 버전
|
||||
// 부 버전
|
||||
// 빌드 번호
|
||||
// 수정 버전
|
||||
//
|
||||
// 모든 값을 지정하거나 아래와 같이 '*'를 사용하여 빌드 번호 및 수정 번호를
|
||||
// 기본값으로 할 수 있습니다.
|
||||
// [assembly: AssemblyVersion("1.0.*")]
|
||||
[assembly: AssemblyVersion("1.0.0.0")]
|
||||
[assembly: AssemblyFileVersion("1.0.0.0")]
|
||||
63
Cs_HMI/AGVLogic/AGVMapEditor/Properties/Resources.Designer.cs
generated
Normal file
63
Cs_HMI/AGVLogic/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/AGVLogic/AGVMapEditor/Properties/Resources.resx
Normal file
120
Cs_HMI/AGVLogic/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>
|
||||
29
Cs_HMI/AGVLogic/AGVMapEditor/build.bat
Normal file
29
Cs_HMI/AGVLogic/AGVMapEditor/build.bat
Normal file
@@ -0,0 +1,29 @@
|
||||
@echo off
|
||||
echo Building V2GDecoder VC++ Project...
|
||||
|
||||
REM Check if Visual Studio 2022 is installed (Professional or Community)
|
||||
set MSBUILD_PRO="C:\Program Files\Microsoft Visual Studio\2022\Professional\MSBuild\Current\Bin\MSBuild.exe"
|
||||
set MSBUILD_COM="C:\Program Files\Microsoft Visual Studio\2022\Community\MSBuild\Current\Bin\MSBuild.exe"
|
||||
set MSBUILD_BT="F:\(VHD) Program Files\Microsoft Visual Studio\2022\MSBuild\Current\Bin\MSBuild.exe"
|
||||
|
||||
if exist %MSBUILD_PRO% (
|
||||
echo "Found Visual Studio 2022 Professional"
|
||||
set MSBUILD=%MSBUILD_PRO%
|
||||
) else if exist %MSBUILD_COM% (
|
||||
echo "Found Visual Studio 2022 Community"
|
||||
set MSBUILD=%MSBUILD_COM%
|
||||
) else if exist %MSBUILD_BT% (
|
||||
echo "Found Visual Studio 2022 BuildTools"
|
||||
set MSBUILD=%MSBUILD_BT%
|
||||
) else (
|
||||
echo "Visual Studio 2022 (Professional or Community) not found!"
|
||||
echo "Please install Visual Studio 2022 or update the MSBuild path."
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM Build Debug x64 configuration
|
||||
echo Building Debug x64 configuration...
|
||||
%MSBUILD% agvmapeditor.csproj
|
||||
|
||||
pause
|
||||
4
Cs_HMI/AGVLogic/AGVMapEditor/packages.config
Normal file
4
Cs_HMI/AGVLogic/AGVMapEditor/packages.config
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<packages>
|
||||
<package id="Newtonsoft.Json" version="13.0.3" targetFramework="net48" />
|
||||
</packages>
|
||||
105
Cs_HMI/AGVLogic/AGVNavigationCore/AGVNavigationCore.csproj
Normal file
105
Cs_HMI/AGVLogic/AGVNavigationCore/AGVNavigationCore.csproj
Normal file
@@ -0,0 +1,105 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
|
||||
<PropertyGroup>
|
||||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||
<ProjectGuid>{C5F7A8B2-8D3E-4A1B-9C6E-7F4D5E2A9B1C}</ProjectGuid>
|
||||
<OutputType>Library</OutputType>
|
||||
<AppDesignerFolder>Properties</AppDesignerFolder>
|
||||
<RootNamespace>AGVNavigationCore</RootNamespace>
|
||||
<AssemblyName>AGVNavigationCore</AssemblyName>
|
||||
<TargetFrameworkVersion>v4.8</TargetFrameworkVersion>
|
||||
<FileAlignment>512</FileAlignment>
|
||||
<Deterministic>true</Deterministic>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
<DebugType>full</DebugType>
|
||||
<Optimize>false</Optimize>
|
||||
<OutputPath>bin\Debug\</OutputPath>
|
||||
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||
<DebugType>pdbonly</DebugType>
|
||||
<Optimize>true</Optimize>
|
||||
<OutputPath>bin\Release\</OutputPath>
|
||||
<DefineConstants>TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x86'">
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
<OutputPath>bin\x86\Debug\</OutputPath>
|
||||
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||
<DebugType>full</DebugType>
|
||||
<PlatformTarget>x86</PlatformTarget>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x86'">
|
||||
<OutputPath>bin\x86\Release\</OutputPath>
|
||||
<DefineConstants>TRACE</DefineConstants>
|
||||
<Optimize>true</Optimize>
|
||||
<DebugType>pdbonly</DebugType>
|
||||
<PlatformTarget>x86</PlatformTarget>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="System" />
|
||||
<Reference Include="System.Core" />
|
||||
<Reference Include="System.Drawing" />
|
||||
<Reference Include="System.Windows.Forms" />
|
||||
<Reference Include="System.Xml.Linq" />
|
||||
<Reference Include="System.Data.DataSetExtensions" />
|
||||
<Reference Include="Microsoft.CSharp" />
|
||||
<Reference Include="System.Data" />
|
||||
<Reference Include="System.Net.Http" />
|
||||
<Reference Include="System.Xml" />
|
||||
<Reference Include="Newtonsoft.Json, Version=13.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\Newtonsoft.Json.13.0.3\lib\net45\Newtonsoft.Json.dll</HintPath>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="Controls\AGVState.cs" />
|
||||
<Compile Include="Controls\IAGV.cs" />
|
||||
<Compile Include="Controls\UnifiedAGVCanvas.Events.cs">
|
||||
<DependentUpon>UnifiedAGVCanvas.cs</DependentUpon>
|
||||
<SubType>UserControl</SubType>
|
||||
</Compile>
|
||||
<Compile Include="Controls\UnifiedAGVCanvas.Mouse.cs">
|
||||
<DependentUpon>UnifiedAGVCanvas.cs</DependentUpon>
|
||||
<SubType>UserControl</SubType>
|
||||
</Compile>
|
||||
<Compile Include="Models\Enums.cs" />
|
||||
<Compile Include="Models\IMovableAGV.cs" />
|
||||
<Compile Include="Models\VirtualAGV.cs" />
|
||||
<Compile Include="Models\MapLoader.cs" />
|
||||
<Compile Include="Models\MapNode.cs" />
|
||||
<Compile Include="PathFinding\Planning\AGVPathfinder.cs" />
|
||||
<Compile Include="PathFinding\Planning\DirectionChangePlanner.cs" />
|
||||
<Compile Include="PathFinding\Validation\DockingValidationResult.cs" />
|
||||
<Compile Include="PathFinding\Validation\PathValidationResult.cs" />
|
||||
<Compile Include="PathFinding\Analysis\JunctionAnalyzer.cs" />
|
||||
<Compile Include="PathFinding\Core\PathNode.cs" />
|
||||
<Compile Include="PathFinding\Core\AStarPathfinder.cs" />
|
||||
<Compile Include="PathFinding\Core\AGVPathResult.cs" />
|
||||
<Compile Include="PathFinding\Planning\NodeMotorInfo.cs" />
|
||||
<Compile Include="Controls\UnifiedAGVCanvas.cs">
|
||||
<SubType>UserControl</SubType>
|
||||
</Compile>
|
||||
<Compile Include="Controls\UnifiedAGVCanvas.Designer.cs">
|
||||
<DependentUpon>UnifiedAGVCanvas.cs</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="Utils\DockingValidator.cs" />
|
||||
<Compile Include="Utils\LiftCalculator.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="build.bat" />
|
||||
<None Include="packages.config" />
|
||||
<None Include="README.md" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
</Project>
|
||||
21
Cs_HMI/AGVLogic/AGVNavigationCore/Controls/AGVState.cs
Normal file
21
Cs_HMI/AGVLogic/AGVNavigationCore/Controls/AGVState.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
namespace AGVNavigationCore.Controls
|
||||
{
|
||||
#region Interfaces
|
||||
|
||||
/// <summary>
|
||||
/// AGV 상태 열거형
|
||||
/// </summary>
|
||||
public enum AGVState
|
||||
{
|
||||
Idle, // 대기
|
||||
Moving, // 이동 중
|
||||
Rotating, // 회전 중
|
||||
Docking, // 도킹 중
|
||||
Charging, // 충전 중
|
||||
Error // 오류
|
||||
}
|
||||
|
||||
|
||||
|
||||
#endregion
|
||||
}
|
||||
30
Cs_HMI/AGVLogic/AGVNavigationCore/Controls/IAGV.cs
Normal file
30
Cs_HMI/AGVLogic/AGVNavigationCore/Controls/IAGV.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using System.Drawing;
|
||||
using AGVNavigationCore.Models;
|
||||
|
||||
namespace AGVNavigationCore.Controls
|
||||
{
|
||||
#region Interfaces
|
||||
|
||||
/// <summary>
|
||||
/// AGV 인터페이스 (가상/실제 AGV 통합)
|
||||
/// </summary>
|
||||
public interface IAGV
|
||||
{
|
||||
string AgvId { get; }
|
||||
Point CurrentPosition { get; set; }
|
||||
AgvDirection CurrentDirection { get; set; }
|
||||
AGVState CurrentState { get; set; }
|
||||
float BatteryLevel { get; }
|
||||
|
||||
// 이동 경로 정보 추가
|
||||
Point? TargetPosition { get; }
|
||||
string CurrentNodeId { get; }
|
||||
string TargetNodeId { get; }
|
||||
DockingDirection DockingDirection { get; }
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
#endregion
|
||||
}
|
||||
41
Cs_HMI/AGVLogic/AGVNavigationCore/Controls/UnifiedAGVCanvas.Designer.cs
generated
Normal file
41
Cs_HMI/AGVLogic/AGVNavigationCore/Controls/UnifiedAGVCanvas.Designer.cs
generated
Normal file
@@ -0,0 +1,41 @@
|
||||
namespace AGVNavigationCore.Controls
|
||||
{
|
||||
partial class UnifiedAGVCanvas
|
||||
{
|
||||
/// <summary>
|
||||
/// 필수 디자이너 변수입니다.
|
||||
/// </summary>
|
||||
private System.ComponentModel.IContainer components = null;
|
||||
|
||||
|
||||
#region 구성 요소 디자이너에서 생성한 코드
|
||||
|
||||
/// <summary>
|
||||
/// 디자이너 지원에 필요한 메서드입니다.
|
||||
/// 이 메서드의 내용을 코드 편집기로 수정하지 마세요.
|
||||
/// </summary>
|
||||
private void InitializeComponent()
|
||||
{
|
||||
this.SuspendLayout();
|
||||
//
|
||||
// UnifiedAGVCanvas
|
||||
//
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 12F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.BackColor = System.Drawing.Color.White;
|
||||
this.Name = "UnifiedAGVCanvas";
|
||||
this.Size = new System.Drawing.Size(800, 600);
|
||||
this.Paint += new System.Windows.Forms.PaintEventHandler(this.UnifiedAGVCanvas_Paint);
|
||||
this.MouseClick += new System.Windows.Forms.MouseEventHandler(this.UnifiedAGVCanvas_MouseClick);
|
||||
this.MouseDoubleClick += new System.Windows.Forms.MouseEventHandler(this.UnifiedAGVCanvas_MouseDoubleClick);
|
||||
this.MouseDown += new System.Windows.Forms.MouseEventHandler(this.UnifiedAGVCanvas_MouseDown);
|
||||
this.MouseMove += new System.Windows.Forms.MouseEventHandler(this.UnifiedAGVCanvas_MouseMove);
|
||||
this.MouseUp += new System.Windows.Forms.MouseEventHandler(this.UnifiedAGVCanvas_MouseUp);
|
||||
this.MouseWheel += new System.Windows.Forms.MouseEventHandler(this.UnifiedAGVCanvas_MouseWheel);
|
||||
this.ResumeLayout(false);
|
||||
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,789 @@
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using System.Windows.Forms;
|
||||
using AGVNavigationCore.Models;
|
||||
|
||||
namespace AGVNavigationCore.Controls
|
||||
{
|
||||
public partial class UnifiedAGVCanvas
|
||||
{
|
||||
#region Mouse Events
|
||||
|
||||
private void UnifiedAGVCanvas_MouseClick(object sender, MouseEventArgs e)
|
||||
{
|
||||
Focus(); // 포커스 설정
|
||||
|
||||
var worldPoint = ScreenToWorld(e.Location);
|
||||
var hitNode = GetNodeAt(worldPoint);
|
||||
|
||||
switch (_editMode)
|
||||
{
|
||||
case EditMode.Select:
|
||||
HandleSelectClick(hitNode, worldPoint);
|
||||
break;
|
||||
|
||||
case EditMode.AddNode:
|
||||
HandleAddNodeClick(worldPoint);
|
||||
break;
|
||||
|
||||
case EditMode.Connect:
|
||||
HandleConnectClick(hitNode);
|
||||
break;
|
||||
|
||||
case EditMode.Delete:
|
||||
HandleDeleteClick(hitNode);
|
||||
break;
|
||||
|
||||
case EditMode.DeleteConnection:
|
||||
HandleDeleteConnectionClick(worldPoint);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void UnifiedAGVCanvas_MouseDoubleClick(object sender, MouseEventArgs e)
|
||||
{
|
||||
var worldPoint = ScreenToWorld(e.Location);
|
||||
var hitNode = GetNodeAt(worldPoint);
|
||||
|
||||
if (hitNode != null)
|
||||
{
|
||||
// 노드 속성 편집 (이벤트 발생)
|
||||
NodeSelected?.Invoke(this, hitNode);
|
||||
}
|
||||
}
|
||||
|
||||
private void UnifiedAGVCanvas_MouseDown(object sender, MouseEventArgs e)
|
||||
{
|
||||
var worldPoint = ScreenToWorld(e.Location);
|
||||
|
||||
if (e.Button == MouseButtons.Left)
|
||||
{
|
||||
if (_editMode == EditMode.Move)
|
||||
{
|
||||
var hitNode = GetNodeAt(worldPoint);
|
||||
if (hitNode != null)
|
||||
{
|
||||
_isDragging = true;
|
||||
_selectedNode = hitNode;
|
||||
_dragOffset = new Point(
|
||||
worldPoint.X - hitNode.Position.X,
|
||||
worldPoint.Y - hitNode.Position.Y
|
||||
);
|
||||
Cursor = Cursors.SizeAll;
|
||||
Invalidate();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 팬 시작 (우클릭 또는 중간버튼)
|
||||
_isPanning = true;
|
||||
_lastMousePosition = e.Location;
|
||||
Cursor = Cursors.SizeAll;
|
||||
}
|
||||
else if (e.Button == MouseButtons.Right)
|
||||
{
|
||||
// 컨텍스트 메뉴 (편집 모드에서만)
|
||||
if (_canvasMode == CanvasMode.Edit)
|
||||
{
|
||||
var hitNode = GetNodeAt(worldPoint);
|
||||
ShowContextMenu(e.Location, hitNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void UnifiedAGVCanvas_MouseMove(object sender, MouseEventArgs e)
|
||||
{
|
||||
var worldPoint = ScreenToWorld(e.Location);
|
||||
|
||||
// 호버 노드 업데이트
|
||||
var newHoveredNode = GetNodeAt(worldPoint);
|
||||
if (newHoveredNode != _hoveredNode)
|
||||
{
|
||||
_hoveredNode = newHoveredNode;
|
||||
Invalidate();
|
||||
}
|
||||
|
||||
if (_isPanning)
|
||||
{
|
||||
// 팬 처리
|
||||
var deltaX = e.X - _lastMousePosition.X;
|
||||
var deltaY = e.Y - _lastMousePosition.Y;
|
||||
|
||||
_panOffset.X += deltaX;
|
||||
_panOffset.Y += deltaY;
|
||||
|
||||
_lastMousePosition = e.Location;
|
||||
Invalidate();
|
||||
}
|
||||
else if (_isDragging && _canvasMode == CanvasMode.Edit)
|
||||
{
|
||||
// 노드 드래그
|
||||
if (_selectedNode != null)
|
||||
{
|
||||
var newPosition = new Point(
|
||||
worldPoint.X - _dragOffset.X,
|
||||
worldPoint.Y - _dragOffset.Y
|
||||
);
|
||||
|
||||
// 그리드 스냅
|
||||
if (ModifierKeys.HasFlag(Keys.Control))
|
||||
{
|
||||
newPosition.X = (newPosition.X / GRID_SIZE) * GRID_SIZE;
|
||||
newPosition.Y = (newPosition.Y / GRID_SIZE) * GRID_SIZE;
|
||||
}
|
||||
|
||||
_selectedNode.Position = newPosition;
|
||||
NodeMoved?.Invoke(this, _selectedNode);
|
||||
MapChanged?.Invoke(this, EventArgs.Empty);
|
||||
Invalidate();
|
||||
}
|
||||
}
|
||||
else if (_isConnectionMode && _canvasMode == CanvasMode.Edit)
|
||||
{
|
||||
// 임시 연결선 업데이트
|
||||
_connectionEndPoint = worldPoint;
|
||||
Invalidate();
|
||||
}
|
||||
|
||||
// 툴팁 표시 (호버된 노드/AGV 정보)
|
||||
UpdateTooltip(worldPoint);
|
||||
}
|
||||
|
||||
private void UnifiedAGVCanvas_MouseUp(object sender, MouseEventArgs e)
|
||||
{
|
||||
if (e.Button == MouseButtons.Left)
|
||||
{
|
||||
if (_isDragging && _canvasMode == CanvasMode.Edit)
|
||||
{
|
||||
_isDragging = false;
|
||||
Cursor = GetCursorForMode(_editMode);
|
||||
}
|
||||
|
||||
if (_isPanning)
|
||||
{
|
||||
_isPanning = false;
|
||||
Cursor = Cursors.Default;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void UnifiedAGVCanvas_MouseWheel(object sender, MouseEventArgs e)
|
||||
{
|
||||
// 현재 마우스 위치를 월드 좌표로 변환 (줌 전)
|
||||
var mouseWorldBefore = ScreenToWorld(e.Location);
|
||||
|
||||
float oldZoom = _zoomFactor;
|
||||
|
||||
// 줌 팩터 계산 (휠 델타 기반) - 더 부드러운 줌
|
||||
if (e.Delta > 0)
|
||||
_zoomFactor = Math.Min(_zoomFactor * 1.15f, 5.0f); // 확대 (더 부드러움)
|
||||
else
|
||||
_zoomFactor = Math.Max(_zoomFactor / 1.15f, 0.1f); // 축소 (더 부드러움)
|
||||
|
||||
// 줌 후 마우스 위치의 월드 좌표
|
||||
var mouseWorldAfter = ScreenToWorld(e.Location);
|
||||
|
||||
// 마우스 위치가 같은 월드 좌표를 가리키도록 팬 오프셋 조정
|
||||
_panOffset.X += (int)((mouseWorldBefore.X - mouseWorldAfter.X) * _zoomFactor);
|
||||
_panOffset.Y += (int)((mouseWorldBefore.Y - mouseWorldAfter.Y) * _zoomFactor);
|
||||
|
||||
Invalidate();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Mouse Helper Methods
|
||||
|
||||
private Point ScreenToWorld(Point screenPoint)
|
||||
{
|
||||
// 변환 행렬 생성 (렌더링과 동일)
|
||||
var transform = new System.Drawing.Drawing2D.Matrix();
|
||||
transform.Scale(_zoomFactor, _zoomFactor);
|
||||
transform.Translate(_panOffset.X, _panOffset.Y);
|
||||
|
||||
// 역변환 행렬로 화면 좌표를 월드 좌표로 변환
|
||||
transform.Invert();
|
||||
var points = new System.Drawing.PointF[] { new System.Drawing.PointF(screenPoint.X, screenPoint.Y) };
|
||||
transform.TransformPoints(points);
|
||||
|
||||
return new Point((int)points[0].X, (int)points[0].Y);
|
||||
}
|
||||
|
||||
private Point WorldToScreen(Point worldPoint)
|
||||
{
|
||||
return new Point(
|
||||
(int)(worldPoint.X * _zoomFactor + _panOffset.X),
|
||||
(int)(worldPoint.Y * _zoomFactor + _panOffset.Y)
|
||||
);
|
||||
}
|
||||
|
||||
private MapNode GetNodeAt(Point worldPoint)
|
||||
{
|
||||
if (_nodes == null) return null;
|
||||
|
||||
// 역순으로 검사하여 위에 그려진 노드부터 확인
|
||||
for (int i = _nodes.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var node = _nodes[i];
|
||||
if (IsPointInNode(worldPoint, node))
|
||||
return node;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private bool IsPointInNode(Point point, MapNode node)
|
||||
{
|
||||
switch (node.Type)
|
||||
{
|
||||
case NodeType.Label:
|
||||
return IsPointInLabelNode(point, node);
|
||||
case NodeType.Image:
|
||||
return IsPointInImageNode(point, node);
|
||||
default:
|
||||
return IsPointInCircularNode(point, node);
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsPointInCircularNode(Point point, MapNode node)
|
||||
{
|
||||
switch (node.Type)
|
||||
{
|
||||
case NodeType.Docking:
|
||||
return IsPointInPentagon(point, node);
|
||||
case NodeType.Charging:
|
||||
return IsPointInTriangle(point, node);
|
||||
default:
|
||||
return IsPointInCircle(point, node);
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsPointInCircle(Point point, MapNode node)
|
||||
{
|
||||
// 화면에서 최소 20픽셀 정도의 히트 영역을 확보하되, 노드 크기보다 작아지지 않게 함
|
||||
var minHitRadiusInScreen = 20;
|
||||
var hitRadius = Math.Max(NODE_RADIUS, minHitRadiusInScreen / _zoomFactor);
|
||||
|
||||
var distance = Math.Sqrt(
|
||||
Math.Pow(node.Position.X - point.X, 2) +
|
||||
Math.Pow(node.Position.Y - point.Y, 2)
|
||||
);
|
||||
return distance <= hitRadius;
|
||||
}
|
||||
|
||||
private bool IsPointInPentagon(Point point, MapNode node)
|
||||
{
|
||||
// 화면에서 최소 20픽셀 정도의 히트 영역을 확보
|
||||
var minHitRadiusInScreen = 20;
|
||||
var radius = Math.Max(NODE_RADIUS, minHitRadiusInScreen / _zoomFactor);
|
||||
var center = node.Position;
|
||||
|
||||
// 5각형 꼭짓점 계산
|
||||
var points = new Point[5];
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
var angle = (Math.PI * 2 * i / 5) - Math.PI / 2;
|
||||
points[i] = new Point(
|
||||
(int)(center.X + radius * Math.Cos(angle)),
|
||||
(int)(center.Y + radius * Math.Sin(angle))
|
||||
);
|
||||
}
|
||||
|
||||
return IsPointInPolygon(point, points);
|
||||
}
|
||||
|
||||
private bool IsPointInTriangle(Point point, MapNode node)
|
||||
{
|
||||
// 화면에서 최소 20픽셀 정도의 히트 영역을 확보하되, 노드 크기보다 작아지지 않게 함
|
||||
var minHitRadiusInScreen = 20;
|
||||
var radius = Math.Max(NODE_RADIUS, minHitRadiusInScreen / _zoomFactor);
|
||||
var center = node.Position;
|
||||
|
||||
// 삼각형 꼭짓점 계산
|
||||
var points = new Point[3];
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
var angle = (Math.PI * 2 * i / 3) - Math.PI / 2;
|
||||
points[i] = new Point(
|
||||
(int)(center.X + radius * Math.Cos(angle)),
|
||||
(int)(center.Y + radius * Math.Sin(angle))
|
||||
);
|
||||
}
|
||||
|
||||
return IsPointInPolygon(point, points);
|
||||
}
|
||||
|
||||
private bool IsPointInPolygon(Point point, Point[] polygon)
|
||||
{
|
||||
// Ray casting 알고리즘 사용
|
||||
bool inside = false;
|
||||
int j = polygon.Length - 1;
|
||||
|
||||
for (int i = 0; i < polygon.Length; i++)
|
||||
{
|
||||
var xi = polygon[i].X;
|
||||
var yi = polygon[i].Y;
|
||||
var xj = polygon[j].X;
|
||||
var yj = polygon[j].Y;
|
||||
|
||||
if (((yi > point.Y) != (yj > point.Y)) &&
|
||||
(point.X < (xj - xi) * (point.Y - yi) / (yj - yi) + xi))
|
||||
{
|
||||
inside = !inside;
|
||||
}
|
||||
j = i;
|
||||
}
|
||||
|
||||
return inside;
|
||||
}
|
||||
|
||||
private bool IsPointInLabelNode(Point point, MapNode node)
|
||||
{
|
||||
var text = string.IsNullOrEmpty(node.LabelText) ? node.NodeId : node.LabelText;
|
||||
|
||||
// 임시 Graphics로 텍스트 크기 측정
|
||||
using (var tempBitmap = new Bitmap(1, 1))
|
||||
using (var tempGraphics = Graphics.FromImage(tempBitmap))
|
||||
{
|
||||
var font = new Font(node.FontFamily, node.FontSize, node.FontStyle);
|
||||
var textSize = tempGraphics.MeasureString(text, font);
|
||||
|
||||
var textRect = new Rectangle(
|
||||
(int)(node.Position.X - textSize.Width / 2),
|
||||
(int)(node.Position.Y - textSize.Height / 2),
|
||||
(int)textSize.Width,
|
||||
(int)textSize.Height
|
||||
);
|
||||
|
||||
font.Dispose();
|
||||
return textRect.Contains(point);
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsPointInImageNode(Point point, MapNode node)
|
||||
{
|
||||
var displaySize = node.GetDisplaySize();
|
||||
if (displaySize.IsEmpty)
|
||||
displaySize = new Size(50, 50); // 기본 크기
|
||||
|
||||
var imageRect = new Rectangle(
|
||||
node.Position.X - displaySize.Width / 2,
|
||||
node.Position.Y - displaySize.Height / 2,
|
||||
displaySize.Width,
|
||||
displaySize.Height
|
||||
);
|
||||
|
||||
return imageRect.Contains(point);
|
||||
}
|
||||
|
||||
private IAGV GetAGVAt(Point worldPoint)
|
||||
{
|
||||
if (_agvList == null) return null;
|
||||
|
||||
var hitRadius = Math.Max(AGV_SIZE / 2, 15 / _zoomFactor);
|
||||
|
||||
return _agvList.FirstOrDefault(agv =>
|
||||
{
|
||||
if (!_agvPositions.ContainsKey(agv.AgvId)) return false;
|
||||
|
||||
var agvPos = _agvPositions[agv.AgvId];
|
||||
var distance = Math.Sqrt(
|
||||
Math.Pow(agvPos.X - worldPoint.X, 2) +
|
||||
Math.Pow(agvPos.Y - worldPoint.Y, 2)
|
||||
);
|
||||
return distance <= hitRadius;
|
||||
});
|
||||
}
|
||||
|
||||
private void HandleSelectClick(MapNode hitNode, Point worldPoint)
|
||||
{
|
||||
if (hitNode != null)
|
||||
{
|
||||
// 노드 선택
|
||||
if (hitNode != _selectedNode)
|
||||
{
|
||||
_selectedNode = hitNode;
|
||||
NodeSelected?.Invoke(this, hitNode);
|
||||
Invalidate();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 노드가 없으면 연결선 체크
|
||||
var connection = GetConnectionAt(worldPoint);
|
||||
if (connection != null)
|
||||
{
|
||||
// 연결선을 클릭했을 때 삭제 확인
|
||||
var (fromNode, toNode) = connection.Value;
|
||||
string fromDisplay = !string.IsNullOrEmpty(fromNode.RfidId) ? fromNode.RfidId : fromNode.NodeId;
|
||||
string toDisplay = !string.IsNullOrEmpty(toNode.RfidId) ? toNode.RfidId : toNode.NodeId;
|
||||
|
||||
var result = MessageBox.Show(
|
||||
$"연결을 삭제하시겠습니까?\n\n{fromDisplay} ↔ {toDisplay}",
|
||||
"연결 삭제 확인",
|
||||
MessageBoxButtons.YesNo,
|
||||
MessageBoxIcon.Question);
|
||||
|
||||
if (result == DialogResult.Yes)
|
||||
{
|
||||
// 단일 연결 삭제 (어느 방향에 저장되어 있는지 확인 후 삭제)
|
||||
if (fromNode.ConnectedNodes.Contains(toNode.NodeId))
|
||||
{
|
||||
fromNode.RemoveConnection(toNode.NodeId);
|
||||
}
|
||||
else if (toNode.ConnectedNodes.Contains(fromNode.NodeId))
|
||||
{
|
||||
toNode.RemoveConnection(fromNode.NodeId);
|
||||
}
|
||||
|
||||
// 이벤트 발생
|
||||
ConnectionDeleted?.Invoke(this, (fromNode, toNode));
|
||||
MapChanged?.Invoke(this, EventArgs.Empty);
|
||||
Invalidate();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 빈 공간 클릭 시 선택 해제
|
||||
if (_selectedNode != null)
|
||||
{
|
||||
_selectedNode = null;
|
||||
NodeSelected?.Invoke(this, null);
|
||||
Invalidate();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleAddNodeClick(Point worldPoint)
|
||||
{
|
||||
// 그리드 스냅
|
||||
if (ModifierKeys.HasFlag(Keys.Control))
|
||||
{
|
||||
worldPoint.X = (worldPoint.X / GRID_SIZE) * GRID_SIZE;
|
||||
worldPoint.Y = (worldPoint.Y / GRID_SIZE) * GRID_SIZE;
|
||||
}
|
||||
|
||||
// 고유한 NodeId 생성
|
||||
string newNodeId = GenerateUniqueNodeId();
|
||||
|
||||
var newNode = new MapNode
|
||||
{
|
||||
NodeId = newNodeId,
|
||||
Position = worldPoint,
|
||||
Type = NodeType.Normal
|
||||
};
|
||||
|
||||
_nodes.Add(newNode);
|
||||
|
||||
NodeAdded?.Invoke(this, newNode);
|
||||
MapChanged?.Invoke(this, EventArgs.Empty);
|
||||
Invalidate();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 중복되지 않는 고유한 NodeId 생성
|
||||
/// </summary>
|
||||
private string GenerateUniqueNodeId()
|
||||
{
|
||||
string nodeId;
|
||||
int counter = _nodeCounter;
|
||||
|
||||
do
|
||||
{
|
||||
nodeId = $"N{counter:D3}";
|
||||
counter++;
|
||||
}
|
||||
while (_nodes.Any(n => n.NodeId == nodeId));
|
||||
|
||||
_nodeCounter = counter;
|
||||
return nodeId;
|
||||
}
|
||||
|
||||
private void HandleConnectClick(MapNode hitNode)
|
||||
{
|
||||
if (hitNode == null) return;
|
||||
|
||||
if (!_isConnectionMode)
|
||||
{
|
||||
// 연결 시작
|
||||
_isConnectionMode = true;
|
||||
_connectionStartNode = hitNode;
|
||||
_selectedNode = hitNode;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 연결 완료
|
||||
if (_connectionStartNode != null && _connectionStartNode != hitNode)
|
||||
{
|
||||
CreateConnection(_connectionStartNode, hitNode);
|
||||
}
|
||||
CancelConnection();
|
||||
}
|
||||
|
||||
Invalidate();
|
||||
}
|
||||
|
||||
private void HandleDeleteClick(MapNode hitNode)
|
||||
{
|
||||
if (hitNode == null) return;
|
||||
|
||||
// 연결된 모든 연결선도 제거
|
||||
foreach (var node in _nodes)
|
||||
{
|
||||
node.RemoveConnection(hitNode.NodeId);
|
||||
}
|
||||
|
||||
_nodes.Remove(hitNode);
|
||||
|
||||
if (_selectedNode == hitNode)
|
||||
_selectedNode = null;
|
||||
|
||||
NodeDeleted?.Invoke(this, hitNode);
|
||||
MapChanged?.Invoke(this, EventArgs.Empty);
|
||||
Invalidate();
|
||||
}
|
||||
|
||||
private void CreateConnection(MapNode fromNode, MapNode toNode)
|
||||
{
|
||||
// 중복 연결 체크 (양방향)
|
||||
if (fromNode.ConnectedNodes.Contains(toNode.NodeId) ||
|
||||
toNode.ConnectedNodes.Contains(fromNode.NodeId))
|
||||
return;
|
||||
|
||||
// 단일 연결 생성 (사전순으로 정렬하여 일관성 유지)
|
||||
if (string.Compare(fromNode.NodeId, toNode.NodeId, StringComparison.Ordinal) < 0)
|
||||
{
|
||||
fromNode.AddConnection(toNode.NodeId);
|
||||
}
|
||||
else
|
||||
{
|
||||
toNode.AddConnection(fromNode.NodeId);
|
||||
}
|
||||
|
||||
MapChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
private float CalculateDistance(Point from, Point to)
|
||||
{
|
||||
var dx = to.X - from.X;
|
||||
var dy = to.Y - from.Y;
|
||||
return (float)Math.Sqrt(dx * dx + dy * dy);
|
||||
}
|
||||
|
||||
private void ShowContextMenu(Point location, MapNode hitNode)
|
||||
{
|
||||
_contextMenu.Items.Clear();
|
||||
|
||||
if (hitNode != null)
|
||||
{
|
||||
_contextMenu.Items.Add("노드 속성...", null, (s, e) => NodeSelected?.Invoke(this, hitNode));
|
||||
_contextMenu.Items.Add("노드 삭제", null, (s, e) => HandleDeleteClick(hitNode));
|
||||
_contextMenu.Items.Add("-");
|
||||
}
|
||||
|
||||
_contextMenu.Items.Add("노드 추가", null, (s, e) =>
|
||||
{
|
||||
var worldPoint = ScreenToWorld(location);
|
||||
HandleAddNodeClick(worldPoint);
|
||||
});
|
||||
|
||||
_contextMenu.Items.Add("전체 맞춤", null, (s, e) => FitToNodes());
|
||||
_contextMenu.Items.Add("줌 리셋", null, (s, e) => ResetZoom());
|
||||
|
||||
_contextMenu.Show(this, location);
|
||||
}
|
||||
|
||||
private void HandleDeleteConnectionClick(Point worldPoint)
|
||||
{
|
||||
// 클릭한 위치 근처의 연결선을 찾아 삭제
|
||||
var connection = GetConnectionAt(worldPoint);
|
||||
if (connection != null)
|
||||
{
|
||||
var (fromNode, toNode) = connection.Value;
|
||||
|
||||
// 단일 연결 삭제 (어느 방향에 저장되어 있는지 확인 후 삭제)
|
||||
if (fromNode.ConnectedNodes.Contains(toNode.NodeId))
|
||||
{
|
||||
fromNode.RemoveConnection(toNode.NodeId);
|
||||
}
|
||||
else if (toNode.ConnectedNodes.Contains(fromNode.NodeId))
|
||||
{
|
||||
toNode.RemoveConnection(fromNode.NodeId);
|
||||
}
|
||||
|
||||
// 이벤트 발생
|
||||
ConnectionDeleted?.Invoke(this, (fromNode, toNode));
|
||||
MapChanged?.Invoke(this, EventArgs.Empty);
|
||||
Invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
private (MapNode From, MapNode To)? GetConnectionAt(Point worldPoint)
|
||||
{
|
||||
const int CONNECTION_HIT_TOLERANCE = 10;
|
||||
|
||||
// 모든 연결선을 확인하여 클릭한 위치와 가장 가까운 연결선 찾기
|
||||
foreach (var fromNode in _nodes)
|
||||
{
|
||||
foreach (var toNodeId in fromNode.ConnectedNodes)
|
||||
{
|
||||
var toNode = _nodes.FirstOrDefault(n => n.NodeId == toNodeId);
|
||||
if (toNode != null)
|
||||
{
|
||||
// 연결선과 클릭 위치 간의 거리 계산
|
||||
var distance = CalculatePointToLineDistance(worldPoint, fromNode.Position, toNode.Position);
|
||||
if (distance <= CONNECTION_HIT_TOLERANCE / _zoomFactor)
|
||||
{
|
||||
return (fromNode, toNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private float CalculatePointToLineDistance(Point point, Point lineStart, Point lineEnd)
|
||||
{
|
||||
// 점에서 선분까지의 거리 계산
|
||||
var A = point.X - lineStart.X;
|
||||
var B = point.Y - lineStart.Y;
|
||||
var C = lineEnd.X - lineStart.X;
|
||||
var D = lineEnd.Y - lineStart.Y;
|
||||
|
||||
var dot = A * C + B * D;
|
||||
var lenSq = C * C + D * D;
|
||||
|
||||
if (lenSq == 0) return CalculateDistance(point, lineStart);
|
||||
|
||||
var param = dot / lenSq;
|
||||
|
||||
Point xx, yy;
|
||||
if (param < 0)
|
||||
{
|
||||
xx = lineStart;
|
||||
yy = lineStart;
|
||||
}
|
||||
else if (param > 1)
|
||||
{
|
||||
xx = lineEnd;
|
||||
yy = lineEnd;
|
||||
}
|
||||
else
|
||||
{
|
||||
xx = new Point((int)(lineStart.X + param * C), (int)(lineStart.Y + param * D));
|
||||
yy = xx;
|
||||
}
|
||||
|
||||
return CalculateDistance(point, xx);
|
||||
}
|
||||
|
||||
private void UpdateTooltip(Point worldPoint)
|
||||
{
|
||||
string tooltipText = "";
|
||||
|
||||
// 노드 툴팁
|
||||
var hitNode = GetNodeAt(worldPoint);
|
||||
if (hitNode != null)
|
||||
{
|
||||
tooltipText = $"노드: {hitNode.NodeId}\n타입: {hitNode.Type}\n위치: ({hitNode.Position.X}, {hitNode.Position.Y})";
|
||||
}
|
||||
else
|
||||
{
|
||||
// AGV 툴팁
|
||||
var hitAGV = GetAGVAt(worldPoint);
|
||||
if (hitAGV != null)
|
||||
{
|
||||
var state = _agvStates.ContainsKey(hitAGV.AgvId) ? _agvStates[hitAGV.AgvId] : AGVState.Idle;
|
||||
tooltipText = $"AGV: {hitAGV.AgvId}\n상태: {state}\n배터리: {hitAGV.BatteryLevel:F1}%\n위치: ({hitAGV.CurrentPosition.X}, {hitAGV.CurrentPosition.Y})";
|
||||
}
|
||||
}
|
||||
|
||||
// 툴팁 업데이트 (기존 ToolTip 컨트롤 사용)
|
||||
if (!string.IsNullOrEmpty(tooltipText))
|
||||
{
|
||||
// ToolTip 설정 (필요시 추가 구현)
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region View Control Methods
|
||||
|
||||
/// <summary>
|
||||
/// 모든 노드가 보이도록 뷰 조정
|
||||
/// </summary>
|
||||
public void FitToNodes()
|
||||
{
|
||||
if (_nodes == null || _nodes.Count == 0) return;
|
||||
|
||||
var minX = _nodes.Min(n => n.Position.X);
|
||||
var maxX = _nodes.Max(n => n.Position.X);
|
||||
var minY = _nodes.Min(n => n.Position.Y);
|
||||
var maxY = _nodes.Max(n => n.Position.Y);
|
||||
|
||||
var margin = 50;
|
||||
var contentWidth = maxX - minX + margin * 2;
|
||||
var contentHeight = maxY - minY + margin * 2;
|
||||
|
||||
var zoomX = (float)Width / contentWidth;
|
||||
var zoomY = (float)Height / contentHeight;
|
||||
_zoomFactor = Math.Min(zoomX, zoomY) * 0.9f;
|
||||
|
||||
var centerX = (minX + maxX) / 2;
|
||||
var centerY = (minY + maxY) / 2;
|
||||
|
||||
_panOffset.X = (int)(Width / 2 - centerX * _zoomFactor);
|
||||
_panOffset.Y = (int)(Height / 2 - centerY * _zoomFactor);
|
||||
|
||||
Invalidate();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 줌 리셋
|
||||
/// </summary>
|
||||
public void ResetZoom()
|
||||
{
|
||||
_zoomFactor = 1.0f;
|
||||
_panOffset = Point.Empty;
|
||||
Invalidate();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 특정 위치로 이동
|
||||
/// </summary>
|
||||
public void PanTo(Point worldPoint)
|
||||
{
|
||||
_panOffset.X = (int)(Width / 2 - worldPoint.X * _zoomFactor);
|
||||
_panOffset.Y = (int)(Height / 2 - worldPoint.Y * _zoomFactor);
|
||||
Invalidate();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 특정 노드로 이동
|
||||
/// </summary>
|
||||
public void PanToNode(string nodeId)
|
||||
{
|
||||
var node = _nodes?.FirstOrDefault(n => n.NodeId == nodeId);
|
||||
if (node != null)
|
||||
{
|
||||
PanTo(node.Position);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 특정 AGV로 이동
|
||||
/// </summary>
|
||||
public void PanToAGV(string agvId)
|
||||
{
|
||||
if (_agvPositions.ContainsKey(agvId))
|
||||
{
|
||||
PanTo(_agvPositions[agvId]);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
691
Cs_HMI/AGVLogic/AGVNavigationCore/Controls/UnifiedAGVCanvas.cs
Normal file
691
Cs_HMI/AGVLogic/AGVNavigationCore/Controls/UnifiedAGVCanvas.cs
Normal file
@@ -0,0 +1,691 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Drawing2D;
|
||||
using System.Linq;
|
||||
using System.Windows.Forms;
|
||||
using AGVNavigationCore.Models;
|
||||
using AGVNavigationCore.PathFinding;
|
||||
using AGVNavigationCore.PathFinding.Core;
|
||||
|
||||
namespace AGVNavigationCore.Controls
|
||||
{
|
||||
/// <summary>
|
||||
/// 통합 AGV 캔버스 컨트롤
|
||||
/// 맵 편집, AGV 시뮬레이션, 실시간 모니터링을 모두 지원
|
||||
/// </summary>
|
||||
public partial class UnifiedAGVCanvas : UserControl
|
||||
{
|
||||
#region Constants
|
||||
|
||||
private const int NODE_SIZE = 24;
|
||||
private const int NODE_RADIUS = NODE_SIZE / 2;
|
||||
private const int GRID_SIZE = 20;
|
||||
private const float CONNECTION_WIDTH = 2.0f;
|
||||
private const int SNAP_DISTANCE = 10;
|
||||
private const int AGV_SIZE = 40;
|
||||
private const int CONNECTION_ARROW_SIZE = 8;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Enums
|
||||
|
||||
/// <summary>
|
||||
/// 캔버스 모드
|
||||
/// </summary>
|
||||
public enum CanvasMode
|
||||
{
|
||||
Edit // 편집 가능 (맵 에디터)
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 편집 모드 (CanvasMode.Edit일 때만 적용)
|
||||
/// </summary>
|
||||
public enum EditMode
|
||||
{
|
||||
Select, // 선택 모드
|
||||
Move, // 이동 모드
|
||||
AddNode, // 노드 추가 모드
|
||||
Connect, // 연결 모드
|
||||
Delete, // 삭제 모드
|
||||
DeleteConnection, // 연결 삭제 모드
|
||||
AddLabel, // 라벨 추가 모드
|
||||
AddImage, // 이미지 추가 모드
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Fields
|
||||
|
||||
// 캔버스 모드
|
||||
private CanvasMode _canvasMode = CanvasMode.Edit;
|
||||
private EditMode _editMode = EditMode.Select;
|
||||
|
||||
// 맵 데이터
|
||||
private List<MapNode> _nodes;
|
||||
private MapNode _selectedNode;
|
||||
private MapNode _hoveredNode;
|
||||
private MapNode _destinationNode;
|
||||
|
||||
// AGV 관련
|
||||
private List<IAGV> _agvList;
|
||||
private Dictionary<string, Point> _agvPositions;
|
||||
private Dictionary<string, AgvDirection> _agvDirections;
|
||||
private Dictionary<string, AGVState> _agvStates;
|
||||
|
||||
// 경로 관련
|
||||
private AGVPathResult _currentPath;
|
||||
private List<AGVPathResult> _allPaths;
|
||||
|
||||
// 도킹 검증 관련
|
||||
private Dictionary<string, bool> _dockingErrors;
|
||||
|
||||
// UI 요소들
|
||||
private Image _companyLogo;
|
||||
private string _companyLogoPath = string.Empty;
|
||||
private string _measurementInfo = "스케일: 1:100\n면적: 1000㎡\n최종 수정: " + DateTime.Now.ToString("yyyy-MM-dd");
|
||||
|
||||
// 편집 관련 (EditMode에서만 사용)
|
||||
private bool _isDragging;
|
||||
private Point _dragOffset;
|
||||
private Point _lastMousePosition;
|
||||
private bool _isConnectionMode;
|
||||
private MapNode _connectionStartNode;
|
||||
private Point _connectionEndPoint;
|
||||
|
||||
// 그리드 및 줌 관련
|
||||
private bool _showGrid = true;
|
||||
private float _zoomFactor = 1.0f;
|
||||
private Point _panOffset = Point.Empty;
|
||||
private bool _isPanning;
|
||||
|
||||
// 자동 증가 카운터
|
||||
private int _nodeCounter = 1;
|
||||
|
||||
// 강조 연결
|
||||
private (string FromNodeId, string ToNodeId)? _highlightedConnection = null;
|
||||
|
||||
// RFID 중복 검사
|
||||
private HashSet<string> _duplicateRfidNodes = new HashSet<string>();
|
||||
|
||||
// 브러쉬 및 펜
|
||||
private Brush _normalNodeBrush;
|
||||
private Brush _rotationNodeBrush;
|
||||
private Brush _dockingNodeBrush;
|
||||
private Brush _chargingNodeBrush;
|
||||
private Brush _selectedNodeBrush;
|
||||
private Brush _hoveredNodeBrush;
|
||||
private Brush _destinationNodeBrush;
|
||||
private Brush _gridBrush;
|
||||
private Brush _agvBrush;
|
||||
private Brush _pathBrush;
|
||||
|
||||
private Pen _connectionPen;
|
||||
private Pen _gridPen;
|
||||
private Pen _tempConnectionPen;
|
||||
private Pen _selectedNodePen;
|
||||
private Pen _destinationNodePen;
|
||||
private Pen _pathPen;
|
||||
private Pen _agvPen;
|
||||
private Pen _highlightedConnectionPen;
|
||||
|
||||
// 컨텍스트 메뉴
|
||||
private ContextMenuStrip _contextMenu;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Events
|
||||
|
||||
// 맵 편집 이벤트
|
||||
public event EventHandler<MapNode> NodeAdded;
|
||||
public event EventHandler<MapNode> NodeSelected;
|
||||
public event EventHandler<MapNode> NodeDeleted;
|
||||
public event EventHandler<MapNode> NodeMoved;
|
||||
public event EventHandler<(MapNode From, MapNode To)> ConnectionDeleted;
|
||||
public event EventHandler MapChanged;
|
||||
|
||||
// AGV 이벤트
|
||||
public event EventHandler<IAGV> AGVSelected;
|
||||
public event EventHandler<IAGV> AGVStateChanged;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Properties
|
||||
|
||||
/// <summary>
|
||||
/// 캔버스 모드
|
||||
/// </summary>
|
||||
public CanvasMode Mode
|
||||
{
|
||||
get => _canvasMode;
|
||||
set
|
||||
{
|
||||
_canvasMode = value;
|
||||
UpdateModeUI();
|
||||
Invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 편집 모드 (CanvasMode.Edit일 때만 적용)
|
||||
/// </summary>
|
||||
public EditMode CurrentEditMode
|
||||
{
|
||||
get => _editMode;
|
||||
set
|
||||
{
|
||||
if (_canvasMode != CanvasMode.Edit) return;
|
||||
|
||||
_editMode = value;
|
||||
if (_editMode != EditMode.Connect)
|
||||
{
|
||||
CancelConnection();
|
||||
}
|
||||
Cursor = GetCursorForMode(_editMode);
|
||||
Invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 그리드 표시 여부
|
||||
/// </summary>
|
||||
public bool ShowGrid
|
||||
{
|
||||
get => _showGrid;
|
||||
set
|
||||
{
|
||||
_showGrid = value;
|
||||
Invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 줌 팩터
|
||||
/// </summary>
|
||||
public float ZoomFactor
|
||||
{
|
||||
get => _zoomFactor;
|
||||
set
|
||||
{
|
||||
_zoomFactor = Math.Max(0.1f, Math.Min(5.0f, value));
|
||||
Invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 선택된 노드
|
||||
/// </summary>
|
||||
public MapNode SelectedNode => _selectedNode;
|
||||
|
||||
/// <summary>
|
||||
/// 노드 목록
|
||||
/// </summary>
|
||||
public List<MapNode> Nodes
|
||||
{
|
||||
get => _nodes ?? new List<MapNode>();
|
||||
set
|
||||
{
|
||||
_nodes = value ?? new List<MapNode>();
|
||||
|
||||
// 기존 노드들의 최대 번호를 찾아서 _nodeCounter 설정
|
||||
UpdateNodeCounter();
|
||||
|
||||
// RFID 중복값 검사
|
||||
DetectDuplicateRfidNodes();
|
||||
|
||||
Invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// AGV 목록
|
||||
/// </summary>
|
||||
public List<IAGV> AGVList
|
||||
{
|
||||
get => _agvList ?? new List<IAGV>();
|
||||
set
|
||||
{
|
||||
_agvList = value ?? new List<IAGV>();
|
||||
UpdateAGVData();
|
||||
Invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 현재 표시할 경로
|
||||
/// </summary>
|
||||
public AGVPathResult CurrentPath
|
||||
{
|
||||
get => _currentPath;
|
||||
set
|
||||
{
|
||||
_currentPath = value;
|
||||
UpdateDestinationNode();
|
||||
Invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 모든 경로 목록 (다중 AGV 경로 표시용)
|
||||
/// </summary>
|
||||
public List<AGVPathResult> AllPaths
|
||||
{
|
||||
get => _allPaths ?? new List<AGVPathResult>();
|
||||
set
|
||||
{
|
||||
_allPaths = value ?? new List<AGVPathResult>();
|
||||
Invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 회사 로고 이미지
|
||||
/// </summary>
|
||||
public Image CompanyLogo
|
||||
{
|
||||
get => _companyLogo;
|
||||
set
|
||||
{
|
||||
_companyLogo = value;
|
||||
Invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 측정 정보 텍스트
|
||||
/// </summary>
|
||||
public string MeasurementInfo
|
||||
{
|
||||
get => _measurementInfo;
|
||||
set
|
||||
{
|
||||
_measurementInfo = value;
|
||||
Invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Connection Highlighting
|
||||
|
||||
/// <summary>
|
||||
/// 특정 연결을 강조 표시
|
||||
/// </summary>
|
||||
/// <param name="fromNodeId">시작 노드 ID</param>
|
||||
/// <param name="toNodeId">끝 노드 ID</param>
|
||||
public void HighlightConnection(string fromNodeId, string toNodeId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(fromNodeId) || string.IsNullOrEmpty(toNodeId))
|
||||
{
|
||||
_highlightedConnection = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 사전순으로 정렬하여 저장 (연결이 단일 방향으로 저장되므로)
|
||||
if (string.Compare(fromNodeId, toNodeId, StringComparison.Ordinal) <= 0)
|
||||
{
|
||||
_highlightedConnection = (fromNodeId, toNodeId);
|
||||
}
|
||||
else
|
||||
{
|
||||
_highlightedConnection = (toNodeId, fromNodeId);
|
||||
}
|
||||
}
|
||||
|
||||
Invalidate();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 연결 강조 표시 해제
|
||||
/// </summary>
|
||||
public void ClearHighlightedConnection()
|
||||
{
|
||||
_highlightedConnection = null;
|
||||
Invalidate();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Constructor
|
||||
|
||||
public UnifiedAGVCanvas()
|
||||
{
|
||||
InitializeComponent();
|
||||
InitializeCanvas();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Initialization
|
||||
|
||||
private void InitializeCanvas()
|
||||
{
|
||||
SetStyle(ControlStyles.AllPaintingInWmPaint |
|
||||
ControlStyles.UserPaint |
|
||||
ControlStyles.DoubleBuffer |
|
||||
ControlStyles.ResizeRedraw, true);
|
||||
|
||||
_nodes = new List<MapNode>();
|
||||
_agvList = new List<IAGV>();
|
||||
_agvPositions = new Dictionary<string, Point>();
|
||||
_agvDirections = new Dictionary<string, AgvDirection>();
|
||||
_agvStates = new Dictionary<string, AGVState>();
|
||||
_allPaths = new List<AGVPathResult>();
|
||||
_dockingErrors = new Dictionary<string, bool>();
|
||||
|
||||
InitializeBrushesAndPens();
|
||||
CreateContextMenu();
|
||||
}
|
||||
|
||||
private void InitializeBrushesAndPens()
|
||||
{
|
||||
// 노드 브러쉬
|
||||
_normalNodeBrush = new SolidBrush(Color.LightBlue);
|
||||
_rotationNodeBrush = new SolidBrush(Color.Yellow);
|
||||
_dockingNodeBrush = new SolidBrush(Color.Orange);
|
||||
_chargingNodeBrush = new SolidBrush(Color.Green);
|
||||
_selectedNodeBrush = new SolidBrush(Color.Red);
|
||||
_hoveredNodeBrush = new SolidBrush(Color.LightCyan);
|
||||
_destinationNodeBrush = new SolidBrush(Color.Gold);
|
||||
|
||||
// AGV 및 경로 브러쉬
|
||||
_agvBrush = new SolidBrush(Color.Red);
|
||||
_pathBrush = new SolidBrush(Color.Purple);
|
||||
|
||||
// 그리드 브러쉬
|
||||
_gridBrush = new SolidBrush(Color.LightGray);
|
||||
|
||||
// 펜
|
||||
_connectionPen = new Pen(Color.DarkBlue, CONNECTION_WIDTH);
|
||||
_connectionPen.EndCap = LineCap.ArrowAnchor;
|
||||
|
||||
_gridPen = new Pen(Color.LightGray, 1);
|
||||
_tempConnectionPen = new Pen(Color.Orange, 2) { DashStyle = DashStyle.Dash };
|
||||
_selectedNodePen = new Pen(Color.Red, 3);
|
||||
_destinationNodePen = new Pen(Color.Orange, 4);
|
||||
_pathPen = new Pen(Color.Purple, 3);
|
||||
_agvPen = new Pen(Color.Red, 3);
|
||||
_highlightedConnectionPen = new Pen(Color.Red, 4) { DashStyle = DashStyle.Solid };
|
||||
}
|
||||
|
||||
private void CreateContextMenu()
|
||||
{
|
||||
_contextMenu = new ContextMenuStrip();
|
||||
// 컨텍스트 메뉴는 EditMode에서만 사용
|
||||
}
|
||||
|
||||
private void UpdateModeUI()
|
||||
{
|
||||
// 모드에 따른 UI 업데이트
|
||||
_contextMenu.Enabled = true;
|
||||
Cursor = GetCursorForMode(_editMode);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region AGV Management
|
||||
|
||||
/// <summary>
|
||||
/// AGV 위치 업데이트
|
||||
/// </summary>
|
||||
public void UpdateAGVPosition(string agvId, Point position)
|
||||
{
|
||||
if (_agvPositions.ContainsKey(agvId))
|
||||
_agvPositions[agvId] = position;
|
||||
else
|
||||
_agvPositions.Add(agvId, position);
|
||||
|
||||
Invalidate();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// AGV 방향 업데이트
|
||||
/// </summary>
|
||||
public void UpdateAGVDirection(string agvId, AgvDirection direction)
|
||||
{
|
||||
if (_agvDirections.ContainsKey(agvId))
|
||||
_agvDirections[agvId] = direction;
|
||||
else
|
||||
_agvDirections.Add(agvId, direction);
|
||||
|
||||
Invalidate();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// AGV 상태 업데이트
|
||||
/// </summary>
|
||||
public void UpdateAGVState(string agvId, AGVState state)
|
||||
{
|
||||
if (_agvStates.ContainsKey(agvId))
|
||||
_agvStates[agvId] = state;
|
||||
else
|
||||
_agvStates.Add(agvId, state);
|
||||
|
||||
Invalidate();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// AGV 위치 설정 (시뮬레이터용)
|
||||
/// </summary>
|
||||
/// <param name="agvId">AGV ID</param>
|
||||
/// <param name="position">새로운 위치</param>
|
||||
public void SetAGVPosition(string agvId, Point position)
|
||||
{
|
||||
UpdateAGVPosition(agvId, position);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// AGV 데이터 동기화
|
||||
/// </summary>
|
||||
private void UpdateAGVData()
|
||||
{
|
||||
if (_agvList == null) return;
|
||||
|
||||
foreach (var agv in _agvList)
|
||||
{
|
||||
UpdateAGVPosition(agv.AgvId, agv.CurrentPosition);
|
||||
UpdateAGVDirection(agv.AgvId, agv.CurrentDirection);
|
||||
UpdateAGVState(agv.AgvId, agv.CurrentState);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Helper Methods
|
||||
|
||||
private Cursor GetCursorForMode(EditMode mode)
|
||||
{
|
||||
if (_canvasMode != CanvasMode.Edit)
|
||||
return Cursors.Default;
|
||||
|
||||
switch (mode)
|
||||
{
|
||||
case EditMode.AddNode:
|
||||
return Cursors.Cross;
|
||||
case EditMode.Move:
|
||||
return Cursors.SizeAll;
|
||||
case EditMode.Connect:
|
||||
return Cursors.Hand;
|
||||
case EditMode.Delete:
|
||||
return Cursors.No;
|
||||
default:
|
||||
return Cursors.Default;
|
||||
}
|
||||
}
|
||||
|
||||
private void CancelConnection()
|
||||
{
|
||||
_isConnectionMode = false;
|
||||
_connectionStartNode = null;
|
||||
_connectionEndPoint = Point.Empty;
|
||||
Invalidate();
|
||||
}
|
||||
|
||||
private void UpdateDestinationNode()
|
||||
{
|
||||
_destinationNode = null;
|
||||
|
||||
if (_currentPath != null && _currentPath.Success && _currentPath.Path != null && _currentPath.Path.Count > 0)
|
||||
{
|
||||
// 경로의 마지막 노드가 목적지
|
||||
string destinationNodeId = _currentPath.Path[_currentPath.Path.Count - 1];
|
||||
|
||||
// 노드 목록에서 해당 노드 찾기
|
||||
_destinationNode = _nodes?.FirstOrDefault(n => n.NodeId == destinationNodeId);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Cleanup
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
|
||||
// 브러쉬 정리
|
||||
_normalNodeBrush?.Dispose();
|
||||
_rotationNodeBrush?.Dispose();
|
||||
_dockingNodeBrush?.Dispose();
|
||||
_chargingNodeBrush?.Dispose();
|
||||
_selectedNodeBrush?.Dispose();
|
||||
_hoveredNodeBrush?.Dispose();
|
||||
_destinationNodeBrush?.Dispose();
|
||||
_gridBrush?.Dispose();
|
||||
_agvBrush?.Dispose();
|
||||
_pathBrush?.Dispose();
|
||||
|
||||
// 펜 정리
|
||||
_connectionPen?.Dispose();
|
||||
_gridPen?.Dispose();
|
||||
_tempConnectionPen?.Dispose();
|
||||
_selectedNodePen?.Dispose();
|
||||
_destinationNodePen?.Dispose();
|
||||
_pathPen?.Dispose();
|
||||
_agvPen?.Dispose();
|
||||
_highlightedConnectionPen?.Dispose();
|
||||
|
||||
// 컨텍스트 메뉴 정리
|
||||
_contextMenu?.Dispose();
|
||||
|
||||
// 이미지 정리
|
||||
_companyLogo?.Dispose();
|
||||
}
|
||||
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// RFID 중복값을 가진 노드들을 감지하고 표시
|
||||
/// 나중에 추가된 노드(인덱스가 더 큰)를 중복으로 간주
|
||||
/// </summary>
|
||||
private void DetectDuplicateRfidNodes()
|
||||
{
|
||||
_duplicateRfidNodes.Clear();
|
||||
|
||||
if (_nodes == null || _nodes.Count == 0)
|
||||
return;
|
||||
|
||||
// RFID값과 해당 노드의 인덱스를 저장
|
||||
var rfidToNodeIndex = new Dictionary<string, List<int>>();
|
||||
|
||||
// 모든 노드의 RFID값 수집
|
||||
for (int i = 0; i < _nodes.Count; i++)
|
||||
{
|
||||
var node = _nodes[i];
|
||||
if (!string.IsNullOrEmpty(node.RfidId))
|
||||
{
|
||||
if (!rfidToNodeIndex.ContainsKey(node.RfidId))
|
||||
{
|
||||
rfidToNodeIndex[node.RfidId] = new List<int>();
|
||||
}
|
||||
rfidToNodeIndex[node.RfidId].Add(i);
|
||||
}
|
||||
}
|
||||
|
||||
// 중복된 RFID를 가진 노드들을 찾아서 나중에 추가된 것들을 표시
|
||||
foreach (var kvp in rfidToNodeIndex)
|
||||
{
|
||||
if (kvp.Value.Count > 1)
|
||||
{
|
||||
// 첫 번째 노드는 원본으로 유지, 나머지는 중복으로 표시
|
||||
for (int i = 1; i < kvp.Value.Count; i++)
|
||||
{
|
||||
int duplicateNodeIndex = kvp.Value[i];
|
||||
_duplicateRfidNodes.Add(_nodes[duplicateNodeIndex].NodeId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 기존 노드들의 최대 번호를 찾아서 _nodeCounter를 업데이트
|
||||
/// </summary>
|
||||
private void UpdateNodeCounter()
|
||||
{
|
||||
if (_nodes == null || _nodes.Count == 0)
|
||||
{
|
||||
_nodeCounter = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
int maxNumber = 0;
|
||||
foreach (var node in _nodes)
|
||||
{
|
||||
// NodeId에서 숫자 부분 추출 (예: "N001" -> 1)
|
||||
if (node.NodeId.StartsWith("N") && int.TryParse(node.NodeId.Substring(1), out int number))
|
||||
{
|
||||
maxNumber = Math.Max(maxNumber, number);
|
||||
}
|
||||
}
|
||||
|
||||
_nodeCounter = maxNumber + 1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 특정 노드에 도킹 오류 표시를 설정/해제합니다.
|
||||
/// </summary>
|
||||
/// <param name="nodeId">노드 ID</param>
|
||||
/// <param name="hasError">오류 여부</param>
|
||||
public void SetDockingError(string nodeId, bool hasError)
|
||||
{
|
||||
if (string.IsNullOrEmpty(nodeId))
|
||||
return;
|
||||
|
||||
if (hasError)
|
||||
{
|
||||
_dockingErrors[nodeId] = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
_dockingErrors.Remove(nodeId);
|
||||
}
|
||||
|
||||
Invalidate(); // 화면 다시 그리기
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 특정 노드에 도킹 오류가 있는지 확인합니다.
|
||||
/// </summary>
|
||||
/// <param name="nodeId">노드 ID</param>
|
||||
/// <returns>도킹 오류 여부</returns>
|
||||
public bool HasDockingError(string nodeId)
|
||||
{
|
||||
return _dockingErrors.ContainsKey(nodeId) && _dockingErrors[nodeId];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 모든 도킹 오류를 초기화합니다.
|
||||
/// </summary>
|
||||
public void ClearDockingErrors()
|
||||
{
|
||||
_dockingErrors.Clear();
|
||||
Invalidate();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
72
Cs_HMI/AGVLogic/AGVNavigationCore/Models/Enums.cs
Normal file
72
Cs_HMI/AGVLogic/AGVNavigationCore/Models/Enums.cs
Normal file
@@ -0,0 +1,72 @@
|
||||
using System;
|
||||
|
||||
namespace AGVNavigationCore.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>
|
||||
DontCare,
|
||||
/// <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
|
||||
}
|
||||
}
|
||||
215
Cs_HMI/AGVLogic/AGVNavigationCore/Models/IMovableAGV.cs
Normal file
215
Cs_HMI/AGVLogic/AGVNavigationCore/Models/IMovableAGV.cs
Normal file
@@ -0,0 +1,215 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using AGVNavigationCore.Controls;
|
||||
using AGVNavigationCore.PathFinding;
|
||||
using AGVNavigationCore.PathFinding.Core;
|
||||
|
||||
namespace AGVNavigationCore.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// 이동 가능한 AGV 인터페이스
|
||||
/// 실제 AGV와 시뮬레이션 AGV 모두 구현해야 하는 기본 인터페이스
|
||||
/// </summary>
|
||||
public interface IMovableAGV
|
||||
{
|
||||
#region Events
|
||||
|
||||
/// <summary>
|
||||
/// AGV 상태 변경 이벤트
|
||||
/// </summary>
|
||||
event EventHandler<AGVState> StateChanged;
|
||||
|
||||
/// <summary>
|
||||
/// 위치 변경 이벤트
|
||||
/// </summary>
|
||||
event EventHandler<(Point, AgvDirection, MapNode)> PositionChanged;
|
||||
|
||||
/// <summary>
|
||||
/// RFID 감지 이벤트
|
||||
/// </summary>
|
||||
event EventHandler<string> RfidDetected;
|
||||
|
||||
/// <summary>
|
||||
/// 경로 완료 이벤트
|
||||
/// </summary>
|
||||
event EventHandler<AGVPathResult> PathCompleted;
|
||||
|
||||
/// <summary>
|
||||
/// 오류 발생 이벤트
|
||||
/// </summary>
|
||||
event EventHandler<string> ErrorOccurred;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Properties
|
||||
|
||||
/// <summary>
|
||||
/// AGV ID
|
||||
/// </summary>
|
||||
string AgvId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 현재 위치
|
||||
/// </summary>
|
||||
Point CurrentPosition { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 현재 방향 (모터 방향)
|
||||
/// </summary>
|
||||
AgvDirection CurrentDirection { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 현재 상태
|
||||
/// </summary>
|
||||
AGVState CurrentState { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 현재 속도
|
||||
/// </summary>
|
||||
float CurrentSpeed { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 배터리 레벨 (0-100%)
|
||||
/// </summary>
|
||||
float BatteryLevel { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 현재 경로
|
||||
/// </summary>
|
||||
AGVPathResult CurrentPath { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 현재 노드 ID
|
||||
/// </summary>
|
||||
string CurrentNodeId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 목표 위치
|
||||
/// </summary>
|
||||
Point? TargetPosition { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 목표 노드 ID
|
||||
/// </summary>
|
||||
string TargetNodeId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 도킹 방향
|
||||
/// </summary>
|
||||
DockingDirection DockingDirection { get; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region Sensor Input Methods (실제 AGV에서 호출)
|
||||
|
||||
/// <summary>
|
||||
/// 현재 위치 설정 (실제 위치 센서에서)
|
||||
/// </summary>
|
||||
void SetCurrentPosition(Point position);
|
||||
|
||||
/// <summary>
|
||||
/// 감지된 RFID 설정 (실제 RFID 센서에서)
|
||||
/// </summary>
|
||||
void SetDetectedRfid(string rfidId);
|
||||
|
||||
/// <summary>
|
||||
/// 모터 방향 설정 (모터 컨트롤러에서)
|
||||
/// </summary>
|
||||
void SetMotorDirection(AgvDirection direction);
|
||||
|
||||
/// <summary>
|
||||
/// 배터리 레벨 설정 (BMS에서)
|
||||
/// </summary>
|
||||
void SetBatteryLevel(float percentage);
|
||||
|
||||
#endregion
|
||||
|
||||
#region State Query Methods
|
||||
|
||||
/// <summary>
|
||||
/// 현재 위치 조회
|
||||
/// </summary>
|
||||
Point GetCurrentPosition();
|
||||
|
||||
/// <summary>
|
||||
/// 현재 상태 조회
|
||||
/// </summary>
|
||||
AGVState GetCurrentState();
|
||||
|
||||
/// <summary>
|
||||
/// 현재 노드 ID 조회
|
||||
/// </summary>
|
||||
string GetCurrentNodeId();
|
||||
|
||||
/// <summary>
|
||||
/// AGV 상태 정보 문자열 조회
|
||||
/// </summary>
|
||||
string GetStatus();
|
||||
|
||||
#endregion
|
||||
|
||||
#region Path Execution Methods
|
||||
|
||||
/// <summary>
|
||||
/// 경로 실행
|
||||
/// </summary>
|
||||
void ExecutePath(AGVPathResult path, List<MapNode> mapNodes);
|
||||
|
||||
/// <summary>
|
||||
/// 경로 정지
|
||||
/// </summary>
|
||||
void StopPath();
|
||||
|
||||
/// <summary>
|
||||
/// 긴급 정지
|
||||
/// </summary>
|
||||
void EmergencyStop();
|
||||
|
||||
#endregion
|
||||
|
||||
#region Update Method
|
||||
|
||||
/// <summary>
|
||||
/// 프레임 업데이트 (외부에서 주기적으로 호출)
|
||||
/// 이 방식으로 타이머에 의존하지 않고 외부에서 제어 가능
|
||||
/// </summary>
|
||||
/// <param name="deltaTimeMs">마지막 업데이트 이후 경과 시간 (밀리초)</param>
|
||||
void Update(float deltaTimeMs);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Manual Control Methods (테스트용)
|
||||
|
||||
/// <summary>
|
||||
/// 수동 이동
|
||||
/// </summary>
|
||||
void MoveTo(Point targetPosition);
|
||||
|
||||
/// <summary>
|
||||
/// 수동 회전
|
||||
/// </summary>
|
||||
void Rotate(AgvDirection direction);
|
||||
|
||||
/// <summary>
|
||||
/// 충전 시작
|
||||
/// </summary>
|
||||
void StartCharging();
|
||||
|
||||
/// <summary>
|
||||
/// 충전 종료
|
||||
/// </summary>
|
||||
void StopCharging();
|
||||
|
||||
#endregion
|
||||
|
||||
#region Cleanup
|
||||
|
||||
/// <summary>
|
||||
/// 리소스 정리
|
||||
/// </summary>
|
||||
void Dispose();
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
355
Cs_HMI/AGVLogic/AGVNavigationCore/Models/MapLoader.cs
Normal file
355
Cs_HMI/AGVLogic/AGVNavigationCore/Models/MapLoader.cs
Normal file
@@ -0,0 +1,355 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace AGVNavigationCore.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// AGV 맵 파일 로딩/저장을 위한 공용 유틸리티 클래스
|
||||
/// AGVMapEditor와 AGVSimulator에서 공통으로 사용
|
||||
/// </summary>
|
||||
public static class MapLoader
|
||||
{
|
||||
/// <summary>
|
||||
/// 맵 파일 로딩 결과
|
||||
/// </summary>
|
||||
public class MapLoadResult
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public List<MapNode> Nodes { get; set; } = new List<MapNode>();
|
||||
public string ErrorMessage { get; set; } = string.Empty;
|
||||
public string Version { get; set; } = string.Empty;
|
||||
public DateTime CreatedDate { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 맵 파일 저장용 데이터 구조
|
||||
/// </summary>
|
||||
public class MapFileData
|
||||
{
|
||||
public List<MapNode> Nodes { get; set; } = new List<MapNode>();
|
||||
public DateTime CreatedDate { get; set; }
|
||||
public string Version { get; set; } = "1.0";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 맵 파일을 로드하여 노드를 반환
|
||||
/// </summary>
|
||||
/// <param name="filePath">맵 파일 경로</param>
|
||||
/// <returns>로딩 결과</returns>
|
||||
public static MapLoadResult LoadMapFromFile(string filePath)
|
||||
{
|
||||
var result = new MapLoadResult();
|
||||
|
||||
try
|
||||
{
|
||||
if (!File.Exists(filePath))
|
||||
{
|
||||
result.ErrorMessage = $"파일을 찾을 수 없습니다: {filePath}";
|
||||
return result;
|
||||
}
|
||||
|
||||
var json = File.ReadAllText(filePath);
|
||||
|
||||
// JSON 역직렬화 설정: 누락된 속성 무시, 안전한 처리
|
||||
var settings = new JsonSerializerSettings
|
||||
{
|
||||
MissingMemberHandling = MissingMemberHandling.Ignore,
|
||||
NullValueHandling = NullValueHandling.Ignore,
|
||||
DefaultValueHandling = DefaultValueHandling.Populate
|
||||
};
|
||||
|
||||
var mapData = JsonConvert.DeserializeObject<MapFileData>(json, settings);
|
||||
|
||||
if (mapData != null)
|
||||
{
|
||||
result.Nodes = mapData.Nodes ?? new List<MapNode>();
|
||||
result.Version = mapData.Version ?? "1.0";
|
||||
result.CreatedDate = mapData.CreatedDate;
|
||||
|
||||
// 기존 Description 데이터를 Name으로 마이그레이션
|
||||
MigrateDescriptionToName(result.Nodes);
|
||||
|
||||
// DockingDirection 마이그레이션 (기존 NodeType 기반으로 설정)
|
||||
MigrateDockingDirection(result.Nodes);
|
||||
|
||||
// 중복된 NodeId 정리
|
||||
FixDuplicateNodeIds(result.Nodes);
|
||||
|
||||
// 중복 연결 정리 (양방향 중복 제거)
|
||||
CleanupDuplicateConnections(result.Nodes);
|
||||
|
||||
// 이미지 노드들의 이미지 로드
|
||||
LoadImageNodes(result.Nodes);
|
||||
|
||||
result.Success = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
result.ErrorMessage = "맵 데이터 파싱에 실패했습니다.";
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
result.ErrorMessage = $"맵 파일 로딩 중 오류 발생: {ex.Message}";
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 맵 데이터를 파일로 저장
|
||||
/// </summary>
|
||||
/// <param name="filePath">저장할 파일 경로</param>
|
||||
/// <param name="nodes">맵 노드 목록</param>
|
||||
/// <returns>저장 성공 여부</returns>
|
||||
public static bool SaveMapToFile(string filePath, List<MapNode> nodes)
|
||||
{
|
||||
try
|
||||
{
|
||||
var mapData = new MapFileData
|
||||
{
|
||||
Nodes = nodes,
|
||||
CreatedDate = DateTime.Now,
|
||||
Version = "1.0"
|
||||
};
|
||||
|
||||
var json = JsonConvert.SerializeObject(mapData, Formatting.Indented);
|
||||
File.WriteAllText(filePath, json);
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 이미지 노드들의 이미지 로드
|
||||
/// </summary>
|
||||
/// <param name="nodes">노드 목록</param>
|
||||
private static void LoadImageNodes(List<MapNode> nodes)
|
||||
{
|
||||
foreach (var node in nodes)
|
||||
{
|
||||
if (node.Type == NodeType.Image)
|
||||
{
|
||||
node.LoadImage();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 기존 Description 데이터를 Name 필드로 마이그레이션
|
||||
/// JSON 파일에서 Description 필드가 있는 경우 Name으로 이동
|
||||
/// </summary>
|
||||
/// <param name="mapNodes">맵 노드 목록</param>
|
||||
private static void MigrateDescriptionToName(List<MapNode> mapNodes)
|
||||
{
|
||||
// JSON에서 Description이 있던 기존 파일들을 위한 마이그레이션
|
||||
// 현재 MapNode 클래스에는 Description 속성이 제거되었으므로
|
||||
// 이 메서드는 호환성을 위해 유지되지만 실제로는 작동하지 않음
|
||||
// 기존 파일들은 다시 저장될 때 Description 없이 저장됨
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 기존 맵 파일의 DockingDirection을 NodeType 기반으로 마이그레이션
|
||||
/// </summary>
|
||||
/// <param name="mapNodes">맵 노드 목록</param>
|
||||
private static void MigrateDockingDirection(List<MapNode> mapNodes)
|
||||
{
|
||||
if (mapNodes == null || mapNodes.Count == 0) return;
|
||||
|
||||
foreach (var node in mapNodes)
|
||||
{
|
||||
// 기존 파일에서 DockingDirection이 기본값(DontCare)인 경우에만 마이그레이션
|
||||
if (node.DockDirection == DockingDirection.DontCare)
|
||||
{
|
||||
switch (node.Type)
|
||||
{
|
||||
case NodeType.Charging:
|
||||
node.DockDirection = DockingDirection.Forward;
|
||||
break;
|
||||
case NodeType.Docking:
|
||||
node.DockDirection = DockingDirection.Backward;
|
||||
break;
|
||||
default:
|
||||
// Normal, Rotation, Label, Image는 DontCare 유지
|
||||
node.DockDirection = DockingDirection.DontCare;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 중복된 NodeId를 가진 노드들을 고유한 NodeId로 수정
|
||||
/// </summary>
|
||||
/// <param name="mapNodes">맵 노드 목록</param>
|
||||
private static void FixDuplicateNodeIds(List<MapNode> mapNodes)
|
||||
{
|
||||
if (mapNodes == null || mapNodes.Count == 0) return;
|
||||
|
||||
var usedIds = new HashSet<string>();
|
||||
var duplicateNodes = new List<MapNode>();
|
||||
|
||||
// 첫 번째 패스: 중복된 노드들 식별
|
||||
foreach (var node in mapNodes)
|
||||
{
|
||||
if (usedIds.Contains(node.NodeId))
|
||||
{
|
||||
duplicateNodes.Add(node);
|
||||
}
|
||||
else
|
||||
{
|
||||
usedIds.Add(node.NodeId);
|
||||
}
|
||||
}
|
||||
|
||||
// 두 번째 패스: 중복된 노드들에게 새로운 NodeId 할당
|
||||
foreach (var duplicateNode in duplicateNodes)
|
||||
{
|
||||
string newNodeId = GenerateUniqueNodeId(usedIds);
|
||||
|
||||
// 다른 노드들의 연결에서 기존 NodeId를 새 NodeId로 업데이트
|
||||
UpdateConnections(mapNodes, duplicateNode.NodeId, newNodeId);
|
||||
|
||||
duplicateNode.NodeId = newNodeId;
|
||||
usedIds.Add(newNodeId);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 사용되지 않는 고유한 NodeId 생성
|
||||
/// </summary>
|
||||
/// <param name="usedIds">이미 사용된 NodeId 목록</param>
|
||||
/// <returns>고유한 NodeId</returns>
|
||||
private static string GenerateUniqueNodeId(HashSet<string> usedIds)
|
||||
{
|
||||
int counter = 1;
|
||||
string nodeId;
|
||||
|
||||
do
|
||||
{
|
||||
nodeId = $"N{counter:D3}";
|
||||
counter++;
|
||||
}
|
||||
while (usedIds.Contains(nodeId));
|
||||
|
||||
return nodeId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 노드 연결에서 NodeId 변경사항 반영
|
||||
/// </summary>
|
||||
/// <param name="mapNodes">맵 노드 목록</param>
|
||||
/// <param name="oldNodeId">기존 NodeId</param>
|
||||
/// <param name="newNodeId">새로운 NodeId</param>
|
||||
private static void UpdateConnections(List<MapNode> mapNodes, string oldNodeId, string newNodeId)
|
||||
{
|
||||
foreach (var node in mapNodes)
|
||||
{
|
||||
if (node.ConnectedNodes != null)
|
||||
{
|
||||
for (int i = 0; i < node.ConnectedNodes.Count; i++)
|
||||
{
|
||||
if (node.ConnectedNodes[i] == oldNodeId)
|
||||
{
|
||||
node.ConnectedNodes[i] = newNodeId;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 중복 연결을 정리합니다. 양방향 중복 연결을 단일 연결로 통합합니다.
|
||||
/// </summary>
|
||||
/// <param name="mapNodes">맵 노드 목록</param>
|
||||
private static void CleanupDuplicateConnections(List<MapNode> mapNodes)
|
||||
{
|
||||
if (mapNodes == null || mapNodes.Count == 0) return;
|
||||
|
||||
var processedPairs = new HashSet<string>();
|
||||
|
||||
foreach (var node in mapNodes)
|
||||
{
|
||||
var connectionsToRemove = new List<string>();
|
||||
|
||||
foreach (var connectedNodeId in node.ConnectedNodes.ToList())
|
||||
{
|
||||
var connectedNode = mapNodes.FirstOrDefault(n => n.NodeId == connectedNodeId);
|
||||
if (connectedNode == null) continue;
|
||||
|
||||
// 연결 쌍의 키 생성 (사전순 정렬)
|
||||
string pairKey = string.Compare(node.NodeId, connectedNodeId, StringComparison.Ordinal) < 0
|
||||
? $"{node.NodeId}-{connectedNodeId}"
|
||||
: $"{connectedNodeId}-{node.NodeId}";
|
||||
|
||||
if (processedPairs.Contains(pairKey))
|
||||
{
|
||||
// 이미 처리된 연결인 경우 중복으로 간주하고 제거
|
||||
connectionsToRemove.Add(connectedNodeId);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 처리되지 않은 연결인 경우
|
||||
processedPairs.Add(pairKey);
|
||||
|
||||
// 양방향 연결인 경우 하나만 유지
|
||||
if (connectedNode.ConnectedNodes.Contains(node.NodeId))
|
||||
{
|
||||
// 사전순으로 더 작은 노드에만 연결을 유지
|
||||
if (string.Compare(node.NodeId, connectedNodeId, StringComparison.Ordinal) > 0)
|
||||
{
|
||||
connectionsToRemove.Add(connectedNodeId);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 반대 방향 연결 제거
|
||||
connectedNode.RemoveConnection(node.NodeId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 중복 연결 제거
|
||||
foreach (var connectionToRemove in connectionsToRemove)
|
||||
{
|
||||
node.RemoveConnection(connectionToRemove);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// MapNode 목록에서 RFID가 없는 노드들에 자동으로 RFID ID를 할당합니다.
|
||||
/// *** 에디터와 시뮬레이터 데이터 불일치 방지를 위해 비활성화됨 ***
|
||||
/// </summary>
|
||||
/// <param name="mapNodes">맵 노드 목록</param>
|
||||
[Obsolete("RFID 자동 할당은 에디터와 시뮬레이터 간 데이터 불일치를 야기하므로 사용하지 않음")]
|
||||
public static void AssignAutoRfidIds(List<MapNode> mapNodes)
|
||||
{
|
||||
// 에디터에서 설정한 RFID 값을 그대로 사용하기 위해 자동 할당 기능 비활성화
|
||||
// 에디터와 시뮬레이터 간 데이터 일관성 유지를 위함
|
||||
return;
|
||||
|
||||
/*
|
||||
foreach (var node in mapNodes)
|
||||
{
|
||||
// 네비게이션 가능한 노드이면서 RFID가 없는 경우에만 자동 할당
|
||||
if (node.IsNavigationNode() && !node.HasRfid())
|
||||
{
|
||||
// 기본 RFID ID 생성 (N001 -> 001)
|
||||
var rfidId = node.NodeId.Replace("N", "").PadLeft(3, '0');
|
||||
|
||||
node.SetRfidInfo(rfidId, "", "정상");
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
481
Cs_HMI/AGVLogic/AGVNavigationCore/Models/MapNode.cs
Normal file
481
Cs_HMI/AGVLogic/AGVNavigationCore/Models/MapNode.cs
Normal file
@@ -0,0 +1,481 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Drawing2D;
|
||||
|
||||
namespace AGVNavigationCore.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>
|
||||
/// 도킹 방향 (도킹/충전 노드인 경우에만 Forward/Backward, 일반 노드는 DontCare)
|
||||
/// </summary>
|
||||
public DockingDirection DockDirection { get; set; } = DockingDirection.DontCare;
|
||||
|
||||
/// <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 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>
|
||||
/// RFID 상태 (정상, 손상, 교체예정 등)
|
||||
/// </summary>
|
||||
public string RfidStatus { get; set; } = "정상";
|
||||
|
||||
/// <summary>
|
||||
/// RFID 설치 위치 설명 (현장 작업자용)
|
||||
/// 예: "로더1번 앞", "충전기2번 입구", "복도 교차점" 등
|
||||
/// </summary>
|
||||
public string RfidDescription { 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 $"{RfidId}({NodeId}): {Name} ({Type}) at ({Position.X}, {Position.Y})";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 리스트박스 표시용 텍스트 (노드ID - 설명 - RFID 순서)
|
||||
/// </summary>
|
||||
public string DisplayText
|
||||
{
|
||||
get
|
||||
{
|
||||
var displayText = NodeId;
|
||||
|
||||
if (!string.IsNullOrEmpty(Name))
|
||||
{
|
||||
displayText += $" - {Name}";
|
||||
}
|
||||
|
||||
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,
|
||||
IsActive = IsActive,
|
||||
DisplayColor = DisplayColor,
|
||||
RfidId = RfidId,
|
||||
RfidStatus = RfidStatus,
|
||||
RfidDescription = RfidDescription,
|
||||
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 = CompositingQuality.HighQuality;
|
||||
graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
|
||||
graphics.SmoothingMode = 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 경로 찾기에 사용 가능한 노드인지 확인
|
||||
/// (라벨, 이미지 노드는 경로 찾기에서 제외)
|
||||
/// </summary>
|
||||
public bool IsNavigationNode()
|
||||
{
|
||||
return Type != NodeType.Label && Type != NodeType.Image && IsActive;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// RFID가 할당되어 있는지 확인
|
||||
/// </summary>
|
||||
public bool HasRfid()
|
||||
{
|
||||
return !string.IsNullOrEmpty(RfidId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// RFID 정보 설정
|
||||
/// </summary>
|
||||
/// <param name="rfidId">RFID ID</param>
|
||||
/// <param name="rfidDescription">설치 위치 설명</param>
|
||||
/// <param name="rfidStatus">RFID 상태</param>
|
||||
public void SetRfidInfo(string rfidId, string rfidDescription = "", string rfidStatus = "정상")
|
||||
{
|
||||
RfidId = rfidId;
|
||||
RfidDescription = rfidDescription;
|
||||
RfidStatus = rfidStatus;
|
||||
ModifiedDate = DateTime.Now;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// RFID 정보 삭제
|
||||
/// </summary>
|
||||
public void ClearRfidInfo()
|
||||
{
|
||||
RfidId = string.Empty;
|
||||
RfidDescription = string.Empty;
|
||||
RfidStatus = "정상";
|
||||
ModifiedDate = DateTime.Now;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// RFID 기반 표시 텍스트 (RFID ID 우선, 없으면 노드ID)
|
||||
/// </summary>
|
||||
public string GetRfidDisplayText()
|
||||
{
|
||||
return HasRfid() ? RfidId : NodeId;
|
||||
}
|
||||
}
|
||||
}
|
||||
613
Cs_HMI/AGVLogic/AGVNavigationCore/Models/VirtualAGV.cs
Normal file
613
Cs_HMI/AGVLogic/AGVNavigationCore/Models/VirtualAGV.cs
Normal file
@@ -0,0 +1,613 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using AGVNavigationCore.Controls;
|
||||
using AGVNavigationCore.Models;
|
||||
using AGVNavigationCore.PathFinding;
|
||||
using AGVNavigationCore.PathFinding.Core;
|
||||
|
||||
namespace AGVNavigationCore.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// 가상 AGV 클래스 (코어 비즈니스 로직)
|
||||
/// 실제 AGV와 시뮬레이터 모두에서 사용 가능한 공용 로직
|
||||
/// 시뮬레이션과 실제 동작이 동일하게 동작하도록 설계됨
|
||||
/// </summary>
|
||||
public class VirtualAGV : IMovableAGV, IAGV
|
||||
{
|
||||
#region Events
|
||||
|
||||
/// <summary>
|
||||
/// AGV 상태 변경 이벤트
|
||||
/// </summary>
|
||||
public event EventHandler<AGVState> StateChanged;
|
||||
|
||||
/// <summary>
|
||||
/// 위치 변경 이벤트
|
||||
/// </summary>
|
||||
public event EventHandler<(Point, AgvDirection, MapNode)> PositionChanged;
|
||||
|
||||
/// <summary>
|
||||
/// RFID 감지 이벤트
|
||||
/// </summary>
|
||||
public event EventHandler<string> RfidDetected;
|
||||
|
||||
/// <summary>
|
||||
/// 경로 완료 이벤트
|
||||
/// </summary>
|
||||
public event EventHandler<AGVPathResult> PathCompleted;
|
||||
|
||||
/// <summary>
|
||||
/// 오류 발생 이벤트
|
||||
/// </summary>
|
||||
public event EventHandler<string> ErrorOccurred;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Fields
|
||||
|
||||
private string _agvId;
|
||||
private Point _currentPosition;
|
||||
private Point _targetPosition;
|
||||
private string _targetId;
|
||||
private string _currentId;
|
||||
private AgvDirection _currentDirection;
|
||||
private AgvDirection _targetDirection;
|
||||
private AGVState _currentState;
|
||||
private float _currentSpeed;
|
||||
|
||||
// 경로 관련
|
||||
private AGVPathResult _currentPath;
|
||||
private List<string> _remainingNodes;
|
||||
private int _currentNodeIndex;
|
||||
private MapNode _currentNode;
|
||||
private MapNode _targetNode;
|
||||
|
||||
// 이동 관련
|
||||
private DateTime _lastUpdateTime;
|
||||
private Point _moveStartPosition;
|
||||
private Point _moveTargetPosition;
|
||||
private float _moveProgress;
|
||||
|
||||
// 도킹 관련
|
||||
private DockingDirection _dockingDirection;
|
||||
|
||||
// 시뮬레이션 설정
|
||||
private readonly float _moveSpeed = 50.0f; // 픽셀/초
|
||||
private readonly float _rotationSpeed = 90.0f; // 도/초
|
||||
private bool _isMoving;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Properties
|
||||
|
||||
/// <summary>
|
||||
/// AGV ID
|
||||
/// </summary>
|
||||
public string AgvId => _agvId;
|
||||
|
||||
/// <summary>
|
||||
/// 현재 위치
|
||||
/// </summary>
|
||||
public Point CurrentPosition
|
||||
{
|
||||
get => _currentPosition;
|
||||
set => _currentPosition = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 현재 방향
|
||||
/// 모터의 동작 방향
|
||||
/// </summary>
|
||||
public AgvDirection CurrentDirection
|
||||
{
|
||||
get => _currentDirection;
|
||||
set => _currentDirection = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 현재 상태
|
||||
/// </summary>
|
||||
public AGVState CurrentState
|
||||
{
|
||||
get => _currentState;
|
||||
set => _currentState = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 현재 속도
|
||||
/// </summary>
|
||||
public float CurrentSpeed => _currentSpeed;
|
||||
|
||||
/// <summary>
|
||||
/// 현재 경로
|
||||
/// </summary>
|
||||
public AGVPathResult CurrentPath => _currentPath;
|
||||
|
||||
/// <summary>
|
||||
/// 현재 노드 ID
|
||||
/// </summary>
|
||||
public string CurrentNodeId => _currentNode?.NodeId;
|
||||
|
||||
/// <summary>
|
||||
/// 목표 위치
|
||||
/// </summary>
|
||||
public Point? TargetPosition => _targetPosition;
|
||||
|
||||
/// <summary>
|
||||
/// 배터리 레벨 (시뮬레이션)
|
||||
/// </summary>
|
||||
public float BatteryLevel { get; set; } = 100.0f;
|
||||
|
||||
/// <summary>
|
||||
/// 목표 노드 ID
|
||||
/// </summary>
|
||||
public string TargetNodeId => _targetNode?.NodeId;
|
||||
|
||||
/// <summary>
|
||||
/// 도킹 방향
|
||||
/// </summary>
|
||||
public DockingDirection DockingDirection => _dockingDirection;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Constructor
|
||||
|
||||
/// <summary>
|
||||
/// 생성자
|
||||
/// </summary>
|
||||
/// <param name="agvId">AGV ID</param>
|
||||
/// <param name="startPosition">시작 위치</param>
|
||||
/// <param name="startDirection">시작 방향</param>
|
||||
public VirtualAGV(string agvId, Point startPosition, AgvDirection startDirection = AgvDirection.Forward)
|
||||
{
|
||||
_agvId = agvId;
|
||||
_currentPosition = startPosition;
|
||||
_currentDirection = startDirection;
|
||||
_currentState = AGVState.Idle;
|
||||
_currentSpeed = 0;
|
||||
_dockingDirection = DockingDirection.Forward; // 기본값: 전진 도킹
|
||||
_currentNode = null;
|
||||
_targetNode = null;
|
||||
_isMoving = false;
|
||||
_lastUpdateTime = DateTime.Now;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public Methods - 센서/RFID 입력 (실제 AGV에서 호출)
|
||||
|
||||
/// <summary>
|
||||
/// 현재 위치 설정 (실제 AGV 센서에서)
|
||||
/// </summary>
|
||||
public void SetCurrentPosition(Point position)
|
||||
{
|
||||
_currentPosition = position;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 감지된 RFID 설정 (실제 RFID 센서에서)
|
||||
/// </summary>
|
||||
public void SetDetectedRfid(string rfidId)
|
||||
{
|
||||
RfidDetected?.Invoke(this, rfidId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 모터 방향 설정 (실제 모터 컨트롤러에서)
|
||||
/// </summary>
|
||||
public void SetMotorDirection(AgvDirection direction)
|
||||
{
|
||||
_currentDirection = direction;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 배터리 레벨 설정 (실제 BMS에서)
|
||||
/// </summary>
|
||||
public void SetBatteryLevel(float percentage)
|
||||
{
|
||||
BatteryLevel = Math.Max(0, Math.Min(100, percentage));
|
||||
|
||||
// 배터리 부족 경고
|
||||
if (BatteryLevel < 20.0f && _currentState != AGVState.Charging)
|
||||
{
|
||||
OnError($"배터리 부족: {BatteryLevel:F1}%");
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public Methods - 상태 조회
|
||||
|
||||
/// <summary>
|
||||
/// 현재 위치 조회
|
||||
/// </summary>
|
||||
public Point GetCurrentPosition() => _currentPosition;
|
||||
|
||||
/// <summary>
|
||||
/// 현재 상태 조회
|
||||
/// </summary>
|
||||
public AGVState GetCurrentState() => _currentState;
|
||||
|
||||
/// <summary>
|
||||
/// 현재 노드 ID 조회
|
||||
/// </summary>
|
||||
public string GetCurrentNodeId() => _currentNode?.NodeId;
|
||||
|
||||
/// <summary>
|
||||
/// AGV 정보 조회
|
||||
/// </summary>
|
||||
public string GetStatus()
|
||||
{
|
||||
return $"AGV[{_agvId}] 위치:({_currentPosition.X},{_currentPosition.Y}) " +
|
||||
$"방향:{_currentDirection} 상태:{_currentState} " +
|
||||
$"속도:{_currentSpeed:F1} 배터리:{BatteryLevel:F1}%";
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public Methods - 경로 실행
|
||||
|
||||
/// <summary>
|
||||
/// 경로 실행 시작
|
||||
/// </summary>
|
||||
/// <param name="path">실행할 경로</param>
|
||||
/// <param name="mapNodes">맵 노드 목록</param>
|
||||
public void ExecutePath(AGVPathResult path, List<MapNode> mapNodes)
|
||||
{
|
||||
if (path == null || !path.Success)
|
||||
{
|
||||
OnError("유효하지 않은 경로입니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
_currentPath = path;
|
||||
_remainingNodes = new List<string>(path.Path);
|
||||
_currentNodeIndex = 0;
|
||||
|
||||
// 시작 노드와 목표 노드 설정
|
||||
if (_remainingNodes.Count > 0)
|
||||
{
|
||||
var startNode = mapNodes.FirstOrDefault(n => n.NodeId == _remainingNodes[0]);
|
||||
if (startNode != null)
|
||||
{
|
||||
_currentNode = startNode;
|
||||
|
||||
// 목표 노드 설정 (경로의 마지막 노드)
|
||||
if (_remainingNodes.Count > 1)
|
||||
{
|
||||
var targetNodeId = _remainingNodes[_remainingNodes.Count - 1];
|
||||
var targetNode = mapNodes.FirstOrDefault(n => n.NodeId == targetNodeId);
|
||||
|
||||
// 목표 노드의 타입에 따라 도킹 방향 결정
|
||||
if (targetNode != null)
|
||||
{
|
||||
_dockingDirection = GetDockingDirection(targetNode.Type);
|
||||
_targetNode = targetNode;
|
||||
}
|
||||
}
|
||||
|
||||
StartMovement();
|
||||
}
|
||||
else
|
||||
{
|
||||
OnError($"시작 노드를 찾을 수 없습니다: {_remainingNodes[0]}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 간단한 경로 실행 (경로 객체 없이 노드만)
|
||||
/// </summary>
|
||||
public void StartPath(AGVPathResult path, List<MapNode> mapNodes)
|
||||
{
|
||||
ExecutePath(path, mapNodes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 경로 정지
|
||||
/// </summary>
|
||||
public void StopPath()
|
||||
{
|
||||
_isMoving = false;
|
||||
_currentPath = null;
|
||||
_remainingNodes?.Clear();
|
||||
SetState(AGVState.Idle);
|
||||
_currentSpeed = 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 긴급 정지
|
||||
/// </summary>
|
||||
public void EmergencyStop()
|
||||
{
|
||||
StopPath();
|
||||
OnError("긴급 정지가 실행되었습니다.");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public Methods - 프레임 업데이트 (외부에서 정기적으로 호출)
|
||||
|
||||
/// <summary>
|
||||
/// 프레임 업데이트 (외부에서 주기적으로 호출)
|
||||
/// 이 방식으로 타이머에 의존하지 않고 외부에서 제어 가능
|
||||
/// </summary>
|
||||
/// <param name="deltaTimeMs">마지막 업데이트 이후 경과 시간 (밀리초)</param>
|
||||
public void Update(float deltaTimeMs)
|
||||
{
|
||||
var deltaTime = deltaTimeMs / 1000.0f; // 초 단위로 변환
|
||||
|
||||
UpdateMovement(deltaTime);
|
||||
UpdateBattery(deltaTime);
|
||||
|
||||
// 위치 변경 이벤트 발생
|
||||
PositionChanged?.Invoke(this, (_currentPosition, _currentDirection, _currentNode));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public Methods - 수동 제어 (테스트용)
|
||||
|
||||
/// <summary>
|
||||
/// 수동 이동 (테스트용)
|
||||
/// </summary>
|
||||
/// <param name="targetPosition">목표 위치</param>
|
||||
public void MoveTo(Point targetPosition)
|
||||
{
|
||||
_targetPosition = targetPosition;
|
||||
_moveStartPosition = _currentPosition;
|
||||
_moveTargetPosition = targetPosition;
|
||||
_moveProgress = 0;
|
||||
|
||||
SetState(AGVState.Moving);
|
||||
_isMoving = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 수동 회전 (테스트용)
|
||||
/// </summary>
|
||||
/// <param name="direction">회전 방향</param>
|
||||
public void Rotate(AgvDirection direction)
|
||||
{
|
||||
if (_currentState != AGVState.Idle)
|
||||
return;
|
||||
|
||||
SetState(AGVState.Rotating);
|
||||
_currentDirection = direction;
|
||||
SetState(AGVState.Idle);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 충전 시작
|
||||
/// </summary>
|
||||
public void StartCharging()
|
||||
{
|
||||
if (_currentState == AGVState.Idle)
|
||||
{
|
||||
SetState(AGVState.Charging);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 충전 종료
|
||||
/// </summary>
|
||||
public void StopCharging()
|
||||
{
|
||||
if (_currentState == AGVState.Charging)
|
||||
{
|
||||
SetState(AGVState.Idle);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public Methods - AGV 위치 설정 (시뮬레이터용)
|
||||
|
||||
/// <summary>
|
||||
/// AGV 위치 직접 설정
|
||||
/// TargetPosition을 이전 위치로 저장하여 리프트 방향 계산이 가능하도록 함
|
||||
/// </summary>
|
||||
/// <param name="node">현재 노드</param>
|
||||
/// <param name="newPosition">새로운 위치</param>
|
||||
/// <param name="motorDirection">모터이동방향</param>
|
||||
public void SetPosition(MapNode node, Point newPosition, AgvDirection motorDirection)
|
||||
{
|
||||
// 현재 위치를 이전 위치로 저장 (리프트 방향 계산용)
|
||||
if (_currentPosition != Point.Empty)
|
||||
{
|
||||
_targetPosition = _currentPosition; // 이전 위치
|
||||
_targetDirection = _currentDirection;
|
||||
_targetNode = node;
|
||||
}
|
||||
|
||||
// 새로운 위치 설정
|
||||
_currentPosition = newPosition;
|
||||
_currentDirection = motorDirection;
|
||||
_currentNode = node;
|
||||
|
||||
// 위치 변경 이벤트 발생
|
||||
PositionChanged?.Invoke(this, (_currentPosition, _currentDirection, _currentNode));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 현재 RFID 시뮬레이션 (현재 위치 기준)
|
||||
/// </summary>
|
||||
public string SimulateRfidReading(List<MapNode> mapNodes)
|
||||
{
|
||||
var closestNode = FindClosestNode(_currentPosition, mapNodes);
|
||||
if (closestNode == null)
|
||||
return null;
|
||||
|
||||
return closestNode.HasRfid() ? closestNode.RfidId : null;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Private Methods
|
||||
|
||||
private void StartMovement()
|
||||
{
|
||||
SetState(AGVState.Moving);
|
||||
_isMoving = true;
|
||||
_lastUpdateTime = DateTime.Now;
|
||||
}
|
||||
|
||||
private void UpdateMovement(float deltaTime)
|
||||
{
|
||||
if (_currentState != AGVState.Moving || !_isMoving)
|
||||
return;
|
||||
|
||||
// 목표 위치까지의 거리 계산
|
||||
var distance = CalculateDistance(_currentPosition, _moveTargetPosition);
|
||||
|
||||
if (distance < 5.0f) // 도달 임계값
|
||||
{
|
||||
// 목표 도달
|
||||
_currentPosition = _moveTargetPosition;
|
||||
_currentSpeed = 0;
|
||||
|
||||
// 다음 노드로 이동
|
||||
ProcessNextNode();
|
||||
}
|
||||
else
|
||||
{
|
||||
// 계속 이동
|
||||
var moveDistance = _moveSpeed * deltaTime;
|
||||
var direction = new PointF(
|
||||
_moveTargetPosition.X - _currentPosition.X,
|
||||
_moveTargetPosition.Y - _currentPosition.Y
|
||||
);
|
||||
|
||||
// 정규화
|
||||
var length = (float)Math.Sqrt(direction.X * direction.X + direction.Y * direction.Y);
|
||||
if (length > 0)
|
||||
{
|
||||
direction.X /= length;
|
||||
direction.Y /= length;
|
||||
}
|
||||
|
||||
// 새 위치 계산
|
||||
_currentPosition = new Point(
|
||||
(int)(_currentPosition.X + direction.X * moveDistance),
|
||||
(int)(_currentPosition.Y + direction.Y * moveDistance)
|
||||
);
|
||||
|
||||
_currentSpeed = _moveSpeed;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateBattery(float deltaTime)
|
||||
{
|
||||
// 배터리 소모 시뮬레이션
|
||||
if (_currentState == AGVState.Moving)
|
||||
{
|
||||
BatteryLevel -= 0.1f * deltaTime; // 이동시 소모
|
||||
}
|
||||
else if (_currentState == AGVState.Charging)
|
||||
{
|
||||
BatteryLevel += 5.0f * deltaTime; // 충전
|
||||
BatteryLevel = Math.Min(100.0f, BatteryLevel);
|
||||
}
|
||||
|
||||
BatteryLevel = Math.Max(0, BatteryLevel);
|
||||
}
|
||||
|
||||
private void ProcessNextNode()
|
||||
{
|
||||
if (_remainingNodes == null || _currentNodeIndex >= _remainingNodes.Count - 1)
|
||||
{
|
||||
// 경로 완료
|
||||
_isMoving = false;
|
||||
SetState(AGVState.Idle);
|
||||
PathCompleted?.Invoke(this, _currentPath);
|
||||
return;
|
||||
}
|
||||
|
||||
// 다음 노드로 이동
|
||||
_currentNodeIndex++;
|
||||
var nextNodeId = _remainingNodes[_currentNodeIndex];
|
||||
|
||||
// RFID 감지 시뮬레이션
|
||||
RfidDetected?.Invoke(this, $"RFID_{nextNodeId}");
|
||||
|
||||
// 다음 목표 위치 설정 (실제로는 맵에서 좌표 가져와야 함)
|
||||
var random = new Random();
|
||||
_moveTargetPosition = new Point(
|
||||
_currentPosition.X + random.Next(-100, 100),
|
||||
_currentPosition.Y + random.Next(-100, 100)
|
||||
);
|
||||
}
|
||||
|
||||
private MapNode FindClosestNode(Point position, List<MapNode> mapNodes)
|
||||
{
|
||||
if (mapNodes == null || mapNodes.Count == 0)
|
||||
return null;
|
||||
|
||||
MapNode closestNode = null;
|
||||
float closestDistance = float.MaxValue;
|
||||
|
||||
foreach (var node in mapNodes)
|
||||
{
|
||||
var distance = CalculateDistance(position, node.Position);
|
||||
if (distance < closestDistance)
|
||||
{
|
||||
closestDistance = distance;
|
||||
closestNode = node;
|
||||
}
|
||||
}
|
||||
|
||||
return closestDistance < 50.0f ? closestNode : null;
|
||||
}
|
||||
|
||||
private float CalculateDistance(Point from, Point to)
|
||||
{
|
||||
var dx = to.X - from.X;
|
||||
var dy = to.Y - from.Y;
|
||||
return (float)Math.Sqrt(dx * dx + dy * dy);
|
||||
}
|
||||
|
||||
private void SetState(AGVState newState)
|
||||
{
|
||||
if (_currentState != newState)
|
||||
{
|
||||
_currentState = newState;
|
||||
StateChanged?.Invoke(this, newState);
|
||||
}
|
||||
}
|
||||
|
||||
private DockingDirection GetDockingDirection(NodeType nodeType)
|
||||
{
|
||||
switch (nodeType)
|
||||
{
|
||||
case NodeType.Charging:
|
||||
return DockingDirection.Forward;
|
||||
case NodeType.Docking:
|
||||
return DockingDirection.Backward;
|
||||
default:
|
||||
return DockingDirection.Forward;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnError(string message)
|
||||
{
|
||||
SetState(AGVState.Error);
|
||||
ErrorOccurred?.Invoke(this, message);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Cleanup
|
||||
|
||||
/// <summary>
|
||||
/// 리소스 정리
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
StopPath();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,305 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using AGVNavigationCore.Models;
|
||||
using AGVNavigationCore.PathFinding.Planning;
|
||||
|
||||
namespace AGVNavigationCore.PathFinding.Analysis
|
||||
{
|
||||
/// <summary>
|
||||
/// AGV 갈림길 분석 및 마그넷 센서 방향 계산 시스템
|
||||
/// </summary>
|
||||
public class JunctionAnalyzer
|
||||
{
|
||||
/// <summary>
|
||||
/// 갈림길 정보
|
||||
/// </summary>
|
||||
public class JunctionInfo
|
||||
{
|
||||
public string NodeId { get; set; }
|
||||
public List<string> ConnectedNodes { get; set; }
|
||||
public Dictionary<string, MagnetDirection> PathDirections { get; set; }
|
||||
public bool IsJunction => ConnectedNodes.Count > 2;
|
||||
|
||||
public JunctionInfo(string nodeId)
|
||||
{
|
||||
NodeId = nodeId;
|
||||
ConnectedNodes = new List<string>();
|
||||
PathDirections = new Dictionary<string, MagnetDirection>();
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
if (!IsJunction)
|
||||
return $"{NodeId}: 일반노드 ({ConnectedNodes.Count}연결)";
|
||||
|
||||
var paths = string.Join(", ", PathDirections.Select(p => $"{p.Key}({p.Value})"));
|
||||
return $"{NodeId}: 갈림길 - {paths}";
|
||||
}
|
||||
}
|
||||
|
||||
private readonly List<MapNode> _mapNodes;
|
||||
private readonly Dictionary<string, JunctionInfo> _junctions;
|
||||
|
||||
public JunctionAnalyzer(List<MapNode> mapNodes)
|
||||
{
|
||||
_mapNodes = mapNodes ?? new List<MapNode>();
|
||||
_junctions = new Dictionary<string, JunctionInfo>();
|
||||
AnalyzeJunctions();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 모든 갈림길 분석
|
||||
/// </summary>
|
||||
private void AnalyzeJunctions()
|
||||
{
|
||||
foreach (var node in _mapNodes)
|
||||
{
|
||||
if (node.IsNavigationNode())
|
||||
{
|
||||
var junctionInfo = AnalyzeNode(node);
|
||||
_junctions[node.NodeId] = junctionInfo;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 개별 노드의 갈림길 정보 분석
|
||||
/// </summary>
|
||||
private JunctionInfo AnalyzeNode(MapNode node)
|
||||
{
|
||||
var junction = new JunctionInfo(node.NodeId);
|
||||
|
||||
// 양방향 연결을 고려하여 모든 연결된 노드 찾기
|
||||
var connectedNodes = GetAllConnectedNodes(node);
|
||||
junction.ConnectedNodes = connectedNodes;
|
||||
|
||||
if (connectedNodes.Count > 2)
|
||||
{
|
||||
// 갈림길인 경우 각 방향별 마그넷 센서 방향 계산
|
||||
CalculateMagnetDirections(node, connectedNodes, junction);
|
||||
}
|
||||
|
||||
return junction;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 양방향 연결을 고려한 모든 연결 노드 검색
|
||||
/// </summary>
|
||||
private List<string> GetAllConnectedNodes(MapNode node)
|
||||
{
|
||||
var connected = new HashSet<string>();
|
||||
|
||||
// 직접 연결된 노드들
|
||||
foreach (var connectedId in node.ConnectedNodes)
|
||||
{
|
||||
connected.Add(connectedId);
|
||||
}
|
||||
|
||||
// 역방향 연결된 노드들 (다른 노드에서 이 노드로 연결)
|
||||
foreach (var otherNode in _mapNodes)
|
||||
{
|
||||
if (otherNode.NodeId != node.NodeId && otherNode.ConnectedNodes.Contains(node.NodeId))
|
||||
{
|
||||
connected.Add(otherNode.NodeId);
|
||||
}
|
||||
}
|
||||
|
||||
return connected.ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 갈림길에서 각 방향별 마그넷 센서 방향 계산
|
||||
/// </summary>
|
||||
private void CalculateMagnetDirections(MapNode junctionNode, List<string> connectedNodes, JunctionInfo junction)
|
||||
{
|
||||
if (connectedNodes.Count < 3) return;
|
||||
|
||||
// 각 연결 노드의 각도 계산
|
||||
var nodeAngles = new List<(string NodeId, double Angle)>();
|
||||
|
||||
foreach (var connectedId in connectedNodes)
|
||||
{
|
||||
var connectedNode = _mapNodes.FirstOrDefault(n => n.NodeId == connectedId);
|
||||
if (connectedNode != null)
|
||||
{
|
||||
double angle = CalculateAngle(junctionNode.Position, connectedNode.Position);
|
||||
nodeAngles.Add((connectedId, angle));
|
||||
}
|
||||
}
|
||||
|
||||
// 각도순으로 정렬
|
||||
nodeAngles.Sort((a, b) => a.Angle.CompareTo(b.Angle));
|
||||
|
||||
// 마그넷 방향 할당
|
||||
AssignMagnetDirections(nodeAngles, junction);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 두 점 사이의 각도 계산 (라디안)
|
||||
/// </summary>
|
||||
private double CalculateAngle(Point from, Point to)
|
||||
{
|
||||
double deltaX = to.X - from.X;
|
||||
double deltaY = to.Y - from.Y;
|
||||
double angle = Math.Atan2(deltaY, deltaX);
|
||||
|
||||
// 0~2π 범위로 정규화
|
||||
if (angle < 0)
|
||||
angle += 2 * Math.PI;
|
||||
|
||||
return angle;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 갈림길에서 마그넷 센서 방향 할당
|
||||
/// </summary>
|
||||
private void AssignMagnetDirections(List<(string NodeId, double Angle)> sortedNodes, JunctionInfo junction)
|
||||
{
|
||||
int nodeCount = sortedNodes.Count;
|
||||
|
||||
for (int i = 0; i < nodeCount; i++)
|
||||
{
|
||||
string nodeId = sortedNodes[i].NodeId;
|
||||
MagnetDirection direction;
|
||||
|
||||
if (nodeCount == 3)
|
||||
{
|
||||
// 3갈래: 직진, 좌측, 우측
|
||||
switch (i)
|
||||
{
|
||||
case 0: direction = MagnetDirection.Straight; break;
|
||||
case 1: direction = MagnetDirection.Left; break;
|
||||
case 2: direction = MagnetDirection.Right; break;
|
||||
default: direction = MagnetDirection.Straight; break;
|
||||
}
|
||||
}
|
||||
else if (nodeCount == 4)
|
||||
{
|
||||
// 4갈래: 교차로
|
||||
switch (i)
|
||||
{
|
||||
case 0: direction = MagnetDirection.Straight; break;
|
||||
case 1: direction = MagnetDirection.Left; break;
|
||||
case 2: direction = MagnetDirection.Straight; break; // 반대편
|
||||
case 3: direction = MagnetDirection.Right; break;
|
||||
default: direction = MagnetDirection.Straight; break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 5갈래 이상: 각도 기반 배정
|
||||
double angleStep = 2 * Math.PI / nodeCount;
|
||||
double normalizedIndex = (double)i / nodeCount;
|
||||
|
||||
if (normalizedIndex < 0.33)
|
||||
direction = MagnetDirection.Left;
|
||||
else if (normalizedIndex < 0.67)
|
||||
direction = MagnetDirection.Straight;
|
||||
else
|
||||
direction = MagnetDirection.Right;
|
||||
}
|
||||
|
||||
junction.PathDirections[nodeId] = direction;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 특정 경로에서 요구되는 마그넷 방향 계산 (전진 방향 기준)
|
||||
/// </summary>
|
||||
public MagnetDirection GetRequiredMagnetDirection(string fromNodeId, string currentNodeId, string toNodeId)
|
||||
{
|
||||
if (!_junctions.ContainsKey(currentNodeId))
|
||||
return MagnetDirection.Straight;
|
||||
|
||||
var junction = _junctions[currentNodeId];
|
||||
if (!junction.IsJunction)
|
||||
return MagnetDirection.Straight;
|
||||
|
||||
// 실제 각도 기반으로 마그넷 방향 계산
|
||||
var fromNode = _mapNodes.FirstOrDefault(n => n.NodeId == fromNodeId);
|
||||
var currentNode = _mapNodes.FirstOrDefault(n => n.NodeId == currentNodeId);
|
||||
var toNode = _mapNodes.FirstOrDefault(n => n.NodeId == toNodeId);
|
||||
|
||||
if (fromNode == null || currentNode == null || toNode == null)
|
||||
return MagnetDirection.Straight;
|
||||
|
||||
// 전진 방향(진행 방향) 계산
|
||||
double incomingAngle = CalculateAngle(fromNode.Position, currentNode.Position);
|
||||
|
||||
// 목표 방향 계산
|
||||
double outgoingAngle = CalculateAngle(currentNode.Position, toNode.Position);
|
||||
|
||||
// 각도 차이 계산 (전진 방향 기준)
|
||||
double angleDiff = outgoingAngle - incomingAngle;
|
||||
|
||||
// 각도를 -π ~ π 범위로 정규화
|
||||
while (angleDiff > Math.PI) angleDiff -= 2 * Math.PI;
|
||||
while (angleDiff < -Math.PI) angleDiff += 2 * Math.PI;
|
||||
|
||||
// 전진 방향 기준으로 마그넷 방향 결정
|
||||
// 각도 차이가 작으면 직진, 음수면 왼쪽, 양수면 오른쪽
|
||||
if (Math.Abs(angleDiff) < Math.PI / 6) // 30도 이내는 직진
|
||||
return MagnetDirection.Straight;
|
||||
else if (angleDiff < 0) // 음수면 왼쪽 회전
|
||||
return MagnetDirection.Left;
|
||||
else // 양수면 오른쪽 회전
|
||||
return MagnetDirection.Right;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 방향 전환 가능한 갈림길 검색
|
||||
/// </summary>
|
||||
public List<string> FindDirectionChangeJunctions(AgvDirection currentDirection, AgvDirection targetDirection)
|
||||
{
|
||||
var availableJunctions = new List<string>();
|
||||
|
||||
if (currentDirection == targetDirection)
|
||||
return availableJunctions;
|
||||
|
||||
foreach (var junction in _junctions.Values)
|
||||
{
|
||||
if (junction.IsJunction)
|
||||
{
|
||||
// 갈림길에서 방향 전환이 가능한지 확인
|
||||
// (실제로는 더 복잡한 로직이 필요하지만, 일단 모든 갈림길을 후보로 함)
|
||||
availableJunctions.Add(junction.NodeId);
|
||||
}
|
||||
}
|
||||
|
||||
return availableJunctions;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 갈림길 정보 반환
|
||||
/// </summary>
|
||||
public JunctionInfo GetJunctionInfo(string nodeId)
|
||||
{
|
||||
return _junctions.ContainsKey(nodeId) ? _junctions[nodeId] : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 모든 갈림길 목록 반환
|
||||
/// </summary>
|
||||
public List<JunctionInfo> GetAllJunctions()
|
||||
{
|
||||
return _junctions.Values.Where(j => j.IsJunction).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 디버깅용 갈림길 정보 출력
|
||||
/// </summary>
|
||||
public List<string> GetJunctionSummary()
|
||||
{
|
||||
var summary = new List<string>();
|
||||
|
||||
foreach (var junction in _junctions.Values.Where(j => j.IsJunction))
|
||||
{
|
||||
summary.Add(junction.ToString());
|
||||
}
|
||||
|
||||
return summary;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,371 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using AGVNavigationCore.Models;
|
||||
using AGVNavigationCore.PathFinding.Planning;
|
||||
using AGVNavigationCore.PathFinding.Validation;
|
||||
|
||||
namespace AGVNavigationCore.PathFinding.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// AGV 경로 계산 결과 (방향성 및 명령어 포함)
|
||||
/// </summary>
|
||||
public class AGVPathResult
|
||||
{
|
||||
/// <summary>
|
||||
/// 경로 찾기 성공 여부
|
||||
/// </summary>
|
||||
public bool Success { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 경로 노드 ID 목록 (시작 → 목적지 순서)
|
||||
/// </summary>
|
||||
public List<string> Path { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// AGV 명령어 목록 (이동 방향 시퀀스)
|
||||
/// </summary>
|
||||
public List<AgvDirection> Commands { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 노드별 모터방향 정보 목록
|
||||
/// </summary>
|
||||
public List<NodeMotorInfo> NodeMotorInfos { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 총 거리
|
||||
/// </summary>
|
||||
public float TotalDistance { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 계산 소요 시간 (밀리초)
|
||||
/// </summary>
|
||||
public long CalculationTimeMs { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 탐색된 노드 수
|
||||
/// </summary>
|
||||
public int ExploredNodeCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 탐색된 노드 수 (호환성용)
|
||||
/// </summary>
|
||||
public int ExploredNodes
|
||||
{
|
||||
get => ExploredNodeCount;
|
||||
set => ExploredNodeCount = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 예상 소요 시간 (초)
|
||||
/// </summary>
|
||||
public float EstimatedTimeSeconds { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 회전 횟수
|
||||
/// </summary>
|
||||
public int RotationCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 오류 메시지 (실패시)
|
||||
/// </summary>
|
||||
public string ErrorMessage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 도킹 검증 결과
|
||||
/// </summary>
|
||||
public DockingValidationResult DockingValidation { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 상세 경로 정보 (NodeMotorInfo 목록)
|
||||
/// </summary>
|
||||
public List<NodeMotorInfo> DetailedPath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 계획 설명
|
||||
/// </summary>
|
||||
public string PlanDescription { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 방향 전환 필요 여부
|
||||
/// </summary>
|
||||
public bool RequiredDirectionChange { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 방향 전환 노드 ID
|
||||
/// </summary>
|
||||
public string DirectionChangeNode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 기본 생성자
|
||||
/// </summary>
|
||||
public AGVPathResult()
|
||||
{
|
||||
Success = false;
|
||||
Path = new List<string>();
|
||||
Commands = new List<AgvDirection>();
|
||||
NodeMotorInfos = new List<NodeMotorInfo>();
|
||||
DetailedPath = new List<NodeMotorInfo>();
|
||||
TotalDistance = 0;
|
||||
CalculationTimeMs = 0;
|
||||
ExploredNodes = 0;
|
||||
EstimatedTimeSeconds = 0;
|
||||
RotationCount = 0;
|
||||
ErrorMessage = string.Empty;
|
||||
PlanDescription = string.Empty;
|
||||
RequiredDirectionChange = false;
|
||||
DirectionChangeNode = string.Empty;
|
||||
DockingValidation = DockingValidationResult.CreateNotRequired();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 성공 결과 생성
|
||||
/// </summary>
|
||||
/// <param name="path">경로</param>
|
||||
/// <param name="commands">AGV 명령어 목록</param>
|
||||
/// <param name="totalDistance">총 거리</param>
|
||||
/// <param name="calculationTimeMs">계산 시간</param>
|
||||
/// <returns>성공 결과</returns>
|
||||
public static AGVPathResult CreateSuccess(List<string> path, List<AgvDirection> commands, float totalDistance, long calculationTimeMs)
|
||||
{
|
||||
var result = new AGVPathResult
|
||||
{
|
||||
Success = true,
|
||||
Path = new List<string>(path),
|
||||
Commands = new List<AgvDirection>(commands),
|
||||
TotalDistance = totalDistance,
|
||||
CalculationTimeMs = calculationTimeMs
|
||||
};
|
||||
|
||||
result.CalculateMetrics();
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 성공 결과 생성 (노드별 모터방향 정보 포함)
|
||||
/// </summary>
|
||||
/// <param name="path">경로</param>
|
||||
/// <param name="commands">AGV 명령어 목록</param>
|
||||
/// <param name="nodeMotorInfos">노드별 모터방향 정보</param>
|
||||
/// <param name="totalDistance">총 거리</param>
|
||||
/// <param name="calculationTimeMs">계산 시간</param>
|
||||
/// <returns>성공 결과</returns>
|
||||
public static AGVPathResult CreateSuccess(List<string> path, List<AgvDirection> commands, List<NodeMotorInfo> nodeMotorInfos, float totalDistance, long calculationTimeMs)
|
||||
{
|
||||
var result = new AGVPathResult
|
||||
{
|
||||
Success = true,
|
||||
Path = new List<string>(path),
|
||||
Commands = new List<AgvDirection>(commands),
|
||||
NodeMotorInfos = new List<NodeMotorInfo>(nodeMotorInfos),
|
||||
TotalDistance = totalDistance,
|
||||
CalculationTimeMs = calculationTimeMs
|
||||
};
|
||||
|
||||
result.CalculateMetrics();
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 실패 결과 생성
|
||||
/// </summary>
|
||||
/// <param name="errorMessage">오류 메시지</param>
|
||||
/// <param name="calculationTimeMs">계산 시간</param>
|
||||
/// <returns>실패 결과</returns>
|
||||
public static AGVPathResult CreateFailure(string errorMessage, long calculationTimeMs)
|
||||
{
|
||||
return new AGVPathResult
|
||||
{
|
||||
Success = false,
|
||||
ErrorMessage = errorMessage,
|
||||
CalculationTimeMs = calculationTimeMs
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 실패 결과 생성 (확장)
|
||||
/// </summary>
|
||||
/// <param name="errorMessage">오류 메시지</param>
|
||||
/// <param name="calculationTimeMs">계산 시간</param>
|
||||
/// <param name="exploredNodes">탐색된 노드 수</param>
|
||||
/// <returns>실패 결과</returns>
|
||||
public static AGVPathResult CreateFailure(string errorMessage, long calculationTimeMs, int exploredNodes)
|
||||
{
|
||||
return new AGVPathResult
|
||||
{
|
||||
Success = false,
|
||||
ErrorMessage = errorMessage,
|
||||
CalculationTimeMs = calculationTimeMs,
|
||||
ExploredNodes = exploredNodes
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 성공 결과 생성 (상세 경로용)
|
||||
/// </summary>
|
||||
/// <param name="detailedPath">상세 경로</param>
|
||||
/// <param name="totalDistance">총 거리</param>
|
||||
/// <param name="calculationTimeMs">계산 시간</param>
|
||||
/// <param name="exploredNodes">탐색된 노드 수</param>
|
||||
/// <param name="planDescription">계획 설명</param>
|
||||
/// <param name="directionChange">방향 전환 여부</param>
|
||||
/// <param name="changeNode">방향 전환 노드</param>
|
||||
/// <returns>성공 결과</returns>
|
||||
public static AGVPathResult CreateSuccess(List<NodeMotorInfo> detailedPath, float totalDistance, long calculationTimeMs, int exploredNodes, string planDescription, bool directionChange = false, string changeNode = null)
|
||||
{
|
||||
var path = detailedPath?.Select(n => n.NodeId).ToList() ?? new List<string>();
|
||||
|
||||
var result = new AGVPathResult
|
||||
{
|
||||
Success = true,
|
||||
Path = path,
|
||||
DetailedPath = detailedPath ?? new List<NodeMotorInfo>(),
|
||||
TotalDistance = totalDistance,
|
||||
CalculationTimeMs = calculationTimeMs,
|
||||
ExploredNodes = exploredNodes,
|
||||
PlanDescription = planDescription ?? string.Empty,
|
||||
RequiredDirectionChange = directionChange,
|
||||
DirectionChangeNode = changeNode ?? string.Empty
|
||||
};
|
||||
|
||||
result.CalculateMetrics();
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 경로 메트릭 계산
|
||||
/// </summary>
|
||||
private void CalculateMetrics()
|
||||
{
|
||||
RotationCount = CountRotations();
|
||||
EstimatedTimeSeconds = CalculateEstimatedTime();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 회전 횟수 계산
|
||||
/// </summary>
|
||||
private int CountRotations()
|
||||
{
|
||||
int count = 0;
|
||||
foreach (var command in Commands)
|
||||
{
|
||||
if (command == AgvDirection.Left || command == AgvDirection.Right)
|
||||
{
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 예상 소요 시간 계산
|
||||
/// </summary>
|
||||
/// <param name="agvSpeed">AGV 속도 (픽셀/초, 기본값: 100)</param>
|
||||
/// <param name="rotationTime">회전 시간 (초, 기본값: 3)</param>
|
||||
/// <returns>예상 소요 시간 (초)</returns>
|
||||
private float CalculateEstimatedTime(float agvSpeed = 100.0f, float rotationTime = 3.0f)
|
||||
{
|
||||
float moveTime = TotalDistance / agvSpeed;
|
||||
float totalRotationTime = RotationCount * rotationTime;
|
||||
return moveTime + totalRotationTime;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 명령어 요약 생성
|
||||
/// </summary>
|
||||
/// <returns>명령어 요약 문자열</returns>
|
||||
public string GetCommandSummary()
|
||||
{
|
||||
if (!Success) return "실패";
|
||||
|
||||
var summary = new List<string>();
|
||||
var currentCommand = AgvDirection.Stop;
|
||||
var count = 0;
|
||||
|
||||
foreach (var command in Commands)
|
||||
{
|
||||
if (command == currentCommand)
|
||||
{
|
||||
count++;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (count > 0)
|
||||
{
|
||||
summary.Add($"{GetCommandText(currentCommand)}×{count}");
|
||||
}
|
||||
currentCommand = command;
|
||||
count = 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (count > 0)
|
||||
{
|
||||
summary.Add($"{GetCommandText(currentCommand)}×{count}");
|
||||
}
|
||||
|
||||
return string.Join(" → ", summary);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 명령어 텍스트 반환
|
||||
/// </summary>
|
||||
private string GetCommandText(AgvDirection command)
|
||||
{
|
||||
switch (command)
|
||||
{
|
||||
case AgvDirection.Forward: return "전진";
|
||||
case AgvDirection.Backward: return "후진";
|
||||
case AgvDirection.Left: return "좌회전";
|
||||
case AgvDirection.Right: return "우회전";
|
||||
case AgvDirection.Stop: return "정지";
|
||||
default: return command.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 상세 경로 정보 반환
|
||||
/// </summary>
|
||||
/// <returns>상세 정보 문자열</returns>
|
||||
public string GetDetailedInfo()
|
||||
{
|
||||
if (!Success)
|
||||
{
|
||||
return $"경로 계산 실패: {ErrorMessage} (계산시간: {CalculationTimeMs}ms)";
|
||||
}
|
||||
|
||||
return $"경로: {Path.Count}개 노드, 거리: {TotalDistance:F1}px, " +
|
||||
$"회전: {RotationCount}회, 예상시간: {EstimatedTimeSeconds:F1}초, " +
|
||||
$"계산시간: {CalculationTimeMs}ms";
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 단순 경로 목록 반환 (호환성용)
|
||||
/// </summary>
|
||||
/// <returns>노드 ID 목록</returns>
|
||||
public List<string> GetSimplePath()
|
||||
{
|
||||
if (DetailedPath != null && DetailedPath.Count > 0)
|
||||
{
|
||||
return DetailedPath.Select(n => n.NodeId).ToList();
|
||||
}
|
||||
return Path ?? new List<string>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 문자열 표현
|
||||
/// </summary>
|
||||
public override string ToString()
|
||||
{
|
||||
if (Success)
|
||||
{
|
||||
return $"Success: {Path.Count} nodes, {TotalDistance:F1}px, {RotationCount} rotations, {EstimatedTimeSeconds:F1}s";
|
||||
}
|
||||
else
|
||||
{
|
||||
return $"Failed: {ErrorMessage}";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,290 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using AGVNavigationCore.Models;
|
||||
|
||||
namespace AGVNavigationCore.PathFinding.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// A* 알고리즘 기반 경로 탐색기
|
||||
/// </summary>
|
||||
public class AStarPathfinder
|
||||
{
|
||||
private Dictionary<string, PathNode> _nodeMap;
|
||||
private List<MapNode> _mapNodes;
|
||||
|
||||
/// <summary>
|
||||
/// 휴리스틱 가중치 (기본값: 1.0)
|
||||
/// 값이 클수록 목적지 방향을 우선시하나 최적 경로를 놓칠 수 있음
|
||||
/// </summary>
|
||||
public float HeuristicWeight { get; set; } = 1.0f;
|
||||
|
||||
/// <summary>
|
||||
/// 최대 탐색 노드 수 (무한 루프 방지)
|
||||
/// </summary>
|
||||
public int MaxSearchNodes { get; set; } = 1000;
|
||||
|
||||
/// <summary>
|
||||
/// 생성자
|
||||
/// </summary>
|
||||
public AStarPathfinder()
|
||||
{
|
||||
_nodeMap = new Dictionary<string, PathNode>();
|
||||
_mapNodes = new List<MapNode>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 맵 노드 설정
|
||||
/// </summary>
|
||||
/// <param name="mapNodes">맵 노드 목록</param>
|
||||
public void SetMapNodes(List<MapNode> mapNodes)
|
||||
{
|
||||
_mapNodes = mapNodes ?? new List<MapNode>();
|
||||
_nodeMap.Clear();
|
||||
|
||||
// 모든 네비게이션 노드를 PathNode로 변환하고 양방향 연결 생성
|
||||
foreach (var mapNode in _mapNodes)
|
||||
{
|
||||
if (mapNode.IsNavigationNode())
|
||||
{
|
||||
var pathNode = new PathNode(mapNode.NodeId, mapNode.Position);
|
||||
_nodeMap[mapNode.NodeId] = pathNode;
|
||||
}
|
||||
}
|
||||
|
||||
// 단일 연결을 양방향으로 확장
|
||||
foreach (var mapNode in _mapNodes)
|
||||
{
|
||||
if (mapNode.IsNavigationNode() && _nodeMap.ContainsKey(mapNode.NodeId))
|
||||
{
|
||||
var pathNode = _nodeMap[mapNode.NodeId];
|
||||
|
||||
foreach (var connectedNodeId in mapNode.ConnectedNodes)
|
||||
{
|
||||
if (_nodeMap.ContainsKey(connectedNodeId))
|
||||
{
|
||||
// 양방향 연결 생성 (단일 연결이 양방향을 의미)
|
||||
if (!pathNode.ConnectedNodes.Contains(connectedNodeId))
|
||||
{
|
||||
pathNode.ConnectedNodes.Add(connectedNodeId);
|
||||
}
|
||||
|
||||
var connectedPathNode = _nodeMap[connectedNodeId];
|
||||
if (!connectedPathNode.ConnectedNodes.Contains(mapNode.NodeId))
|
||||
{
|
||||
connectedPathNode.ConnectedNodes.Add(mapNode.NodeId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 경로 찾기 (A* 알고리즘)
|
||||
/// </summary>
|
||||
/// <param name="startNodeId">시작 노드 ID</param>
|
||||
/// <param name="endNodeId">목적지 노드 ID</param>
|
||||
/// <returns>경로 계산 결과</returns>
|
||||
public AGVPathResult FindPath(string startNodeId, string endNodeId)
|
||||
{
|
||||
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
|
||||
|
||||
try
|
||||
{
|
||||
if (!_nodeMap.ContainsKey(startNodeId))
|
||||
{
|
||||
return AGVPathResult.CreateFailure($"시작 노드를 찾을 수 없습니다: {startNodeId}", stopwatch.ElapsedMilliseconds, 0);
|
||||
}
|
||||
|
||||
if (!_nodeMap.ContainsKey(endNodeId))
|
||||
{
|
||||
return AGVPathResult.CreateFailure($"목적지 노드를 찾을 수 없습니다: {endNodeId}", stopwatch.ElapsedMilliseconds, 0);
|
||||
}
|
||||
|
||||
if (startNodeId == endNodeId)
|
||||
{
|
||||
var singlePath = new List<string> { startNodeId };
|
||||
return AGVPathResult.CreateSuccess(singlePath, new List<AgvDirection>(), 0, stopwatch.ElapsedMilliseconds);
|
||||
}
|
||||
|
||||
var startNode = _nodeMap[startNodeId];
|
||||
var endNode = _nodeMap[endNodeId];
|
||||
|
||||
var openSet = new List<PathNode>();
|
||||
var closedSet = new HashSet<string>();
|
||||
var exploredCount = 0;
|
||||
|
||||
startNode.GCost = 0;
|
||||
startNode.HCost = CalculateHeuristic(startNode, endNode);
|
||||
startNode.Parent = null;
|
||||
openSet.Add(startNode);
|
||||
|
||||
while (openSet.Count > 0 && exploredCount < MaxSearchNodes)
|
||||
{
|
||||
var currentNode = GetLowestFCostNode(openSet);
|
||||
openSet.Remove(currentNode);
|
||||
closedSet.Add(currentNode.NodeId);
|
||||
exploredCount++;
|
||||
|
||||
if (currentNode.NodeId == endNodeId)
|
||||
{
|
||||
var path = ReconstructPath(currentNode);
|
||||
var totalDistance = CalculatePathDistance(path);
|
||||
return AGVPathResult.CreateSuccess(path, new List<AgvDirection>(), totalDistance, stopwatch.ElapsedMilliseconds);
|
||||
}
|
||||
|
||||
foreach (var neighborId in currentNode.ConnectedNodes)
|
||||
{
|
||||
if (closedSet.Contains(neighborId) || !_nodeMap.ContainsKey(neighborId))
|
||||
continue;
|
||||
|
||||
var neighbor = _nodeMap[neighborId];
|
||||
var tentativeGCost = currentNode.GCost + currentNode.DistanceTo(neighbor);
|
||||
|
||||
if (!openSet.Contains(neighbor))
|
||||
{
|
||||
neighbor.Parent = currentNode;
|
||||
neighbor.GCost = tentativeGCost;
|
||||
neighbor.HCost = CalculateHeuristic(neighbor, endNode);
|
||||
openSet.Add(neighbor);
|
||||
}
|
||||
else if (tentativeGCost < neighbor.GCost)
|
||||
{
|
||||
neighbor.Parent = currentNode;
|
||||
neighbor.GCost = tentativeGCost;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return AGVPathResult.CreateFailure("경로를 찾을 수 없습니다", stopwatch.ElapsedMilliseconds, exploredCount);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return AGVPathResult.CreateFailure($"경로 계산 중 오류: {ex.Message}", stopwatch.ElapsedMilliseconds, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 여러 목적지 중 가장 가까운 노드로의 경로 찾기
|
||||
/// </summary>
|
||||
/// <param name="startNodeId">시작 노드 ID</param>
|
||||
/// <param name="targetNodeIds">목적지 후보 노드 ID 목록</param>
|
||||
/// <returns>경로 계산 결과</returns>
|
||||
public AGVPathResult FindNearestPath(string startNodeId, List<string> targetNodeIds)
|
||||
{
|
||||
if (targetNodeIds == null || targetNodeIds.Count == 0)
|
||||
{
|
||||
return AGVPathResult.CreateFailure("목적지 노드가 지정되지 않았습니다", 0, 0);
|
||||
}
|
||||
|
||||
AGVPathResult bestResult = null;
|
||||
foreach (var targetId in targetNodeIds)
|
||||
{
|
||||
var result = FindPath(startNodeId, targetId);
|
||||
if (result.Success && (bestResult == null || result.TotalDistance < bestResult.TotalDistance))
|
||||
{
|
||||
bestResult = result;
|
||||
}
|
||||
}
|
||||
|
||||
return bestResult ?? AGVPathResult.CreateFailure("모든 목적지로의 경로를 찾을 수 없습니다", 0, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 휴리스틱 거리 계산 (유클리드 거리)
|
||||
/// </summary>
|
||||
private float CalculateHeuristic(PathNode from, PathNode to)
|
||||
{
|
||||
return from.DistanceTo(to) * HeuristicWeight;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// F cost가 가장 낮은 노드 선택
|
||||
/// </summary>
|
||||
private PathNode GetLowestFCostNode(List<PathNode> nodes)
|
||||
{
|
||||
PathNode lowest = nodes[0];
|
||||
foreach (var node in nodes)
|
||||
{
|
||||
if (node.FCost < lowest.FCost ||
|
||||
(Math.Abs(node.FCost - lowest.FCost) < 0.001f && node.HCost < lowest.HCost))
|
||||
{
|
||||
lowest = node;
|
||||
}
|
||||
}
|
||||
return lowest;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 경로 재구성 (부모 노드를 따라 역추적)
|
||||
/// </summary>
|
||||
private List<string> ReconstructPath(PathNode endNode)
|
||||
{
|
||||
var path = new List<string>();
|
||||
var current = endNode;
|
||||
|
||||
while (current != null)
|
||||
{
|
||||
path.Add(current.NodeId);
|
||||
current = current.Parent;
|
||||
}
|
||||
|
||||
path.Reverse();
|
||||
return path;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 경로의 총 거리 계산
|
||||
/// </summary>
|
||||
private float CalculatePathDistance(List<string> path)
|
||||
{
|
||||
if (path.Count < 2) return 0;
|
||||
|
||||
float totalDistance = 0;
|
||||
for (int i = 0; i < path.Count - 1; i++)
|
||||
{
|
||||
if (_nodeMap.ContainsKey(path[i]) && _nodeMap.ContainsKey(path[i + 1]))
|
||||
{
|
||||
totalDistance += _nodeMap[path[i]].DistanceTo(_nodeMap[path[i + 1]]);
|
||||
}
|
||||
}
|
||||
|
||||
return totalDistance;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 두 노드가 연결되어 있는지 확인
|
||||
/// </summary>
|
||||
/// <param name="nodeId1">노드 1 ID</param>
|
||||
/// <param name="nodeId2">노드 2 ID</param>
|
||||
/// <returns>연결 여부</returns>
|
||||
public bool AreNodesConnected(string nodeId1, string nodeId2)
|
||||
{
|
||||
if (!_nodeMap.ContainsKey(nodeId1) || !_nodeMap.ContainsKey(nodeId2))
|
||||
return false;
|
||||
|
||||
return _nodeMap[nodeId1].ConnectedNodes.Contains(nodeId2);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 네비게이션 가능한 노드 목록 반환
|
||||
/// </summary>
|
||||
/// <returns>노드 ID 목록</returns>
|
||||
public List<string> GetNavigationNodes()
|
||||
{
|
||||
return _nodeMap.Keys.ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 노드 정보 반환
|
||||
/// </summary>
|
||||
/// <param name="nodeId">노드 ID</param>
|
||||
/// <returns>노드 정보 또는 null</returns>
|
||||
public PathNode GetNode(string nodeId)
|
||||
{
|
||||
return _nodeMap.ContainsKey(nodeId) ? _nodeMap[nodeId] : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
101
Cs_HMI/AGVLogic/AGVNavigationCore/PathFinding/Core/PathNode.cs
Normal file
101
Cs_HMI/AGVLogic/AGVNavigationCore/PathFinding/Core/PathNode.cs
Normal file
@@ -0,0 +1,101 @@
|
||||
using System;
|
||||
using System.Drawing;
|
||||
|
||||
namespace AGVNavigationCore.PathFinding.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// A* 알고리즘에서 사용하는 경로 노드
|
||||
/// </summary>
|
||||
public class PathNode
|
||||
{
|
||||
/// <summary>
|
||||
/// 노드 ID
|
||||
/// </summary>
|
||||
public string NodeId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 노드 위치
|
||||
/// </summary>
|
||||
public Point Position { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 시작점으로부터의 실제 거리 (G cost)
|
||||
/// </summary>
|
||||
public float GCost { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 목적지까지의 추정 거리 (H cost - 휴리스틱)
|
||||
/// </summary>
|
||||
public float HCost { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 총 비용 (F cost = G cost + H cost)
|
||||
/// </summary>
|
||||
public float FCost => GCost + HCost;
|
||||
|
||||
/// <summary>
|
||||
/// 부모 노드 (경로 추적용)
|
||||
/// </summary>
|
||||
public PathNode Parent { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 연결된 노드 ID 목록
|
||||
/// </summary>
|
||||
public System.Collections.Generic.List<string> ConnectedNodes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 생성자
|
||||
/// </summary>
|
||||
/// <param name="nodeId">노드 ID</param>
|
||||
/// <param name="position">위치</param>
|
||||
public PathNode(string nodeId, Point position)
|
||||
{
|
||||
NodeId = nodeId;
|
||||
Position = position;
|
||||
GCost = 0;
|
||||
HCost = 0;
|
||||
Parent = null;
|
||||
ConnectedNodes = new System.Collections.Generic.List<string>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 다른 노드까지의 유클리드 거리 계산
|
||||
/// </summary>
|
||||
/// <param name="other">대상 노드</param>
|
||||
/// <returns>거리</returns>
|
||||
public float DistanceTo(PathNode other)
|
||||
{
|
||||
float dx = Position.X - other.Position.X;
|
||||
float dy = Position.Y - other.Position.Y;
|
||||
return (float)Math.Sqrt(dx * dx + dy * dy);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 문자열 표현
|
||||
/// </summary>
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{NodeId} - F:{FCost:F1} G:{GCost:F1} H:{HCost:F1}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 같음 비교 (NodeId 기준)
|
||||
/// </summary>
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
if (obj is PathNode other)
|
||||
{
|
||||
return NodeId == other.NodeId;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 해시코드 (NodeId 기준)
|
||||
/// </summary>
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return NodeId?.GetHashCode() ?? 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,377 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using AGVNavigationCore.Models;
|
||||
using AGVNavigationCore.Utils;
|
||||
using AGVNavigationCore.PathFinding.Core;
|
||||
using AGVNavigationCore.PathFinding.Analysis;
|
||||
|
||||
namespace AGVNavigationCore.PathFinding.Planning
|
||||
{
|
||||
/// <summary>
|
||||
/// AGV 경로 계획기
|
||||
/// 물리적 제약사항과 마그넷 센서를 고려한 실제 AGV 경로 생성
|
||||
/// </summary>
|
||||
public class AGVPathfinder
|
||||
{
|
||||
|
||||
private readonly List<MapNode> _mapNodes;
|
||||
private readonly AStarPathfinder _basicPathfinder;
|
||||
private readonly JunctionAnalyzer _junctionAnalyzer;
|
||||
private readonly DirectionChangePlanner _directionChangePlanner;
|
||||
|
||||
public AGVPathfinder(List<MapNode> mapNodes)
|
||||
{
|
||||
_mapNodes = mapNodes ?? new List<MapNode>();
|
||||
_basicPathfinder = new AStarPathfinder();
|
||||
_basicPathfinder.SetMapNodes(_mapNodes);
|
||||
_junctionAnalyzer = new JunctionAnalyzer(_mapNodes);
|
||||
_directionChangePlanner = new DirectionChangePlanner(_mapNodes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// AGV 경로 계산
|
||||
/// </summary>
|
||||
public AGVPathResult FindPath(MapNode startNode, MapNode targetNode,
|
||||
MapNode prevNode, AgvDirection currentDirection = AgvDirection.Forward)
|
||||
{
|
||||
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
|
||||
|
||||
try
|
||||
{
|
||||
// 입력 검증
|
||||
if (startNode == null)
|
||||
return AGVPathResult.CreateFailure("시작 노드가 null입니다.", 0, 0);
|
||||
if (targetNode == null)
|
||||
return AGVPathResult.CreateFailure("목적지 노드가 null입니다.", 0, 0);
|
||||
if (prevNode == null)
|
||||
return AGVPathResult.CreateFailure("이전위치 노드가 null입니다.", 0, 0);
|
||||
|
||||
// 1. 목적지 도킹 방향 요구사항 확인 (노드의 도킹방향 속성에서 확인)
|
||||
var requiredDirection = GetRequiredDockingDirection(targetNode.DockDirection);
|
||||
|
||||
|
||||
// 통합된 경로 계획 함수 사용
|
||||
AGVPathResult result = PlanPath(startNode, targetNode, prevNode, requiredDirection, currentDirection);
|
||||
|
||||
result.CalculationTimeMs = stopwatch.ElapsedMilliseconds;
|
||||
|
||||
// 도킹 검증 수행
|
||||
if (result.Success && _mapNodes != null)
|
||||
{
|
||||
result.DockingValidation = DockingValidator.ValidateDockingDirection(result, _mapNodes, currentDirection);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return AGVPathResult.CreateFailure($"경로 계산 중 오류: {ex.Message}", stopwatch.ElapsedMilliseconds, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 노드 도킹 방향에 따른 필요한 AGV 방향 반환
|
||||
/// </summary>
|
||||
private AgvDirection? GetRequiredDockingDirection(DockingDirection dockDirection)
|
||||
{
|
||||
switch (dockDirection)
|
||||
{
|
||||
case DockingDirection.Forward:
|
||||
return AgvDirection.Forward; // 전진 도킹
|
||||
case DockingDirection.Backward:
|
||||
return AgvDirection.Backward; // 후진 도킹
|
||||
case DockingDirection.DontCare:
|
||||
default:
|
||||
return null; // 도킹 방향 상관없음
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 통합 경로 계획 (직접 경로 또는 방향 전환 경로)
|
||||
/// </summary>
|
||||
private AGVPathResult PlanPath(MapNode startNode, MapNode targetNode, MapNode prevNode, AgvDirection? requiredDirection = null, AgvDirection currentDirection = AgvDirection.Forward)
|
||||
{
|
||||
|
||||
bool needDirectionChange = requiredDirection.HasValue && (currentDirection != requiredDirection.Value);
|
||||
|
||||
//현재 위치에서 목적지까지의 최단 거리 모록을 찾는다.
|
||||
var DirectPathResult = _basicPathfinder.FindPath(startNode.NodeId, targetNode.NodeId);
|
||||
|
||||
//이전 위치에서 목적지까지의 최단 거리를 모록을 찾는다.
|
||||
var DirectPathResultP = _basicPathfinder.FindPath(prevNode.NodeId, targetNode.NodeId);
|
||||
|
||||
//
|
||||
if (DirectPathResultP.Path.Contains(startNode.NodeId))
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (needDirectionChange)
|
||||
{
|
||||
// 방향 전환 경로 계획
|
||||
var directionChangePlan = _directionChangePlanner.PlanDirectionChange(
|
||||
startNode.NodeId, targetNode.NodeId, currentDirection, requiredDirection.Value);
|
||||
|
||||
if (!directionChangePlan.Success)
|
||||
{
|
||||
return AGVPathResult.CreateFailure(directionChangePlan.ErrorMessage, 0, 0);
|
||||
}
|
||||
|
||||
var detailedPath = ConvertDirectionChangePath(directionChangePlan, currentDirection, requiredDirection.Value);
|
||||
float totalDistance = CalculatePathDistance(detailedPath);
|
||||
|
||||
return AGVPathResult.CreateSuccess(
|
||||
detailedPath,
|
||||
totalDistance,
|
||||
0,
|
||||
0,
|
||||
directionChangePlan.PlanDescription,
|
||||
true,
|
||||
directionChangePlan.DirectionChangeNode
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 직접 경로 계획
|
||||
var basicResult = _basicPathfinder.FindPath(startNode.NodeId, targetNode.NodeId);
|
||||
|
||||
if (!basicResult.Success)
|
||||
{
|
||||
return AGVPathResult.CreateFailure(basicResult.ErrorMessage, basicResult.CalculationTimeMs, basicResult.ExploredNodeCount);
|
||||
}
|
||||
|
||||
var detailedPath = ConvertToDetailedPath(basicResult.Path, currentDirection);
|
||||
|
||||
return AGVPathResult.CreateSuccess(
|
||||
detailedPath,
|
||||
basicResult.TotalDistance,
|
||||
basicResult.CalculationTimeMs,
|
||||
basicResult.ExploredNodeCount,
|
||||
"직접 경로 - 방향 전환 불필요"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 기본 경로를 상세 경로로 변환
|
||||
/// </summary>
|
||||
private List<NodeMotorInfo> ConvertToDetailedPath(List<string> simplePath, AgvDirection initialDirection)
|
||||
{
|
||||
var detailedPath = new List<NodeMotorInfo>();
|
||||
var currentDirection = initialDirection;
|
||||
|
||||
for (int i = 0; i < simplePath.Count; i++)
|
||||
{
|
||||
string currentNodeId = simplePath[i];
|
||||
string nextNodeId = (i + 1 < simplePath.Count) ? simplePath[i + 1] : null;
|
||||
|
||||
// 마그넷 방향 계산
|
||||
MagnetDirection magnetDirection = MagnetDirection.Straight;
|
||||
if (i > 0 && nextNodeId != null)
|
||||
{
|
||||
string prevNodeId = simplePath[i - 1];
|
||||
magnetDirection = _junctionAnalyzer.GetRequiredMagnetDirection(prevNodeId, currentNodeId, nextNodeId);
|
||||
}
|
||||
|
||||
// 노드 정보 생성
|
||||
var nodeMotorInfo = new NodeMotorInfo(
|
||||
currentNodeId,
|
||||
currentDirection,
|
||||
nextNodeId,
|
||||
magnetDirection
|
||||
);
|
||||
|
||||
// 회전 가능 노드 설정
|
||||
var mapNode = _mapNodes.FirstOrDefault(n => n.NodeId == currentNodeId);
|
||||
if (mapNode != null)
|
||||
{
|
||||
nodeMotorInfo.CanRotate = mapNode.CanRotate;
|
||||
}
|
||||
|
||||
detailedPath.Add(nodeMotorInfo);
|
||||
}
|
||||
|
||||
return detailedPath;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 방향 전환 경로를 상세 경로로 변환
|
||||
/// </summary>
|
||||
private List<NodeMotorInfo> ConvertDirectionChangePath(DirectionChangePlanner.DirectionChangePlan plan, AgvDirection startDirection, AgvDirection endDirection)
|
||||
{
|
||||
var detailedPath = new List<NodeMotorInfo>();
|
||||
var currentDirection = startDirection;
|
||||
|
||||
for (int i = 0; i < plan.DirectionChangePath.Count; i++)
|
||||
{
|
||||
string currentNodeId = plan.DirectionChangePath[i];
|
||||
string nextNodeId = (i + 1 < plan.DirectionChangePath.Count) ? plan.DirectionChangePath[i + 1] : null;
|
||||
|
||||
// 방향 전환 노드에서 방향 변경
|
||||
if (currentNodeId == plan.DirectionChangeNode && currentDirection != endDirection)
|
||||
{
|
||||
currentDirection = endDirection;
|
||||
}
|
||||
|
||||
// 마그넷 방향 계산
|
||||
MagnetDirection magnetDirection = MagnetDirection.Straight;
|
||||
if (i > 0 && nextNodeId != null)
|
||||
{
|
||||
string prevNodeId = plan.DirectionChangePath[i - 1];
|
||||
magnetDirection = _junctionAnalyzer.GetRequiredMagnetDirection(prevNodeId, currentNodeId, nextNodeId);
|
||||
}
|
||||
|
||||
// 특수 동작 확인
|
||||
bool requiresSpecialAction = false;
|
||||
string specialActionDescription = "";
|
||||
|
||||
if (currentNodeId == plan.DirectionChangeNode)
|
||||
{
|
||||
requiresSpecialAction = true;
|
||||
specialActionDescription = $"방향전환: {startDirection} → {endDirection}";
|
||||
}
|
||||
|
||||
// 노드 정보 생성
|
||||
var nodeMotorInfo = new NodeMotorInfo(
|
||||
currentNodeId,
|
||||
currentDirection,
|
||||
nextNodeId,
|
||||
true, // 방향 전환 경로의 경우 회전 가능으로 설정
|
||||
currentNodeId == plan.DirectionChangeNode,
|
||||
magnetDirection,
|
||||
requiresSpecialAction,
|
||||
specialActionDescription
|
||||
);
|
||||
|
||||
detailedPath.Add(nodeMotorInfo);
|
||||
}
|
||||
|
||||
return detailedPath;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 경로 총 거리 계산
|
||||
/// </summary>
|
||||
private float CalculatePathDistance(List<NodeMotorInfo> detailedPath)
|
||||
{
|
||||
float totalDistance = 0;
|
||||
|
||||
for (int i = 0; i < detailedPath.Count - 1; i++)
|
||||
{
|
||||
var currentNode = _mapNodes.FirstOrDefault(n => n.NodeId == detailedPath[i].NodeId);
|
||||
var nextNode = _mapNodes.FirstOrDefault(n => n.NodeId == detailedPath[i + 1].NodeId);
|
||||
|
||||
if (currentNode != null && nextNode != null)
|
||||
{
|
||||
float dx = nextNode.Position.X - currentNode.Position.X;
|
||||
float dy = nextNode.Position.Y - currentNode.Position.Y;
|
||||
totalDistance += (float)Math.Sqrt(dx * dx + dy * dy);
|
||||
}
|
||||
}
|
||||
|
||||
return totalDistance;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 경로 유효성 검증
|
||||
/// </summary>
|
||||
public bool ValidatePath(List<NodeMotorInfo> detailedPath)
|
||||
{
|
||||
if (detailedPath == null || detailedPath.Count == 0)
|
||||
return false;
|
||||
|
||||
// 1. 모든 노드가 존재하는지 확인
|
||||
foreach (var nodeInfo in detailedPath)
|
||||
{
|
||||
if (!_mapNodes.Any(n => n.NodeId == nodeInfo.NodeId))
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2. 연결성 확인
|
||||
for (int i = 0; i < detailedPath.Count - 1; i++)
|
||||
{
|
||||
string currentId = detailedPath[i].NodeId;
|
||||
string nextId = detailedPath[i + 1].NodeId;
|
||||
|
||||
if (!_basicPathfinder.AreNodesConnected(currentId, nextId))
|
||||
return false;
|
||||
}
|
||||
|
||||
// 3. 물리적 제약사항 확인
|
||||
return ValidatePhysicalConstraints(detailedPath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 물리적 제약사항 검증
|
||||
/// </summary>
|
||||
private bool ValidatePhysicalConstraints(List<NodeMotorInfo> detailedPath)
|
||||
{
|
||||
for (int i = 1; i < detailedPath.Count; i++)
|
||||
{
|
||||
var prevNode = detailedPath[i - 1];
|
||||
var currentNode = detailedPath[i];
|
||||
|
||||
// 급작스러운 방향 전환 검증
|
||||
if (prevNode.MotorDirection != currentNode.MotorDirection)
|
||||
{
|
||||
// 방향 전환은 반드시 회전 가능 노드에서만
|
||||
if (!currentNode.CanRotate && !currentNode.IsDirectionChangePoint)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 경로 최적화
|
||||
/// </summary>
|
||||
public AGVPathResult OptimizePath(AGVPathResult originalResult)
|
||||
{
|
||||
if (!originalResult.Success)
|
||||
return originalResult;
|
||||
|
||||
// TODO: 경로 최적화 로직 구현
|
||||
// - 불필요한 중간 노드 제거
|
||||
// - 마그넷 방향 최적화
|
||||
// - 방향 전환 최소화
|
||||
|
||||
return originalResult;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 디버깅용 경로 정보
|
||||
/// </summary>
|
||||
public string GetPathSummary(AGVPathResult result)
|
||||
{
|
||||
if (!result.Success)
|
||||
return $"경로 계산 실패: {result.ErrorMessage}";
|
||||
|
||||
var summary = new List<string>
|
||||
{
|
||||
$"=== AGV 고급 경로 계획 결과 ===",
|
||||
$"총 노드 수: {result.DetailedPath.Count}",
|
||||
$"총 거리: {result.TotalDistance:F1}px",
|
||||
$"계산 시간: {result.CalculationTimeMs}ms",
|
||||
$"방향 전환: {(result.RequiredDirectionChange ? $"필요 (노드: {result.DirectionChangeNode})" : "불필요")}",
|
||||
$"설명: {result.PlanDescription}",
|
||||
"",
|
||||
"=== 상세 경로 ===",
|
||||
};
|
||||
|
||||
foreach (var nodeInfo in result.DetailedPath)
|
||||
{
|
||||
summary.Add(nodeInfo.ToString());
|
||||
}
|
||||
|
||||
return string.Join("\n", summary);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,768 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using AGVNavigationCore.Models;
|
||||
using AGVNavigationCore.PathFinding.Core;
|
||||
using AGVNavigationCore.PathFinding.Analysis;
|
||||
using AGVNavigationCore.PathFinding.Validation;
|
||||
|
||||
namespace AGVNavigationCore.PathFinding.Planning
|
||||
{
|
||||
/// <summary>
|
||||
/// AGV 방향 전환 경로 계획 시스템
|
||||
/// 물리적 제약사항을 고려한 방향 전환 경로 생성
|
||||
/// </summary>
|
||||
public class DirectionChangePlanner
|
||||
{
|
||||
/// <summary>
|
||||
/// 방향 전환 계획 결과
|
||||
/// </summary>
|
||||
public class DirectionChangePlan
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public List<string> DirectionChangePath { get; set; }
|
||||
public string DirectionChangeNode { get; set; }
|
||||
public string ErrorMessage { get; set; }
|
||||
public string PlanDescription { get; set; }
|
||||
|
||||
public DirectionChangePlan()
|
||||
{
|
||||
DirectionChangePath = new List<string>();
|
||||
ErrorMessage = string.Empty;
|
||||
PlanDescription = string.Empty;
|
||||
}
|
||||
|
||||
public static DirectionChangePlan CreateSuccess(List<string> path, string changeNode, string description)
|
||||
{
|
||||
return new DirectionChangePlan
|
||||
{
|
||||
Success = true,
|
||||
DirectionChangePath = path,
|
||||
DirectionChangeNode = changeNode,
|
||||
PlanDescription = description
|
||||
};
|
||||
}
|
||||
|
||||
public static DirectionChangePlan CreateFailure(string error)
|
||||
{
|
||||
return new DirectionChangePlan
|
||||
{
|
||||
Success = false,
|
||||
ErrorMessage = error
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private readonly List<MapNode> _mapNodes;
|
||||
private readonly JunctionAnalyzer _junctionAnalyzer;
|
||||
private readonly AStarPathfinder _pathfinder;
|
||||
|
||||
public DirectionChangePlanner(List<MapNode> mapNodes)
|
||||
{
|
||||
_mapNodes = mapNodes ?? new List<MapNode>();
|
||||
_junctionAnalyzer = new JunctionAnalyzer(_mapNodes);
|
||||
_pathfinder = new AStarPathfinder();
|
||||
_pathfinder.SetMapNodes(_mapNodes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 방향 전환이 필요한 경로 계획
|
||||
/// </summary>
|
||||
public DirectionChangePlan PlanDirectionChange(string startNodeId, string targetNodeId, AgvDirection currentDirection, AgvDirection requiredDirection)
|
||||
{
|
||||
// 방향이 같으면 직접 경로 계산
|
||||
if (currentDirection == requiredDirection)
|
||||
{
|
||||
var directPath = _pathfinder.FindPath(startNodeId, targetNodeId);
|
||||
if (directPath.Success)
|
||||
{
|
||||
return DirectionChangePlan.CreateSuccess(
|
||||
directPath.Path,
|
||||
null,
|
||||
"방향 전환 불필요 - 직접 경로 사용"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 방향 전환이 필요한 경우 - 먼저 간단한 직접 경로 확인
|
||||
var directPath2 = _pathfinder.FindPath(startNodeId, targetNodeId);
|
||||
if (directPath2.Success)
|
||||
{
|
||||
// 직접 경로에 갈림길이 포함된 경우 그 갈림길에서 방향 전환
|
||||
foreach (var nodeId in directPath2.Path.Skip(1).Take(directPath2.Path.Count - 2)) // 시작과 끝 제외
|
||||
{
|
||||
var junctionInfo = _junctionAnalyzer.GetJunctionInfo(nodeId);
|
||||
if (junctionInfo != null && junctionInfo.IsJunction)
|
||||
{
|
||||
// 간단한 방향 전환: 직접 경로 사용하되 방향 전환 노드 표시
|
||||
return DirectionChangePlan.CreateSuccess(
|
||||
directPath2.Path,
|
||||
nodeId,
|
||||
$"갈림길 {nodeId}에서 방향 전환: {currentDirection} → {requiredDirection}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 복잡한 방향 전환이 필요한 경우
|
||||
return PlanDirectionChangeRoute(startNodeId, targetNodeId, currentDirection, requiredDirection);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 방향 전환 경로 계획
|
||||
/// </summary>
|
||||
private DirectionChangePlan PlanDirectionChangeRoute(string startNodeId, string targetNodeId, AgvDirection currentDirection, AgvDirection requiredDirection)
|
||||
{
|
||||
// 1. 방향 전환 가능한 갈림길 찾기
|
||||
var changeJunctions = FindSuitableChangeJunctions(startNodeId, targetNodeId, currentDirection, requiredDirection);
|
||||
|
||||
if (changeJunctions.Count == 0)
|
||||
{
|
||||
return DirectionChangePlan.CreateFailure("방향 전환 가능한 갈림길을 찾을 수 없습니다.");
|
||||
}
|
||||
|
||||
// 2. 각 갈림길에 대해 경로 계획 시도
|
||||
foreach (var junction in changeJunctions)
|
||||
{
|
||||
var plan = TryDirectionChangeAtJunction(startNodeId, targetNodeId, junction, currentDirection, requiredDirection);
|
||||
if (plan.Success)
|
||||
{
|
||||
return plan;
|
||||
}
|
||||
}
|
||||
|
||||
return DirectionChangePlan.CreateFailure("모든 갈림길에서 방향 전환 경로 계획이 실패했습니다.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 방향 전환에 적합한 갈림길 검색 (인근 우회 경로 우선)
|
||||
/// </summary>
|
||||
private List<string> FindSuitableChangeJunctions(string startNodeId, string targetNodeId, AgvDirection currentDirection, AgvDirection requiredDirection)
|
||||
{
|
||||
var suitableJunctions = new List<string>();
|
||||
|
||||
// 1. 시작점 인근의 갈림길들을 우선 검색 (경로 진행 중 우회용)
|
||||
var nearbyJunctions = FindNearbyJunctions(startNodeId, 2); // 2단계 내의 갈림길
|
||||
foreach (var junction in nearbyJunctions)
|
||||
{
|
||||
if (junction == startNodeId) continue; // 시작점 제외
|
||||
|
||||
var junctionInfo = _junctionAnalyzer.GetJunctionInfo(junction);
|
||||
if (junctionInfo != null && junctionInfo.IsJunction)
|
||||
{
|
||||
// 이 갈림길을 통해 목적지로 갈 수 있는지 확인
|
||||
if (CanReachTargetViaJunction(junction, targetNodeId) &&
|
||||
HasSuitableDetourOptions(junction, startNodeId))
|
||||
{
|
||||
suitableJunctions.Add(junction);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 직진 경로상의 갈림길들도 검색 (단, 되돌아가기 방지)
|
||||
var directPath = _pathfinder.FindPath(startNodeId, targetNodeId);
|
||||
if (directPath.Success)
|
||||
{
|
||||
foreach (var nodeId in directPath.Path.Skip(2)) // 시작점과 다음 노드는 제외
|
||||
{
|
||||
var junctionInfo = _junctionAnalyzer.GetJunctionInfo(nodeId);
|
||||
if (junctionInfo != null && junctionInfo.IsJunction)
|
||||
{
|
||||
// 직진 경로상에서는 더 엄격한 조건 적용
|
||||
if (!suitableJunctions.Contains(nodeId) &&
|
||||
HasMultipleExitOptions(nodeId))
|
||||
{
|
||||
suitableJunctions.Add(nodeId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 거리순으로 정렬 (가까운 갈림길 우선 - 인근 우회용)
|
||||
return SortJunctionsByDistance(startNodeId, suitableJunctions);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 특정 노드 주변의 갈림길 검색
|
||||
/// </summary>
|
||||
private List<string> FindNearbyJunctions(string nodeId, int maxSteps)
|
||||
{
|
||||
var junctions = new List<string>();
|
||||
var visited = new HashSet<string>();
|
||||
var queue = new Queue<(string NodeId, int Steps)>();
|
||||
|
||||
queue.Enqueue((nodeId, 0));
|
||||
visited.Add(nodeId);
|
||||
|
||||
while (queue.Count > 0)
|
||||
{
|
||||
var (currentNodeId, steps) = queue.Dequeue();
|
||||
|
||||
if (steps > maxSteps) continue;
|
||||
|
||||
var junctionInfo = _junctionAnalyzer.GetJunctionInfo(currentNodeId);
|
||||
if (junctionInfo != null && junctionInfo.IsJunction && currentNodeId != nodeId)
|
||||
{
|
||||
junctions.Add(currentNodeId);
|
||||
}
|
||||
|
||||
// 연결된 노드들을 큐에 추가
|
||||
var connectedNodes = GetAllConnectedNodes(currentNodeId);
|
||||
foreach (var connectedId in connectedNodes)
|
||||
{
|
||||
if (!visited.Contains(connectedId))
|
||||
{
|
||||
visited.Add(connectedId);
|
||||
queue.Enqueue((connectedId, steps + 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return junctions;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 양방향 연결을 고려한 연결 노드 검색
|
||||
/// </summary>
|
||||
private List<string> GetAllConnectedNodes(string nodeId)
|
||||
{
|
||||
var node = _mapNodes.FirstOrDefault(n => n.NodeId == nodeId);
|
||||
if (node == null) return new List<string>();
|
||||
|
||||
var connected = new HashSet<string>();
|
||||
|
||||
// 직접 연결
|
||||
foreach (var connectedId in node.ConnectedNodes)
|
||||
{
|
||||
connected.Add(connectedId);
|
||||
}
|
||||
|
||||
// 역방향 연결
|
||||
foreach (var otherNode in _mapNodes)
|
||||
{
|
||||
if (otherNode.NodeId != nodeId && otherNode.ConnectedNodes.Contains(nodeId))
|
||||
{
|
||||
connected.Add(otherNode.NodeId);
|
||||
}
|
||||
}
|
||||
|
||||
return connected.ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 갈림길을 거리순으로 정렬
|
||||
/// </summary>
|
||||
private List<string> SortJunctionsByDistance(string startNodeId, List<string> junctions)
|
||||
{
|
||||
var distances = new List<(string NodeId, double Distance)>();
|
||||
|
||||
foreach (var junction in junctions)
|
||||
{
|
||||
var path = _pathfinder.FindPath(startNodeId, junction);
|
||||
double distance = path.Success ? path.TotalDistance : double.MaxValue;
|
||||
distances.Add((junction, distance));
|
||||
}
|
||||
|
||||
return distances.OrderBy(d => d.Distance).Select(d => d.NodeId).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 특정 갈림길에서 방향 전환 시도
|
||||
/// </summary>
|
||||
private DirectionChangePlan TryDirectionChangeAtJunction(string startNodeId, string targetNodeId, string junctionNodeId, AgvDirection currentDirection, AgvDirection requiredDirection)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 방향 전환 경로 생성
|
||||
var changePath = GenerateDirectionChangePath(startNodeId, targetNodeId, junctionNodeId, currentDirection, requiredDirection);
|
||||
|
||||
if (changePath.Count > 0)
|
||||
{
|
||||
// **VALIDATION**: 되돌아가기 패턴 검증
|
||||
var validationResult = ValidateDirectionChangePath(changePath, startNodeId, junctionNodeId);
|
||||
if (!validationResult.IsValid)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"[DirectionChangePlanner] ❌ 갈림길 {junctionNodeId} 경로 검증 실패: {validationResult.ValidationError}");
|
||||
return DirectionChangePlan.CreateFailure($"갈림길 {junctionNodeId} 검증 실패: {validationResult.ValidationError}");
|
||||
}
|
||||
|
||||
// 실제 방향 전환 노드 찾기 (우회 노드)
|
||||
string actualDirectionChangeNode = FindActualDirectionChangeNode(changePath, junctionNodeId);
|
||||
|
||||
string description = $"갈림길 {GetDisplayName(junctionNodeId)}를 통해 {GetDisplayName(actualDirectionChangeNode)}에서 방향 전환: {currentDirection} → {requiredDirection}";
|
||||
System.Diagnostics.Debug.WriteLine($"[DirectionChangePlanner] ✅ 유효한 방향전환 경로: {string.Join(" → ", changePath)}");
|
||||
return DirectionChangePlan.CreateSuccess(changePath, actualDirectionChangeNode, description);
|
||||
}
|
||||
|
||||
return DirectionChangePlan.CreateFailure($"갈림길 {junctionNodeId}에서 방향 전환 경로 생성 실패");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return DirectionChangePlan.CreateFailure($"갈림길 {junctionNodeId}에서 오류: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 방향 전환 경로 생성 (인근 갈림길 우회 방식)
|
||||
/// </summary>
|
||||
private List<string> GenerateDirectionChangePath(string startNodeId, string targetNodeId, string junctionNodeId, AgvDirection currentDirection, AgvDirection requiredDirection)
|
||||
{
|
||||
var fullPath = new List<string>();
|
||||
|
||||
// 1. 시작점에서 갈림길까지의 경로
|
||||
var toJunctionPath = _pathfinder.FindPath(startNodeId, junctionNodeId);
|
||||
if (!toJunctionPath.Success)
|
||||
return fullPath;
|
||||
|
||||
// 2. 인근 갈림길을 통한 우회인지, 직진 경로상 갈림길인지 판단
|
||||
var directPath = _pathfinder.FindPath(startNodeId, targetNodeId);
|
||||
bool isNearbyDetour = !directPath.Success || !directPath.Path.Contains(junctionNodeId);
|
||||
|
||||
if (isNearbyDetour)
|
||||
{
|
||||
// 인근 갈림길 우회: 직진하다가 마그넷으로 방향 전환
|
||||
return GenerateNearbyDetourPath(startNodeId, targetNodeId, junctionNodeId, currentDirection, requiredDirection);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 직진 경로상 갈림길: 기존 방식으로 처리 (단, 되돌아가기 방지)
|
||||
return GenerateDirectPathChangeRoute(startNodeId, targetNodeId, junctionNodeId, currentDirection, requiredDirection);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 인근 갈림길을 통한 우회 경로 생성 (예: 012 → 013 → 마그넷으로 016 방향)
|
||||
/// </summary>
|
||||
private List<string> GenerateNearbyDetourPath(string startNodeId, string targetNodeId, string junctionNodeId, AgvDirection currentDirection, AgvDirection requiredDirection)
|
||||
{
|
||||
var fullPath = new List<string>();
|
||||
|
||||
// 1. 시작점에서 갈림길까지 직진 (현재 방향 유지)
|
||||
var toJunctionPath = _pathfinder.FindPath(startNodeId, junctionNodeId);
|
||||
if (!toJunctionPath.Success)
|
||||
return fullPath;
|
||||
|
||||
fullPath.AddRange(toJunctionPath.Path);
|
||||
|
||||
// 2. 갈림길에서 방향 전환 후 목적지로
|
||||
// 이때 마그넷 센서를 이용해 목적지 방향으로 진입
|
||||
var fromJunctionPath = _pathfinder.FindPath(junctionNodeId, targetNodeId);
|
||||
if (fromJunctionPath.Success && fromJunctionPath.Path.Count > 1)
|
||||
{
|
||||
fullPath.AddRange(fromJunctionPath.Path.Skip(1)); // 중복 노드 제거
|
||||
}
|
||||
|
||||
return fullPath;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 직진 경로상 갈림길에서 방향 전환 경로 생성 (기존 방식 개선)
|
||||
/// </summary>
|
||||
private List<string> GenerateDirectPathChangeRoute(string startNodeId, string targetNodeId, string junctionNodeId, AgvDirection currentDirection, AgvDirection requiredDirection)
|
||||
{
|
||||
var fullPath = new List<string>();
|
||||
|
||||
// 1. 시작점에서 갈림길까지의 경로
|
||||
var toJunctionPath = _pathfinder.FindPath(startNodeId, junctionNodeId);
|
||||
if (!toJunctionPath.Success)
|
||||
return fullPath;
|
||||
|
||||
fullPath.AddRange(toJunctionPath.Path);
|
||||
|
||||
// 2. 갈림길에서 방향 전환 처리 (되돌아가기 방지)
|
||||
if (currentDirection != requiredDirection)
|
||||
{
|
||||
string fromNodeId = toJunctionPath.Path.Count >= 2 ?
|
||||
toJunctionPath.Path[toJunctionPath.Path.Count - 2] : startNodeId;
|
||||
|
||||
var changeSequence = GenerateDirectionChangeSequence(junctionNodeId, fromNodeId, currentDirection, requiredDirection);
|
||||
if (changeSequence.Count > 1)
|
||||
{
|
||||
fullPath.AddRange(changeSequence.Skip(1));
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 갈림길에서 목표점까지의 경로
|
||||
string lastNode = fullPath.LastOrDefault() ?? junctionNodeId;
|
||||
var fromJunctionPath = _pathfinder.FindPath(lastNode, targetNodeId);
|
||||
if (fromJunctionPath.Success && fromJunctionPath.Path.Count > 1)
|
||||
{
|
||||
fullPath.AddRange(fromJunctionPath.Path.Skip(1));
|
||||
}
|
||||
|
||||
return fullPath;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 갈림길에서 방향 전환 시퀀스 생성
|
||||
/// 물리적으로 실현 가능한 방향 전환 경로 생성
|
||||
/// </summary>
|
||||
private List<string> GenerateDirectionChangeSequence(string junctionNodeId, string fromNodeId, AgvDirection currentDirection, AgvDirection requiredDirection)
|
||||
{
|
||||
var sequence = new List<string> { junctionNodeId };
|
||||
|
||||
// 방향이 같으면 변경 불필요
|
||||
if (currentDirection == requiredDirection)
|
||||
return sequence;
|
||||
|
||||
var junctionInfo = _junctionAnalyzer.GetJunctionInfo(junctionNodeId);
|
||||
if (junctionInfo == null || !junctionInfo.IsJunction)
|
||||
return sequence;
|
||||
|
||||
// 물리적으로 실현 가능한 방향 전환 시퀀스 생성
|
||||
// 핵심 원리: AGV는 RFID 태그를 읽자마자 바로 방향전환하면 안됨
|
||||
// 왔던 길로 되돌아가지 않도록 다른 노드로 우회한 후 방향전환
|
||||
var connectedNodes = junctionInfo.ConnectedNodes;
|
||||
|
||||
// 왔던 노드(fromNodeId)를 제외한 연결 노드들만 후보로 선택
|
||||
// 이렇게 해야 AGV가 되돌아가는 것을 방지할 수 있음
|
||||
var availableNodes = connectedNodes.Where(nodeId => nodeId != fromNodeId).ToList();
|
||||
|
||||
if (availableNodes.Count > 0)
|
||||
{
|
||||
// 방향 전환을 위한 우회 경로 생성
|
||||
// 예시: 003→004(전진) 상태에서 후진 필요한 경우
|
||||
// 잘못된 방법: 004→003 (왔던 길로 되돌아감)
|
||||
// 올바른 방법: 004→005→004 (005로 우회하여 방향전환)
|
||||
|
||||
// 가장 적합한 우회 노드 선택 (직진 방향 우선, 각도 변화 최소)
|
||||
string detourNode = FindBestDetourNode(junctionNodeId, availableNodes, fromNodeId);
|
||||
if (!string.IsNullOrEmpty(detourNode))
|
||||
{
|
||||
// 1단계: 갈림길에서 우회 노드로 이동 (현재 방향 유지)
|
||||
// AGV는 계속 전진하여 한 태그 더 지나감
|
||||
sequence.Add(detourNode);
|
||||
|
||||
// 2단계: 우회 노드에서 갈림길로 다시 돌아옴 (요구 방향으로 변경)
|
||||
// 이때 AGV는 안전한 위치에서 방향을 전환할 수 있음
|
||||
sequence.Add(junctionNodeId);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 사용 가능한 우회 노드가 없는 경우 (2갈래 길목)
|
||||
// 이 경우 물리적으로 방향 전환이 불가능할 수 있음
|
||||
// 별도의 처리 로직이 필요할 수 있음
|
||||
return sequence;
|
||||
}
|
||||
|
||||
return sequence;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 방향 전환을 위한 최적의 우회 노드 선택
|
||||
/// AGV의 물리적 특성을 고려한 각도 기반 선택
|
||||
/// </summary>
|
||||
private string FindBestDetourNode(string junctionNodeId, List<string> availableNodes, string excludeNodeId)
|
||||
{
|
||||
// 왔던 길(excludeNodeId)를 제외한 노드 중에서 최적의 우회 노드 선택
|
||||
// 우선순위: 1) 막다른 길이 아닌 노드 (우회 후 복귀 가능) 2) 직진방향 3) 목적지 방향
|
||||
|
||||
var junctionNode = _mapNodes.FirstOrDefault(n => n.NodeId == junctionNodeId);
|
||||
var fromNode = _mapNodes.FirstOrDefault(n => n.NodeId == excludeNodeId);
|
||||
|
||||
if (junctionNode == null || fromNode == null)
|
||||
return availableNodes.FirstOrDefault();
|
||||
|
||||
string bestNode = null;
|
||||
double minAngleChange = double.MaxValue;
|
||||
bool foundNonDeadEnd = false;
|
||||
|
||||
// AGV가 들어온 방향 벡터 계산 (fromNode → junctionNode)
|
||||
double incomingAngle = CalculateAngle(fromNode.Position, junctionNode.Position);
|
||||
|
||||
foreach (var nodeId in availableNodes)
|
||||
{
|
||||
if (nodeId == excludeNodeId) continue; // 왔던 길 제외
|
||||
|
||||
var candidateNode = _mapNodes.FirstOrDefault(n => n.NodeId == nodeId);
|
||||
if (candidateNode == null) continue;
|
||||
|
||||
// 갈림길에서 후보 노드로의 방향 벡터 계산 (junctionNode → candidateNode)
|
||||
double outgoingAngle = CalculateAngle(junctionNode.Position, candidateNode.Position);
|
||||
|
||||
// 방향 변화 각도 계산 (0도가 직진, 180도가 유턴)
|
||||
double angleChange = CalculateAngleChange(incomingAngle, outgoingAngle);
|
||||
|
||||
// 막다른 길 여부 확인
|
||||
var nodeConnections = GetAllConnectedNodes(nodeId);
|
||||
bool isDeadEnd = nodeConnections.Count <= 1;
|
||||
|
||||
// 최적 노드 선택 로직
|
||||
bool shouldUpdate = false;
|
||||
|
||||
if (!foundNonDeadEnd && !isDeadEnd)
|
||||
{
|
||||
// 첫 번째 막다른 길이 아닌 노드 발견
|
||||
shouldUpdate = true;
|
||||
foundNonDeadEnd = true;
|
||||
}
|
||||
else if (foundNonDeadEnd && isDeadEnd)
|
||||
{
|
||||
// 이미 막다른 길이 아닌 노드를 찾았으므로 막다른 길은 제외
|
||||
continue;
|
||||
}
|
||||
else if (foundNonDeadEnd == isDeadEnd)
|
||||
{
|
||||
// 같은 조건(둘 다 막다른길 or 둘 다 아님)에서는 각도가 작은 것 선택
|
||||
shouldUpdate = angleChange < minAngleChange;
|
||||
}
|
||||
|
||||
if (shouldUpdate)
|
||||
{
|
||||
minAngleChange = angleChange;
|
||||
bestNode = nodeId;
|
||||
}
|
||||
}
|
||||
|
||||
return bestNode ?? availableNodes.FirstOrDefault(n => n != excludeNodeId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 두 점 사이의 각도 계산 (라디안 단위)
|
||||
/// </summary>
|
||||
private double CalculateAngle(System.Drawing.Point from, System.Drawing.Point to)
|
||||
{
|
||||
double dx = to.X - from.X;
|
||||
double dy = to.Y - from.Y;
|
||||
return Math.Atan2(dy, dx);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 두 방향 사이의 각도 변화량 계산 (0~180도 범위)
|
||||
/// 0도에 가까울수록 직진, 180도에 가까울수록 유턴
|
||||
/// </summary>
|
||||
private double CalculateAngleChange(double fromAngle, double toAngle)
|
||||
{
|
||||
// 각도 차이 계산
|
||||
double angleDiff = Math.Abs(toAngle - fromAngle);
|
||||
|
||||
// 0~π 범위로 정규화 (0~180도)
|
||||
if (angleDiff > Math.PI)
|
||||
{
|
||||
angleDiff = 2 * Math.PI - angleDiff;
|
||||
}
|
||||
|
||||
return angleDiff;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 실제 방향 전환이 일어나는 노드 찾기
|
||||
/// </summary>
|
||||
private string FindActualDirectionChangeNode(List<string> changePath, string junctionNodeId)
|
||||
{
|
||||
// 방향전환 경로 구조: [start...junction, detourNode, junction...target]
|
||||
// 실제 방향전환은 detourNode에서 일어남 (AGV가 한 태그 더 지나간 후)
|
||||
|
||||
if (changePath.Count < 3)
|
||||
return junctionNodeId; // 기본값으로 갈림길 반환
|
||||
|
||||
// 갈림길이 두 번 나타나는 위치 찾기
|
||||
int firstJunctionIndex = changePath.IndexOf(junctionNodeId);
|
||||
int lastJunctionIndex = changePath.LastIndexOf(junctionNodeId);
|
||||
|
||||
// 갈림길이 두 번 나타나고, 그 사이에 노드가 있는 경우
|
||||
if (firstJunctionIndex != lastJunctionIndex && lastJunctionIndex - firstJunctionIndex == 2)
|
||||
{
|
||||
// 첫 번째와 두 번째 갈림길 사이에 있는 노드가 실제 방향전환 노드
|
||||
string detourNode = changePath[firstJunctionIndex + 1];
|
||||
return detourNode;
|
||||
}
|
||||
|
||||
// 방향전환 구조를 찾지 못한 경우 기본값 반환
|
||||
return junctionNodeId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 갈림길에서 적절한 우회 옵션이 있는지 확인
|
||||
/// </summary>
|
||||
private bool HasSuitableDetourOptions(string junctionNodeId, string excludeNodeId)
|
||||
{
|
||||
var junctionInfo = _junctionAnalyzer.GetJunctionInfo(junctionNodeId);
|
||||
if (junctionInfo == null || !junctionInfo.IsJunction)
|
||||
return false;
|
||||
|
||||
// 제외할 노드(직전 노드)를 뺀 연결된 노드가 2개 이상이어야 적절한 우회 가능
|
||||
var availableConnections = junctionInfo.ConnectedNodes
|
||||
.Where(nodeId => nodeId != excludeNodeId)
|
||||
.ToList();
|
||||
|
||||
// 최소 2개의 우회 옵션이 있어야 함 (갈림길에서 방향전환 후 다시 나갈 수 있어야 함)
|
||||
return availableConnections.Count >= 2;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 갈림길을 통해 목적지에 도달할 수 있는지 확인
|
||||
/// </summary>
|
||||
private bool CanReachTargetViaJunction(string junctionNodeId, string targetNodeId)
|
||||
{
|
||||
// 갈림길에서 목적지까지의 경로가 존재하는지 확인
|
||||
var pathToTarget = _pathfinder.FindPath(junctionNodeId, targetNodeId);
|
||||
return pathToTarget.Success;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 갈림길에서 여러 출구 옵션이 있는지 확인 (직진 경로상 갈림길용)
|
||||
/// </summary>
|
||||
private bool HasMultipleExitOptions(string junctionNodeId)
|
||||
{
|
||||
var junctionInfo = _junctionAnalyzer.GetJunctionInfo(junctionNodeId);
|
||||
if (junctionInfo == null || !junctionInfo.IsJunction)
|
||||
return false;
|
||||
|
||||
// 최소 3개 이상의 연결 노드가 있어야 적절한 방향전환 가능
|
||||
return junctionInfo.ConnectedNodes.Count >= 3;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 방향전환 경로 검증 - 되돌아가기 패턴 및 물리적 실현성 검증
|
||||
/// </summary>
|
||||
private PathValidationResult ValidateDirectionChangePath(List<string> path, string startNodeId, string junctionNodeId)
|
||||
{
|
||||
if (path == null || path.Count == 0)
|
||||
{
|
||||
return PathValidationResult.CreateInvalid(startNodeId, "", "경로가 비어있습니다.");
|
||||
}
|
||||
|
||||
// 1. 되돌아가기 패턴 검증 (A → B → A)
|
||||
var backtrackingPatterns = DetectBacktrackingPatterns(path);
|
||||
if (backtrackingPatterns.Count > 0)
|
||||
{
|
||||
var issues = new List<string>();
|
||||
foreach (var pattern in backtrackingPatterns)
|
||||
{
|
||||
issues.Add($"되돌아가기 패턴 발견: {pattern}");
|
||||
}
|
||||
|
||||
string errorMessage = $"되돌아가기 패턴 검출 ({backtrackingPatterns.Count}개): {string.Join(", ", issues)}";
|
||||
System.Diagnostics.Debug.WriteLine($"[PathValidation] ❌ 경로: {string.Join(" → ", path)}");
|
||||
System.Diagnostics.Debug.WriteLine($"[PathValidation] ❌ 되돌아가기 패턴: {errorMessage}");
|
||||
|
||||
return PathValidationResult.CreateInvalidWithBacktracking(
|
||||
path, backtrackingPatterns, startNodeId, "", junctionNodeId, errorMessage);
|
||||
}
|
||||
|
||||
// 2. 연속된 중복 노드 검증
|
||||
var duplicates = DetectConsecutiveDuplicates(path);
|
||||
if (duplicates.Count > 0)
|
||||
{
|
||||
string errorMessage = $"연속된 중복 노드 발견: {string.Join(", ", duplicates)}";
|
||||
return PathValidationResult.CreateInvalid(startNodeId, "", errorMessage);
|
||||
}
|
||||
|
||||
// 3. 경로 연결성 검증
|
||||
var connectivity = ValidatePathConnectivity(path);
|
||||
if (!connectivity.IsValid)
|
||||
{
|
||||
return PathValidationResult.CreateInvalid(startNodeId, "", $"경로 연결성 오류: {connectivity.ValidationError}");
|
||||
}
|
||||
|
||||
// 4. 갈림길 포함 여부 검증
|
||||
if (!path.Contains(junctionNodeId))
|
||||
{
|
||||
return PathValidationResult.CreateInvalid(startNodeId, "", $"갈림길 {junctionNodeId}이 경로에 포함되지 않음");
|
||||
}
|
||||
|
||||
System.Diagnostics.Debug.WriteLine($"[PathValidation] ✅ 유효한 경로: {string.Join(" → ", path)}");
|
||||
return PathValidationResult.CreateValid(path, startNodeId, "", junctionNodeId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 되돌아가기 패턴 검출 (A → B → A)
|
||||
/// </summary>
|
||||
private List<BacktrackingPattern> DetectBacktrackingPatterns(List<string> path)
|
||||
{
|
||||
var patterns = new List<BacktrackingPattern>();
|
||||
|
||||
for (int i = 0; i < path.Count - 2; i++)
|
||||
{
|
||||
string nodeA = path[i];
|
||||
string nodeB = path[i + 1];
|
||||
string nodeC = path[i + 2];
|
||||
|
||||
// A → B → A 패턴 검출
|
||||
if (nodeA == nodeC && nodeA != nodeB)
|
||||
{
|
||||
var pattern = BacktrackingPattern.Create(nodeA, nodeB, nodeA, i, i + 2);
|
||||
patterns.Add(pattern);
|
||||
}
|
||||
}
|
||||
|
||||
return patterns;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 연속된 중복 노드 검출
|
||||
/// </summary>
|
||||
private List<string> DetectConsecutiveDuplicates(List<string> path)
|
||||
{
|
||||
var duplicates = new List<string>();
|
||||
|
||||
for (int i = 0; i < path.Count - 1; i++)
|
||||
{
|
||||
if (path[i] == path[i + 1])
|
||||
{
|
||||
duplicates.Add(path[i]);
|
||||
}
|
||||
}
|
||||
|
||||
return duplicates;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 경로 연결성 검증
|
||||
/// </summary>
|
||||
private PathValidationResult ValidatePathConnectivity(List<string> path)
|
||||
{
|
||||
for (int i = 0; i < path.Count - 1; i++)
|
||||
{
|
||||
string currentNode = path[i];
|
||||
string nextNode = path[i + 1];
|
||||
|
||||
// 두 노드간 직접 연결성 확인 (맵 노드의 ConnectedNodes 리스트 사용)
|
||||
var currentMapNode = _mapNodes.FirstOrDefault(n => n.NodeId == currentNode);
|
||||
if (currentMapNode == null || !currentMapNode.ConnectedNodes.Contains(nextNode))
|
||||
{
|
||||
return PathValidationResult.CreateInvalid(currentNode, nextNode, $"노드 {currentNode}와 {nextNode} 사이에 연결이 없음");
|
||||
}
|
||||
}
|
||||
|
||||
return PathValidationResult.CreateNotRequired();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 두 점 사이의 거리 계산
|
||||
/// </summary>
|
||||
private float CalculateDistance(System.Drawing.Point p1, System.Drawing.Point p2)
|
||||
{
|
||||
float dx = p2.X - p1.X;
|
||||
float dy = p2.Y - p1.Y;
|
||||
return (float)Math.Sqrt(dx * dx + dy * dy);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 경로 계획 요약 정보
|
||||
/// </summary>
|
||||
public string GetPlanSummary()
|
||||
{
|
||||
var junctions = _junctionAnalyzer.GetJunctionSummary();
|
||||
return string.Join("\n", junctions);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 노드의 표시명 가져오기 (RFID 우선, 없으면 (NodeID) 형태)
|
||||
/// </summary>
|
||||
/// <param name="nodeId">노드 ID</param>
|
||||
/// <returns>표시할 이름</returns>
|
||||
private string GetDisplayName(string nodeId)
|
||||
{
|
||||
var node = _mapNodes.FirstOrDefault(n => n.NodeId == nodeId);
|
||||
if (node != null && !string.IsNullOrEmpty(node.RfidId))
|
||||
{
|
||||
return node.RfidId;
|
||||
}
|
||||
return $"({nodeId})";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
using AGVNavigationCore.Models;
|
||||
|
||||
namespace AGVNavigationCore.PathFinding.Planning
|
||||
{
|
||||
/// <summary>
|
||||
/// AGV 마그넷 센서 방향 제어
|
||||
/// </summary>
|
||||
public enum MagnetDirection
|
||||
{
|
||||
/// <summary>
|
||||
/// 직진 - 기본 마그넷 라인 추종
|
||||
/// </summary>
|
||||
Straight = 0,
|
||||
|
||||
/// <summary>
|
||||
/// 좌측 - 마그넷 센서 가중치를 좌측으로 조정
|
||||
/// </summary>
|
||||
Left = 1,
|
||||
|
||||
/// <summary>
|
||||
/// 우측 - 마그넷 센서 가중치를 우측으로 조정
|
||||
/// </summary>
|
||||
Right = 2
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 노드별 모터방향 정보 (방향 전환 지원 포함)
|
||||
/// </summary>
|
||||
public class NodeMotorInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// 노드 ID
|
||||
/// </summary>
|
||||
public string NodeId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 해당 노드에서의 모터방향
|
||||
/// </summary>
|
||||
public AgvDirection MotorDirection { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 마그넷 센서 방향 제어 (갈림길 처리용)
|
||||
/// </summary>
|
||||
public MagnetDirection MagnetDirection { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 다음 노드 ID (경로예측용)
|
||||
/// </summary>
|
||||
public string NextNodeId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 회전 가능 노드 여부
|
||||
/// </summary>
|
||||
public bool CanRotate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 방향 전환이 발생하는 노드 여부
|
||||
/// </summary>
|
||||
public bool IsDirectionChangePoint { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 특수 동작이 필요한 노드 여부 (갈림길 전진/후진 반복)
|
||||
/// </summary>
|
||||
public bool RequiresSpecialAction { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 특수 동작 설명
|
||||
/// </summary>
|
||||
public string SpecialActionDescription { get; set; }
|
||||
|
||||
public NodeMotorInfo(string nodeId, AgvDirection motorDirection, string nextNodeId = null, MagnetDirection magnetDirection = MagnetDirection.Straight)
|
||||
{
|
||||
NodeId = nodeId;
|
||||
MotorDirection = motorDirection;
|
||||
MagnetDirection = magnetDirection;
|
||||
NextNodeId = nextNodeId;
|
||||
CanRotate = false;
|
||||
IsDirectionChangePoint = false;
|
||||
RequiresSpecialAction = false;
|
||||
SpecialActionDescription = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 방향 전환 정보를 포함한 생성자
|
||||
/// </summary>
|
||||
public NodeMotorInfo(string nodeId, AgvDirection motorDirection, string nextNodeId, bool canRotate, bool isDirectionChangePoint, MagnetDirection magnetDirection = MagnetDirection.Straight, bool requiresSpecialAction = false, string specialActionDescription = "")
|
||||
{
|
||||
NodeId = nodeId;
|
||||
MotorDirection = motorDirection;
|
||||
MagnetDirection = magnetDirection;
|
||||
NextNodeId = nextNodeId;
|
||||
CanRotate = canRotate;
|
||||
IsDirectionChangePoint = isDirectionChangePoint;
|
||||
RequiresSpecialAction = requiresSpecialAction;
|
||||
SpecialActionDescription = specialActionDescription ?? string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 디버깅용 문자열 표현
|
||||
/// </summary>
|
||||
public override string ToString()
|
||||
{
|
||||
var result = $"{NodeId}:{MotorDirection}";
|
||||
|
||||
// 마그넷 방향이 직진이 아닌 경우 표시
|
||||
if (MagnetDirection != MagnetDirection.Straight)
|
||||
result += $"({MagnetDirection})";
|
||||
|
||||
if (IsDirectionChangePoint)
|
||||
result += " [방향전환]";
|
||||
|
||||
if (CanRotate)
|
||||
result += " [회전가능]";
|
||||
|
||||
if (RequiresSpecialAction)
|
||||
result += $" [특수동작:{SpecialActionDescription}]";
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
using AGVNavigationCore.Models;
|
||||
|
||||
namespace AGVNavigationCore.PathFinding.Validation
|
||||
{
|
||||
/// <summary>
|
||||
/// 도킹 검증 결과
|
||||
/// </summary>
|
||||
public class DockingValidationResult
|
||||
{
|
||||
/// <summary>
|
||||
/// 도킹 검증이 필요한지 여부 (목적지가 도킹 대상인 경우)
|
||||
/// </summary>
|
||||
public bool IsValidationRequired { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 도킹 검증 통과 여부
|
||||
/// </summary>
|
||||
public bool IsValid { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 목적지 노드 ID
|
||||
/// </summary>
|
||||
public string TargetNodeId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 목적지 노드 타입
|
||||
/// </summary>
|
||||
public NodeType TargetNodeType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 필요한 도킹 방향
|
||||
/// </summary>
|
||||
public AgvDirection RequiredDockingDirection { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 계산된 경로의 마지막 방향
|
||||
/// </summary>
|
||||
public AgvDirection CalculatedFinalDirection { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 검증 오류 메시지 (실패시)
|
||||
/// </summary>
|
||||
public string ValidationError { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 기본 생성자
|
||||
/// </summary>
|
||||
public DockingValidationResult()
|
||||
{
|
||||
IsValidationRequired = false;
|
||||
IsValid = true;
|
||||
TargetNodeId = string.Empty;
|
||||
RequiredDockingDirection = AgvDirection.Forward;
|
||||
CalculatedFinalDirection = AgvDirection.Forward;
|
||||
ValidationError = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 검증 불필요한 경우 생성
|
||||
/// </summary>
|
||||
public static DockingValidationResult CreateNotRequired()
|
||||
{
|
||||
return new DockingValidationResult
|
||||
{
|
||||
IsValidationRequired = false,
|
||||
IsValid = true
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 검증 성공 결과 생성
|
||||
/// </summary>
|
||||
public static DockingValidationResult CreateValid(string targetNodeId, NodeType nodeType, AgvDirection requiredDirection, AgvDirection calculatedDirection)
|
||||
{
|
||||
return new DockingValidationResult
|
||||
{
|
||||
IsValidationRequired = true,
|
||||
IsValid = true,
|
||||
TargetNodeId = targetNodeId,
|
||||
TargetNodeType = nodeType,
|
||||
RequiredDockingDirection = requiredDirection,
|
||||
CalculatedFinalDirection = calculatedDirection
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 검증 실패 결과 생성
|
||||
/// </summary>
|
||||
public static DockingValidationResult CreateInvalid(string targetNodeId, NodeType nodeType, AgvDirection requiredDirection, AgvDirection calculatedDirection, string error)
|
||||
{
|
||||
return new DockingValidationResult
|
||||
{
|
||||
IsValidationRequired = true,
|
||||
IsValid = false,
|
||||
TargetNodeId = targetNodeId,
|
||||
TargetNodeType = nodeType,
|
||||
RequiredDockingDirection = requiredDirection,
|
||||
CalculatedFinalDirection = calculatedDirection,
|
||||
ValidationError = error
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
using System.Collections.Generic;
|
||||
using AGVNavigationCore.Models;
|
||||
|
||||
namespace AGVNavigationCore.PathFinding.Validation
|
||||
{
|
||||
/// <summary>
|
||||
/// 경로 검증 결과 (되돌아가기 패턴 검증 포함)
|
||||
/// </summary>
|
||||
public class PathValidationResult
|
||||
{
|
||||
/// <summary>
|
||||
/// 경로 검증이 필요한지 여부
|
||||
/// </summary>
|
||||
public bool IsValidationRequired { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 경로 검증 통과 여부
|
||||
/// </summary>
|
||||
public bool IsValid { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 검증된 경로
|
||||
/// </summary>
|
||||
public List<string> ValidatedPath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 검출된 되돌아가기 패턴 목록 (A → B → A 형태)
|
||||
/// </summary>
|
||||
public List<BacktrackingPattern> BacktrackingPatterns { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 갈림길 노드 목록
|
||||
/// </summary>
|
||||
public List<string> JunctionNodes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 시작 노드 ID
|
||||
/// </summary>
|
||||
public string StartNodeId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 목표 노드 ID
|
||||
/// </summary>
|
||||
public string TargetNodeId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 갈림길 노드 ID (방향 전환용)
|
||||
/// </summary>
|
||||
public string JunctionNodeId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 검증 오류 메시지 (실패시)
|
||||
/// </summary>
|
||||
public string ValidationError { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 기본 생성자
|
||||
/// </summary>
|
||||
public PathValidationResult()
|
||||
{
|
||||
IsValidationRequired = false;
|
||||
IsValid = true;
|
||||
ValidatedPath = new List<string>();
|
||||
BacktrackingPatterns = new List<BacktrackingPattern>();
|
||||
JunctionNodes = new List<string>();
|
||||
StartNodeId = string.Empty;
|
||||
TargetNodeId = string.Empty;
|
||||
JunctionNodeId = string.Empty;
|
||||
ValidationError = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 검증 불필요한 경우 생성
|
||||
/// </summary>
|
||||
public static PathValidationResult CreateNotRequired()
|
||||
{
|
||||
return new PathValidationResult
|
||||
{
|
||||
IsValidationRequired = false,
|
||||
IsValid = true
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 검증 성공 결과 생성
|
||||
/// </summary>
|
||||
public static PathValidationResult CreateValid(List<string> path, string startNodeId, string targetNodeId, string junctionNodeId = "")
|
||||
{
|
||||
return new PathValidationResult
|
||||
{
|
||||
IsValidationRequired = true,
|
||||
IsValid = true,
|
||||
ValidatedPath = new List<string>(path),
|
||||
StartNodeId = startNodeId,
|
||||
TargetNodeId = targetNodeId,
|
||||
JunctionNodeId = junctionNodeId
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 검증 실패 결과 생성 (되돌아가기 패턴 검출)
|
||||
/// </summary>
|
||||
public static PathValidationResult CreateInvalidWithBacktracking(
|
||||
List<string> path,
|
||||
List<BacktrackingPattern> backtrackingPatterns,
|
||||
string startNodeId,
|
||||
string targetNodeId,
|
||||
string junctionNodeId,
|
||||
string error)
|
||||
{
|
||||
return new PathValidationResult
|
||||
{
|
||||
IsValidationRequired = true,
|
||||
IsValid = false,
|
||||
ValidatedPath = new List<string>(path),
|
||||
BacktrackingPatterns = new List<BacktrackingPattern>(backtrackingPatterns),
|
||||
StartNodeId = startNodeId,
|
||||
TargetNodeId = targetNodeId,
|
||||
JunctionNodeId = junctionNodeId,
|
||||
ValidationError = error
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 일반 검증 실패 결과 생성
|
||||
/// </summary>
|
||||
public static PathValidationResult CreateInvalid(string startNodeId, string targetNodeId, string error)
|
||||
{
|
||||
return new PathValidationResult
|
||||
{
|
||||
IsValidationRequired = true,
|
||||
IsValid = false,
|
||||
StartNodeId = startNodeId,
|
||||
TargetNodeId = targetNodeId,
|
||||
ValidationError = error
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 되돌아가기 패턴 정보 (A → B → A)
|
||||
/// </summary>
|
||||
public class BacktrackingPattern
|
||||
{
|
||||
/// <summary>
|
||||
/// 시작 노드 (A)
|
||||
/// </summary>
|
||||
public string StartNode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 중간 노드 (B)
|
||||
/// </summary>
|
||||
public string MiddleNode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 되돌아간 노드 (다시 A)
|
||||
/// </summary>
|
||||
public string ReturnNode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 경로에서의 시작 인덱스
|
||||
/// </summary>
|
||||
public int StartIndex { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 경로에서의 종료 인덱스
|
||||
/// </summary>
|
||||
public int EndIndex { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 기본 생성자
|
||||
/// </summary>
|
||||
public BacktrackingPattern()
|
||||
{
|
||||
StartNode = string.Empty;
|
||||
MiddleNode = string.Empty;
|
||||
ReturnNode = string.Empty;
|
||||
StartIndex = -1;
|
||||
EndIndex = -1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 되돌아가기 패턴 생성
|
||||
/// </summary>
|
||||
public static BacktrackingPattern Create(string startNode, string middleNode, string returnNode, int startIndex, int endIndex)
|
||||
{
|
||||
return new BacktrackingPattern
|
||||
{
|
||||
StartNode = startNode,
|
||||
MiddleNode = middleNode,
|
||||
ReturnNode = returnNode,
|
||||
StartIndex = startIndex,
|
||||
EndIndex = endIndex
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 패턴 설명 문자열
|
||||
/// </summary>
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{StartNode} → {MiddleNode} → {ReturnNode} (인덱스: {StartIndex}-{EndIndex})";
|
||||
}
|
||||
}
|
||||
}
|
||||
36
Cs_HMI/AGVLogic/AGVNavigationCore/Properties/AssemblyInfo.cs
Normal file
36
Cs_HMI/AGVLogic/AGVNavigationCore/Properties/AssemblyInfo.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
// General Information about an assembly is controlled through the following
|
||||
// set of attributes. Change these attribute values to modify the information
|
||||
// associated with an assembly.
|
||||
[assembly: AssemblyTitle("AGVNavigationCore")]
|
||||
[assembly: AssemblyDescription("AGV Navigation and Pathfinding Core Library")]
|
||||
[assembly: AssemblyConfiguration("")]
|
||||
[assembly: AssemblyCompany("ENIG")]
|
||||
[assembly: AssemblyProduct("AGV Navigation System")]
|
||||
[assembly: AssemblyCopyright("Copyright © ENIG 2024")]
|
||||
[assembly: AssemblyTrademark("")]
|
||||
[assembly: AssemblyCulture("")]
|
||||
|
||||
// Setting ComVisible to false makes the types in this assembly not visible
|
||||
// to COM components. If you need to access a type in this assembly from
|
||||
// COM, set the ComVisible attribute to true on that type.
|
||||
[assembly: ComVisible(false)]
|
||||
|
||||
// The following GUID is for the ID of the typelib if this project is exposed to COM
|
||||
[assembly: Guid("c5f7a8b2-8d3e-4a1b-9c6e-7f4d5e2a9b1c")]
|
||||
|
||||
// Version information for an assembly consists of the following four values:
|
||||
//
|
||||
// Major Version
|
||||
// Minor Version
|
||||
// Build Number
|
||||
// Revision
|
||||
//
|
||||
// You can specify all the values or you can default the Build and Revision Numbers
|
||||
// by using the '*' as shown below:
|
||||
// [assembly: AssemblyVersion("1.0.*")]
|
||||
[assembly: AssemblyVersion("1.0.0.0")]
|
||||
[assembly: AssemblyFileVersion("1.0.0.0")]
|
||||
155
Cs_HMI/AGVLogic/AGVNavigationCore/README.md
Normal file
155
Cs_HMI/AGVLogic/AGVNavigationCore/README.md
Normal file
@@ -0,0 +1,155 @@
|
||||
# AGVNavigationCore
|
||||
|
||||
ENIG AGV 시스템을 위한 핵심 네비게이션 및 경로 탐색 라이브러리
|
||||
|
||||
## 📋 개요
|
||||
|
||||
AGVNavigationCore는 자동 유도 차량(AGV) 시스템의 경로 계획, 맵 편집, 시뮬레이션, 실시간 모니터링 기능을 제공하는 .NET Framework 4.8 라이브러리입니다.
|
||||
|
||||
## 🏗️ 프로젝트 구조
|
||||
|
||||
### 📁 Controls/
|
||||
**AGV 관련 사용자 인터페이스 컨트롤 및 AGV 추상화 계층**
|
||||
|
||||
- **AGVState.cs** - AGV 상태 열거형 (Idle, Moving, Rotating, Docking, Charging, Error)
|
||||
- **IAGV.cs** - AGV 인터페이스 정의 (가상/실제 AGV 통합)
|
||||
- **UnifiedAGVCanvas.cs** - 통합 AGV 캔버스 컨트롤 메인 클래스
|
||||
- **UnifiedAGVCanvas.Events.cs** - 그리기 및 렌더링 로직 (AGV, 노드, 경로 시각화)
|
||||
- **UnifiedAGVCanvas.Mouse.cs** - 마우스 이벤트 처리 (클릭, 드래그, 줌, 팬)
|
||||
|
||||
### 📁 Models/
|
||||
**데이터 모델 및 핵심 비즈니스 엔티티 정의**
|
||||
|
||||
- **Enums.cs** - 핵심 열거형 정의 (NodeType, DockingDirection, AgvDirection, StationType)
|
||||
- **MapNode.cs** - 맵 노드 엔티티 클래스 (논리적 노드 ID, 위치, 타입, 연결 정보, RFID 정보)
|
||||
- **MapLoader.cs** - 맵 파일 로딩/저장 유틸리티 (JSON 직렬화, 데이터 마이그레이션, 검증)
|
||||
|
||||
### 📁 PathFinding/
|
||||
**AGV 경로 탐색 및 계산 알고리즘**
|
||||
|
||||
#### 🟢 활발히 사용되는 클래스
|
||||
- **AGVPathfinder.cs** - 메인 AGV 경로 계획기 (물리적 제약사항 고려)
|
||||
- **AGVPathResult.cs** - 경로 계산 결과 데이터 클래스
|
||||
- **DockingValidationResult.cs** - 도킹 검증 결과 데이터 클래스
|
||||
|
||||
#### 🟡 내부 구현 클래스
|
||||
- **AStarPathfinder.cs** - A* 알고리즘 기반 기본 경로 탐색
|
||||
- **DirectionChangePlanner.cs** - AGV 방향 전환 경로 계획 시스템
|
||||
- **JunctionAnalyzer.cs** - 교차점 분석 및 마그넷 센서 방향 계산
|
||||
- **NodeMotorInfo.cs** - 노드별 모터방향 정보 (방향 전환 지원 포함)
|
||||
- **PathNode.cs** - A* 알고리즘용 경로 노드
|
||||
|
||||
### 📁 Utils/
|
||||
**유틸리티 및 계산 헬퍼 클래스**
|
||||
|
||||
- **DockingValidator.cs** - AGV 도킹 방향 검증 유틸리티
|
||||
- **LiftCalculator.cs** - AGV 리프트 방향 계산 유틸리티
|
||||
|
||||
### 📁 Properties/
|
||||
- **AssemblyInfo.cs** - 어셈블리 정보 및 버전 관리
|
||||
|
||||
## 🎯 주요 기능
|
||||
|
||||
### 🗺️ 맵 관리
|
||||
- **논리적 노드 시스템**: 물리적 RFID와 분리된 논리적 노드 ID 관리
|
||||
- **노드 타입**: Normal, Rotation, Docking, Charging 등 다양한 노드 타입 지원
|
||||
- **연결 관리**: 노드 간 방향성 연결 관리
|
||||
- **JSON 저장/로드**: 표준 JSON 형식으로 맵 데이터 관리
|
||||
|
||||
### 🧭 경로 탐색
|
||||
- **A* 알고리즘**: 효율적인 최단 경로 탐색
|
||||
- **AGV 물리적 제약**: 전진/후진 모터 방향, 회전 제약 고려
|
||||
- **방향 전환 계획**: 마그넷 센서 위치에서의 방향 전환 최적화
|
||||
- **도킹 검증**: 목적지 타입에 따른 도킹 방향 검증
|
||||
|
||||
### 🎮 시각화 및 편집
|
||||
- **통합 캔버스**: 맵 편집, 시뮬레이션, 모니터링 모드 지원
|
||||
- **실시간 렌더링**: AGV 위치, 경로, 상태 실시간 표시
|
||||
- **인터랙티브 편집**: 드래그앤드롭 노드 편집, 연결 관리
|
||||
- **줌/팬**: 대형 맵 탐색을 위한 줌/팬 기능
|
||||
|
||||
## 🔧 아키텍처 특징
|
||||
|
||||
### ✅ 장점
|
||||
- **계층화 아키텍처**: Models → Utils → PathFinding → Controls 의존성 구조
|
||||
- **관심사 분리**: 각 폴더별 명확한 책임 분담
|
||||
- **인터페이스 기반**: IAGV 인터페이스로 가상/실제 AGV 통합
|
||||
- **확장성**: 새로운 알고리즘, AGV 타입 추가 용이
|
||||
|
||||
### ⚠️ 개선 영역
|
||||
- **코드 크기**: 일부 클래스가 과도하게 큼 (UnifiedAGVCanvas.Events.cs: 1,699행)
|
||||
- **복잡도**: DirectionChangePlanner 등 복잡한 로직 포함
|
||||
- **분할 필요**: UnifiedAGVCanvas의 다중 책임 분리 필요
|
||||
|
||||
## 🚀 사용 방법
|
||||
|
||||
### 기본 맵 로딩
|
||||
```csharp
|
||||
var mapLoader = new MapLoader();
|
||||
var mapNodes = mapLoader.LoadMap("path/to/map.json");
|
||||
```
|
||||
|
||||
### 경로 계산
|
||||
```csharp
|
||||
var pathfinder = new AGVPathfinder();
|
||||
pathfinder.SetMapNodes(mapNodes);
|
||||
|
||||
var result = pathfinder.FindPath("START_NODE", "TARGET_NODE", AgvDirection.Forward);
|
||||
if (result.Success)
|
||||
{
|
||||
Console.WriteLine($"경로: {string.Join(" -> ", result.Path)}");
|
||||
Console.WriteLine($"거리: {result.TotalDistance:F1}px");
|
||||
}
|
||||
```
|
||||
|
||||
### 캔버스 사용
|
||||
```csharp
|
||||
var canvas = new UnifiedAGVCanvas();
|
||||
canvas.Nodes = mapNodes;
|
||||
canvas.CurrentPath = result;
|
||||
canvas.CurrentEditMode = UnifiedAGVCanvas.EditMode.Select;
|
||||
```
|
||||
|
||||
## 📈 최근 업데이트 (2024.12)
|
||||
|
||||
### ✅ 완료된 개선사항
|
||||
- **중복 코드 정리**: PathResult, RfidPathResult 등 중복 클래스 제거
|
||||
- **아키텍처 통합**: AdvancedAGVPathfinder → AGVPathfinder 통합
|
||||
- **좌표 정확성**: 줌/팬 시 노드 선택 정확도 개선
|
||||
- **미사용 코드 제거**: PathfindingOptions 등 미사용 클래스 삭제
|
||||
|
||||
### 🔄 진행 중인 개선사항
|
||||
- **방향 계산 최적화**: 리프트 방향 계산 로직 개선
|
||||
- **도킹 검증**: 도킹 방향 검증 시스템 강화
|
||||
- **성능 최적화**: 대형 맵 처리 성능 개선
|
||||
|
||||
## 🏃♂️ 향후 계획
|
||||
|
||||
### 우선순위 1 (즉시)
|
||||
- UnifiedAGVCanvas 분할 (Rendering, Editing, Simulation 분리)
|
||||
- [완료] PathFinding 폴더 세분화 (Core, Validation, Planning, Analysis)
|
||||
|
||||
### 우선순위 2 (중기)
|
||||
- 인터페이스 표준화 (I접두사 통일)
|
||||
- Utils 폴더 확장 (Calculations, Validators, Converters)
|
||||
|
||||
### 우선순위 3 (장기)
|
||||
- 의존성 주입 도입
|
||||
- 성능 모니터링 시스템
|
||||
- 단위 테스트 확충
|
||||
|
||||
## 📦 의존성
|
||||
- .NET Framework 4.8
|
||||
- Newtonsoft.Json 13.0.3
|
||||
- System.Drawing
|
||||
- System.Windows.Forms
|
||||
|
||||
## 🔗 관련 프로젝트
|
||||
|
||||
- **AGVMapEditor**: 맵 편집 전용 애플리케이션
|
||||
- **AGVSimulator**: AGV 시뮬레이션 애플리케이션
|
||||
- **AGVCSharp**: 메인 AGV 제어 시스템
|
||||
|
||||
## 📞 연락처
|
||||
|
||||
ENIG AGV 개발팀 - 2024년 12월 업데이트
|
||||
217
Cs_HMI/AGVLogic/AGVNavigationCore/Utils/DockingValidator.cs
Normal file
217
Cs_HMI/AGVLogic/AGVNavigationCore/Utils/DockingValidator.cs
Normal file
@@ -0,0 +1,217 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using AGVNavigationCore.Models;
|
||||
using AGVNavigationCore.PathFinding.Core;
|
||||
using AGVNavigationCore.PathFinding.Validation;
|
||||
|
||||
namespace AGVNavigationCore.Utils
|
||||
{
|
||||
/// <summary>
|
||||
/// AGV 도킹 방향 검증 유틸리티
|
||||
/// 경로 계산 후 마지막 도킹 방향이 올바른지 검증
|
||||
/// </summary>
|
||||
public static class DockingValidator
|
||||
{
|
||||
/// <summary>
|
||||
/// 경로의 도킹 방향 검증
|
||||
/// </summary>
|
||||
/// <param name="pathResult">경로 계산 결과</param>
|
||||
/// <param name="mapNodes">맵 노드 목록</param>
|
||||
/// <param name="currentDirection">AGV 현재 방향</param>
|
||||
/// <returns>도킹 검증 결과</returns>
|
||||
public static DockingValidationResult ValidateDockingDirection(AGVPathResult pathResult, List<MapNode> mapNodes, AgvDirection currentDirection)
|
||||
{
|
||||
// 경로가 없거나 실패한 경우
|
||||
if (pathResult == null || !pathResult.Success || pathResult.Path == null || pathResult.Path.Count == 0)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"[DockingValidator] 도킹 검증 불필요: 경로 없음");
|
||||
return DockingValidationResult.CreateNotRequired();
|
||||
}
|
||||
|
||||
// 목적지 노드 찾기
|
||||
string targetNodeId = pathResult.Path[pathResult.Path.Count - 1];
|
||||
var targetNode = mapNodes?.FirstOrDefault(n => n.NodeId == targetNodeId);
|
||||
|
||||
System.Diagnostics.Debug.WriteLine($"[DockingValidator] 목적지 노드: {targetNodeId}");
|
||||
|
||||
if (targetNode == null)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"[DockingValidator] 목적지 노드 찾을 수 없음: {targetNodeId}");
|
||||
return DockingValidationResult.CreateNotRequired();
|
||||
}
|
||||
|
||||
System.Diagnostics.Debug.WriteLine($"[DockingValidator] 목적지 노드 타입: {targetNode.Type} ({(int)targetNode.Type})");
|
||||
|
||||
// 도킹이 필요한 노드인지 확인 (DockDirection이 DontCare가 아닌 경우)
|
||||
if (!IsDockingRequired(targetNode.DockDirection))
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"[DockingValidator] 도킹 불필요: {targetNode.DockDirection}");
|
||||
return DockingValidationResult.CreateNotRequired();
|
||||
}
|
||||
|
||||
// 필요한 도킹 방향 확인
|
||||
var requiredDirection = GetRequiredDockingDirection(targetNode.DockDirection);
|
||||
System.Diagnostics.Debug.WriteLine($"[DockingValidator] 필요한 도킹 방향: {requiredDirection}");
|
||||
|
||||
// 경로 기반 최종 방향 계산
|
||||
var calculatedDirection = CalculateFinalDirection(pathResult.Path, mapNodes, currentDirection);
|
||||
System.Diagnostics.Debug.WriteLine($"[DockingValidator] 계산된 최종 방향: {calculatedDirection}");
|
||||
System.Diagnostics.Debug.WriteLine($"[DockingValidator] AGV 현재 방향: {currentDirection}");
|
||||
|
||||
// 검증 수행
|
||||
if (calculatedDirection == requiredDirection)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"[DockingValidator] ✅ 도킹 검증 성공");
|
||||
return DockingValidationResult.CreateValid(
|
||||
targetNodeId,
|
||||
targetNode.Type,
|
||||
requiredDirection,
|
||||
calculatedDirection);
|
||||
}
|
||||
else
|
||||
{
|
||||
string error = $"도킹 방향 불일치: 필요={GetDirectionText(requiredDirection)}, 계산됨={GetDirectionText(calculatedDirection)}";
|
||||
System.Diagnostics.Debug.WriteLine($"[DockingValidator] ❌ 도킹 검증 실패: {error}");
|
||||
return DockingValidationResult.CreateInvalid(
|
||||
targetNodeId,
|
||||
targetNode.Type,
|
||||
requiredDirection,
|
||||
calculatedDirection,
|
||||
error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 도킹이 필요한 노드인지 확인 (도킹방향이 DontCare가 아닌 경우)
|
||||
/// </summary>
|
||||
private static bool IsDockingRequired(DockingDirection dockDirection)
|
||||
{
|
||||
return dockDirection != DockingDirection.DontCare;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 노드 도킹 방향에 따른 필요한 AGV 방향 반환
|
||||
/// </summary>
|
||||
private static AgvDirection GetRequiredDockingDirection(DockingDirection dockDirection)
|
||||
{
|
||||
switch (dockDirection)
|
||||
{
|
||||
case DockingDirection.Forward:
|
||||
return AgvDirection.Forward; // 전진 도킹
|
||||
case DockingDirection.Backward:
|
||||
return AgvDirection.Backward; // 후진 도킹
|
||||
case DockingDirection.DontCare:
|
||||
default:
|
||||
return AgvDirection.Forward; // 기본값 (사실상 사용되지 않음)
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 경로 기반 최종 방향 계산
|
||||
/// 개선된 구현: 경로 진행 방향과 목적지 노드 타입을 고려
|
||||
/// </summary>
|
||||
private static AgvDirection CalculateFinalDirection(List<string> path, List<MapNode> mapNodes, AgvDirection currentDirection)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"[CalculateFinalDirection] 입력 - 경로 수: {path?.Count}, 현재 방향: {currentDirection}");
|
||||
|
||||
// 경로가 1개 이하면 현재 방향 유지
|
||||
if (path.Count < 2)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"[CalculateFinalDirection] 경로가 짧음, 현재 방향 유지: {currentDirection}");
|
||||
return currentDirection;
|
||||
}
|
||||
|
||||
// 목적지 노드 확인
|
||||
var lastNodeId = path[path.Count - 1];
|
||||
var lastNode = mapNodes?.FirstOrDefault(n => n.NodeId == lastNodeId);
|
||||
|
||||
if (lastNode == null)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"[CalculateFinalDirection] 목적지 노드 찾을 수 없음: {lastNodeId}");
|
||||
return currentDirection;
|
||||
}
|
||||
|
||||
// 도킹 노드인 경우, 필요한 도킹 방향으로 설정
|
||||
if (IsDockingRequired(lastNode.DockDirection))
|
||||
{
|
||||
var requiredDockingDirection = GetRequiredDockingDirection(lastNode.DockDirection);
|
||||
System.Diagnostics.Debug.WriteLine($"[CalculateFinalDirection] 도킹 노드(DockDirection={lastNode.DockDirection}) 감지, 필요 방향: {requiredDockingDirection}");
|
||||
|
||||
// 현재 방향이 필요한 도킹 방향과 다르면 경고 로그
|
||||
if (currentDirection != requiredDockingDirection)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"[CalculateFinalDirection] ⚠️ 현재 방향({currentDirection})과 필요 도킹 방향({requiredDockingDirection}) 불일치");
|
||||
}
|
||||
|
||||
// 도킹 노드의 경우 항상 필요한 도킹 방향 반환
|
||||
return requiredDockingDirection;
|
||||
}
|
||||
|
||||
// 일반 노드인 경우 마지막 구간의 이동 방향 분석
|
||||
var secondLastNodeId = path[path.Count - 2];
|
||||
var secondLastNode = mapNodes?.FirstOrDefault(n => n.NodeId == secondLastNodeId);
|
||||
|
||||
if (secondLastNode == null)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"[CalculateFinalDirection] 이전 노드 찾을 수 없음: {secondLastNodeId}");
|
||||
return currentDirection;
|
||||
}
|
||||
|
||||
// 마지막 구간의 이동 벡터 계산
|
||||
var deltaX = lastNode.Position.X - secondLastNode.Position.X;
|
||||
var deltaY = lastNode.Position.Y - secondLastNode.Position.Y;
|
||||
var distance = Math.Sqrt(deltaX * deltaX + deltaY * deltaY);
|
||||
|
||||
System.Diagnostics.Debug.WriteLine($"[CalculateFinalDirection] 마지막 구간: {secondLastNodeId} → {lastNodeId}, 벡터: ({deltaX}, {deltaY}), 거리: {distance:F2}");
|
||||
|
||||
// 이동 거리가 매우 작으면 현재 방향 유지
|
||||
if (distance < 1.0)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"[CalculateFinalDirection] 이동 거리 너무 짧음, 현재 방향 유지: {currentDirection}");
|
||||
return currentDirection;
|
||||
}
|
||||
|
||||
// 일반 노드의 경우 현재 방향 유지 (방향 전환은 회전 노드에서만 발생)
|
||||
System.Diagnostics.Debug.WriteLine($"[CalculateFinalDirection] 일반 노드, 현재 방향 유지: {currentDirection}");
|
||||
return currentDirection;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 방향을 텍스트로 변환
|
||||
/// </summary>
|
||||
private static string GetDirectionText(AgvDirection direction)
|
||||
{
|
||||
switch (direction)
|
||||
{
|
||||
case AgvDirection.Forward:
|
||||
return "전진";
|
||||
case AgvDirection.Backward:
|
||||
return "후진";
|
||||
default:
|
||||
return direction.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 도킹 검증 결과를 문자열로 변환 (디버깅용)
|
||||
/// </summary>
|
||||
public static string GetValidationSummary(DockingValidationResult validation)
|
||||
{
|
||||
if (validation == null)
|
||||
return "검증 결과 없음";
|
||||
|
||||
if (!validation.IsValidationRequired)
|
||||
return "도킹 검증 불필요";
|
||||
|
||||
if (validation.IsValid)
|
||||
{
|
||||
return $"도킹 검증 통과: {validation.TargetNodeId}({validation.TargetNodeType}) - {GetDirectionText(validation.RequiredDockingDirection)} 도킹";
|
||||
}
|
||||
else
|
||||
{
|
||||
return $"도킹 검증 실패: {validation.TargetNodeId}({validation.TargetNodeType}) - {validation.ValidationError}";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
281
Cs_HMI/AGVLogic/AGVNavigationCore/Utils/LiftCalculator.cs
Normal file
281
Cs_HMI/AGVLogic/AGVNavigationCore/Utils/LiftCalculator.cs
Normal file
@@ -0,0 +1,281 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using AGVNavigationCore.Models;
|
||||
|
||||
namespace AGVNavigationCore.Utils
|
||||
{
|
||||
/// <summary>
|
||||
/// AGV 리프트 방향 계산 유틸리티 클래스
|
||||
/// 모든 리프트 방향 계산 로직을 중앙화하여 일관성 보장
|
||||
/// </summary>
|
||||
public static class LiftCalculator
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// 경로 예측 기반 리프트 방향 계산
|
||||
/// 현재 노드에서 연결된 다음 노드들을 분석하여 리프트 방향 결정
|
||||
/// </summary>
|
||||
/// <param name="currentPos">현재 위치</param>
|
||||
/// <param name="previousPos">이전 위치</param>
|
||||
/// <param name="motorDirection">모터 방향</param>
|
||||
/// <param name="mapNodes">맵 노드 리스트 (경로 예측용)</param>
|
||||
/// <param name="tolerance">위치 허용 오차</param>
|
||||
/// <returns>리프트 계산 결과</returns>
|
||||
public static LiftCalculationResult CalculateLiftInfoWithPathPrediction(
|
||||
Point currentPos, Point previousPos, AgvDirection motorDirection,
|
||||
List<MapNode> mapNodes, int tolerance = 10)
|
||||
{
|
||||
if (mapNodes == null || mapNodes.Count == 0)
|
||||
{
|
||||
// 맵 노드 정보가 없으면 기존 방식 사용
|
||||
return CalculateLiftInfo(previousPos, currentPos, motorDirection);
|
||||
}
|
||||
|
||||
// 현재 위치에 해당하는 노드 찾기
|
||||
var currentNode = FindNodeByPosition(mapNodes, currentPos, tolerance);
|
||||
|
||||
if (currentNode == null)
|
||||
{
|
||||
// 현재 노드를 찾을 수 없으면 기존 방식 사용
|
||||
return CalculateLiftInfo(previousPos, currentPos, motorDirection);
|
||||
}
|
||||
|
||||
// 이전 위치에 해당하는 노드 찾기
|
||||
var previousNode = FindNodeByPosition(mapNodes, previousPos, tolerance);
|
||||
|
||||
Point targetPosition;
|
||||
string calculationMethod;
|
||||
|
||||
// 모터 방향에 따른 예측 방향 결정
|
||||
if (motorDirection == AgvDirection.Backward)
|
||||
{
|
||||
// 후진 모터: AGV가 리프트 쪽(목표 위치)으로 이동
|
||||
// 경로 예측 없이 단순히 현재→목표 방향 사용
|
||||
return CalculateLiftInfo(currentPos, previousPos, motorDirection);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 전진 모터: 기존 로직 (다음 노드 예측)
|
||||
var nextNodes = GetConnectedNodes(mapNodes, currentNode);
|
||||
|
||||
// 이전 노드 제외 (되돌아가는 방향 제외)
|
||||
if (previousNode != null)
|
||||
{
|
||||
nextNodes = nextNodes.Where(n => n.NodeId != previousNode.NodeId).ToList();
|
||||
}
|
||||
|
||||
if (nextNodes.Count == 1)
|
||||
{
|
||||
// 직선 경로: 다음 노드 방향으로 예측
|
||||
targetPosition = nextNodes.First().Position;
|
||||
calculationMethod = $"전진 경로 예측 ({currentNode.NodeId}→{nextNodes.First().NodeId})";
|
||||
}
|
||||
else if (nextNodes.Count > 1)
|
||||
{
|
||||
// 갈래길: 이전 위치 기반 계산 사용
|
||||
var prevResult = CalculateLiftInfo(previousPos, currentPos, motorDirection);
|
||||
prevResult.CalculationMethod += " (전진 갈래길)";
|
||||
return prevResult;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 연결된 노드가 없으면 기존 방식 사용
|
||||
return CalculateLiftInfo(previousPos, currentPos, motorDirection);
|
||||
}
|
||||
}
|
||||
|
||||
// 리프트 각도 계산
|
||||
var angleRadians = CalculateLiftAngleRadians(currentPos, targetPosition, motorDirection);
|
||||
var angleDegrees = angleRadians * 180.0 / Math.PI;
|
||||
|
||||
// 0-360도 범위로 정규화
|
||||
while (angleDegrees < 0) angleDegrees += 360;
|
||||
while (angleDegrees >= 360) angleDegrees -= 360;
|
||||
|
||||
var directionString = AngleToDirectionString(angleDegrees);
|
||||
|
||||
return new LiftCalculationResult
|
||||
{
|
||||
AngleRadians = angleRadians,
|
||||
AngleDegrees = angleDegrees,
|
||||
DirectionString = directionString,
|
||||
CalculationMethod = calculationMethod,
|
||||
MotorDirection = motorDirection
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// AGV 이동 방향과 모터 방향을 기반으로 리프트 각도 계산
|
||||
/// </summary>
|
||||
/// <param name="currentPos">현재 위치</param>
|
||||
/// <param name="targetPos">목표 위치</param>
|
||||
/// <param name="motorDirection">모터 방향</param>
|
||||
/// <returns>리프트 각도 (라디안)</returns>
|
||||
public static double CalculateLiftAngleRadians(Point currentPos, Point targetPos, AgvDirection motorDirection)
|
||||
{
|
||||
// 모터 방향에 따른 리프트 위치 계산
|
||||
if (motorDirection == AgvDirection.Forward)
|
||||
{
|
||||
// 전진 모터: AGV가 앞으로 가므로 리프트는 뒤쪽 (타겟 → 현재 방향)
|
||||
var dx = currentPos.X - targetPos.X;
|
||||
var dy = currentPos.Y - targetPos.Y;
|
||||
return Math.Atan2(dy, dx);
|
||||
}
|
||||
else if (motorDirection == AgvDirection.Backward)
|
||||
{
|
||||
// 후진 모터: AGV가 리프트 쪽으로 이동하므로 리프트는 AGV 이동 방향에 위치
|
||||
// 007→006 후진시: 리프트는 006방향(이동방향)을 향해야 함 (타겟→현재 반대방향)
|
||||
var dx = currentPos.X - targetPos.X;
|
||||
var dy = currentPos.Y - targetPos.Y;
|
||||
return Math.Atan2(dy, dx);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 기본값: 전진 모터와 동일
|
||||
var dx = currentPos.X - targetPos.X;
|
||||
var dy = currentPos.Y - targetPos.Y;
|
||||
return Math.Atan2(dy, dx);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// AGV 이동 방향과 모터 방향을 기반으로 리프트 각도 계산 (도 단위)
|
||||
/// </summary>
|
||||
/// <param name="currentPos">현재 위치</param>
|
||||
/// <param name="targetPos">목표 위치</param>
|
||||
/// <param name="motorDirection">모터 방향</param>
|
||||
/// <returns>리프트 각도 (도)</returns>
|
||||
public static double CalculateLiftAngleDegrees(Point currentPos, Point targetPos, AgvDirection motorDirection)
|
||||
{
|
||||
var radians = CalculateLiftAngleRadians(currentPos, targetPos, motorDirection);
|
||||
var degrees = radians * 180.0 / Math.PI;
|
||||
|
||||
// 0-360도 범위로 정규화
|
||||
while (degrees < 0) degrees += 360;
|
||||
while (degrees >= 360) degrees -= 360;
|
||||
|
||||
return degrees;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 각도를 8방향 문자열로 변환 (화면 좌표계 기준)
|
||||
/// 화면 좌표계: 0°=동쪽, 90°=남쪽, 180°=서쪽, 270°=북쪽
|
||||
/// </summary>
|
||||
/// <param name="angleDegrees">각도 (도)</param>
|
||||
/// <returns>방향 문자열</returns>
|
||||
public static string AngleToDirectionString(double angleDegrees)
|
||||
{
|
||||
// 0-360도 범위로 정규화
|
||||
while (angleDegrees < 0) angleDegrees += 360;
|
||||
while (angleDegrees >= 360) angleDegrees -= 360;
|
||||
|
||||
// 8방향으로 분류 (화면 좌표계)
|
||||
if (angleDegrees >= 337.5 || angleDegrees < 22.5)
|
||||
return "동쪽(→)";
|
||||
else if (angleDegrees >= 22.5 && angleDegrees < 67.5)
|
||||
return "남동쪽(↘)";
|
||||
else if (angleDegrees >= 67.5 && angleDegrees < 112.5)
|
||||
return "남쪽(↓)";
|
||||
else if (angleDegrees >= 112.5 && angleDegrees < 157.5)
|
||||
return "남서쪽(↙)";
|
||||
else if (angleDegrees >= 157.5 && angleDegrees < 202.5)
|
||||
return "서쪽(←)";
|
||||
else if (angleDegrees >= 202.5 && angleDegrees < 247.5)
|
||||
return "북서쪽(↖)";
|
||||
else if (angleDegrees >= 247.5 && angleDegrees < 292.5)
|
||||
return "북쪽(↑)";
|
||||
else if (angleDegrees >= 292.5 && angleDegrees < 337.5)
|
||||
return "북동쪽(↗)";
|
||||
else
|
||||
return "알 수 없음";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 리프트 계산 결과 정보
|
||||
/// </summary>
|
||||
public class LiftCalculationResult
|
||||
{
|
||||
public double AngleRadians { get; set; }
|
||||
public double AngleDegrees { get; set; }
|
||||
public string DirectionString { get; set; }
|
||||
public string CalculationMethod { get; set; }
|
||||
public AgvDirection MotorDirection { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 종합적인 리프트 계산 (모든 정보 포함)
|
||||
/// </summary>
|
||||
/// <param name="currentPos">현재 위치</param>
|
||||
/// <param name="targetPos">목표 위치</param>
|
||||
/// <param name="motorDirection">모터 방향</param>
|
||||
/// <returns>리프트 계산 결과</returns>
|
||||
public static LiftCalculationResult CalculateLiftInfo(Point currentPos, Point targetPos, AgvDirection motorDirection)
|
||||
{
|
||||
var angleRadians = CalculateLiftAngleRadians(currentPos, targetPos, motorDirection);
|
||||
var angleDegrees = angleRadians * 180.0 / Math.PI;
|
||||
|
||||
// 0-360도 범위로 정규화
|
||||
while (angleDegrees < 0) angleDegrees += 360;
|
||||
while (angleDegrees >= 360) angleDegrees -= 360;
|
||||
|
||||
var directionString = AngleToDirectionString(angleDegrees);
|
||||
|
||||
string calculationMethod;
|
||||
if (motorDirection == AgvDirection.Forward)
|
||||
calculationMethod = "이동방향 + 180도 (전진모터)";
|
||||
else if (motorDirection == AgvDirection.Backward)
|
||||
calculationMethod = "이동방향과 동일 (후진모터 - 리프트는 이동방향에 위치)";
|
||||
else
|
||||
calculationMethod = "기본값 (전진모터)";
|
||||
|
||||
return new LiftCalculationResult
|
||||
{
|
||||
AngleRadians = angleRadians,
|
||||
AngleDegrees = angleDegrees,
|
||||
DirectionString = directionString,
|
||||
CalculationMethod = calculationMethod,
|
||||
MotorDirection = motorDirection
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 위치 기반 노드 찾기
|
||||
/// </summary>
|
||||
/// <param name="mapNodes">맵 노드 리스트</param>
|
||||
/// <param name="position">찾을 위치</param>
|
||||
/// <param name="tolerance">허용 오차</param>
|
||||
/// <returns>해당하는 노드 또는 null</returns>
|
||||
private static MapNode FindNodeByPosition(List<MapNode> mapNodes, Point position, int tolerance)
|
||||
{
|
||||
return mapNodes.FirstOrDefault(node =>
|
||||
Math.Abs(node.Position.X - position.X) <= tolerance &&
|
||||
Math.Abs(node.Position.Y - position.Y) <= tolerance);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 노드에서 연결된 다른 노드들 찾기
|
||||
/// </summary>
|
||||
/// <param name="mapNodes">맵 노드 리스트</param>
|
||||
/// <param name="currentNode">현재 노드</param>
|
||||
/// <returns>연결된 노드 리스트</returns>
|
||||
private static List<MapNode> GetConnectedNodes(List<MapNode> mapNodes, MapNode currentNode)
|
||||
{
|
||||
var connectedNodes = new List<MapNode>();
|
||||
|
||||
foreach (var nodeId in currentNode.ConnectedNodes)
|
||||
{
|
||||
var connectedNode = mapNodes.FirstOrDefault(n => n.NodeId == nodeId);
|
||||
if (connectedNode != null)
|
||||
{
|
||||
connectedNodes.Add(connectedNode);
|
||||
}
|
||||
}
|
||||
|
||||
return connectedNodes;
|
||||
}
|
||||
}
|
||||
}
|
||||
29
Cs_HMI/AGVLogic/AGVNavigationCore/build.bat
Normal file
29
Cs_HMI/AGVLogic/AGVNavigationCore/build.bat
Normal file
@@ -0,0 +1,29 @@
|
||||
@echo off
|
||||
echo Building V2GDecoder VC++ Project...
|
||||
|
||||
REM Check if Visual Studio 2022 is installed (Professional or Community)
|
||||
set MSBUILD_PRO="C:\Program Files\Microsoft Visual Studio\2022\Professional\MSBuild\Current\Bin\MSBuild.exe"
|
||||
set MSBUILD_COM="C:\Program Files\Microsoft Visual Studio\2022\Community\MSBuild\Current\Bin\MSBuild.exe"
|
||||
set MSBUILD_BT="F:\(VHD) Program Files\Microsoft Visual Studio\2022\MSBuild\Current\Bin\MSBuild.exe"
|
||||
|
||||
if exist %MSBUILD_PRO% (
|
||||
echo "Found Visual Studio 2022 Professional"
|
||||
set MSBUILD=%MSBUILD_PRO%
|
||||
) else if exist %MSBUILD_COM% (
|
||||
echo "Found Visual Studio 2022 Community"
|
||||
set MSBUILD=%MSBUILD_COM%
|
||||
) else if exist %MSBUILD_BT% (
|
||||
echo "Found Visual Studio 2022 BuildTools"
|
||||
set MSBUILD=%MSBUILD_BT%
|
||||
) else (
|
||||
echo "Visual Studio 2022 (Professional or Community) not found!"
|
||||
echo "Please install Visual Studio 2022 or update the MSBuild path."
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM Build Debug x64 configuration
|
||||
echo Building Debug x64 configuration...
|
||||
%MSBUILD% AGVNavigationCore.csproj
|
||||
|
||||
pause
|
||||
4
Cs_HMI/AGVLogic/AGVNavigationCore/packages.config
Normal file
4
Cs_HMI/AGVLogic/AGVNavigationCore/packages.config
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<packages>
|
||||
<package id="Newtonsoft.Json" version="13.0.3" targetFramework="net48" />
|
||||
</packages>
|
||||
80
Cs_HMI/AGVLogic/AGVSimulator/AGVSimulator.csproj
Normal file
80
Cs_HMI/AGVLogic/AGVSimulator/AGVSimulator.csproj
Normal file
@@ -0,0 +1,80 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
|
||||
<PropertyGroup>
|
||||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||
<ProjectGuid>{B2C3D4E5-0000-0000-0000-000000000000}</ProjectGuid>
|
||||
<OutputType>Exe</OutputType>
|
||||
<RootNamespace>AGVSimulator</RootNamespace>
|
||||
<AssemblyName>AGVSimulator</AssemblyName>
|
||||
<TargetFrameworkVersion>v4.8</TargetFrameworkVersion>
|
||||
<FileAlignment>512</FileAlignment>
|
||||
<UseWindowsForms>true</UseWindowsForms>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
<DebugType>full</DebugType>
|
||||
<Optimize>false</Optimize>
|
||||
<OutputPath>bin\Debug\</OutputPath>
|
||||
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||
<DebugType>pdbonly</DebugType>
|
||||
<Optimize>true</Optimize>
|
||||
<OutputPath>bin\Release\</OutputPath>
|
||||
<DefineConstants>TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<StartupObject />
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="System" />
|
||||
<Reference Include="System.Core" />
|
||||
<Reference Include="System.Drawing" />
|
||||
<Reference Include="System.Windows.Forms" />
|
||||
<Reference Include="System.Xml" />
|
||||
<Reference Include="Newtonsoft.Json, Version=13.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed">
|
||||
<HintPath>..\packages\Newtonsoft.Json.13.0.3\lib\net45\Newtonsoft.Json.dll</HintPath>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="Models\SimulatorConfig.cs" />
|
||||
<Compile Include="Models\VirtualAGV.cs" />
|
||||
<Compile Include="Models\SimulationState.cs" />
|
||||
<Compile Include="Forms\SimulatorForm.cs">
|
||||
<SubType>Form</SubType>
|
||||
</Compile>
|
||||
<Compile Include="Forms\SimulatorForm.Designer.cs">
|
||||
<DependentUpon>SimulatorForm.cs</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="Program.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Forms\SimulatorForm.resx">
|
||||
<DependentUpon>SimulatorForm.cs</DependentUpon>
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="build.bat" />
|
||||
<None Include="packages.config" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\AGVNavigationCore\AGVNavigationCore.csproj">
|
||||
<Project>{C5F7A8B2-8D3E-4A1B-9C6E-7F4D5E2A9B1C}</Project>
|
||||
<Name>AGVNavigationCore</Name>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\AGVMapEditor\AGVMapEditor.csproj">
|
||||
<Project>{a1b2c3d4-e5f6-7890-abcd-ef1234567890}</Project>
|
||||
<Name>AGVMapEditor</Name>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
</Project>
|
||||
833
Cs_HMI/AGVLogic/AGVSimulator/Forms/SimulatorForm.Designer.cs
generated
Normal file
833
Cs_HMI/AGVLogic/AGVSimulator/Forms/SimulatorForm.Designer.cs
generated
Normal file
@@ -0,0 +1,833 @@
|
||||
namespace AGVSimulator.Forms
|
||||
{
|
||||
partial class SimulatorForm
|
||||
{
|
||||
/// <summary>
|
||||
/// 필수 디자이너 변수입니다.
|
||||
/// </summary>
|
||||
private System.ComponentModel.IContainer components = null;
|
||||
|
||||
/// <summary>
|
||||
/// 사용 중인 모든 리소스를 정리합니다.
|
||||
/// </summary>
|
||||
/// <param name="disposing">관리되는 리소스를 삭제해야 하면 true이고, 그렇지 않으면 false입니다.</param>
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && (components != null))
|
||||
{
|
||||
components.Dispose();
|
||||
}
|
||||
|
||||
// 시뮬레이션 정지
|
||||
if (_simulationTimer != null)
|
||||
{
|
||||
_simulationTimer.Stop();
|
||||
_simulationTimer.Dispose();
|
||||
}
|
||||
|
||||
// AGV 정리
|
||||
if (_agvList != null)
|
||||
{
|
||||
foreach (var agv in _agvList)
|
||||
{
|
||||
agv.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
#region Windows Form 디자이너에서 생성한 코드
|
||||
|
||||
/// <summary>
|
||||
/// 디자이너 지원에 필요한 메서드입니다.
|
||||
/// 이 메서드의 내용을 코드 편집기로 수정하지 마세요.
|
||||
/// </summary>
|
||||
private void InitializeComponent()
|
||||
{
|
||||
this._menuStrip = new System.Windows.Forms.MenuStrip();
|
||||
this.fileToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.openMapToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.reloadMapToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.toolStripSeparator1 = new System.Windows.Forms.ToolStripSeparator();
|
||||
this.launchMapEditorToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.toolStripSeparator4 = new System.Windows.Forms.ToolStripSeparator();
|
||||
this.exitToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.simulationToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.startSimulationToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.stopSimulationToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.resetToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.viewToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.fitToMapToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.resetZoomToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.helpToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.aboutToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this._toolStrip = new System.Windows.Forms.ToolStrip();
|
||||
this.openMapToolStripButton = new System.Windows.Forms.ToolStripButton();
|
||||
this.reloadMapToolStripButton = new System.Windows.Forms.ToolStripButton();
|
||||
this.launchMapEditorToolStripButton = new System.Windows.Forms.ToolStripButton();
|
||||
this.toolStripSeparator2 = new System.Windows.Forms.ToolStripSeparator();
|
||||
this.startSimulationToolStripButton = new System.Windows.Forms.ToolStripButton();
|
||||
this.stopSimulationToolStripButton = new System.Windows.Forms.ToolStripButton();
|
||||
this.resetToolStripButton = new System.Windows.Forms.ToolStripButton();
|
||||
this.btAllReset = new System.Windows.Forms.ToolStripButton();
|
||||
this.toolStripSeparator3 = new System.Windows.Forms.ToolStripSeparator();
|
||||
this.fitToMapToolStripButton = new System.Windows.Forms.ToolStripButton();
|
||||
this.resetZoomToolStripButton = new System.Windows.Forms.ToolStripButton();
|
||||
this._statusStrip = new System.Windows.Forms.StatusStrip();
|
||||
this._statusLabel = new System.Windows.Forms.ToolStripStatusLabel();
|
||||
this._coordLabel = new System.Windows.Forms.ToolStripStatusLabel();
|
||||
this._controlPanel = new System.Windows.Forms.Panel();
|
||||
this._statusGroup = new System.Windows.Forms.GroupBox();
|
||||
this._pathLengthLabel = new System.Windows.Forms.Label();
|
||||
this._agvCountLabel = new System.Windows.Forms.Label();
|
||||
this._simulationStatusLabel = new System.Windows.Forms.Label();
|
||||
this._pathGroup = new System.Windows.Forms.GroupBox();
|
||||
this._clearPathButton = new System.Windows.Forms.Button();
|
||||
this._startPathButton = new System.Windows.Forms.Button();
|
||||
this._calculatePathButton = new System.Windows.Forms.Button();
|
||||
this._targetCalcButton = new System.Windows.Forms.Button();
|
||||
this._avoidRotationCheckBox = new System.Windows.Forms.CheckBox();
|
||||
this._targetNodeCombo = new System.Windows.Forms.ComboBox();
|
||||
this.targetNodeLabel = new System.Windows.Forms.Label();
|
||||
this._startNodeCombo = new System.Windows.Forms.ComboBox();
|
||||
this.startNodeLabel = new System.Windows.Forms.Label();
|
||||
this._agvControlGroup = new System.Windows.Forms.GroupBox();
|
||||
this._setPositionButton = new System.Windows.Forms.Button();
|
||||
this._rfidTextBox = new System.Windows.Forms.TextBox();
|
||||
this._rfidLabel = new System.Windows.Forms.Label();
|
||||
this._directionCombo = new System.Windows.Forms.ComboBox();
|
||||
this._directionLabel = new System.Windows.Forms.Label();
|
||||
this._stopSimulationButton = new System.Windows.Forms.Button();
|
||||
this._startSimulationButton = new System.Windows.Forms.Button();
|
||||
this._removeAgvButton = new System.Windows.Forms.Button();
|
||||
this._addAgvButton = new System.Windows.Forms.Button();
|
||||
this._agvListCombo = new System.Windows.Forms.ComboBox();
|
||||
this._canvasPanel = new System.Windows.Forms.Panel();
|
||||
this._agvInfoPanel = new System.Windows.Forms.Panel();
|
||||
this._agvInfoTitleLabel = new System.Windows.Forms.Label();
|
||||
this._liftDirectionLabel = new System.Windows.Forms.Label();
|
||||
this._motorDirectionLabel = new System.Windows.Forms.Label();
|
||||
this._pathDebugLabel = new System.Windows.Forms.Label();
|
||||
this._menuStrip.SuspendLayout();
|
||||
this._toolStrip.SuspendLayout();
|
||||
this._statusStrip.SuspendLayout();
|
||||
this._controlPanel.SuspendLayout();
|
||||
this._statusGroup.SuspendLayout();
|
||||
this._pathGroup.SuspendLayout();
|
||||
this._agvControlGroup.SuspendLayout();
|
||||
this._agvInfoPanel.SuspendLayout();
|
||||
this.SuspendLayout();
|
||||
//
|
||||
// _menuStrip
|
||||
//
|
||||
this._menuStrip.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
|
||||
this.fileToolStripMenuItem,
|
||||
this.simulationToolStripMenuItem,
|
||||
this.viewToolStripMenuItem,
|
||||
this.helpToolStripMenuItem});
|
||||
this._menuStrip.Location = new System.Drawing.Point(0, 0);
|
||||
this._menuStrip.Name = "_menuStrip";
|
||||
this._menuStrip.Size = new System.Drawing.Size(1200, 24);
|
||||
this._menuStrip.TabIndex = 0;
|
||||
this._menuStrip.Text = "menuStrip";
|
||||
//
|
||||
// fileToolStripMenuItem
|
||||
//
|
||||
this.fileToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
|
||||
this.openMapToolStripMenuItem,
|
||||
this.reloadMapToolStripMenuItem,
|
||||
this.toolStripSeparator1,
|
||||
this.launchMapEditorToolStripMenuItem,
|
||||
this.toolStripSeparator4,
|
||||
this.exitToolStripMenuItem});
|
||||
this.fileToolStripMenuItem.Name = "fileToolStripMenuItem";
|
||||
this.fileToolStripMenuItem.Size = new System.Drawing.Size(57, 20);
|
||||
this.fileToolStripMenuItem.Text = "파일(&F)";
|
||||
//
|
||||
// openMapToolStripMenuItem
|
||||
//
|
||||
this.openMapToolStripMenuItem.Name = "openMapToolStripMenuItem";
|
||||
this.openMapToolStripMenuItem.ShortcutKeys = ((System.Windows.Forms.Keys)((System.Windows.Forms.Keys.Control | System.Windows.Forms.Keys.O)));
|
||||
this.openMapToolStripMenuItem.Size = new System.Drawing.Size(221, 22);
|
||||
this.openMapToolStripMenuItem.Text = "맵 열기(&O)...";
|
||||
this.openMapToolStripMenuItem.Click += new System.EventHandler(this.OnOpenMap_Click);
|
||||
//
|
||||
// reloadMapToolStripMenuItem
|
||||
//
|
||||
this.reloadMapToolStripMenuItem.Name = "reloadMapToolStripMenuItem";
|
||||
this.reloadMapToolStripMenuItem.ShortcutKeys = ((System.Windows.Forms.Keys)((System.Windows.Forms.Keys.Control | System.Windows.Forms.Keys.R)));
|
||||
this.reloadMapToolStripMenuItem.Size = new System.Drawing.Size(221, 22);
|
||||
this.reloadMapToolStripMenuItem.Text = "맵 다시열기(&R)";
|
||||
this.reloadMapToolStripMenuItem.Click += new System.EventHandler(this.OnReloadMap_Click);
|
||||
//
|
||||
// toolStripSeparator1
|
||||
//
|
||||
this.toolStripSeparator1.Name = "toolStripSeparator1";
|
||||
this.toolStripSeparator1.Size = new System.Drawing.Size(218, 6);
|
||||
//
|
||||
// launchMapEditorToolStripMenuItem
|
||||
//
|
||||
this.launchMapEditorToolStripMenuItem.Name = "launchMapEditorToolStripMenuItem";
|
||||
this.launchMapEditorToolStripMenuItem.ShortcutKeys = ((System.Windows.Forms.Keys)((System.Windows.Forms.Keys.Control | System.Windows.Forms.Keys.M)));
|
||||
this.launchMapEditorToolStripMenuItem.Size = new System.Drawing.Size(221, 22);
|
||||
this.launchMapEditorToolStripMenuItem.Text = "MapEditor 실행(&M)";
|
||||
this.launchMapEditorToolStripMenuItem.Click += new System.EventHandler(this.OnLaunchMapEditor_Click);
|
||||
//
|
||||
// toolStripSeparator4
|
||||
//
|
||||
this.toolStripSeparator4.Name = "toolStripSeparator4";
|
||||
this.toolStripSeparator4.Size = new System.Drawing.Size(218, 6);
|
||||
//
|
||||
// exitToolStripMenuItem
|
||||
//
|
||||
this.exitToolStripMenuItem.Name = "exitToolStripMenuItem";
|
||||
this.exitToolStripMenuItem.ShortcutKeys = ((System.Windows.Forms.Keys)((System.Windows.Forms.Keys.Alt | System.Windows.Forms.Keys.F4)));
|
||||
this.exitToolStripMenuItem.Size = new System.Drawing.Size(221, 22);
|
||||
this.exitToolStripMenuItem.Text = "종료(&X)";
|
||||
this.exitToolStripMenuItem.Click += new System.EventHandler(this.OnExit_Click);
|
||||
//
|
||||
// simulationToolStripMenuItem
|
||||
//
|
||||
this.simulationToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
|
||||
this.startSimulationToolStripMenuItem,
|
||||
this.stopSimulationToolStripMenuItem,
|
||||
this.resetToolStripMenuItem});
|
||||
this.simulationToolStripMenuItem.Name = "simulationToolStripMenuItem";
|
||||
this.simulationToolStripMenuItem.Size = new System.Drawing.Size(94, 20);
|
||||
this.simulationToolStripMenuItem.Text = "시뮬레이션(&S)";
|
||||
//
|
||||
// startSimulationToolStripMenuItem
|
||||
//
|
||||
this.startSimulationToolStripMenuItem.Name = "startSimulationToolStripMenuItem";
|
||||
this.startSimulationToolStripMenuItem.ShortcutKeys = System.Windows.Forms.Keys.F5;
|
||||
this.startSimulationToolStripMenuItem.Size = new System.Drawing.Size(145, 22);
|
||||
this.startSimulationToolStripMenuItem.Text = "시작(&S)";
|
||||
this.startSimulationToolStripMenuItem.Click += new System.EventHandler(this.OnStartSimulation_Click);
|
||||
//
|
||||
// stopSimulationToolStripMenuItem
|
||||
//
|
||||
this.stopSimulationToolStripMenuItem.Name = "stopSimulationToolStripMenuItem";
|
||||
this.stopSimulationToolStripMenuItem.ShortcutKeys = System.Windows.Forms.Keys.F6;
|
||||
this.stopSimulationToolStripMenuItem.Size = new System.Drawing.Size(145, 22);
|
||||
this.stopSimulationToolStripMenuItem.Text = "정지(&T)";
|
||||
this.stopSimulationToolStripMenuItem.Click += new System.EventHandler(this.OnStopSimulation_Click);
|
||||
//
|
||||
// resetToolStripMenuItem
|
||||
//
|
||||
this.resetToolStripMenuItem.Name = "resetToolStripMenuItem";
|
||||
this.resetToolStripMenuItem.ShortcutKeys = System.Windows.Forms.Keys.F7;
|
||||
this.resetToolStripMenuItem.Size = new System.Drawing.Size(145, 22);
|
||||
this.resetToolStripMenuItem.Text = "초기화(&R)";
|
||||
this.resetToolStripMenuItem.Click += new System.EventHandler(this.OnReset_Click);
|
||||
//
|
||||
// viewToolStripMenuItem
|
||||
//
|
||||
this.viewToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
|
||||
this.fitToMapToolStripMenuItem,
|
||||
this.resetZoomToolStripMenuItem});
|
||||
this.viewToolStripMenuItem.Name = "viewToolStripMenuItem";
|
||||
this.viewToolStripMenuItem.Size = new System.Drawing.Size(59, 20);
|
||||
this.viewToolStripMenuItem.Text = "보기(&V)";
|
||||
//
|
||||
// fitToMapToolStripMenuItem
|
||||
//
|
||||
this.fitToMapToolStripMenuItem.Name = "fitToMapToolStripMenuItem";
|
||||
this.fitToMapToolStripMenuItem.ShortcutKeys = ((System.Windows.Forms.Keys)((System.Windows.Forms.Keys.Control | System.Windows.Forms.Keys.F)));
|
||||
this.fitToMapToolStripMenuItem.Size = new System.Drawing.Size(182, 22);
|
||||
this.fitToMapToolStripMenuItem.Text = "맵 맞춤(&F)";
|
||||
this.fitToMapToolStripMenuItem.Click += new System.EventHandler(this.OnFitToMap_Click);
|
||||
//
|
||||
// resetZoomToolStripMenuItem
|
||||
//
|
||||
this.resetZoomToolStripMenuItem.Name = "resetZoomToolStripMenuItem";
|
||||
this.resetZoomToolStripMenuItem.ShortcutKeys = ((System.Windows.Forms.Keys)((System.Windows.Forms.Keys.Control | System.Windows.Forms.Keys.D0)));
|
||||
this.resetZoomToolStripMenuItem.Size = new System.Drawing.Size(182, 22);
|
||||
this.resetZoomToolStripMenuItem.Text = "줌 초기화(&Z)";
|
||||
this.resetZoomToolStripMenuItem.Click += new System.EventHandler(this.OnResetZoom_Click);
|
||||
//
|
||||
// helpToolStripMenuItem
|
||||
//
|
||||
this.helpToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
|
||||
this.aboutToolStripMenuItem});
|
||||
this.helpToolStripMenuItem.Name = "helpToolStripMenuItem";
|
||||
this.helpToolStripMenuItem.Size = new System.Drawing.Size(72, 20);
|
||||
this.helpToolStripMenuItem.Text = "도움말(&H)";
|
||||
//
|
||||
// aboutToolStripMenuItem
|
||||
//
|
||||
this.aboutToolStripMenuItem.Name = "aboutToolStripMenuItem";
|
||||
this.aboutToolStripMenuItem.Size = new System.Drawing.Size(123, 22);
|
||||
this.aboutToolStripMenuItem.Text = "정보(&A)...";
|
||||
this.aboutToolStripMenuItem.Click += new System.EventHandler(this.OnAbout_Click);
|
||||
//
|
||||
// _toolStrip
|
||||
//
|
||||
this._toolStrip.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
|
||||
this.openMapToolStripButton,
|
||||
this.reloadMapToolStripButton,
|
||||
this.launchMapEditorToolStripButton,
|
||||
this.toolStripSeparator2,
|
||||
this.startSimulationToolStripButton,
|
||||
this.stopSimulationToolStripButton,
|
||||
this.resetToolStripButton,
|
||||
this.btAllReset,
|
||||
this.toolStripSeparator3,
|
||||
this.fitToMapToolStripButton,
|
||||
this.resetZoomToolStripButton});
|
||||
this._toolStrip.Location = new System.Drawing.Point(0, 24);
|
||||
this._toolStrip.Name = "_toolStrip";
|
||||
this._toolStrip.Size = new System.Drawing.Size(1200, 25);
|
||||
this._toolStrip.TabIndex = 1;
|
||||
this._toolStrip.Text = "toolStrip";
|
||||
//
|
||||
// openMapToolStripButton
|
||||
//
|
||||
this.openMapToolStripButton.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
|
||||
this.openMapToolStripButton.Name = "openMapToolStripButton";
|
||||
this.openMapToolStripButton.Size = new System.Drawing.Size(51, 22);
|
||||
this.openMapToolStripButton.Text = "맵 열기";
|
||||
this.openMapToolStripButton.ToolTipText = "맵 파일을 엽니다";
|
||||
this.openMapToolStripButton.Click += new System.EventHandler(this.OnOpenMap_Click);
|
||||
//
|
||||
// reloadMapToolStripButton
|
||||
//
|
||||
this.reloadMapToolStripButton.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
|
||||
this.reloadMapToolStripButton.Name = "reloadMapToolStripButton";
|
||||
this.reloadMapToolStripButton.Size = new System.Drawing.Size(59, 22);
|
||||
this.reloadMapToolStripButton.Text = "다시열기";
|
||||
this.reloadMapToolStripButton.ToolTipText = "현재 맵을 다시 로드합니다";
|
||||
this.reloadMapToolStripButton.Click += new System.EventHandler(this.OnReloadMap_Click);
|
||||
//
|
||||
// launchMapEditorToolStripButton
|
||||
//
|
||||
this.launchMapEditorToolStripButton.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
|
||||
this.launchMapEditorToolStripButton.Name = "launchMapEditorToolStripButton";
|
||||
this.launchMapEditorToolStripButton.Size = new System.Drawing.Size(66, 22);
|
||||
this.launchMapEditorToolStripButton.Text = "MapEditor";
|
||||
this.launchMapEditorToolStripButton.ToolTipText = "MapEditor를 실행합니다";
|
||||
this.launchMapEditorToolStripButton.Click += new System.EventHandler(this.OnLaunchMapEditor_Click);
|
||||
//
|
||||
// toolStripSeparator2
|
||||
//
|
||||
this.toolStripSeparator2.Name = "toolStripSeparator2";
|
||||
this.toolStripSeparator2.Size = new System.Drawing.Size(6, 25);
|
||||
//
|
||||
// startSimulationToolStripButton
|
||||
//
|
||||
this.startSimulationToolStripButton.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
|
||||
this.startSimulationToolStripButton.Name = "startSimulationToolStripButton";
|
||||
this.startSimulationToolStripButton.Size = new System.Drawing.Size(99, 22);
|
||||
this.startSimulationToolStripButton.Text = "시뮬레이션 시작";
|
||||
this.startSimulationToolStripButton.ToolTipText = "시뮬레이션을 시작합니다";
|
||||
this.startSimulationToolStripButton.Click += new System.EventHandler(this.OnStartSimulation_Click);
|
||||
//
|
||||
// stopSimulationToolStripButton
|
||||
//
|
||||
this.stopSimulationToolStripButton.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
|
||||
this.stopSimulationToolStripButton.Name = "stopSimulationToolStripButton";
|
||||
this.stopSimulationToolStripButton.Size = new System.Drawing.Size(99, 22);
|
||||
this.stopSimulationToolStripButton.Text = "시뮬레이션 정지";
|
||||
this.stopSimulationToolStripButton.ToolTipText = "시뮬레이션을 정지합니다";
|
||||
this.stopSimulationToolStripButton.Click += new System.EventHandler(this.OnStopSimulation_Click);
|
||||
//
|
||||
// resetToolStripButton
|
||||
//
|
||||
this.resetToolStripButton.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
|
||||
this.resetToolStripButton.Name = "resetToolStripButton";
|
||||
this.resetToolStripButton.Size = new System.Drawing.Size(47, 22);
|
||||
this.resetToolStripButton.Text = "초기화";
|
||||
this.resetToolStripButton.ToolTipText = "시뮬레이션을 초기화합니다";
|
||||
this.resetToolStripButton.Click += new System.EventHandler(this.OnReset_Click);
|
||||
//
|
||||
// btAllReset
|
||||
//
|
||||
this.btAllReset.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
|
||||
this.btAllReset.Name = "btAllReset";
|
||||
this.btAllReset.Size = new System.Drawing.Size(71, 22);
|
||||
this.btAllReset.Text = "전체초기화";
|
||||
this.btAllReset.ToolTipText = "시뮬레이션을 초기화합니다";
|
||||
this.btAllReset.Click += new System.EventHandler(this.btAllReset_Click);
|
||||
//
|
||||
// toolStripSeparator3
|
||||
//
|
||||
this.toolStripSeparator3.Name = "toolStripSeparator3";
|
||||
this.toolStripSeparator3.Size = new System.Drawing.Size(6, 25);
|
||||
//
|
||||
// fitToMapToolStripButton
|
||||
//
|
||||
this.fitToMapToolStripButton.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
|
||||
this.fitToMapToolStripButton.Name = "fitToMapToolStripButton";
|
||||
this.fitToMapToolStripButton.Size = new System.Drawing.Size(51, 22);
|
||||
this.fitToMapToolStripButton.Text = "맵 맞춤";
|
||||
this.fitToMapToolStripButton.ToolTipText = "맵 전체를 화면에 맞춥니다";
|
||||
this.fitToMapToolStripButton.Click += new System.EventHandler(this.OnFitToMap_Click);
|
||||
//
|
||||
// resetZoomToolStripButton
|
||||
//
|
||||
this.resetZoomToolStripButton.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
|
||||
this.resetZoomToolStripButton.Name = "resetZoomToolStripButton";
|
||||
this.resetZoomToolStripButton.Size = new System.Drawing.Size(63, 22);
|
||||
this.resetZoomToolStripButton.Text = "줌 초기화";
|
||||
this.resetZoomToolStripButton.ToolTipText = "줌을 초기화합니다";
|
||||
this.resetZoomToolStripButton.Click += new System.EventHandler(this.OnResetZoom_Click);
|
||||
//
|
||||
// _statusStrip
|
||||
//
|
||||
this._statusStrip.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
|
||||
this._statusLabel,
|
||||
this._coordLabel});
|
||||
this._statusStrip.Location = new System.Drawing.Point(0, 778);
|
||||
this._statusStrip.Name = "_statusStrip";
|
||||
this._statusStrip.Size = new System.Drawing.Size(1200, 22);
|
||||
this._statusStrip.TabIndex = 2;
|
||||
this._statusStrip.Text = "statusStrip";
|
||||
//
|
||||
// _statusLabel
|
||||
//
|
||||
this._statusLabel.Name = "_statusLabel";
|
||||
this._statusLabel.Size = new System.Drawing.Size(31, 17);
|
||||
this._statusLabel.Text = "준비";
|
||||
//
|
||||
// _coordLabel
|
||||
//
|
||||
this._coordLabel.Name = "_coordLabel";
|
||||
this._coordLabel.Size = new System.Drawing.Size(0, 17);
|
||||
//
|
||||
// _controlPanel
|
||||
//
|
||||
this._controlPanel.BackColor = System.Drawing.SystemColors.Control;
|
||||
this._controlPanel.Controls.Add(this._statusGroup);
|
||||
this._controlPanel.Controls.Add(this._pathGroup);
|
||||
this._controlPanel.Controls.Add(this._agvControlGroup);
|
||||
this._controlPanel.Dock = System.Windows.Forms.DockStyle.Right;
|
||||
this._controlPanel.Location = new System.Drawing.Point(967, 49);
|
||||
this._controlPanel.Name = "_controlPanel";
|
||||
this._controlPanel.Size = new System.Drawing.Size(233, 729);
|
||||
this._controlPanel.TabIndex = 3;
|
||||
//
|
||||
// _statusGroup
|
||||
//
|
||||
this._statusGroup.Controls.Add(this._pathLengthLabel);
|
||||
this._statusGroup.Controls.Add(this._agvCountLabel);
|
||||
this._statusGroup.Controls.Add(this._simulationStatusLabel);
|
||||
this._statusGroup.Dock = System.Windows.Forms.DockStyle.Top;
|
||||
this._statusGroup.Location = new System.Drawing.Point(0, 446);
|
||||
this._statusGroup.Name = "_statusGroup";
|
||||
this._statusGroup.Size = new System.Drawing.Size(233, 100);
|
||||
this._statusGroup.TabIndex = 3;
|
||||
this._statusGroup.TabStop = false;
|
||||
this._statusGroup.Text = "상태 정보";
|
||||
//
|
||||
// _pathLengthLabel
|
||||
//
|
||||
this._pathLengthLabel.AutoSize = true;
|
||||
this._pathLengthLabel.Location = new System.Drawing.Point(10, 65);
|
||||
this._pathLengthLabel.Name = "_pathLengthLabel";
|
||||
this._pathLengthLabel.Size = new System.Drawing.Size(71, 12);
|
||||
this._pathLengthLabel.TabIndex = 2;
|
||||
this._pathLengthLabel.Text = "경로 길이: -";
|
||||
//
|
||||
// _agvCountLabel
|
||||
//
|
||||
this._agvCountLabel.AutoSize = true;
|
||||
this._agvCountLabel.Location = new System.Drawing.Point(10, 45);
|
||||
this._agvCountLabel.Name = "_agvCountLabel";
|
||||
this._agvCountLabel.Size = new System.Drawing.Size(60, 12);
|
||||
this._agvCountLabel.TabIndex = 1;
|
||||
this._agvCountLabel.Text = "AGV 수: 0";
|
||||
//
|
||||
// _simulationStatusLabel
|
||||
//
|
||||
this._simulationStatusLabel.AutoSize = true;
|
||||
this._simulationStatusLabel.Location = new System.Drawing.Point(10, 25);
|
||||
this._simulationStatusLabel.Name = "_simulationStatusLabel";
|
||||
this._simulationStatusLabel.Size = new System.Drawing.Size(97, 12);
|
||||
this._simulationStatusLabel.TabIndex = 0;
|
||||
this._simulationStatusLabel.Text = "시뮬레이션: 정지";
|
||||
//
|
||||
// _pathGroup
|
||||
//
|
||||
this._pathGroup.Controls.Add(this._clearPathButton);
|
||||
this._pathGroup.Controls.Add(this._startPathButton);
|
||||
this._pathGroup.Controls.Add(this._calculatePathButton);
|
||||
this._pathGroup.Controls.Add(this._targetCalcButton);
|
||||
this._pathGroup.Controls.Add(this._avoidRotationCheckBox);
|
||||
this._pathGroup.Controls.Add(this._targetNodeCombo);
|
||||
this._pathGroup.Controls.Add(this.targetNodeLabel);
|
||||
this._pathGroup.Controls.Add(this._startNodeCombo);
|
||||
this._pathGroup.Controls.Add(this.startNodeLabel);
|
||||
this._pathGroup.Dock = System.Windows.Forms.DockStyle.Top;
|
||||
this._pathGroup.Location = new System.Drawing.Point(0, 214);
|
||||
this._pathGroup.Name = "_pathGroup";
|
||||
this._pathGroup.Size = new System.Drawing.Size(233, 232);
|
||||
this._pathGroup.TabIndex = 1;
|
||||
this._pathGroup.TabStop = false;
|
||||
this._pathGroup.Text = "경로 제어";
|
||||
//
|
||||
// _clearPathButton
|
||||
//
|
||||
this._clearPathButton.Location = new System.Drawing.Point(150, 177);
|
||||
this._clearPathButton.Name = "_clearPathButton";
|
||||
this._clearPathButton.Size = new System.Drawing.Size(70, 25);
|
||||
this._clearPathButton.TabIndex = 6;
|
||||
this._clearPathButton.Text = "경로 지우기";
|
||||
this._clearPathButton.UseVisualStyleBackColor = true;
|
||||
this._clearPathButton.Click += new System.EventHandler(this.OnClearPath_Click);
|
||||
//
|
||||
// _startPathButton
|
||||
//
|
||||
this._startPathButton.Location = new System.Drawing.Point(80, 177);
|
||||
this._startPathButton.Name = "_startPathButton";
|
||||
this._startPathButton.Size = new System.Drawing.Size(65, 25);
|
||||
this._startPathButton.TabIndex = 5;
|
||||
this._startPathButton.Text = "경로 시작";
|
||||
this._startPathButton.UseVisualStyleBackColor = true;
|
||||
this._startPathButton.Click += new System.EventHandler(this.OnStartPath_Click);
|
||||
//
|
||||
// _calculatePathButton
|
||||
//
|
||||
this._calculatePathButton.Location = new System.Drawing.Point(10, 177);
|
||||
this._calculatePathButton.Name = "_calculatePathButton";
|
||||
this._calculatePathButton.Size = new System.Drawing.Size(65, 25);
|
||||
this._calculatePathButton.TabIndex = 4;
|
||||
this._calculatePathButton.Text = "경로 계산";
|
||||
this._calculatePathButton.UseVisualStyleBackColor = true;
|
||||
this._calculatePathButton.Click += new System.EventHandler(this.OnCalculatePath_Click);
|
||||
//
|
||||
//
|
||||
// _targetCalcButton
|
||||
//
|
||||
this._targetCalcButton.Location = new System.Drawing.Point(10, 148);
|
||||
this._targetCalcButton.Name = "_targetCalcButton";
|
||||
this._targetCalcButton.Size = new System.Drawing.Size(70, 25);
|
||||
this._targetCalcButton.TabIndex = 9;
|
||||
this._targetCalcButton.Text = "타겟계산";
|
||||
this._targetCalcButton.UseVisualStyleBackColor = true;
|
||||
this._targetCalcButton.Click += new System.EventHandler(this.OnTargetCalc_Click);
|
||||
//
|
||||
// _avoidRotationCheckBox
|
||||
//
|
||||
this._avoidRotationCheckBox.AutoSize = true;
|
||||
this._avoidRotationCheckBox.Location = new System.Drawing.Point(10, 126);
|
||||
this._avoidRotationCheckBox.Name = "_avoidRotationCheckBox";
|
||||
this._avoidRotationCheckBox.Size = new System.Drawing.Size(104, 16);
|
||||
this._avoidRotationCheckBox.TabIndex = 7;
|
||||
this._avoidRotationCheckBox.Text = "회전 구간 회피";
|
||||
this._avoidRotationCheckBox.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// _targetNodeCombo
|
||||
//
|
||||
this._targetNodeCombo.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
|
||||
this._targetNodeCombo.Location = new System.Drawing.Point(10, 97);
|
||||
this._targetNodeCombo.Name = "_targetNodeCombo";
|
||||
this._targetNodeCombo.Size = new System.Drawing.Size(210, 20);
|
||||
this._targetNodeCombo.TabIndex = 3;
|
||||
//
|
||||
// targetNodeLabel
|
||||
//
|
||||
this.targetNodeLabel.AutoSize = true;
|
||||
this.targetNodeLabel.Location = new System.Drawing.Point(10, 75);
|
||||
this.targetNodeLabel.Name = "targetNodeLabel";
|
||||
this.targetNodeLabel.Size = new System.Drawing.Size(63, 12);
|
||||
this.targetNodeLabel.TabIndex = 2;
|
||||
this.targetNodeLabel.Text = "목표 RFID:";
|
||||
//
|
||||
// _startNodeCombo
|
||||
//
|
||||
this._startNodeCombo.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
|
||||
this._startNodeCombo.Location = new System.Drawing.Point(10, 45);
|
||||
this._startNodeCombo.Name = "_startNodeCombo";
|
||||
this._startNodeCombo.Size = new System.Drawing.Size(210, 20);
|
||||
this._startNodeCombo.TabIndex = 1;
|
||||
//
|
||||
// startNodeLabel
|
||||
//
|
||||
this.startNodeLabel.AutoSize = true;
|
||||
this.startNodeLabel.Location = new System.Drawing.Point(10, 25);
|
||||
this.startNodeLabel.Name = "startNodeLabel";
|
||||
this.startNodeLabel.Size = new System.Drawing.Size(63, 12);
|
||||
this.startNodeLabel.TabIndex = 0;
|
||||
this.startNodeLabel.Text = "시작 RFID:";
|
||||
//
|
||||
// _agvControlGroup
|
||||
//
|
||||
this._agvControlGroup.Controls.Add(this._setPositionButton);
|
||||
this._agvControlGroup.Controls.Add(this._rfidTextBox);
|
||||
this._agvControlGroup.Controls.Add(this._rfidLabel);
|
||||
this._agvControlGroup.Controls.Add(this._directionCombo);
|
||||
this._agvControlGroup.Controls.Add(this._directionLabel);
|
||||
this._agvControlGroup.Controls.Add(this._stopSimulationButton);
|
||||
this._agvControlGroup.Controls.Add(this._startSimulationButton);
|
||||
this._agvControlGroup.Controls.Add(this._removeAgvButton);
|
||||
this._agvControlGroup.Controls.Add(this._addAgvButton);
|
||||
this._agvControlGroup.Controls.Add(this._agvListCombo);
|
||||
this._agvControlGroup.Dock = System.Windows.Forms.DockStyle.Top;
|
||||
this._agvControlGroup.Location = new System.Drawing.Point(0, 0);
|
||||
this._agvControlGroup.Name = "_agvControlGroup";
|
||||
this._agvControlGroup.Size = new System.Drawing.Size(233, 214);
|
||||
this._agvControlGroup.TabIndex = 0;
|
||||
this._agvControlGroup.TabStop = false;
|
||||
this._agvControlGroup.Text = "AGV 제어";
|
||||
//
|
||||
// _setPositionButton
|
||||
//
|
||||
this._setPositionButton.Location = new System.Drawing.Point(160, 138);
|
||||
this._setPositionButton.Name = "_setPositionButton";
|
||||
this._setPositionButton.Size = new System.Drawing.Size(60, 67);
|
||||
this._setPositionButton.TabIndex = 7;
|
||||
this._setPositionButton.Text = "위치설정";
|
||||
this._setPositionButton.UseVisualStyleBackColor = true;
|
||||
this._setPositionButton.Click += new System.EventHandler(this.OnSetPosition_Click);
|
||||
//
|
||||
// _rfidTextBox
|
||||
//
|
||||
this._rfidTextBox.Location = new System.Drawing.Point(10, 140);
|
||||
this._rfidTextBox.Name = "_rfidTextBox";
|
||||
this._rfidTextBox.Size = new System.Drawing.Size(140, 21);
|
||||
this._rfidTextBox.TabIndex = 6;
|
||||
this._rfidTextBox.KeyPress += new System.Windows.Forms.KeyPressEventHandler(this.OnRfidTextBox_KeyPress);
|
||||
//
|
||||
// _rfidLabel
|
||||
//
|
||||
this._rfidLabel.AutoSize = true;
|
||||
this._rfidLabel.Location = new System.Drawing.Point(10, 120);
|
||||
this._rfidLabel.Name = "_rfidLabel";
|
||||
this._rfidLabel.Size = new System.Drawing.Size(87, 12);
|
||||
this._rfidLabel.TabIndex = 5;
|
||||
this._rfidLabel.Text = "RFID 현재위치:";
|
||||
//
|
||||
// _directionCombo
|
||||
//
|
||||
this._directionCombo.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
|
||||
this._directionCombo.FormattingEnabled = true;
|
||||
this._directionCombo.Location = new System.Drawing.Point(10, 185);
|
||||
this._directionCombo.Name = "_directionCombo";
|
||||
this._directionCombo.Size = new System.Drawing.Size(140, 20);
|
||||
this._directionCombo.TabIndex = 8;
|
||||
//
|
||||
// _directionLabel
|
||||
//
|
||||
this._directionLabel.AutoSize = true;
|
||||
this._directionLabel.Location = new System.Drawing.Point(10, 165);
|
||||
this._directionLabel.Name = "_directionLabel";
|
||||
this._directionLabel.Size = new System.Drawing.Size(85, 12);
|
||||
this._directionLabel.TabIndex = 9;
|
||||
this._directionLabel.Text = "모터 구동방향:";
|
||||
//
|
||||
// _stopSimulationButton
|
||||
//
|
||||
this._stopSimulationButton.Location = new System.Drawing.Point(120, 85);
|
||||
this._stopSimulationButton.Name = "_stopSimulationButton";
|
||||
this._stopSimulationButton.Size = new System.Drawing.Size(100, 25);
|
||||
this._stopSimulationButton.TabIndex = 4;
|
||||
this._stopSimulationButton.Text = "시뮬레이션 정지";
|
||||
this._stopSimulationButton.UseVisualStyleBackColor = true;
|
||||
this._stopSimulationButton.Click += new System.EventHandler(this.OnStopSimulation_Click);
|
||||
//
|
||||
// _startSimulationButton
|
||||
//
|
||||
this._startSimulationButton.Location = new System.Drawing.Point(10, 85);
|
||||
this._startSimulationButton.Name = "_startSimulationButton";
|
||||
this._startSimulationButton.Size = new System.Drawing.Size(100, 25);
|
||||
this._startSimulationButton.TabIndex = 3;
|
||||
this._startSimulationButton.Text = "시뮬레이션 시작";
|
||||
this._startSimulationButton.UseVisualStyleBackColor = true;
|
||||
this._startSimulationButton.Click += new System.EventHandler(this.OnStartSimulation_Click);
|
||||
//
|
||||
// _removeAgvButton
|
||||
//
|
||||
this._removeAgvButton.Location = new System.Drawing.Point(120, 55);
|
||||
this._removeAgvButton.Name = "_removeAgvButton";
|
||||
this._removeAgvButton.Size = new System.Drawing.Size(100, 25);
|
||||
this._removeAgvButton.TabIndex = 2;
|
||||
this._removeAgvButton.Text = "AGV 제거";
|
||||
this._removeAgvButton.UseVisualStyleBackColor = true;
|
||||
this._removeAgvButton.Click += new System.EventHandler(this.OnRemoveAGV_Click);
|
||||
//
|
||||
// _addAgvButton
|
||||
//
|
||||
this._addAgvButton.Location = new System.Drawing.Point(10, 55);
|
||||
this._addAgvButton.Name = "_addAgvButton";
|
||||
this._addAgvButton.Size = new System.Drawing.Size(100, 25);
|
||||
this._addAgvButton.TabIndex = 1;
|
||||
this._addAgvButton.Text = "AGV 추가";
|
||||
this._addAgvButton.UseVisualStyleBackColor = true;
|
||||
this._addAgvButton.Click += new System.EventHandler(this.OnAddAGV_Click);
|
||||
//
|
||||
// _agvListCombo
|
||||
//
|
||||
this._agvListCombo.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
|
||||
this._agvListCombo.Location = new System.Drawing.Point(10, 25);
|
||||
this._agvListCombo.Name = "_agvListCombo";
|
||||
this._agvListCombo.Size = new System.Drawing.Size(210, 20);
|
||||
this._agvListCombo.TabIndex = 0;
|
||||
this._agvListCombo.SelectedIndexChanged += new System.EventHandler(this.OnAGVList_SelectedIndexChanged);
|
||||
//
|
||||
// _canvasPanel
|
||||
//
|
||||
this._canvasPanel.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
this._canvasPanel.Location = new System.Drawing.Point(0, 109);
|
||||
this._canvasPanel.Name = "_canvasPanel";
|
||||
this._canvasPanel.Size = new System.Drawing.Size(967, 669);
|
||||
this._canvasPanel.TabIndex = 4;
|
||||
//
|
||||
// _agvInfoPanel
|
||||
//
|
||||
this._agvInfoPanel.BackColor = System.Drawing.Color.LightBlue;
|
||||
this._agvInfoPanel.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
|
||||
this._agvInfoPanel.Controls.Add(this._agvInfoTitleLabel);
|
||||
this._agvInfoPanel.Controls.Add(this._liftDirectionLabel);
|
||||
this._agvInfoPanel.Controls.Add(this._motorDirectionLabel);
|
||||
this._agvInfoPanel.Controls.Add(this._pathDebugLabel);
|
||||
this._agvInfoPanel.Dock = System.Windows.Forms.DockStyle.Top;
|
||||
this._agvInfoPanel.Location = new System.Drawing.Point(0, 49);
|
||||
this._agvInfoPanel.Name = "_agvInfoPanel";
|
||||
this._agvInfoPanel.Size = new System.Drawing.Size(967, 60);
|
||||
this._agvInfoPanel.TabIndex = 5;
|
||||
//
|
||||
// _agvInfoTitleLabel
|
||||
//
|
||||
this._agvInfoTitleLabel.AutoSize = true;
|
||||
this._agvInfoTitleLabel.Font = new System.Drawing.Font("맑은 고딕", 9F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(129)));
|
||||
this._agvInfoTitleLabel.Location = new System.Drawing.Point(10, 12);
|
||||
this._agvInfoTitleLabel.Name = "_agvInfoTitleLabel";
|
||||
this._agvInfoTitleLabel.Size = new System.Drawing.Size(91, 15);
|
||||
this._agvInfoTitleLabel.TabIndex = 0;
|
||||
this._agvInfoTitleLabel.Text = "AGV 상태 정보:";
|
||||
//
|
||||
// _liftDirectionLabel
|
||||
//
|
||||
this._liftDirectionLabel.AutoSize = true;
|
||||
this._liftDirectionLabel.Font = new System.Drawing.Font("맑은 고딕", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(129)));
|
||||
this._liftDirectionLabel.Location = new System.Drawing.Point(120, 12);
|
||||
this._liftDirectionLabel.Name = "_liftDirectionLabel";
|
||||
this._liftDirectionLabel.Size = new System.Drawing.Size(83, 15);
|
||||
this._liftDirectionLabel.TabIndex = 1;
|
||||
this._liftDirectionLabel.Text = "리프트 방향: -";
|
||||
//
|
||||
// _motorDirectionLabel
|
||||
//
|
||||
this._motorDirectionLabel.AutoSize = true;
|
||||
this._motorDirectionLabel.Font = new System.Drawing.Font("맑은 고딕", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(129)));
|
||||
this._motorDirectionLabel.Location = new System.Drawing.Point(250, 12);
|
||||
this._motorDirectionLabel.Name = "_motorDirectionLabel";
|
||||
this._motorDirectionLabel.Size = new System.Drawing.Size(71, 15);
|
||||
this._motorDirectionLabel.TabIndex = 2;
|
||||
this._motorDirectionLabel.Text = "모터 방향: -";
|
||||
//
|
||||
// _pathDebugLabel
|
||||
//
|
||||
this._pathDebugLabel.AutoSize = true;
|
||||
this._pathDebugLabel.Font = new System.Drawing.Font("맑은 고딕", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(129)));
|
||||
this._pathDebugLabel.ForeColor = System.Drawing.Color.DarkBlue;
|
||||
this._pathDebugLabel.Location = new System.Drawing.Point(10, 30);
|
||||
this._pathDebugLabel.Name = "_pathDebugLabel";
|
||||
this._pathDebugLabel.Size = new System.Drawing.Size(114, 15);
|
||||
this._pathDebugLabel.TabIndex = 3;
|
||||
this._pathDebugLabel.Text = "경로: 설정되지 않음";
|
||||
//
|
||||
// SimulatorForm
|
||||
//
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 12F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.ClientSize = new System.Drawing.Size(1200, 800);
|
||||
this.Controls.Add(this._canvasPanel);
|
||||
this.Controls.Add(this._agvInfoPanel);
|
||||
this.Controls.Add(this._controlPanel);
|
||||
this.Controls.Add(this._statusStrip);
|
||||
this.Controls.Add(this._toolStrip);
|
||||
this.Controls.Add(this._menuStrip);
|
||||
this.MainMenuStrip = this._menuStrip;
|
||||
this.Name = "SimulatorForm";
|
||||
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen;
|
||||
this.Text = "AGV 시뮬레이터";
|
||||
this.WindowState = System.Windows.Forms.FormWindowState.Maximized;
|
||||
this._menuStrip.ResumeLayout(false);
|
||||
this._menuStrip.PerformLayout();
|
||||
this._toolStrip.ResumeLayout(false);
|
||||
this._toolStrip.PerformLayout();
|
||||
this._statusStrip.ResumeLayout(false);
|
||||
this._statusStrip.PerformLayout();
|
||||
this._controlPanel.ResumeLayout(false);
|
||||
this._statusGroup.ResumeLayout(false);
|
||||
this._statusGroup.PerformLayout();
|
||||
this._pathGroup.ResumeLayout(false);
|
||||
this._pathGroup.PerformLayout();
|
||||
this._agvControlGroup.ResumeLayout(false);
|
||||
this._agvControlGroup.PerformLayout();
|
||||
this._agvInfoPanel.ResumeLayout(false);
|
||||
this._agvInfoPanel.PerformLayout();
|
||||
this.ResumeLayout(false);
|
||||
this.PerformLayout();
|
||||
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private System.Windows.Forms.MenuStrip _menuStrip;
|
||||
private System.Windows.Forms.ToolStripMenuItem fileToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripMenuItem openMapToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripSeparator toolStripSeparator1;
|
||||
private System.Windows.Forms.ToolStripMenuItem exitToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripMenuItem simulationToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripMenuItem startSimulationToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripMenuItem stopSimulationToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripMenuItem resetToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripMenuItem viewToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripMenuItem fitToMapToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripMenuItem resetZoomToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripMenuItem helpToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripMenuItem aboutToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStrip _toolStrip;
|
||||
private System.Windows.Forms.ToolStripButton openMapToolStripButton;
|
||||
private System.Windows.Forms.ToolStripSeparator toolStripSeparator2;
|
||||
private System.Windows.Forms.ToolStripButton startSimulationToolStripButton;
|
||||
private System.Windows.Forms.ToolStripButton stopSimulationToolStripButton;
|
||||
private System.Windows.Forms.ToolStripButton resetToolStripButton;
|
||||
private System.Windows.Forms.ToolStripSeparator toolStripSeparator3;
|
||||
private System.Windows.Forms.ToolStripButton fitToMapToolStripButton;
|
||||
private System.Windows.Forms.ToolStripButton resetZoomToolStripButton;
|
||||
private System.Windows.Forms.StatusStrip _statusStrip;
|
||||
private System.Windows.Forms.ToolStripStatusLabel _statusLabel;
|
||||
private System.Windows.Forms.ToolStripStatusLabel _coordLabel;
|
||||
private System.Windows.Forms.Panel _controlPanel;
|
||||
private System.Windows.Forms.GroupBox _agvControlGroup;
|
||||
private System.Windows.Forms.ComboBox _agvListCombo;
|
||||
private System.Windows.Forms.Button _addAgvButton;
|
||||
private System.Windows.Forms.Button _removeAgvButton;
|
||||
private System.Windows.Forms.Button _startSimulationButton;
|
||||
private System.Windows.Forms.Button _stopSimulationButton;
|
||||
private System.Windows.Forms.GroupBox _pathGroup;
|
||||
private System.Windows.Forms.Label startNodeLabel;
|
||||
private System.Windows.Forms.ComboBox _startNodeCombo;
|
||||
private System.Windows.Forms.Label targetNodeLabel;
|
||||
private System.Windows.Forms.ComboBox _targetNodeCombo;
|
||||
private System.Windows.Forms.Button _calculatePathButton;
|
||||
private System.Windows.Forms.Button _startPathButton;
|
||||
private System.Windows.Forms.Button _clearPathButton;
|
||||
private System.Windows.Forms.Button _targetCalcButton;
|
||||
private System.Windows.Forms.CheckBox _avoidRotationCheckBox;
|
||||
private System.Windows.Forms.GroupBox _statusGroup;
|
||||
private System.Windows.Forms.Label _simulationStatusLabel;
|
||||
private System.Windows.Forms.Label _agvCountLabel;
|
||||
private System.Windows.Forms.Label _pathLengthLabel;
|
||||
private System.Windows.Forms.Panel _canvasPanel;
|
||||
private System.Windows.Forms.Label _rfidLabel;
|
||||
private System.Windows.Forms.TextBox _rfidTextBox;
|
||||
private System.Windows.Forms.Button _setPositionButton;
|
||||
private System.Windows.Forms.ComboBox _directionCombo;
|
||||
private System.Windows.Forms.Label _directionLabel;
|
||||
private System.Windows.Forms.ToolStripButton btAllReset;
|
||||
private System.Windows.Forms.ToolStripMenuItem reloadMapToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripMenuItem launchMapEditorToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripSeparator toolStripSeparator4;
|
||||
private System.Windows.Forms.ToolStripButton reloadMapToolStripButton;
|
||||
private System.Windows.Forms.ToolStripButton launchMapEditorToolStripButton;
|
||||
private System.Windows.Forms.Panel _agvInfoPanel;
|
||||
private System.Windows.Forms.Label _liftDirectionLabel;
|
||||
private System.Windows.Forms.Label _motorDirectionLabel;
|
||||
private System.Windows.Forms.Label _agvInfoTitleLabel;
|
||||
private System.Windows.Forms.Label _pathDebugLabel;
|
||||
}
|
||||
}
|
||||
1339
Cs_HMI/AGVLogic/AGVSimulator/Forms/SimulatorForm.cs
Normal file
1339
Cs_HMI/AGVLogic/AGVSimulator/Forms/SimulatorForm.cs
Normal file
File diff suppressed because it is too large
Load Diff
129
Cs_HMI/AGVLogic/AGVSimulator/Forms/SimulatorForm.resx
Normal file
129
Cs_HMI/AGVLogic/AGVSimulator/Forms/SimulatorForm.resx
Normal file
@@ -0,0 +1,129 @@
|
||||
<?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>
|
||||
<metadata name="_menuStrip.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
|
||||
<value>17, 17</value>
|
||||
</metadata>
|
||||
<metadata name="_toolStrip.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
|
||||
<value>132, 17</value>
|
||||
</metadata>
|
||||
<metadata name="_statusStrip.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
|
||||
<value>237, 17</value>
|
||||
</metadata>
|
||||
</root>
|
||||
135
Cs_HMI/AGVLogic/AGVSimulator/Models/SimulationState.cs
Normal file
135
Cs_HMI/AGVLogic/AGVSimulator/Models/SimulationState.cs
Normal file
@@ -0,0 +1,135 @@
|
||||
using System;
|
||||
|
||||
namespace AGVSimulator.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// 시뮬레이션 상태 관리 클래스
|
||||
/// </summary>
|
||||
public class SimulationState
|
||||
{
|
||||
#region Properties
|
||||
|
||||
/// <summary>
|
||||
/// 시뮬레이션 실행 중 여부
|
||||
/// </summary>
|
||||
public bool IsRunning { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 시뮬레이션 시작 시간
|
||||
/// </summary>
|
||||
public DateTime? StartTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 시뮬레이션 경과 시간
|
||||
/// </summary>
|
||||
public TimeSpan ElapsedTime => StartTime.HasValue ? DateTime.Now - StartTime.Value : TimeSpan.Zero;
|
||||
|
||||
/// <summary>
|
||||
/// 시뮬레이션 속도 배율 (1.0 = 실시간, 2.0 = 2배속)
|
||||
/// </summary>
|
||||
public float SpeedMultiplier { get; set; } = 1.0f;
|
||||
|
||||
/// <summary>
|
||||
/// 총 처리된 이벤트 수
|
||||
/// </summary>
|
||||
public int TotalEvents { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 총 이동 거리 (모든 AGV 합계)
|
||||
/// </summary>
|
||||
public float TotalDistance { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 발생한 오류 수
|
||||
/// </summary>
|
||||
public int ErrorCount { get; set; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region Constructor
|
||||
|
||||
/// <summary>
|
||||
/// 기본 생성자
|
||||
/// </summary>
|
||||
public SimulationState()
|
||||
{
|
||||
Reset();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public Methods
|
||||
|
||||
/// <summary>
|
||||
/// 시뮬레이션 시작
|
||||
/// </summary>
|
||||
public void Start()
|
||||
{
|
||||
if (!IsRunning)
|
||||
{
|
||||
IsRunning = true;
|
||||
StartTime = DateTime.Now;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 시뮬레이션 정지
|
||||
/// </summary>
|
||||
public void Stop()
|
||||
{
|
||||
IsRunning = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 시뮬레이션 상태 초기화
|
||||
/// </summary>
|
||||
public void Reset()
|
||||
{
|
||||
IsRunning = false;
|
||||
StartTime = null;
|
||||
SpeedMultiplier = 1.0f;
|
||||
TotalEvents = 0;
|
||||
TotalDistance = 0;
|
||||
ErrorCount = 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 이벤트 발생 시 호출
|
||||
/// </summary>
|
||||
public void RecordEvent()
|
||||
{
|
||||
TotalEvents++;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 이동 거리 추가
|
||||
/// </summary>
|
||||
/// <param name="distance">이동한 거리</param>
|
||||
public void AddDistance(float distance)
|
||||
{
|
||||
TotalDistance += distance;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 오류 발생 시 호출
|
||||
/// </summary>
|
||||
public void RecordError()
|
||||
{
|
||||
ErrorCount++;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 통계 정보 조회
|
||||
/// </summary>
|
||||
/// <returns>통계 정보 문자열</returns>
|
||||
public string GetStatistics()
|
||||
{
|
||||
return $"실행시간: {ElapsedTime:hh\\:mm\\:ss}, " +
|
||||
$"이벤트: {TotalEvents}, " +
|
||||
$"총거리: {TotalDistance:F1}, " +
|
||||
$"오류: {ErrorCount}";
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
128
Cs_HMI/AGVLogic/AGVSimulator/Models/SimulatorConfig.cs
Normal file
128
Cs_HMI/AGVLogic/AGVSimulator/Models/SimulatorConfig.cs
Normal file
@@ -0,0 +1,128 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace AGVSimulator.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// 시뮬레이터 환경 설정 클래스
|
||||
/// </summary>
|
||||
public class SimulatorConfig
|
||||
{
|
||||
#region Properties
|
||||
|
||||
/// <summary>
|
||||
/// MapEditor 실행 파일 경로
|
||||
/// </summary>
|
||||
public string MapEditorExecutablePath { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 마지막으로 로드한 맵 파일 경로
|
||||
/// </summary>
|
||||
public string LastMapFilePath { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 설정 파일 자동 저장 여부
|
||||
/// </summary>
|
||||
public bool AutoSave { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 프로그램 시작시 마지막 맵 파일을 자동으로 로드할지 여부
|
||||
/// </summary>
|
||||
public bool AutoLoadLastMapFile { get; set; } = true;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Static Methods
|
||||
|
||||
/// <summary>
|
||||
/// 설정 파일 기본 경로
|
||||
/// </summary>
|
||||
private static string ConfigFilePath => Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
||||
"AGVSimulator",
|
||||
"config.json");
|
||||
|
||||
/// <summary>
|
||||
/// 설정을 파일에서 로드
|
||||
/// </summary>
|
||||
/// <returns>로드된 설정 객체</returns>
|
||||
public static SimulatorConfig Load()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (File.Exists(ConfigFilePath))
|
||||
{
|
||||
var json = File.ReadAllText(ConfigFilePath);
|
||||
return JsonConvert.DeserializeObject<SimulatorConfig>(json) ?? new SimulatorConfig();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"설정 로드 실패: {ex.Message}");
|
||||
}
|
||||
|
||||
return new SimulatorConfig();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 설정을 파일에 저장
|
||||
/// </summary>
|
||||
/// <param name="config">저장할 설정 객체</param>
|
||||
/// <returns>저장 성공 여부</returns>
|
||||
public static bool Save(SimulatorConfig config)
|
||||
{
|
||||
try
|
||||
{
|
||||
var directory = Path.GetDirectoryName(ConfigFilePath);
|
||||
if (!Directory.Exists(directory))
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
}
|
||||
|
||||
var json = JsonConvert.SerializeObject(config, Formatting.Indented);
|
||||
File.WriteAllText(ConfigFilePath, json);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"설정 저장 실패: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Instance Methods
|
||||
|
||||
/// <summary>
|
||||
/// 현재 설정을 저장
|
||||
/// </summary>
|
||||
/// <returns>저장 성공 여부</returns>
|
||||
public bool Save()
|
||||
{
|
||||
return Save(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// MapEditor 실행 파일 경로 유효성 확인
|
||||
/// </summary>
|
||||
/// <returns>유효한 경로인지 여부</returns>
|
||||
public bool IsMapEditorPathValid()
|
||||
{
|
||||
return !string.IsNullOrEmpty(MapEditorExecutablePath) &&
|
||||
File.Exists(MapEditorExecutablePath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 마지막 맵 파일이 존재하는지 확인
|
||||
/// </summary>
|
||||
/// <returns>마지막 맵 파일이 유효한지 여부</returns>
|
||||
public bool HasValidLastMapFile()
|
||||
{
|
||||
return !string.IsNullOrEmpty(LastMapFilePath) && File.Exists(LastMapFilePath);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
561
Cs_HMI/AGVLogic/AGVSimulator/Models/VirtualAGV.cs
Normal file
561
Cs_HMI/AGVLogic/AGVSimulator/Models/VirtualAGV.cs
Normal file
@@ -0,0 +1,561 @@
|
||||
//using System;
|
||||
//using System.Collections.Generic;
|
||||
//using System.Drawing;
|
||||
//using System.Linq;
|
||||
//using AGVMapEditor.Models;
|
||||
//using AGVNavigationCore.Models;
|
||||
//using AGVNavigationCore.PathFinding;
|
||||
//using AGVNavigationCore.PathFinding.Core;
|
||||
//using AGVNavigationCore.Controls;
|
||||
|
||||
//namespace AGVSimulator.Models
|
||||
//{
|
||||
|
||||
// /// <summary>
|
||||
// /// 가상 AGV 클래스
|
||||
// /// 실제 AGV의 동작을 시뮬레이션
|
||||
// /// </summary>
|
||||
// public class VirtualAGV : IAGV
|
||||
// {
|
||||
// #region Events
|
||||
|
||||
// /// <summary>
|
||||
// /// AGV 상태 변경 이벤트
|
||||
// /// </summary>
|
||||
// public event EventHandler<AGVState> StateChanged;
|
||||
|
||||
// /// <summary>
|
||||
// /// 위치 변경 이벤트
|
||||
// /// </summary>
|
||||
// public event EventHandler<(Point, AgvDirection, MapNode)> PositionChanged;
|
||||
|
||||
// /// <summary>
|
||||
// /// RFID 감지 이벤트
|
||||
// /// </summary>
|
||||
// public event EventHandler<string> RfidDetected;
|
||||
|
||||
// /// <summary>
|
||||
// /// 경로 완료 이벤트
|
||||
// /// </summary>
|
||||
// public event EventHandler<AGVPathResult> PathCompleted;
|
||||
|
||||
// /// <summary>
|
||||
// /// 오류 발생 이벤트
|
||||
// /// </summary>
|
||||
// public event EventHandler<string> ErrorOccurred;
|
||||
|
||||
// #endregion
|
||||
|
||||
// #region Fields
|
||||
|
||||
// private string _agvId;
|
||||
// private Point _currentPosition;
|
||||
// private Point _targetPosition;
|
||||
// private string _targetId;
|
||||
// private string _currentId;
|
||||
// private AgvDirection _currentDirection;
|
||||
// private AgvDirection _targetDirection;
|
||||
// private AGVState _currentState;
|
||||
// private float _currentSpeed;
|
||||
|
||||
// // 경로 관련
|
||||
// private AGVPathResult _currentPath;
|
||||
// private List<string> _remainingNodes;
|
||||
// private int _currentNodeIndex;
|
||||
// private MapNode _currentNode;
|
||||
// private MapNode _targetNode;
|
||||
|
||||
// // 이동 관련
|
||||
// private System.Windows.Forms.Timer _moveTimer;
|
||||
// private DateTime _lastMoveTime;
|
||||
// private Point _moveStartPosition;
|
||||
// private Point _moveTargetPosition;
|
||||
// private float _moveProgress;
|
||||
|
||||
// // 도킹 관련
|
||||
// private DockingDirection _dockingDirection;
|
||||
|
||||
// // 시뮬레이션 설정
|
||||
// private readonly float _moveSpeed = 50.0f; // 픽셀/초
|
||||
// private readonly float _rotationSpeed = 90.0f; // 도/초
|
||||
// private readonly int _updateInterval = 50; // ms
|
||||
|
||||
// #endregion
|
||||
|
||||
// #region Properties
|
||||
|
||||
// /// <summary>
|
||||
// /// AGV ID
|
||||
// /// </summary>
|
||||
// public string AgvId => _agvId;
|
||||
|
||||
// /// <summary>
|
||||
// /// 현재 위치
|
||||
// /// </summary>
|
||||
// public Point CurrentPosition
|
||||
// {
|
||||
// get => _currentPosition;
|
||||
// set => _currentPosition = value;
|
||||
// }
|
||||
|
||||
// /// <summary>
|
||||
// /// 현재 방향
|
||||
// /// 모터의 동작 방향
|
||||
// /// </summary>
|
||||
// public AgvDirection CurrentDirection
|
||||
// {
|
||||
// get => _currentDirection;
|
||||
// set => _currentDirection = value;
|
||||
// }
|
||||
|
||||
// /// <summary>
|
||||
// /// 현재 상태
|
||||
// /// </summary>
|
||||
// public AGVState CurrentState
|
||||
// {
|
||||
// get => _currentState;
|
||||
// set => _currentState = value;
|
||||
// }
|
||||
|
||||
// /// <summary>
|
||||
// /// 현재 속도
|
||||
// /// </summary>
|
||||
// public float CurrentSpeed => _currentSpeed;
|
||||
|
||||
// /// <summary>
|
||||
// /// 현재 경로
|
||||
// /// </summary>
|
||||
// public AGVPathResult CurrentPath => _currentPath;
|
||||
|
||||
// /// <summary>
|
||||
// /// 현재 노드 ID
|
||||
// /// </summary>
|
||||
// public string CurrentNodeId => _currentNode.NodeId;
|
||||
|
||||
// /// <summary>
|
||||
// /// 목표 위치
|
||||
// /// </summary>
|
||||
// public Point? TargetPosition => _targetPosition;
|
||||
|
||||
// /// <summary>
|
||||
// /// 배터리 레벨 (시뮬레이션)
|
||||
// /// </summary>
|
||||
// public float BatteryLevel { get; set; } = 100.0f;
|
||||
|
||||
// /// <summary>
|
||||
// /// 목표 노드 ID
|
||||
// /// </summary>
|
||||
// public string TargetNodeId => _targetNode.NodeId;
|
||||
|
||||
// /// <summary>
|
||||
// /// 도킹 방향
|
||||
// /// </summary>
|
||||
// public DockingDirection DockingDirection => _dockingDirection;
|
||||
|
||||
// #endregion
|
||||
|
||||
// #region Constructor
|
||||
|
||||
// /// <summary>
|
||||
// /// 생성자
|
||||
// /// </summary>
|
||||
// /// <param name="agvId">AGV ID</param>
|
||||
// /// <param name="startPosition">시작 위치</param>
|
||||
// /// <param name="startDirection">시작 방향</param>
|
||||
// public VirtualAGV(string agvId, Point startPosition, AgvDirection startDirection = AgvDirection.Forward)
|
||||
// {
|
||||
// _agvId = agvId;
|
||||
// _currentPosition = startPosition;
|
||||
// _currentDirection = startDirection;
|
||||
// _currentState = AGVState.Idle;
|
||||
// _currentSpeed = 0;
|
||||
// _dockingDirection = DockingDirection.Forward; // 기본값: 전진 도킹
|
||||
// _currentNode = null; // = string.Empty;
|
||||
// _targetNode = null;// string.Empty;
|
||||
|
||||
// InitializeTimer();
|
||||
// }
|
||||
|
||||
// #endregion
|
||||
|
||||
// #region Initialization
|
||||
|
||||
// private void InitializeTimer()
|
||||
// {
|
||||
// _moveTimer = new System.Windows.Forms.Timer();
|
||||
// _moveTimer.Interval = _updateInterval;
|
||||
// _moveTimer.Tick += OnMoveTimer_Tick;
|
||||
// _lastMoveTime = DateTime.Now;
|
||||
// }
|
||||
|
||||
// #endregion
|
||||
|
||||
// #region Public Methods
|
||||
|
||||
// /// <summary>
|
||||
// /// 경로 실행 시작
|
||||
// /// </summary>
|
||||
// /// <param name="path">실행할 경로</param>
|
||||
// /// <param name="mapNodes">맵 노드 목록</param>
|
||||
// public void StartPath(AGVPathResult path, List<MapNode> mapNodes)
|
||||
// {
|
||||
// if (path == null || !path.Success)
|
||||
// {
|
||||
// OnError("유효하지 않은 경로입니다.");
|
||||
// return;
|
||||
// }
|
||||
|
||||
// _currentPath = path;
|
||||
// _remainingNodes = new List<string>(path.Path);
|
||||
// _currentNodeIndex = 0;
|
||||
|
||||
// // 시작 노드와 목표 노드 설정
|
||||
// if (_remainingNodes.Count > 0)
|
||||
// {
|
||||
// var startNode = mapNodes.FirstOrDefault(n => n.NodeId == _remainingNodes[0]);
|
||||
// if (startNode != null)
|
||||
// {
|
||||
// _currentNode = startNode;
|
||||
|
||||
// // 목표 노드 설정 (경로의 마지막 노드)
|
||||
// if (_remainingNodes.Count > 1)
|
||||
// {
|
||||
// var _targetNodeId = _remainingNodes[_remainingNodes.Count - 1];
|
||||
// var targetNode = mapNodes.FirstOrDefault(n => n.NodeId == _targetNodeId);
|
||||
|
||||
// // 목표 노드의 타입에 따라 도킹 방향 결정
|
||||
// if (targetNode != null)
|
||||
// {
|
||||
// _dockingDirection = GetDockingDirection(targetNode.Type);
|
||||
// }
|
||||
// }
|
||||
|
||||
// StartMovement();
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// OnError($"시작 노드를 찾을 수 없습니다: {_remainingNodes[0]}");
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// /// <summary>
|
||||
// /// 경로 정지
|
||||
// /// </summary>
|
||||
// public void StopPath()
|
||||
// {
|
||||
// _moveTimer.Stop();
|
||||
// _currentPath = null;
|
||||
// _remainingNodes?.Clear();
|
||||
// SetState(AGVState.Idle);
|
||||
// _currentSpeed = 0;
|
||||
// }
|
||||
|
||||
// /// <summary>
|
||||
// /// 긴급 정지
|
||||
// /// </summary>
|
||||
// public void EmergencyStop()
|
||||
// {
|
||||
// StopPath();
|
||||
// OnError("긴급 정지가 실행되었습니다.");
|
||||
// }
|
||||
|
||||
// /// <summary>
|
||||
// /// 수동 이동 (테스트용)
|
||||
// /// </summary>
|
||||
// /// <param name="targetPosition">목표 위치</param>
|
||||
// public void MoveTo(Point targetPosition)
|
||||
// {
|
||||
// _targetPosition = targetPosition;
|
||||
// _moveStartPosition = _currentPosition;
|
||||
// _moveTargetPosition = targetPosition;
|
||||
// _moveProgress = 0;
|
||||
|
||||
// SetState(AGVState.Moving);
|
||||
// _moveTimer.Start();
|
||||
// }
|
||||
|
||||
// /// <summary>
|
||||
// /// 수동 회전 (테스트용)
|
||||
// /// </summary>
|
||||
// /// <param name="direction">회전 방향</param>
|
||||
// public void Rotate(AgvDirection direction)
|
||||
// {
|
||||
// if (_currentState != AGVState.Idle)
|
||||
// return;
|
||||
|
||||
// SetState(AGVState.Rotating);
|
||||
|
||||
// // 시뮬레이션: 즉시 방향 변경 (실제로는 시간이 걸림)
|
||||
// _currentDirection = direction;
|
||||
|
||||
// System.Threading.Thread.Sleep(500); // 회전 시간 시뮬레이션
|
||||
// SetState(AGVState.Idle);
|
||||
// }
|
||||
|
||||
|
||||
|
||||
|
||||
// /// <summary>
|
||||
// /// AGV 위치 직접 설정 (시뮬레이터용)
|
||||
// /// TargetPosition을 이전 위치로 저장하여 리프트 방향 계산이 가능하도록 함
|
||||
// /// </summary>
|
||||
// /// <param name="newPosition">새로운 위치</param>
|
||||
// /// <param name="motorDirection">모터이동방향</param>
|
||||
// public void SetPosition(MapNode node, Point newPosition, AgvDirection motorDirection)
|
||||
// {
|
||||
// // 현재 위치를 이전 위치로 저장 (리프트 방향 계산용)
|
||||
// if (_currentPosition != Point.Empty)
|
||||
// {
|
||||
// _targetPosition = _currentPosition; // 이전 위치 (previousPos 역할)
|
||||
// _targetDirection = _currentDirection;
|
||||
// _targetNode = node;
|
||||
// }
|
||||
|
||||
// // 새로운 위치 설정
|
||||
// _currentPosition = newPosition;
|
||||
// _currentDirection = motorDirection;
|
||||
// _currentNode = node;
|
||||
|
||||
// // 위치 변경 이벤트 발생
|
||||
// PositionChanged?.Invoke(this, (_currentPosition, _currentDirection, _currentNode));
|
||||
// }
|
||||
|
||||
// /// <summary>
|
||||
// /// 충전 시작 (시뮬레이션)
|
||||
// /// </summary>
|
||||
// public void StartCharging()
|
||||
// {
|
||||
// if (_currentState == AGVState.Idle)
|
||||
// {
|
||||
// SetState(AGVState.Charging);
|
||||
// // 충전 시뮬레이션 시작
|
||||
// }
|
||||
// }
|
||||
|
||||
// /// <summary>
|
||||
// /// 충전 종료
|
||||
// /// </summary>
|
||||
// public void StopCharging()
|
||||
// {
|
||||
// if (_currentState == AGVState.Charging)
|
||||
// {
|
||||
// SetState(AGVState.Idle);
|
||||
// }
|
||||
// }
|
||||
|
||||
// /// <summary>
|
||||
// /// AGV 정보 조회
|
||||
// /// </summary>
|
||||
// public string GetStatus()
|
||||
// {
|
||||
// return $"AGV[{_agvId}] 위치:({_currentPosition.X},{_currentPosition.Y}) " +
|
||||
// $"방향:{_currentDirection} 상태:{_currentState} " +
|
||||
// $"속도:{_currentSpeed:F1} 배터리:{BatteryLevel:F1}%";
|
||||
// }
|
||||
|
||||
// /// <summary>
|
||||
// /// 현재 RFID 시뮬레이션 (현재 위치 기준)
|
||||
// /// </summary>
|
||||
// public string SimulateRfidReading(List<MapNode> mapNodes)
|
||||
// {
|
||||
// // 현재 위치에서 가장 가까운 노드 찾기
|
||||
// var closestNode = FindClosestNode(_currentPosition, mapNodes);
|
||||
// if (closestNode == null)
|
||||
// return null;
|
||||
|
||||
// // 해당 노드의 RFID 정보 반환 (MapNode에 RFID 정보 포함)
|
||||
// return closestNode.HasRfid() ? closestNode.RfidId : null;
|
||||
// }
|
||||
|
||||
// #endregion
|
||||
|
||||
// #region Private Methods
|
||||
|
||||
// private void StartMovement()
|
||||
// {
|
||||
// SetState(AGVState.Moving);
|
||||
// _moveTimer.Start();
|
||||
// _lastMoveTime = DateTime.Now;
|
||||
// }
|
||||
|
||||
// private void OnMoveTimer_Tick(object sender, EventArgs e)
|
||||
// {
|
||||
// var now = DateTime.Now;
|
||||
// var deltaTime = (float)(now - _lastMoveTime).TotalSeconds;
|
||||
// _lastMoveTime = now;
|
||||
|
||||
// UpdateMovement(deltaTime);
|
||||
// UpdateBattery(deltaTime);
|
||||
|
||||
// // 위치 변경 이벤트 발생
|
||||
// PositionChanged?.Invoke(this, (_currentPosition, _currentDirection, _currentNode));
|
||||
// }
|
||||
|
||||
// private void UpdateMovement(float deltaTime)
|
||||
// {
|
||||
// if (_currentState != AGVState.Moving)
|
||||
// return;
|
||||
|
||||
// // 목표 위치까지의 거리 계산
|
||||
// var distance = CalculateDistance(_currentPosition, _moveTargetPosition);
|
||||
|
||||
// if (distance < 5.0f) // 도달 임계값
|
||||
// {
|
||||
// // 목표 도달
|
||||
// _currentPosition = _moveTargetPosition;
|
||||
// _currentSpeed = 0;
|
||||
|
||||
// // 다음 노드로 이동
|
||||
// ProcessNextNode();
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// // 계속 이동
|
||||
// var moveDistance = _moveSpeed * deltaTime;
|
||||
// var direction = new PointF(
|
||||
// _moveTargetPosition.X - _currentPosition.X,
|
||||
// _moveTargetPosition.Y - _currentPosition.Y
|
||||
// );
|
||||
|
||||
// // 정규화
|
||||
// var length = (float)Math.Sqrt(direction.X * direction.X + direction.Y * direction.Y);
|
||||
// if (length > 0)
|
||||
// {
|
||||
// direction.X /= length;
|
||||
// direction.Y /= length;
|
||||
// }
|
||||
|
||||
// // 새 위치 계산
|
||||
// _currentPosition = new Point(
|
||||
// (int)(_currentPosition.X + direction.X * moveDistance),
|
||||
// (int)(_currentPosition.Y + direction.Y * moveDistance)
|
||||
// );
|
||||
|
||||
// _currentSpeed = _moveSpeed;
|
||||
// }
|
||||
// }
|
||||
|
||||
// private void UpdateBattery(float deltaTime)
|
||||
// {
|
||||
// // 배터리 소모 시뮬레이션
|
||||
// if (_currentState == AGVState.Moving)
|
||||
// {
|
||||
// BatteryLevel -= 0.1f * deltaTime; // 이동시 소모
|
||||
// }
|
||||
// else if (_currentState == AGVState.Charging)
|
||||
// {
|
||||
// BatteryLevel += 5.0f * deltaTime; // 충전
|
||||
// BatteryLevel = Math.Min(100.0f, BatteryLevel);
|
||||
// }
|
||||
|
||||
// BatteryLevel = Math.Max(0, BatteryLevel);
|
||||
|
||||
// // 배터리 부족 경고
|
||||
// if (BatteryLevel < 20.0f && _currentState != AGVState.Charging)
|
||||
// {
|
||||
// OnError($"배터리 부족: {BatteryLevel:F1}%");
|
||||
// }
|
||||
// }
|
||||
|
||||
// private void ProcessNextNode()
|
||||
// {
|
||||
// if (_remainingNodes == null || _currentNodeIndex >= _remainingNodes.Count - 1)
|
||||
// {
|
||||
// // 경로 완료
|
||||
// _moveTimer.Stop();
|
||||
// SetState(AGVState.Idle);
|
||||
// PathCompleted?.Invoke(this, _currentPath);
|
||||
// return;
|
||||
// }
|
||||
|
||||
// // 다음 노드로 이동
|
||||
// _currentNodeIndex++;
|
||||
// var nextNodeId = _remainingNodes[_currentNodeIndex];
|
||||
|
||||
// // RFID 감지 시뮬레이션
|
||||
// RfidDetected?.Invoke(this, $"RFID_{nextNodeId}");
|
||||
// //_currentNodeId = nextNodeId;
|
||||
|
||||
// // 다음 목표 위치 설정 (실제로는 맵에서 좌표 가져와야 함)
|
||||
// // 여기서는 간단히 현재 위치에서 랜덤 오프셋으로 설정
|
||||
// var random = new Random();
|
||||
// _moveTargetPosition = new Point(
|
||||
// _currentPosition.X + random.Next(-100, 100),
|
||||
// _currentPosition.Y + random.Next(-100, 100)
|
||||
// );
|
||||
// }
|
||||
|
||||
// private MapNode FindClosestNode(Point position, List<MapNode> mapNodes)
|
||||
// {
|
||||
// if (mapNodes == null || mapNodes.Count == 0)
|
||||
// return null;
|
||||
|
||||
// MapNode closestNode = null;
|
||||
// float closestDistance = float.MaxValue;
|
||||
|
||||
// foreach (var node in mapNodes)
|
||||
// {
|
||||
// var distance = CalculateDistance(position, node.Position);
|
||||
// if (distance < closestDistance)
|
||||
// {
|
||||
// closestDistance = distance;
|
||||
// closestNode = node;
|
||||
// }
|
||||
// }
|
||||
|
||||
// // 일정 거리 내에 있는 노드만 반환
|
||||
// return closestDistance < 50.0f ? closestNode : null;
|
||||
// }
|
||||
|
||||
// private float CalculateDistance(Point from, Point to)
|
||||
// {
|
||||
// var dx = to.X - from.X;
|
||||
// var dy = to.Y - from.Y;
|
||||
// return (float)Math.Sqrt(dx * dx + dy * dy);
|
||||
// }
|
||||
|
||||
// private void SetState(AGVState newState)
|
||||
// {
|
||||
// if (_currentState != newState)
|
||||
// {
|
||||
// _currentState = newState;
|
||||
// StateChanged?.Invoke(this, newState);
|
||||
// }
|
||||
// }
|
||||
|
||||
// private DockingDirection GetDockingDirection(NodeType nodeType)
|
||||
// {
|
||||
// switch (nodeType)
|
||||
// {
|
||||
// case NodeType.Charging:
|
||||
// return DockingDirection.Forward; // 충전기: 전진 도킹
|
||||
// case NodeType.Docking:
|
||||
// return DockingDirection.Backward; // 장비 (로더, 클리너, 오프로더, 버퍼): 후진 도킹
|
||||
// default:
|
||||
// return DockingDirection.Forward; // 기본값: 전진
|
||||
// }
|
||||
// }
|
||||
|
||||
// private void OnError(string message)
|
||||
// {
|
||||
// SetState(AGVState.Error);
|
||||
// ErrorOccurred?.Invoke(this, message);
|
||||
// }
|
||||
|
||||
// #endregion
|
||||
|
||||
// #region Cleanup
|
||||
|
||||
// /// <summary>
|
||||
// /// 리소스 정리
|
||||
// /// </summary>
|
||||
// public void Dispose()
|
||||
// {
|
||||
// _moveTimer?.Stop();
|
||||
// _moveTimer?.Dispose();
|
||||
// }
|
||||
|
||||
// #endregion
|
||||
// }
|
||||
//}
|
||||
44
Cs_HMI/AGVLogic/AGVSimulator/Program.cs
Normal file
44
Cs_HMI/AGVLogic/AGVSimulator/Program.cs
Normal file
@@ -0,0 +1,44 @@
|
||||
using System;
|
||||
using System.Windows.Forms;
|
||||
using AGVSimulator.Forms;
|
||||
|
||||
namespace AGVSimulator
|
||||
{
|
||||
/// <summary>
|
||||
/// AGV 시뮬레이터 프로그램 진입점
|
||||
/// </summary>
|
||||
static class Program
|
||||
{
|
||||
/// <summary>
|
||||
/// 콘솔 출력 (타임스탬프 포함)
|
||||
/// </summary>
|
||||
public static void WriteLine(string message)
|
||||
{
|
||||
string timestampedMessage = $"[{DateTime.Now:HH:mm:ss.fff}] {message}";
|
||||
Console.WriteLine(timestampedMessage);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 애플리케이션의 주 진입점입니다.
|
||||
/// </summary>
|
||||
/// <param name="args">명령줄 인수</param>
|
||||
[STAThread]
|
||||
static void Main(string[] args)
|
||||
{
|
||||
try
|
||||
{
|
||||
Application.EnableVisualStyles();
|
||||
Application.SetCompatibleTextRenderingDefault(false);
|
||||
Application.Run(new SimulatorForm());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[ERROR] 시뮬레이터 실행 중 오류: {ex.Message}");
|
||||
Console.WriteLine($"[ERROR] 스택 트레이스: {ex.StackTrace}");
|
||||
|
||||
MessageBox.Show($"시뮬레이터 실행 중 오류가 발생했습니다:\n{ex.Message}",
|
||||
"시스템 오류", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
36
Cs_HMI/AGVLogic/AGVSimulator/Properties/AssemblyInfo.cs
Normal file
36
Cs_HMI/AGVLogic/AGVSimulator/Properties/AssemblyInfo.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
// 어셈블리에 대한 일반 정보는 다음 특성 집합을 통해
|
||||
// 제어됩니다. 어셈블리와 관련된 정보를 수정하려면
|
||||
// 이러한 특성 값을 변경하세요.
|
||||
[assembly: AssemblyTitle("AGV Simulator")]
|
||||
[assembly: AssemblyDescription("ENIG AGV System Simulator")]
|
||||
[assembly: AssemblyConfiguration("")]
|
||||
[assembly: AssemblyCompany("ENIG")]
|
||||
[assembly: AssemblyProduct("AGV HMI System")]
|
||||
[assembly: AssemblyCopyright("Copyright © ENIG 2024")]
|
||||
[assembly: AssemblyTrademark("")]
|
||||
[assembly: AssemblyCulture("")]
|
||||
|
||||
// ComVisible을 false로 설정하면 이 어셈블리의 형식이 COM 구성 요소에
|
||||
// 표시되지 않습니다. COM에서 이 어셈블리의 형식에 액세스하려면
|
||||
// 해당 형식에 대해 ComVisible 특성을 true로 설정하세요.
|
||||
[assembly: ComVisible(false)]
|
||||
|
||||
// 이 프로젝트가 COM에 노출되는 경우 다음 GUID는 typelib의 ID를 나타냅니다.
|
||||
[assembly: Guid("b2c3d4e5-f6a7-4901-bcde-f23456789012")]
|
||||
|
||||
// 어셈블리의 버전 정보는 다음 네 개의 값으로 구성됩니다.
|
||||
//
|
||||
// 주 버전
|
||||
// 부 버전
|
||||
// 빌드 번호
|
||||
// 수정 버전
|
||||
//
|
||||
// 모든 값을 지정하거나 아래와 같이 '*'를 사용하여 빌드 번호 및 수정 번호를
|
||||
// 기본값으로 할 수 있습니다.
|
||||
// [assembly: AssemblyVersion("1.0.*")]
|
||||
[assembly: AssemblyVersion("1.0.0.0")]
|
||||
[assembly: AssemblyFileVersion("1.0.0.0")]
|
||||
29
Cs_HMI/AGVLogic/AGVSimulator/build.bat
Normal file
29
Cs_HMI/AGVLogic/AGVSimulator/build.bat
Normal file
@@ -0,0 +1,29 @@
|
||||
@echo off
|
||||
echo Building V2GDecoder VC++ Project...
|
||||
|
||||
REM Check if Visual Studio 2022 is installed (Professional or Community)
|
||||
set MSBUILD_PRO="C:\Program Files\Microsoft Visual Studio\2022\Professional\MSBuild\Current\Bin\MSBuild.exe"
|
||||
set MSBUILD_COM="C:\Program Files\Microsoft Visual Studio\2022\Community\MSBuild\Current\Bin\MSBuild.exe"
|
||||
set MSBUILD_BT="F:\(VHD) Program Files\Microsoft Visual Studio\2022\MSBuild\Current\Bin\MSBuild.exe"
|
||||
|
||||
if exist %MSBUILD_PRO% (
|
||||
echo "Found Visual Studio 2022 Professional"
|
||||
set MSBUILD=%MSBUILD_PRO%
|
||||
) else if exist %MSBUILD_COM% (
|
||||
echo "Found Visual Studio 2022 Community"
|
||||
set MSBUILD=%MSBUILD_COM%
|
||||
) else if exist %MSBUILD_BT% (
|
||||
echo "Found Visual Studio 2022 BuildTools"
|
||||
set MSBUILD=%MSBUILD_BT%
|
||||
) else (
|
||||
echo "Visual Studio 2022 (Professional or Community) not found!"
|
||||
echo "Please install Visual Studio 2022 or update the MSBuild path."
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM Build Debug x64 configuration
|
||||
echo Building Debug x64 configuration...
|
||||
%MSBUILD% AGVSimulator.csproj
|
||||
|
||||
pause
|
||||
4
Cs_HMI/AGVLogic/AGVSimulator/packages.config
Normal file
4
Cs_HMI/AGVLogic/AGVSimulator/packages.config
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<packages>
|
||||
<package id="Newtonsoft.Json" version="13.0.3" targetFramework="net48" />
|
||||
</packages>
|
||||
221
Cs_HMI/AGVLogic/CLAUDE.md
Normal file
221
Cs_HMI/AGVLogic/CLAUDE.md
Normal file
@@ -0,0 +1,221 @@
|
||||
# CLAUDE.md (AGVLogic 폴더)
|
||||
|
||||
이 파일은 AGVLogic 폴더에서 개발 중인 AGV 관련 프로젝트들을 위한 개발 가이드입니다.
|
||||
|
||||
**현재 폴더 위치**: `C:\Data\Source\(5613#) ENIG AGV\Source\Cs_HMI\AGVLogic\`
|
||||
**맵데이터**: `../Data/NewMap.agvmap` 파일을 기준으로 사용
|
||||
|
||||
---
|
||||
|
||||
## 프로젝트 개요
|
||||
|
||||
현재 AGVLogic 폴더에서 다음 3개의 독립 프로젝트를 개발 중입니다:
|
||||
|
||||
### 1. AGVMapEditor (맵 에디터)
|
||||
**위치**: `./AGVMapEditor/`
|
||||
**실행파일**: `./AGVMapEditor/bin/Debug/AGVMapEditor.exe`
|
||||
|
||||
#### 핵심 기능
|
||||
- **맵 노드 관리**: 논리적 노드 생성, 연결, 속성 설정
|
||||
- **RFID 매핑**: 물리적 RFID ID ↔ 논리적 노드 ID 매핑
|
||||
- **시각적 편집**: 드래그앤드롭으로 노드 배치 및 연결
|
||||
- **JSON 저장**: 맵 데이터를 JSON 형식으로 저장/로드
|
||||
- **노드 연결 관리**: 연결 목록 표시 및 직접 삭제 기능
|
||||
|
||||
#### 핵심 클래스
|
||||
- **MapNode**: 논리적 맵 노드 (NodeId, 위치, 타입, 연결 정보)
|
||||
- **RfidMapping**: RFID 물리적 ID ↔ 논리적 노드 ID 매핑
|
||||
- **NodeResolver**: RFID ID를 통한 노드 해석기
|
||||
- **MapCanvas**: 시각적 맵 편집 컨트롤
|
||||
|
||||
### 2. AGVNavigationCore (경로 탐색 라이브러리)
|
||||
**위치**: `./AGVNavigationCore/`
|
||||
|
||||
#### 핵심 기능
|
||||
- **A* 경로 탐색**: 최적 경로 계산 알고리즘
|
||||
- **방향 제어**: 전진/후진 모터 방향 결정
|
||||
- **도킹 검증**: 충전기/장비 도킹 방향 검증
|
||||
- **리프트 계산**: AGV 리프트 각도 계산
|
||||
- **경로 최적화**: 회전 구간 회피 등 고급 옵션
|
||||
|
||||
#### 핵심 클래스
|
||||
- **PathFinding/Core/AStarPathfinder.cs**: A* 알고리즘 구현
|
||||
- **PathFinding/Planning/AGVPathfinder.cs**: 경로 탐색 메인 클래스
|
||||
- **PathFinding/Planning/DirectionChangePlanner.cs**: 방향 변경 계획
|
||||
- **Utils/LiftCalculator.cs**: 리프트 각도 계산
|
||||
- **Utils/DockingValidator.cs**: 도킹 유효성 검증
|
||||
- **Controls/UnifiedAGVCanvas.cs**: 맵 및 AGV 시각화
|
||||
|
||||
### 3. AGVSimulator (AGV 시뮬레이터)
|
||||
**위치**: `./AGVSimulator/`
|
||||
**실행파일**: `./AGVSimulator/bin/Debug/AGVSimulator.exe`
|
||||
|
||||
#### 핵심 기능
|
||||
- **가상 AGV 시뮬레이션**: 실시간 AGV 움직임 및 상태 관리
|
||||
- **맵 시각화**: 맵 에디터에서 생성한 맵 파일 로드 및 표시
|
||||
- **경로 실행**: 계산된 경로를 따라 AGV 시뮬레이션
|
||||
- **상태 모니터링**: AGV 상태, 위치, 배터리 등 실시간 표시
|
||||
|
||||
#### 핵심 클래스
|
||||
- **VirtualAGV**: 가상 AGV 동작 시뮬레이션 (이동, 회전, 도킹, 충전)
|
||||
- **SimulatorCanvas**: AGV 및 맵 시각화 캔버스
|
||||
- **SimulatorForm**: 시뮬레이터 메인 인터페이스
|
||||
- **SimulationState**: 시뮬레이션 상태 관리
|
||||
|
||||
#### AGV 상태
|
||||
- **Idle**: 대기
|
||||
- **Moving**: 이동 중
|
||||
- **Rotating**: 회전 중
|
||||
- **Docking**: 도킹 중
|
||||
- **Charging**: 충전 중
|
||||
- **Error**: 오류
|
||||
|
||||
---
|
||||
|
||||
## AGV 방향 제어 및 도킹 시스템
|
||||
|
||||
### AGV 하드웨어 레이아웃
|
||||
```
|
||||
LIFT --- AGV --- MONITOR
|
||||
↑ ↑ ↑
|
||||
후진시 AGV본체 전진시
|
||||
도달위치 도달위치
|
||||
```
|
||||
|
||||
### 모터 방향과 이동 방향
|
||||
- **전진 모터 (Forward)**: AGV가 모니터 방향으로 이동 (→)
|
||||
- **후진 모터 (Backward)**: AGV가 리프트 방향으로 이동 (←)
|
||||
|
||||
### 도킹 방향 규칙
|
||||
- **충전기 (Charging)**: 전진 도킹 (Forward) - 모니터가 충전기 면
|
||||
- **장비 (Docking)**: 후진 도킹 (Backward) - 리프트가 장비 면
|
||||
|
||||
### 핵심 계산 파일들
|
||||
1. **LiftCalculator.cs** - 리프트 방향 계산
|
||||
- `CalculateLiftAngleRadians(Point currentPos, Point targetPos, AgvDirection motorDirection)`
|
||||
|
||||
2. **DirectionChangePlanner.cs** - 도킹 방향 결정
|
||||
- `GetRequiredDockingDirection(string targetNodeId)` - 노드타입별 도킹 방향 반환
|
||||
|
||||
3. **VirtualAGV.cs** - AGV 위치/방향 관리
|
||||
- `SetPosition(Point newPosition)` - AGV 위치 및 방향 설정
|
||||
|
||||
---
|
||||
|
||||
## AGVNavigationCore 프로젝트 구조
|
||||
|
||||
### 📁 폴더 구조
|
||||
```
|
||||
AGVNavigationCore/
|
||||
├── Controls/
|
||||
│ ├── UnifiedAGVCanvas.cs # AGV 및 맵 시각화 메인 캔버스
|
||||
│ ├── UnifiedAGVCanvas.Events.cs # 그리기 및 이벤트 처리
|
||||
│ ├── UnifiedAGVCanvas.Mouse.cs # 마우스 인터랙션
|
||||
│ ├── AGVState.cs # AGV 상태 정의
|
||||
│ └── IAGV.cs # AGV 인터페이스
|
||||
│
|
||||
├── Models/
|
||||
│ ├── MapNode.cs # 맵 노드 데이터 모델
|
||||
│ ├── MapLoader.cs # JSON 맵 파일 로더
|
||||
│ └── Enums.cs # 열거형 정의 (NodeType, AgvDirection 등)
|
||||
│
|
||||
├── Utils/
|
||||
│ ├── LiftCalculator.cs # 리프트 각도 계산
|
||||
│ └── DockingValidator.cs # 도킹 유효성 검증
|
||||
│
|
||||
└── PathFinding/
|
||||
├── Analysis/
|
||||
│ └── JunctionAnalyzer.cs # 교차점 분석
|
||||
│
|
||||
├── Core/
|
||||
│ ├── AStarPathfinder.cs # A* 알고리즘
|
||||
│ ├── PathNode.cs # 경로 노드
|
||||
│ └── AGVPathResult.cs # 경로 계산 결과
|
||||
│
|
||||
├── Planning/
|
||||
│ ├── AGVPathfinder.cs # 경로 탐색 메인 클래스
|
||||
│ ├── AdvancedAGVPathfinder.cs # 고급 경로 탐색
|
||||
│ ├── DirectionChangePlanner.cs # 방향 변경 계획
|
||||
│ ├── NodeMotorInfo.cs # 노드별 모터 정보
|
||||
│ └── PathfindingOptions.cs # 경로 탐색 옵션
|
||||
│
|
||||
└── Validation/
|
||||
├── DockingValidationResult.cs # 도킹 검증 결과
|
||||
└── PathValidationResult.cs # 경로 검증 결과
|
||||
```
|
||||
|
||||
### 🎯 클래스 배치 원칙
|
||||
|
||||
#### PathFinding/Validation/
|
||||
- **검증 결과 클래스**: `*ValidationResult.cs` 패턴 사용
|
||||
- **패턴**: 정적 팩토리 메서드 (CreateValid, CreateInvalid, CreateNotRequired)
|
||||
- **속성**: IsValid, ValidationError, 관련 상세 정보
|
||||
|
||||
#### PathFinding/Planning/
|
||||
- **경로 계획 클래스**: 실제 경로 탐색 및 계획 로직
|
||||
- **방향 변경 로직**: DirectionChangePlanner.cs
|
||||
- **경로 최적화**: 경로 생성과 관련된 전략
|
||||
|
||||
#### PathFinding/Core/
|
||||
- **핵심 알고리즘**: A* 알고리즘 등 기본 경로 탐색
|
||||
- **기본 경로 탐색**: 단순한 점-to-점 경로 계산
|
||||
|
||||
#### PathFinding/Analysis/
|
||||
- **경로 분석**: 생성된 경로의 품질 및 특성 분석
|
||||
- **성능 분석**: 경로 효율성 및 최적화 분석
|
||||
|
||||
---
|
||||
|
||||
## 개발 워크플로우
|
||||
|
||||
### 권장 개발 순서
|
||||
1. **맵 데이터 준비**: AGVMapEditor로 맵 노드 배치 및 RFID 매핑 설정
|
||||
2. **경로 탐색 구현**: AGVNavigationCore에서 경로 계산 알고리즘 개발
|
||||
3. **시뮬레이션 테스트**: AGVSimulator로 AGV 동작 검증
|
||||
4. **메인 프로젝트 통합**: 개발 완료 후 부모 폴더(Cs_HMI)에 병합
|
||||
|
||||
### 중요한 개발 패턴
|
||||
- **이벤트 기반 아키텍처**: UI 업데이트는 이벤트를 통해 자동화
|
||||
- **상태 관리**: _hasChanges 플래그로 변경사항 추적
|
||||
- **에러 처리**: 사용자 확인 다이얼로그와 상태바 메시지 활용
|
||||
- **코드 재사용**: UnifiedAGVCanvas를 맵에디터와 시뮬레이터에서 공통 사용
|
||||
|
||||
### 주의사항
|
||||
- **PathFinding 로직 변경시**: 반드시 시뮬레이터에서 테스트 후 적용
|
||||
- **노드 연결 관리**: 물리적 RFID와 논리적 노드 ID 분리 원칙 유지
|
||||
- **JSON 파일 형식**: 맵 데이터는 MapNodes, RfidMappings 두 섹션으로 구성
|
||||
- **좌표 시스템**: 줌/팬 상태에서 좌표 변환 정확성 지속 모니터링
|
||||
|
||||
---
|
||||
|
||||
## 최근 구현 완료 기능
|
||||
|
||||
### ✅ 회전 구간 회피 기능 (PathFinding)
|
||||
- **목적**: AGV 회전 오류를 피하기 위한 선택적 회전 구간 회피
|
||||
- **파일**: `PathFinding/PathfindingOptions.cs`
|
||||
- **UI**: AGVSimulator에 "회전 구간 회피" 체크박스
|
||||
|
||||
### ✅ 맵 에디터 마우스 좌표 오차 수정
|
||||
- **문제**: 줌 인/아웃 시 노드 선택 히트 영역이 너무 작음
|
||||
- **해결**: 최소 화면 히트 영역(20픽셀) 보장
|
||||
- **파일**: `AGVNavigationCore/Controls/UnifiedAGVCanvas.Mouse.cs`
|
||||
|
||||
### ✅ 노드 연결 관리 시스템
|
||||
- **기능**: 노드 연결 목록 표시 및 삭제
|
||||
- **파일들**:
|
||||
- `AGVMapEditor/Forms/MainForm.cs` - UI 및 이벤트 처리
|
||||
- `UnifiedAGVCanvas.cs` - 편집 모드 및 이벤트 정의
|
||||
- `UnifiedAGVCanvas.Mouse.cs` - 마우스 연결 삭제 기능
|
||||
|
||||
---
|
||||
|
||||
## 향후 개발 우선순위
|
||||
|
||||
1. **방향 전환 기능**: AGV 현재 방향과 목표 방향 불일치 시 회전 노드 경유 로직
|
||||
2. **맵 검증 기능**: 연결 무결성, 고립된 노드, 순환 경로 등 검증
|
||||
3. **성능 최적화**: 대형 맵에서 경로 계산 및 연결 목록 표시 성능 개선
|
||||
4. **실시간 동기화**: 맵 에디터와 시뮬레이터 간 실시간 맵 동기화
|
||||
|
||||
---
|
||||
|
||||
**최종 업데이트**: 2025-10-23 - AGVLogic 폴더 기준으로 정리
|
||||
44
Cs_HMI/AGVLogic/E2ETEST.md
Normal file
44
Cs_HMI/AGVLogic/E2ETEST.md
Normal file
@@ -0,0 +1,44 @@
|
||||
# E2E 테스트 계획
|
||||
|
||||
AGV 시스템의 종단간 테스트 시나리오 문서
|
||||
|
||||
## 테스트 시나리오
|
||||
|
||||
### 1. 기본 경로 계산 테스트
|
||||
- 시작점과 목적지 설정
|
||||
- 경로 계산 수행
|
||||
- 경로 유효성 검증
|
||||
|
||||
### 2. 방향 전환 테스트
|
||||
- 전진/후진 방향 전환이 필요한 경로
|
||||
- 갈림길에서의 방향 전환 검증
|
||||
- 회전 구간 회피 옵션 테스트
|
||||
|
||||
### 3. RFID 매핑 테스트
|
||||
- RFID-NodeID 매핑 검증
|
||||
- 중복 RFID 감지 테스트
|
||||
- 노드 해석 정확성 검증
|
||||
|
||||
### 4. 시뮬레이션 테스트
|
||||
- AGV 가상 이동 시뮬레이션
|
||||
- 경로 추적 정확성
|
||||
- 상태 변화 모니터링
|
||||
|
||||
### 5. 목적지 선택 기능 테스트
|
||||
- 목적지 선택 모드 활성화/비활성화
|
||||
- 노드 클릭으로 목적지 설정
|
||||
- 자동 경로 계산 수행
|
||||
|
||||
## 테스트 데이터
|
||||
|
||||
### 맵 데이터
|
||||
- NewMap.agvmap 기준 테스트
|
||||
- 다양한 노드 타입 검증
|
||||
- 복잡한 갈림길 구조 테스트
|
||||
|
||||
### 시나리오별 테스트 케이스
|
||||
1. 단순 직선 경로
|
||||
2. 다중 갈림길 경로
|
||||
3. 방향 전환이 필요한 경로
|
||||
4. 충전/도킹 노드 경로
|
||||
5. 회전 노드 회피 경로
|
||||
100
Cs_HMI/AGVLogic/PATHSCENARIO.md
Normal file
100
Cs_HMI/AGVLogic/PATHSCENARIO.md
Normal file
@@ -0,0 +1,100 @@
|
||||
## 경로시뮬레이션 설명
|
||||
## AGV는 같은경로상에서 방향을 전환할 수 없음
|
||||
## 경로계산을 위해서는 반드시 AGV는 2개 이상의 RFID를 읽어야 한다. (최소 2개를 읽어야 모터방향과 RFID의 읽히는 순서를 가지고 현재 AGV의 방향을 결정지을 수 있다)
|
||||
## 하기 케이스의 경우 케이스 설명전에 AGV가 어떻게 이동했는지 최소 2개의 RFID정보를 제공한다.
|
||||
## AGV의 RFID로 위치이동하는 것은 시뮬레이터폼의 SetAGVPositionByRfid 함수를 참고하면 됨
|
||||
## 방향전환이 필요할 때에 갈림길은 AGV와 가장 가까운 갈림길을 사용한다.
|
||||
|
||||
## case 1 (AGV가 전진방향으로 이동하는 경우)
|
||||
## AGV는 모터전진방향으로 008 -> 007 로 이동 (최종위치는 007)
|
||||
|
||||
Q1.목적지 : 015 (충전기 이므로 전진 방향 도킹해야하는 곳)
|
||||
A. 목적지 도킹방향과 현재 AGV도킹 방향이 동일하므로 방향전환이 필요없다. 목적지 까지의 최단거리를 계산한 후 그대로 이동하면됨
|
||||
007 - 006 - 005 - 004 - 012 - 013 - 014 - 015
|
||||
|
||||
Q2.목적지 : 019 (충전기 이므로 전진 방향 도킹해야하는 곳)
|
||||
A. 목적지 도킹방향과 현재 AGV도킹 방향이 동일하므로 방향전환이 필요없다. 목적지 까지의 최단거리를 계산한 후 그대로 이동하면됨
|
||||
007 - 006 - 005 - 004 - 012 - 016 - 017 - 018 - 019
|
||||
|
||||
Q3.목적지 : 001 (장비 이므로 후진 방향 도킹해야하는 곳)
|
||||
A. 목적지 도킹방향과 현재 AGV도킹 방향이 일치하지 않으니 방향전환이 필요하다,
|
||||
목적지까의 RFID목록은 007 - 006 - 005 - 004 - 003 - 002 - 001
|
||||
갈림길은 005 , 004 총 2개가 있으나 AGV 이동 방향상 가장 가까운 갈림길은 005이다. 전환은 005에서 하기로 한다.
|
||||
005갈림길은 내경로상의 006 과 037이 있다. 내 경로상에서 방향전환은 할 수 없으니 005 갈림길에서는 037로 방향을 틀어서 (Magnet Left) 전진이동을 한후
|
||||
037이 발견되면 방향을 후진으로 전환하면서 005를 거쳐 004방향으로 가도록 (Magnet Right) 로 유도해서 진행한다.
|
||||
그렇게하면 005를 지나 004를 갈때에는 후진방향으로 이동하게 된다. 후진시에는 전진과 magtnet 방향전환이 반대로 필요하다,
|
||||
037 -> 005 -> 004 의 경우 후진이동으로 좌회전을 해야하는데. 후진이기때문에 magnet 은 right 로 유도한다.
|
||||
|
||||
최종 경로는 아래와 같다
|
||||
|
||||
007(F) - 006(F) - 005(F) - 037(B) - 005(B) - 004(B) - 003(B) - 002(B) - 001(B)
|
||||
|
||||
Q4.목적지 : 011 (장비 이므로 후진 방향 도킹해야하는 곳)
|
||||
A. 목적지 도킹방향과 현재 AGV도킹 방향이 일치하지 않으니 방향전환이 필요하다,
|
||||
목적지까의 RFID목록은 007 - 006 - 005 - 004 - 030 - 009 - 010 - 011
|
||||
갈림길은 005 , 004 총 2개가 있으나 AGV 이동 방향상 가장 가까운 갈림길은 005이다. 전환은 005에서 하기로 한다.
|
||||
005갈림길은 내 경로상의 006 과 037이 있다. 내 경로상에서 방향전환은 할 수 없으니 005 갈림길에서는 037로 방향을 틀어서 (Magnet Left) 전진이동을 한후
|
||||
037이 발견되면 방향을 후진으로 전환하면서 005를 거쳐 004방향으로 가도록 (Magnet Right) 로 유도해서 진행한다.
|
||||
그렇게하면 005를 지나 004를 갈때에는 후진방향으로 이동하게 된다. 후진시에는 전진과 magtnet 방향전환이 반대로 필요하다,
|
||||
037 -> 005 -> 004 의 경우 후진이동으로 좌회전을 해야하는데. 후진이기때문에 magnet 은 right 로 유도한다.
|
||||
|
||||
최종 경로는 아래와 같다
|
||||
|
||||
007(F) - 006(F) - 005(F) - 037(B) - 005(B) - 004(B) - 030(B) - 009(B) - 010(B) - 011(B)
|
||||
|
||||
Q.목적지 : 041 (장비 이므로 후진 방향 도킹해야하는 곳)
|
||||
A. 목적지 도킹방향과 현재 AGV도킹 방향이 일치하지 않으니 방향전환이 필요하다,
|
||||
목적지까의 RFID목록은 007 - 006 - 005 - 037 - 036 - 035 - 034 - 033 - 032 - 031 - 041
|
||||
경로상 갈림길은 005 총 1개가 있으므로 전환은 005에서 하기로 한다.
|
||||
005갈림길은 내 경로상의 006 과 037(이 경우엔 037도 내 경로는 맞다)
|
||||
이 경우에는 006도 037도 내 경로이므로 005에 연결된 004포인트로 이동하면서 방향전환이 필요하다
|
||||
005 갈림길에서는 004까지 전진으로 진행하고 004도착시 후진을 하고 005에서 037로 방향을 틀도록 마그넷을(left)로 유도한다
|
||||
그렇게하면 005를 지나 037를 갈때에는 후진방향으로 이동하게 된다.
|
||||
|
||||
최종 경로는 아래와 같다
|
||||
|
||||
007(F) - 006(F) - 005(F) - 004(F) - 005(B) - 037(B) - 036(B) - 035(B) - 034(B) - 033(B) - 032(B) - 031(B) - 041(B)
|
||||
|
||||
Q5.8 (장비 이므로 후진 방향 도킹해야하는 곳)
|
||||
A. 목적지 도킹방향과 현재 AGV도킹 방향이 일치하지 않으니 방향전환이 필요하다,
|
||||
목적지까의 RFID목록은 007 - 006 - 005 - 037 - 036 - 035 - 034 - 038
|
||||
경로상 갈림길은 005 총 1개가 있으므로 전환은 005에서 하기로 한다.
|
||||
005갈림길은 내 경로상의 006 과 037(이 경우엔 037도 내 경로는 맞다)
|
||||
이 경우에는 006도 037도 내 경로이므로 005에 연결된 004포인트로 이동하면서 방향전환이 필요하다
|
||||
005 갈림길에서는 004까지 전진으로 진행하고 004도착시 후진을 하고 005에서 037로 방향을 틀도록 마그넷을(left)로 유도한다
|
||||
그렇게하면 005를 지나 037를 갈때에는 후진방향으로 이동하게 된다.
|
||||
|
||||
최종 경로는 아래와 같다
|
||||
|
||||
007(F) - 006(F) - 005(F) - 004(F) - 005(B) - 037(B) - 036(B) - 035(B) - 034(B) - 038(B)
|
||||
|
||||
|
||||
## AGV는 모터전진방향으로 037 -> 036 로 이동 (최종위치는 036)
|
||||
Q6.목적지 : 038 (장비 이므로 후진 방향 도킹해야하는 곳)
|
||||
A. 목적지 도킹방향과 현재 AGV도킹 방향이 일치하지 않으니 방향전환이 필요하다,
|
||||
목적지까의 RFID목록은 036 - 035 - 034 - 038
|
||||
경로상 갈림길이 없다, 가장 가까운 갈림길은 005이므로 전환은 005에서 하기로 한다.
|
||||
005갈림길은 내 경로상 포인트가 없으니 전환은 004 혹은 006 어떤쪽이던 상관없다.
|
||||
다만 이러한 경우 일관성을 위해 Magnet 유도를 Left를 사용한다
|
||||
036에서 후진으로 이동을 시작하면 037 -> 005 순으로 후진 이동을 한다. 여기서 방향전환을 해야하고 마그넷이 left로 유도가 되면
|
||||
AGV는 006방향으로 틀게된다. 이제 이러면 바로위의 Q5와 동일한 조건이 완성된다. 위치 006에서는 005 037 모두 목적지까지 포함되므로 004로
|
||||
이동해서 전환을 해야한다. 005(f), 004(f) 까지 이동을 한 후 이제 방향전환을 해서 후진으로 005까지 이동이 필요하다. 후진이므로
|
||||
magnet을 left유도하여 037로 이동할 수 있게한다
|
||||
|
||||
최종 경로는 아래와 같다
|
||||
|
||||
036(B) - 037(B) - 005(B) - 006(B) - 005(F) - 004(F) - 005(F) - 037(B) - 036(B) - 035(B) - 034(B) - 038(B)
|
||||
|
||||
|
||||
## case 2 (AGV가 후진방향으로 이동하는 경우)
|
||||
AGV는 모터후진방향으로 008 -> 007 로 이동 (최종위치는 007)
|
||||
Q7.목적지 : 015 (충전기는 전진 도킹해야합니다.)
|
||||
A. 목적지 도킹방향과 현재 AGV도킹 방향이 일치하지 않으니 방향전환이 필요하다,
|
||||
목적지까의 RFID목록은 007 - 006 - 005 - 004 - 012 - 013 -014 -015
|
||||
경로상 갈림길은 005, 004, 012 총 3개가 있다, 가장 가까운 갈림길은 005이므로 전환은 005에서 하기로 한다.
|
||||
005 갈림길은 내 경로상 포인트 (006,004)가 있으니 037 포인트를 이용하여 전환을 하면 된다.
|
||||
006(B) -> 005(B - 마그넷유도 RIGHT) -> 037(F) -> 그런후 방향전화을 해서 005까지 전진으로 이동을 하고 004로 방향을 틀면된다.
|
||||
|
||||
최종 경로는 아래와 같다
|
||||
|
||||
007(B) - 006(B) - 005(B-maget right) - 037(에 B로 도착하면 F로 전환한다) - 005(F) - 004(F) - 012(F) - 013(F) - 014(F) - 015(F)
|
||||
353
Cs_HMI/AGVLogic/PROJECT_SUMMARY.md
Normal file
353
Cs_HMI/AGVLogic/PROJECT_SUMMARY.md
Normal file
@@ -0,0 +1,353 @@
|
||||
# 프로젝트 요약 (AGVMapEditor, AGVNavigationCore, AGVSimulator)
|
||||
|
||||
## 📊 프로젝트 개요
|
||||
|
||||
3개의 주요 프로젝트가 **AGVNavigationCore** 라이브러리를 공유하며 연동되는 구조입니다.
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ AGVNavigationCore (공유 라이브러리) │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ • Models: MapNode, MapLoader, Enums, IMovableAGV, VirtualAGV
|
||||
│ • Controls: UnifiedAGVCanvas (통합 UI 렌더링)
|
||||
│ • PathFinding: A*, AGVPathfinder, DirectionChangePlanner
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
▲ ▲ ▲
|
||||
│ 참조 │ 참조 │ 참조
|
||||
│ │ │
|
||||
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
|
||||
│ AGVMapEditor │ │ AGVSimulator │ │ AGV4 (미사용)
|
||||
│ (맵 편집) │ │ (시뮬레이션) │ │ (메인앱)
|
||||
└──────────────┘ └──────────────┘ └──────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 각 프로젝트의 기능
|
||||
|
||||
### 1️⃣ **AGVMapEditor** (맵 편집 도구)
|
||||
|
||||
**목적**: AGV 맵 데이터를 시각적으로 생성/편집
|
||||
|
||||
#### 제공 기능
|
||||
| 기능 | 설명 |
|
||||
|------|------|
|
||||
| **노드 생성** | 맵 상에 AGV 네비게이션 노드 추가 |
|
||||
| **노드 편집** | 노드 이름, RFID, 타입, 도킹방향 설정 |
|
||||
| **노드 이동** | 드래그로 노드 위치 변경 (그리드 스냅 지원) |
|
||||
| **연결 관리** | 노드 간 경로 연결 생성/삭제 |
|
||||
| **노드 삭제** | 선택된 노드 제거 |
|
||||
| **라벨 추가** | 맵에 텍스트 라벨 추가 |
|
||||
| **이미지 추가** | 맵에 배경 이미지 추가 |
|
||||
| **맵 저장/로드** | JSON 형식 맵 파일 저장/로드 |
|
||||
|
||||
#### 기술 구성
|
||||
```
|
||||
MainForm (WinForms)
|
||||
└─ UnifiedAGVCanvas (UI 렌더링)
|
||||
├─ MapNode 목록 관리
|
||||
├─ 편집 모드 (Select, Move, AddNode, Connect, Delete, DeleteConnection, AddLabel, AddImage)
|
||||
└─ MapLoader 사용 (JSON 저장/로드)
|
||||
```
|
||||
|
||||
#### 주요 UI 요소
|
||||
- **메뉴바**: File (Open/Save), Edit, View
|
||||
- **툴바**: 편집 모드 전환 버튼 (선택, 이동, 노드추가, 연결, 삭제, 라벨, 이미지)
|
||||
- **속성 패널**: 선택된 노드 정보 표시/편집
|
||||
- **캔버스**: 맵 시각화 및 편집
|
||||
|
||||
---
|
||||
|
||||
### 2️⃣ **AGVNavigationCore** (경로 계산 엔진 라이브러리)
|
||||
|
||||
**목적**: AGV 경로 계산 및 네비게이션 핵심 로직 제공
|
||||
|
||||
#### 제공 기능
|
||||
| 영역 | 모듈 | 기능 |
|
||||
|------|------|------|
|
||||
| **Models** | MapNode | AGV 네비게이션 노드 데이터 |
|
||||
| | MapLoader | 맵 파일 로드/저장 |
|
||||
| | IMovableAGV | AGV 동작 인터페이스 |
|
||||
| | VirtualAGV | 가상 AGV 시뮬레이션 로직 ⚠️ **미완성** |
|
||||
| **Controls** | UnifiedAGVCanvas | 맵 렌더링/편집/모니터링 UI |
|
||||
| **PathFinding** | AStarPathfinder | 기본 A* 경로 탐색 |
|
||||
| | AGVPathfinder | AGV 제약 고려 경로 계산 ⚠️ **미완성** |
|
||||
| | DirectionChangePlanner | 방향 전환 경로 계획 ⚠️ **미완성** |
|
||||
|
||||
#### 핵심 기능 (구현 상태)
|
||||
```
|
||||
✅ 완성
|
||||
├─ MapNode 데이터 모델
|
||||
├─ MapLoader (파일 I/O)
|
||||
├─ UnifiedAGVCanvas (UI 렌더링/편집)
|
||||
└─ AStarPathfinder (기본 경로 탐색)
|
||||
|
||||
❌ 미완성 (개발 대상)
|
||||
├─ VirtualAGV.ExecutePath() - 경로 실행
|
||||
├─ VirtualAGV.Update() - 프레임 업데이트
|
||||
├─ AGVPathfinder 핵심 로직 - 경로 상세화, 마그넷 방향 계산
|
||||
└─ DirectionChangePlanner - 4단계 방향 전환 알고리즘
|
||||
```
|
||||
|
||||
#### 기술 구성
|
||||
```
|
||||
AGVNavigationCore (Class Library)
|
||||
├── Models/
|
||||
│ ├── MapNode.cs
|
||||
│ ├── MapLoader.cs
|
||||
│ ├── VirtualAGV.cs (← 경로 추적 미완성)
|
||||
│ ├── IMovableAGV.cs
|
||||
│ └── Enums.cs
|
||||
├── Controls/
|
||||
│ ├── UnifiedAGVCanvas.cs (메인 UI)
|
||||
│ ├── UnifiedAGVCanvas.Designer.cs
|
||||
│ ├── UnifiedAGVCanvas.Events.cs
|
||||
│ ├── UnifiedAGVCanvas.Mouse.cs (← 줌 기능)
|
||||
│ ├── UnifiedAGVCanvas.Rendering.cs
|
||||
│ └── UnifiedAGVCanvas.Utilities.cs
|
||||
├── PathFinding/
|
||||
│ ├── Core/
|
||||
│ │ ├── AStarPathfinder.cs ✅
|
||||
│ │ ├── PathNode.cs
|
||||
│ │ └── AGVPathResult.cs
|
||||
│ ├── Planning/
|
||||
│ │ ├── AGVPathfinder.cs (❌ 미완성)
|
||||
│ │ ├── DirectionChangePlanner.cs (❌ 미완성)
|
||||
│ │ └── NodeMotorInfo.cs
|
||||
│ ├── Analysis/
|
||||
│ │ └── JunctionAnalyzer.cs
|
||||
│ └── Validation/
|
||||
│ ├── PathValidationResult.cs
|
||||
│ └── DockingValidationResult.cs
|
||||
└── Utils/
|
||||
└── LiftCalculator.cs
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3️⃣ **AGVSimulator** (AGV 시뮬레이터)
|
||||
|
||||
**목적**: AGV 경로 계산 및 동작을 시각적으로 검증
|
||||
|
||||
#### 제공 기능
|
||||
| 기능 | 설명 |
|
||||
|------|------|
|
||||
| **맵 로드** | 저장된 맵 파일 로드 |
|
||||
| **AGV 시뮬레이션** | 가상 AGV 생성 및 경로 따라 이동 |
|
||||
| **경로 계산** | 시작점/목적지 선택 후 경로 자동 계산 |
|
||||
| **경로 시각화** | 계산된 경로 맵에 표시 |
|
||||
| **실시간 상태 모니터링** | AGV 위치, 방향, 상태 실시간 표시 |
|
||||
| **도킹 검증** | AGV 도킹 방향 검증 |
|
||||
|
||||
#### 기술 구성
|
||||
```
|
||||
SimulatorForm (WinForms)
|
||||
├─ UnifiedAGVCanvas (맵 렌더링)
|
||||
├─ VirtualAGV 리스트 (가상 AGV들)
|
||||
├─ MapLoader (맵 파일 로드)
|
||||
├─ AGVPathfinder (경로 계산)
|
||||
└─ 시뮬레이션 타이머 (매 프레임 AGV 업데이트)
|
||||
```
|
||||
|
||||
#### 주요 UI 요소
|
||||
- **메뉴바**: File (Open Map)
|
||||
- **경로 제어 그룹**: 시작RFID, 목적지RFID, 타겟계산 버튼
|
||||
- **시뮬레이션 제어**: 시작, 일시정지, 정지 버튼
|
||||
- **캔버스**: 맵 + AGV + 경로 시각화
|
||||
|
||||
---
|
||||
|
||||
## 🎨 UnifiedAGVCanvas (통합 UI 컨트롤)
|
||||
|
||||
**위치**: `AGVNavigationCore/Controls/UnifiedAGVCanvas.cs` (4개 파일)
|
||||
|
||||
### 핵심 기능
|
||||
| 기능 | 설명 |
|
||||
|------|------|
|
||||
| **맵 렌더링** | 노드, 연결선, 그리드, AGV, 경로 표시 |
|
||||
| **편집 기능** | 노드 추가/이동/삭제, 연결 관리 (AGVMapEditor) |
|
||||
| **모니터링** | 실시간 AGV 상태 표시 (AGVSimulator) |
|
||||
| **줌/팬** | 마우스 휠 줌, 좌클릭 드래그 팬 |
|
||||
| **선택/호버** | 노드 선택, 호버 표시 |
|
||||
| **경로 시각화** | 계산된 경로를 색상으로 표시 |
|
||||
|
||||
### 모드 및 상태
|
||||
```csharp
|
||||
// CanvasMode
|
||||
Edit // 편집 모드 (맵 에디터)
|
||||
|
||||
// EditMode (Edit 모드에서만 적용)
|
||||
Select // 노드 선택
|
||||
Move // 노드 이동
|
||||
AddNode // 노드 추가
|
||||
Connect // 연결 생성
|
||||
Delete // 노드/연결 삭제
|
||||
DeleteConnection // 연결 삭제
|
||||
AddLabel // 라벨 추가
|
||||
AddImage // 이미지 추가
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🖱️ 줌/팬 기능 (현재 상태)
|
||||
|
||||
### 현재 구현
|
||||
```csharp
|
||||
// UnifiedAGVCanvas.Mouse.cs : 171-188
|
||||
|
||||
private void UnifiedAGVCanvas_MouseWheel(object sender, MouseEventArgs e)
|
||||
{
|
||||
// 줌 처리
|
||||
var mouseWorldPoint = ScreenToWorld(e.Location);
|
||||
var oldZoom = _zoomFactor;
|
||||
|
||||
if (e.Delta > 0)
|
||||
_zoomFactor = Math.Min(_zoomFactor * 1.2f, 5.0f); // 확대 (1.2배)
|
||||
else
|
||||
_zoomFactor = Math.Max(_zoomFactor / 1.2f, 0.1f); // 축소 (1.2배)
|
||||
|
||||
// 마우스 위치를 중심으로 줌
|
||||
var zoomRatio = _zoomFactor / oldZoom;
|
||||
_panOffset.X = (int)(e.X - (e.X - _panOffset.X) * zoomRatio);
|
||||
_panOffset.Y = (int)(e.Y - (e.Y - _panOffset.Y) * zoomRatio);
|
||||
|
||||
Invalidate();
|
||||
}
|
||||
```
|
||||
|
||||
### 문제점 및 개선 사항
|
||||
|
||||
#### ⚠️ 현재 문제
|
||||
1. **줌 계산 로직**: 수식이 복잡하고 정확하지 않을 수 있음
|
||||
2. **좌표계 혼동**: 스크린 좌표와 월드 좌표 변환이 일관성 없음
|
||||
3. **매끄러움**: 줌 비율 계산에서 부자연스러운 동작 가능
|
||||
|
||||
#### ✅ 개선 방안
|
||||
마우스 커서 위치를 기준점으로 하는 스무스한 줌을 구현:
|
||||
|
||||
```csharp
|
||||
private void UnifiedAGVCanvas_MouseWheel(object sender, MouseEventArgs e)
|
||||
{
|
||||
// 현재 마우스 위치를 월드 좌표로 변환
|
||||
var mouseWorldBefore = ScreenToWorld(e.Location);
|
||||
|
||||
float oldZoom = _zoomFactor;
|
||||
|
||||
// 줌 팩터 계산 (휠 델타 기반)
|
||||
if (e.Delta > 0)
|
||||
_zoomFactor = Math.Min(_zoomFactor * 1.15f, 5.0f); // 확대 (부드러움)
|
||||
else
|
||||
_zoomFactor = Math.Max(_zoomFactor / 1.15f, 0.1f); // 축소 (부드러움)
|
||||
|
||||
// 마우스 위치가 같은 월드 좌표를 가리키도록 팬 오프셋 조정
|
||||
var mouseWorldAfter = ScreenToWorld(e.Location);
|
||||
|
||||
_panOffset.X += (int)((mouseWorldBefore.X - mouseWorldAfter.X) * _zoomFactor);
|
||||
_panOffset.Y += (int)((mouseWorldBefore.Y - mouseWorldAfter.Y) * _zoomFactor);
|
||||
|
||||
Invalidate();
|
||||
}
|
||||
```
|
||||
|
||||
#### 주요 개선점
|
||||
1. **더 부드러운 줌**: 1.2배 → 1.15배로 조정
|
||||
2. **명확한 로직**: 마우스 위치 기준으로 명시적으로 계산
|
||||
3. **정확한 좌표 변환**: ScreenToWorld() 사용으로 일관성 보장
|
||||
4. **자연스러운 동작**: 마우스 아래의 점이 같은 위치를 가리킴
|
||||
|
||||
---
|
||||
|
||||
## 📂 파일 구조 (48개 C# 파일)
|
||||
|
||||
### AGVMapEditor (10개 파일)
|
||||
```
|
||||
AGVMapEditor/
|
||||
├── Forms/
|
||||
│ ├── MainForm.cs (메인 폼, 편집 로직)
|
||||
│ └── MainForm.Designer.cs (UI 디자인)
|
||||
├── Models/
|
||||
│ ├── EditorSettings.cs (에디터 설정)
|
||||
│ ├── MapImage.cs (맵 이미지 데이터)
|
||||
│ ├── MapLabel.cs (맵 라벨 데이터)
|
||||
│ └── NodePropertyWrapper.cs (노드 속성 래퍼)
|
||||
├── Program.cs (진입점)
|
||||
└── Properties/
|
||||
└── AssemblyInfo.cs (어셈블리 정보)
|
||||
```
|
||||
|
||||
### AGVNavigationCore (30개 파일)
|
||||
```
|
||||
AGVNavigationCore/
|
||||
├── Models/ (8개)
|
||||
│ ├── MapNode.cs, MapLoader.cs, VirtualAGV.cs, IMovableAGV.cs, etc.
|
||||
├── Controls/ (6개)
|
||||
│ ├── UnifiedAGVCanvas.cs, UnifiedAGVCanvas.Designer.cs, etc.
|
||||
├── PathFinding/ (13개)
|
||||
│ ├── Core/ (AStarPathfinder, PathNode, AGVPathResult)
|
||||
│ ├── Planning/ (AGVPathfinder, DirectionChangePlanner, etc.)
|
||||
│ ├── Analysis/ (JunctionAnalyzer)
|
||||
│ └── Validation/ (PathValidationResult, DockingValidationResult)
|
||||
└── Utils/ (3개)
|
||||
├── LiftCalculator.cs, DockingValidator.cs, etc.
|
||||
```
|
||||
|
||||
### AGVSimulator (8개 파일)
|
||||
```
|
||||
AGVSimulator/
|
||||
├── Forms/
|
||||
│ ├── SimulatorForm.cs (시뮬레이터 메인 폼)
|
||||
│ └── SimulatorForm.Designer.cs
|
||||
├── Models/
|
||||
│ └── (VirtualAGV는 AGVNavigationCore에서 참조)
|
||||
├── Program.cs
|
||||
└── Properties/
|
||||
└── AssemblyInfo.cs
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔄 데이터 흐름
|
||||
|
||||
### 맵 편집 → 저장 흐름
|
||||
```
|
||||
AGVMapEditor.MainForm
|
||||
↓
|
||||
UnifiedAGVCanvas (편집 모드)
|
||||
↓
|
||||
MapLoader.SaveMapToFile()
|
||||
↓
|
||||
NewMap.agvmap (JSON 파일)
|
||||
```
|
||||
|
||||
### 맵 로드 → 시뮬레이션 흐름
|
||||
```
|
||||
NewMap.agvmap (JSON 파일)
|
||||
↓
|
||||
MapLoader.LoadMapFromFile()
|
||||
↓
|
||||
AGVSimulator.SimulatorForm
|
||||
↓
|
||||
UnifiedAGVCanvas (모니터링 모드)
|
||||
↓
|
||||
VirtualAGV (경로 실행) ⚠️ 미완성
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 현재 미완성 부분
|
||||
|
||||
### 🔴 우선순위 높음
|
||||
1. **VirtualAGV.ExecutePath()** - 경로 실행 로직
|
||||
2. **VirtualAGV.Update()** - 매 프레임 위치 업데이트
|
||||
3. **AGVPathfinder 핵심** - 경로 상세화, 마그넷 방향 계산
|
||||
|
||||
### 🟡 우선순위 중간
|
||||
4. **DirectionChangePlanner** - 4단계 방향 전환 알고리즘
|
||||
5. **UnifiedAGVCanvas 줌 개선** - 마우스 기준 스무스 줌
|
||||
|
||||
---
|
||||
|
||||
## 📝 참고 문서
|
||||
- `CLAUDE.md` - 개발 가이드 및 AGV 하드웨어 설명
|
||||
- `CHANGELOG.md` - 변경 로그
|
||||
- `Data/NewMap.agvmap` - 실제 맵 데이터 샘플
|
||||
191
Cs_HMI/AGVLogic/TODO.md
Normal file
191
Cs_HMI/AGVLogic/TODO.md
Normal file
@@ -0,0 +1,191 @@
|
||||
# AGV 네비게이션 시스템 개발 현황
|
||||
|
||||
## 📊 프로젝트 개요
|
||||
**AGV 이동 시스템 설계 및 개발 - RFID 기반 네비게이션 시스템**
|
||||
|
||||
최근 리팩토링을 통해 전문 라이브러리 **AGVNavigationCore** 중심의 현대적 아키텍처로 재구성됨.
|
||||
|
||||
---
|
||||
|
||||
## ✅ **완료된 핵심 시스템**
|
||||
|
||||
### 🏗️ **AGVNavigationCore 라이브러리** (완료)
|
||||
**전문 AGV 네비게이션 라이브러리 - 상업적 수준 완성도**
|
||||
|
||||
#### **Models 패키지** ✅
|
||||
- **MapNode.cs**: 고도화된 노드 모델
|
||||
- RFID 매핑 통합, 라벨/이미지 지원
|
||||
- 도킹 방향, 장비 타입, 회전 가능 여부
|
||||
- 이미지 자동 리사이즈, 투명도, 회전 지원
|
||||
- **RfidMapping.cs**: RFID ↔ NodeId 매핑 시스템
|
||||
- **Enums.cs**: 완전한 열거형 (NodeType, AgvDirection, DockingDirection, StationType)
|
||||
|
||||
#### **PathFinding 패키지** ✅
|
||||
- **AStarPathfinder.cs**: 표준 A* 알고리즘 완전 구현
|
||||
- 양방향 연결 자동 생성
|
||||
- 휴리스틱 가중치, 최대 탐색 노드 제한
|
||||
- 다중 목표 최단 경로 탐색
|
||||
- **AGVPathfinder.cs**: AGV 특화 제약사항 완전 반영
|
||||
- 방향성 제약 (전진/후진만 가능)
|
||||
- 회전 제약 (특정 지점에서만 180도 회전)
|
||||
- 도킹 방향 강제 (충전기:전진, 장비:후진)
|
||||
- 실행 가능한 AGV 명령어 생성
|
||||
- **RfidBasedPathfinder.cs**: 현장 운영 완전 대응
|
||||
- RFID 기반 실시간 경로 계산
|
||||
- 물리적 RFID와 논리적 노드 분리
|
||||
- 현장 유지보수성 극대화
|
||||
- **PathResult/AGVPathResult/RfidPathResult**: 계층적 결과 시스템
|
||||
|
||||
#### **Controls 패키지** ✅
|
||||
- **UnifiedAGVCanvas.cs**: 통합 캔버스 컨트롤
|
||||
- 맵 편집, 시뮬레이션, 모니터링 통합
|
||||
- ViewOnly/Edit 모드 분리
|
||||
- 그리드, 줌, 패닝 지원
|
||||
|
||||
### 🎯 **개발 도구들** (리팩토링 완료)
|
||||
|
||||
#### **AGVMapEditor** ✅ (현대화됨)
|
||||
- UnifiedAGVCanvas 기반 리팩토링
|
||||
- RFID 매핑 분리 아키텍처 적용
|
||||
- 라벨/이미지 추가 기능 강화
|
||||
- JSON 파일 형식 개선
|
||||
|
||||
#### **AGVSimulator** ✅ (개선됨)
|
||||
- VirtualAGV 클래스 고도화
|
||||
- UnifiedAGVCanvas 통합
|
||||
- 실시간 상태 시뮬레이션
|
||||
|
||||
---
|
||||
|
||||
## 🚀 **현재 개발 진척도**
|
||||
|
||||
### **Phase 1: 기반 시스템** ✅ **100% 완료**
|
||||
1. **맵 에디터** ✅ **완료 + 현대화**
|
||||
- [x] UnifiedAGVCanvas 기반 리팩토링
|
||||
- [x] RFID 매핑 분리 아키텍처 적용
|
||||
- [x] 라벨/이미지 고급 기능 (투명도, 회전, 스케일)
|
||||
- [x] JSON 저장/로드 개선
|
||||
|
||||
2. **경로 계산 엔진** ✅ **100% 완료**
|
||||
- [x] **A* 알고리즘** - AStarPathfinder 완전 구현
|
||||
- [x] **방향성 고려 라우팅** - AGVPathfinder 완전 구현
|
||||
- [x] **도킹 방향 고려** - 충전기(전진), 장비(후진) 강제
|
||||
- [x] **동적 경로 재계산** - 실시간 RFID 기반 검증
|
||||
|
||||
### **Phase 2: 이동 제어 시스템** ✅ **90% 완료**
|
||||
3. **AGV 모션 컨트롤러** ✅ **완료**
|
||||
- [x] **실행 가능한 명령어 생성** - [전진, 후진, 좌회전, 우회전, 정지]
|
||||
- [x] **방향 전환 로직** - 회전 지점에서만 180도 회전
|
||||
- [x] **도킹 시퀀스 제어** - 방향별 자동 접근 전략
|
||||
|
||||
4. **위치 추적 시스템** ✅ **80% 완료**
|
||||
- [x] **RFID 기반 위치 인식** - RfidBasedPathfinder
|
||||
- [x] **실시간 경로 검증** - ValidatePath 기능
|
||||
- [ ] **하드웨어 RFID 리더 연동** (메인 애플리케이션 통합 필요)
|
||||
|
||||
### **Phase 3: 통합 및 테스트** ✅ **70% 완료**
|
||||
5. **시뮬레이션 도구** ✅ **완료**
|
||||
- [x] **가상 AGV 시뮬레이터** - VirtualAGV 클래스
|
||||
- [x] **경로 시각화** - UnifiedAGVCanvas 통합
|
||||
- [x] **실시간 디버깅** - 상태별 색상 표시
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ **현재 시스템 아키텍처**
|
||||
|
||||
### 📊 **실제 구현된 컴포넌트 구조**
|
||||
```
|
||||
AGVNavigationCore (전문 라이브러리)
|
||||
├── PathFinding Engine
|
||||
│ ├── AStarPathfinder ✅ // 표준 A* 알고리즘
|
||||
│ ├── AGVPathfinder ✅ // AGV 제약사항 특화
|
||||
│ └── RfidBasedPathfinder ✅ // 현장 운영 최적화
|
||||
├── Data Models
|
||||
│ ├── MapNode ✅ // 고도화된 노드 모델
|
||||
│ ├── RfidMapping ✅ // RFID 매핑 시스템
|
||||
│ └── Result Classes ✅ // 계층적 결과 체계
|
||||
└── UI Controls
|
||||
└── UnifiedAGVCanvas ✅ // 통합 캔버스
|
||||
|
||||
AGVMapEditor ✅ // 맵 편집 도구
|
||||
└── UnifiedAGVCanvas 기반 현대화
|
||||
|
||||
AGVSimulator ✅ // AGV 시뮬레이터
|
||||
└── VirtualAGV + UnifiedAGVCanvas
|
||||
|
||||
메인 애플리케이션 (AGV4)
|
||||
└── AGVNavigationCore 참조 (통합 예정)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 **AGV 동작 제약사항 (완전 반영됨)**
|
||||
|
||||
### **물리적 제약사항** ✅
|
||||
- **전진**: 모니터 방향으로만 이동 가능
|
||||
- **후진**: 리프트 방향으로만 이동 가능
|
||||
- **회전**: 특정 회전 지점에서만 180도 회전 가능
|
||||
- **좌우 이동**: 불가능 (실제 AGV 한계 반영)
|
||||
|
||||
### **도킹 제약사항** ✅
|
||||
```
|
||||
장비별 도킹 방향 (강제 적용):
|
||||
├── 로더, 클리너, 오프로더, 버퍼 (8대) → 후진 도킹
|
||||
└── 충전기 1, 충전기 2 (2대) → 전진 도킹
|
||||
```
|
||||
|
||||
### **RFID 매핑 시스템** ✅
|
||||
```csharp
|
||||
// 실제 구현된 매핑 시스템
|
||||
RFID: "1234567890" → NodeId: "LOADER1" → 실제 의미: "1번 로더"
|
||||
RFID: "9876543210" → NodeId: "CHARGE1" → 실제 의미: "1번 충전기"
|
||||
|
||||
// 현장 작업자용 정보
|
||||
RfidDescription: "로더1번 입구", "충전기2번 도킹 지점"
|
||||
Status: "정상", "손상", "교체예정"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📋 **다음 단계 (우선순위별)**
|
||||
|
||||
### 🔥 **우선순위 1: 메인 애플리케이션 통합**
|
||||
- [ ] **AGV4 프로젝트에 AGVNavigationCore 통합**
|
||||
- [ ] **기존 AGV 컨트롤러와 인터페이스 연동**
|
||||
- [ ] **실제 RFID 리더 하드웨어 연동**
|
||||
|
||||
### ⚡ **우선순위 2: 현장 검증**
|
||||
- [ ] **실제 맵 데이터 생성 및 검증** (NewMap.agvmap 활용)
|
||||
- [ ] **실제 AGV로 경로 추적 테스트**
|
||||
- [ ] **RFID 태그 현장 설치 및 매핑**
|
||||
|
||||
### 🛠️ **우선순위 3: 운영 최적화**
|
||||
- [ ] **성능 최적화** (대규모 맵 대응)
|
||||
- [ ] **에러 처리 강화** (RFID 인식 실패, 경로 차단 등)
|
||||
- [ ] **로깅 및 모니터링 시스템**
|
||||
|
||||
---
|
||||
|
||||
## 🌟 **주요 성과 및 차별화 포인트**
|
||||
|
||||
### **기술적 성과**
|
||||
1. **3단계 API 아키텍처**: Basic(A*) → AGV특화 → RFID기반
|
||||
2. **실행 가능한 명령어 생성**: 경로가 아닌 AGV 제어 명령어 직접 출력
|
||||
3. **현장 친화적 설계**: RFID 물리/논리 분리로 유지보수성 극대화
|
||||
4. **통합 캔버스**: 편집/시뮬레이션/모니터링 단일 컨트롤
|
||||
|
||||
### **실용적 가치**
|
||||
- **즉시 운영 가능**: 상업적 수준의 완성된 네비게이션 엔진
|
||||
- **확장성**: 새로운 AGV 타입이나 장비 쉽게 추가
|
||||
- **안정성**: 실제 AGV 제약사항 완전 반영으로 안전한 경로 생성
|
||||
|
||||
---
|
||||
|
||||
## 📖 **참고 문서**
|
||||
- **AGVNavigationCore/README.md**: 상세 기능 설명 및 사용법
|
||||
- **Data/NewMap.agvmap**: 실제 맵 데이터 샘플
|
||||
- **CLAUDE.md**: 개발 환경 및 빌드 정보
|
||||
|
||||
---
|
||||
|
||||
*최종 업데이트: 2024.09.12 - AGVNavigationCore 리팩토링 완료 기준*
|
||||
13
Cs_HMI/AGVLogic/build.bat
Normal file
13
Cs_HMI/AGVLogic/build.bat
Normal file
@@ -0,0 +1,13 @@
|
||||
@echo off
|
||||
echo Building AGV C# HMI Project...
|
||||
|
||||
REM Set MSBuild path
|
||||
REM set MSBUILD="C:\Program Files\Microsoft Visual Studio\2022\Professional\MSBuild\Current\Bin\MSBuild.exe"
|
||||
|
||||
set MSBUILD="C:\Program Files (x86)\Microsoft Visual Studio\2017\WDExpress\MSBuild\15.0\Bin\MSBuild.exe"
|
||||
|
||||
|
||||
REM Rebuild Debug x86 configuration (VS-style Rebuild)
|
||||
%MSBUILD% AGVCSharp.sln -property:Configuration=Debug -property:Platform=x86 -verbosity:quiet -nologo -t:Rebuild
|
||||
|
||||
pause
|
||||
1
Cs_HMI/AGVLogic/run_claude.bat
Normal file
1
Cs_HMI/AGVLogic/run_claude.bat
Normal file
@@ -0,0 +1 @@
|
||||
claude --dangerously-skip-permissions
|
||||
Reference in New Issue
Block a user