fix: 맵 에디터 연결 버그 수정 및 기능 개선
주요 변경사항: - ConnectedMapNodes 속성 추가로 런타임 객체 참조 지원 - 이미지 에디터 UI 개선 (ImageEditorCanvas 추가) - 연결 생성 버그 수정: 양방향 연결 생성 - 연결 삭제 버그 수정: 양방향 모두 제거 - CleanupDuplicateConnections 비활성화 (단방향 변환 버그) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -49,9 +49,15 @@
|
|||||||
</ProjectReference>
|
</ProjectReference>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<Compile Include="Controls\ImageEditorCanvas.cs">
|
||||||
|
<SubType>UserControl</SubType>
|
||||||
|
</Compile>
|
||||||
<Compile Include="Forms\ImageEditorForm.cs">
|
<Compile Include="Forms\ImageEditorForm.cs">
|
||||||
<SubType>Form</SubType>
|
<SubType>Form</SubType>
|
||||||
</Compile>
|
</Compile>
|
||||||
|
<Compile Include="Forms\ImageEditorForm.Designer.cs">
|
||||||
|
<DependentUpon>ImageEditorForm.cs</DependentUpon>
|
||||||
|
</Compile>
|
||||||
<Compile Include="Models\EditorSettings.cs" />
|
<Compile Include="Models\EditorSettings.cs" />
|
||||||
<Compile Include="Models\ImagePathEditor.cs" />
|
<Compile Include="Models\ImagePathEditor.cs" />
|
||||||
<Compile Include="Models\MapImage.cs" />
|
<Compile Include="Models\MapImage.cs" />
|
||||||
@@ -86,7 +92,6 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Folder Include="Data\" />
|
<Folder Include="Data\" />
|
||||||
<Folder Include="Utils\" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||||
</Project>
|
</Project>
|
||||||
413
Cs_HMI/AGVLogic/AGVMapEditor/Controls/ImageEditorCanvas.cs
Normal file
413
Cs_HMI/AGVLogic/AGVMapEditor/Controls/ImageEditorCanvas.cs
Normal file
@@ -0,0 +1,413 @@
|
|||||||
|
using System;
|
||||||
|
using System.Drawing;
|
||||||
|
using System.Drawing.Drawing2D;
|
||||||
|
using System.Windows.Forms;
|
||||||
|
|
||||||
|
namespace AGVMapEditor.Controls
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 이미지 편집용 사용자 정의 캔버스 컨트롤
|
||||||
|
/// 이미지 중앙 정렬, 크기 조정 핸들, 브러시 그리기 기능 제공
|
||||||
|
/// </summary>
|
||||||
|
public class ImageEditorCanvas : UserControl
|
||||||
|
{
|
||||||
|
private Bitmap _editingImage;
|
||||||
|
private Graphics _imageGraphics;
|
||||||
|
private Rectangle _imageRect = Rectangle.Empty;
|
||||||
|
private float _imageDisplayWidth = 0;
|
||||||
|
private float _imageDisplayHeight = 0;
|
||||||
|
|
||||||
|
// 브러시 그리기
|
||||||
|
private bool _isDrawing = false;
|
||||||
|
private Point _lastDrawPoint = Point.Empty;
|
||||||
|
private Color _drawColor = Color.Black;
|
||||||
|
private int _brushSize = 3;
|
||||||
|
private bool _brushModeEnabled = false;
|
||||||
|
|
||||||
|
// 크기 조정
|
||||||
|
private bool _isResizing = false;
|
||||||
|
private ResizeHandle _activeHandle = ResizeHandle.None;
|
||||||
|
private Point _resizeStartPoint = Point.Empty;
|
||||||
|
private float _resizeStartWidth = 0;
|
||||||
|
private float _resizeStartHeight = 0;
|
||||||
|
private const int HANDLE_SIZE = 8;
|
||||||
|
|
||||||
|
private enum ResizeHandle
|
||||||
|
{
|
||||||
|
None,
|
||||||
|
TopLeft, Top, TopRight,
|
||||||
|
Right, BottomRight, Bottom,
|
||||||
|
BottomLeft, Left
|
||||||
|
}
|
||||||
|
|
||||||
|
public ImageEditorCanvas()
|
||||||
|
{
|
||||||
|
this.DoubleBuffered = true;
|
||||||
|
this.BackColor = Color.White;
|
||||||
|
this.AutoScroll = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Properties
|
||||||
|
|
||||||
|
public Bitmap EditingImage
|
||||||
|
{
|
||||||
|
get => _editingImage;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_editingImage = value;
|
||||||
|
if (_editingImage != null)
|
||||||
|
{
|
||||||
|
_imageGraphics?.Dispose();
|
||||||
|
_imageGraphics = Graphics.FromImage(_editingImage);
|
||||||
|
_imageDisplayWidth = _editingImage.Width;
|
||||||
|
_imageDisplayHeight = _editingImage.Height;
|
||||||
|
UpdateImageRect();
|
||||||
|
}
|
||||||
|
Invalidate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Color DrawColor
|
||||||
|
{
|
||||||
|
get => _drawColor;
|
||||||
|
set => _drawColor = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int BrushSize
|
||||||
|
{
|
||||||
|
get => _brushSize;
|
||||||
|
set => _brushSize = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool BrushModeEnabled
|
||||||
|
{
|
||||||
|
get => _brushModeEnabled;
|
||||||
|
set => _brushModeEnabled = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Size ImageDisplaySize => new Size((int)_imageDisplayWidth, (int)_imageDisplayHeight);
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Paint
|
||||||
|
|
||||||
|
protected override void OnPaint(PaintEventArgs e)
|
||||||
|
{
|
||||||
|
base.OnPaint(e);
|
||||||
|
|
||||||
|
if (_editingImage == null)
|
||||||
|
{
|
||||||
|
e.Graphics.Clear(BackColor);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 배경 채우기
|
||||||
|
e.Graphics.Clear(BackColor);
|
||||||
|
|
||||||
|
// 이미지 영역 업데이트
|
||||||
|
UpdateImageRect();
|
||||||
|
|
||||||
|
// 이미지 그리기 (고품질)
|
||||||
|
e.Graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
|
||||||
|
e.Graphics.DrawImage(_editingImage, _imageRect);
|
||||||
|
|
||||||
|
// 크기 조정 핸들 그리기
|
||||||
|
DrawResizeHandles(e.Graphics);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateImageRect()
|
||||||
|
{
|
||||||
|
if (_editingImage == null || Width == 0 || Height == 0)
|
||||||
|
{
|
||||||
|
_imageRect = Rectangle.Empty;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 이미지를 중앙 정렬
|
||||||
|
float x = (Width - _imageDisplayWidth) / 2f;
|
||||||
|
float y = (Height - _imageDisplayHeight) / 2f;
|
||||||
|
|
||||||
|
// 음수 방지
|
||||||
|
if (x < 0) x = 0;
|
||||||
|
if (y < 0) y = 0;
|
||||||
|
|
||||||
|
_imageRect = new Rectangle((int)x, (int)y, (int)_imageDisplayWidth, (int)_imageDisplayHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawResizeHandles(Graphics g)
|
||||||
|
{
|
||||||
|
if (_imageRect.IsEmpty)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var handles = GetResizeHandles();
|
||||||
|
foreach (var handle in handles)
|
||||||
|
{
|
||||||
|
g.FillRectangle(Brushes.Blue, handle);
|
||||||
|
g.DrawRectangle(Pens.White, handle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Rectangle[] GetResizeHandles()
|
||||||
|
{
|
||||||
|
int x = _imageRect.X;
|
||||||
|
int y = _imageRect.Y;
|
||||||
|
int w = _imageRect.Width;
|
||||||
|
int h = _imageRect.Height;
|
||||||
|
int hs = HANDLE_SIZE;
|
||||||
|
|
||||||
|
return new Rectangle[]
|
||||||
|
{
|
||||||
|
new Rectangle(x - hs/2, y - hs/2, hs, hs), // TopLeft
|
||||||
|
new Rectangle(x + w/2 - hs/2, y - hs/2, hs, hs), // Top
|
||||||
|
new Rectangle(x + w - hs/2, y - hs/2, hs, hs), // TopRight
|
||||||
|
new Rectangle(x + w - hs/2, y + h/2 - hs/2, hs, hs), // Right
|
||||||
|
new Rectangle(x + w - hs/2, y + h - hs/2, hs, hs), // BottomRight
|
||||||
|
new Rectangle(x + w/2 - hs/2, y + h - hs/2, hs, hs), // Bottom
|
||||||
|
new Rectangle(x - hs/2, y + h - hs/2, hs, hs), // BottomLeft
|
||||||
|
new Rectangle(x - hs/2, y + h/2 - hs/2, hs, hs) // Left
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Mouse Events
|
||||||
|
|
||||||
|
protected override void OnMouseDown(MouseEventArgs e)
|
||||||
|
{
|
||||||
|
base.OnMouseDown(e);
|
||||||
|
|
||||||
|
if (_editingImage == null || e.Button != MouseButtons.Left)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// 크기 조정 핸들 확인
|
||||||
|
_activeHandle = GetHandleAtPoint(e.Location);
|
||||||
|
if (_activeHandle != ResizeHandle.None)
|
||||||
|
{
|
||||||
|
_isResizing = true;
|
||||||
|
_resizeStartPoint = e.Location;
|
||||||
|
_resizeStartWidth = _imageDisplayWidth;
|
||||||
|
_resizeStartHeight = _imageDisplayHeight;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 브러시 모드: 그리기
|
||||||
|
if (_brushModeEnabled && _imageRect.Contains(e.Location))
|
||||||
|
{
|
||||||
|
_isDrawing = true;
|
||||||
|
_lastDrawPoint = ImagePointFromScreen(e.Location);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnMouseMove(MouseEventArgs e)
|
||||||
|
{
|
||||||
|
base.OnMouseMove(e);
|
||||||
|
|
||||||
|
if (_editingImage == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// 크기 조정 중
|
||||||
|
if (_isResizing && _activeHandle != ResizeHandle.None)
|
||||||
|
{
|
||||||
|
ResizeImageDisplay(e.Location);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 크기 조정 핸들 위에 마우스가 있으면 커서 변경
|
||||||
|
var handle = GetHandleAtPoint(e.Location);
|
||||||
|
if (handle != ResizeHandle.None)
|
||||||
|
{
|
||||||
|
Cursor = GetCursorForHandle(handle);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Cursor = Cursors.Default;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 브러시 모드: 그리기
|
||||||
|
if (_isDrawing && _lastDrawPoint != Point.Empty && _brushModeEnabled && _imageRect.Contains(e.Location))
|
||||||
|
{
|
||||||
|
Point currentImagePoint = ImagePointFromScreen(e.Location);
|
||||||
|
_imageGraphics.DrawLine(new Pen(_drawColor, _brushSize), _lastDrawPoint, currentImagePoint);
|
||||||
|
_lastDrawPoint = currentImagePoint;
|
||||||
|
Invalidate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnMouseUp(MouseEventArgs e)
|
||||||
|
{
|
||||||
|
base.OnMouseUp(e);
|
||||||
|
|
||||||
|
if (_isResizing)
|
||||||
|
{
|
||||||
|
_isResizing = false;
|
||||||
|
_activeHandle = ResizeHandle.None;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_isDrawing)
|
||||||
|
{
|
||||||
|
_isDrawing = false;
|
||||||
|
_lastDrawPoint = Point.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnResize(EventArgs e)
|
||||||
|
{
|
||||||
|
base.OnResize(e);
|
||||||
|
UpdateImageRect();
|
||||||
|
Invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Helper Methods
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 화면 좌표를 이미지 좌표로 변환
|
||||||
|
/// </summary>
|
||||||
|
private Point ImagePointFromScreen(Point screenPoint)
|
||||||
|
{
|
||||||
|
if (_imageRect.IsEmpty || _editingImage == null)
|
||||||
|
return Point.Empty;
|
||||||
|
|
||||||
|
// 화면 좌표를 이미지 비율로 변환
|
||||||
|
float scaleX = (float)_editingImage.Width / _imageRect.Width;
|
||||||
|
float scaleY = (float)_editingImage.Height / _imageRect.Height;
|
||||||
|
|
||||||
|
int imageX = (int)((screenPoint.X - _imageRect.X) * scaleX);
|
||||||
|
int imageY = (int)((screenPoint.Y - _imageRect.Y) * scaleY);
|
||||||
|
|
||||||
|
return new Point(imageX, imageY);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ResizeHandle GetHandleAtPoint(Point pt)
|
||||||
|
{
|
||||||
|
var handles = GetResizeHandles();
|
||||||
|
var handleTypes = new[]
|
||||||
|
{
|
||||||
|
ResizeHandle.TopLeft, ResizeHandle.Top, ResizeHandle.TopRight,
|
||||||
|
ResizeHandle.Right, ResizeHandle.BottomRight, ResizeHandle.Bottom,
|
||||||
|
ResizeHandle.BottomLeft, ResizeHandle.Left
|
||||||
|
};
|
||||||
|
|
||||||
|
for (int i = 0; i < handles.Length; i++)
|
||||||
|
{
|
||||||
|
if (handles[i].Contains(pt))
|
||||||
|
return handleTypes[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
return ResizeHandle.None;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Cursor GetCursorForHandle(ResizeHandle handle)
|
||||||
|
{
|
||||||
|
switch (handle)
|
||||||
|
{
|
||||||
|
case ResizeHandle.TopLeft:
|
||||||
|
case ResizeHandle.BottomRight:
|
||||||
|
return Cursors.SizeNWSE;
|
||||||
|
case ResizeHandle.TopRight:
|
||||||
|
case ResizeHandle.BottomLeft:
|
||||||
|
return Cursors.SizeNESW;
|
||||||
|
case ResizeHandle.Top:
|
||||||
|
case ResizeHandle.Bottom:
|
||||||
|
return Cursors.SizeNS;
|
||||||
|
case ResizeHandle.Left:
|
||||||
|
case ResizeHandle.Right:
|
||||||
|
return Cursors.SizeWE;
|
||||||
|
default:
|
||||||
|
return Cursors.Default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ResizeImageDisplay(Point currentPoint)
|
||||||
|
{
|
||||||
|
int deltaX = currentPoint.X - _resizeStartPoint.X;
|
||||||
|
int deltaY = currentPoint.Y - _resizeStartPoint.Y;
|
||||||
|
|
||||||
|
float newWidth = _resizeStartWidth;
|
||||||
|
float newHeight = _resizeStartHeight;
|
||||||
|
|
||||||
|
switch (_activeHandle)
|
||||||
|
{
|
||||||
|
case ResizeHandle.TopLeft:
|
||||||
|
newWidth -= deltaX;
|
||||||
|
newHeight -= deltaY;
|
||||||
|
break;
|
||||||
|
case ResizeHandle.Top:
|
||||||
|
newHeight -= deltaY;
|
||||||
|
break;
|
||||||
|
case ResizeHandle.TopRight:
|
||||||
|
newWidth += deltaX;
|
||||||
|
newHeight -= deltaY;
|
||||||
|
break;
|
||||||
|
case ResizeHandle.Right:
|
||||||
|
newWidth += deltaX;
|
||||||
|
break;
|
||||||
|
case ResizeHandle.BottomRight:
|
||||||
|
newWidth += deltaX;
|
||||||
|
newHeight += deltaY;
|
||||||
|
break;
|
||||||
|
case ResizeHandle.Bottom:
|
||||||
|
newHeight += deltaY;
|
||||||
|
break;
|
||||||
|
case ResizeHandle.BottomLeft:
|
||||||
|
newWidth -= deltaX;
|
||||||
|
newHeight += deltaY;
|
||||||
|
break;
|
||||||
|
case ResizeHandle.Left:
|
||||||
|
newWidth -= deltaX;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 최소 크기 제한
|
||||||
|
if (newWidth < 50) newWidth = 50;
|
||||||
|
if (newHeight < 50) newHeight = 50;
|
||||||
|
|
||||||
|
_imageDisplayWidth = newWidth;
|
||||||
|
_imageDisplayHeight = newHeight;
|
||||||
|
|
||||||
|
UpdateImageRect();
|
||||||
|
Invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 표시 크기로 실제 이미지 리사이즈
|
||||||
|
/// </summary>
|
||||||
|
public Bitmap GetResizedImage()
|
||||||
|
{
|
||||||
|
if (_editingImage == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
int targetWidth = (int)_imageDisplayWidth;
|
||||||
|
int targetHeight = (int)_imageDisplayHeight;
|
||||||
|
|
||||||
|
// 크기가 같으면 원본 반환
|
||||||
|
if (targetWidth == _editingImage.Width && targetHeight == _editingImage.Height)
|
||||||
|
return new Bitmap(_editingImage);
|
||||||
|
|
||||||
|
// 리사이즈
|
||||||
|
var resized = new Bitmap(targetWidth, targetHeight);
|
||||||
|
using (var g = Graphics.FromImage(resized))
|
||||||
|
{
|
||||||
|
g.CompositingQuality = CompositingQuality.HighQuality;
|
||||||
|
g.InterpolationMode = InterpolationMode.HighQualityBicubic;
|
||||||
|
g.SmoothingMode = SmoothingMode.HighQuality;
|
||||||
|
g.DrawImage(_editingImage, 0, 0, targetWidth, targetHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
return resized;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
protected override void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
if (disposing)
|
||||||
|
{
|
||||||
|
_imageGraphics?.Dispose();
|
||||||
|
}
|
||||||
|
base.Dispose(disposing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
188
Cs_HMI/AGVLogic/AGVMapEditor/Forms/ImageEditorForm.Designer.cs
generated
Normal file
188
Cs_HMI/AGVLogic/AGVMapEditor/Forms/ImageEditorForm.Designer.cs
generated
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
namespace AGVMapEditor.Forms
|
||||||
|
{
|
||||||
|
partial class ImageEditorForm
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Required designer variable.
|
||||||
|
/// </summary>
|
||||||
|
private System.ComponentModel.IContainer components = null;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Clean up any resources being used.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
|
||||||
|
protected override void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
if (disposing && (components != null))
|
||||||
|
{
|
||||||
|
components.Dispose();
|
||||||
|
}
|
||||||
|
base.Dispose(disposing);
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Windows Form Designer generated code
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Required method for Designer support - do not modify
|
||||||
|
/// the contents of this method with the code editor.
|
||||||
|
/// </summary>
|
||||||
|
private void InitializeComponent()
|
||||||
|
{
|
||||||
|
this.toolPanel = new System.Windows.Forms.Panel();
|
||||||
|
this.chkBrushMode = new System.Windows.Forms.CheckBox();
|
||||||
|
this.btnColor = new System.Windows.Forms.Button();
|
||||||
|
this.trackBrush = new System.Windows.Forms.TrackBar();
|
||||||
|
this.lblBrush = new System.Windows.Forms.Label();
|
||||||
|
this.btnSave = new System.Windows.Forms.Button();
|
||||||
|
this.btnResize = new System.Windows.Forms.Button();
|
||||||
|
this.btnOpen = new System.Windows.Forms.Button();
|
||||||
|
this.canvasPanel = new System.Windows.Forms.Panel();
|
||||||
|
this.imageCanvas = new AGVMapEditor.Controls.ImageEditorCanvas();
|
||||||
|
this.toolPanel.SuspendLayout();
|
||||||
|
((System.ComponentModel.ISupportInitialize)(this.trackBrush)).BeginInit();
|
||||||
|
this.canvasPanel.SuspendLayout();
|
||||||
|
this.SuspendLayout();
|
||||||
|
//
|
||||||
|
// toolPanel
|
||||||
|
//
|
||||||
|
this.toolPanel.BackColor = System.Drawing.Color.LightGray;
|
||||||
|
this.toolPanel.Controls.Add(this.chkBrushMode);
|
||||||
|
this.toolPanel.Controls.Add(this.btnColor);
|
||||||
|
this.toolPanel.Controls.Add(this.trackBrush);
|
||||||
|
this.toolPanel.Controls.Add(this.lblBrush);
|
||||||
|
this.toolPanel.Controls.Add(this.btnSave);
|
||||||
|
this.toolPanel.Controls.Add(this.btnResize);
|
||||||
|
this.toolPanel.Controls.Add(this.btnOpen);
|
||||||
|
this.toolPanel.Dock = System.Windows.Forms.DockStyle.Top;
|
||||||
|
this.toolPanel.Location = new System.Drawing.Point(0, 0);
|
||||||
|
this.toolPanel.Name = "toolPanel";
|
||||||
|
this.toolPanel.Size = new System.Drawing.Size(800, 50);
|
||||||
|
this.toolPanel.TabIndex = 0;
|
||||||
|
//
|
||||||
|
// chkBrushMode
|
||||||
|
//
|
||||||
|
this.chkBrushMode.AutoSize = true;
|
||||||
|
this.chkBrushMode.Location = new System.Drawing.Point(590, 15);
|
||||||
|
this.chkBrushMode.Name = "chkBrushMode";
|
||||||
|
this.chkBrushMode.Size = new System.Drawing.Size(96, 16);
|
||||||
|
this.chkBrushMode.TabIndex = 6;
|
||||||
|
this.chkBrushMode.Text = "브러시 모드";
|
||||||
|
this.chkBrushMode.UseVisualStyleBackColor = true;
|
||||||
|
//
|
||||||
|
// btnColor
|
||||||
|
//
|
||||||
|
this.btnColor.BackColor = System.Drawing.Color.Black;
|
||||||
|
this.btnColor.ForeColor = System.Drawing.Color.White;
|
||||||
|
this.btnColor.Location = new System.Drawing.Point(520, 10);
|
||||||
|
this.btnColor.Name = "btnColor";
|
||||||
|
this.btnColor.Size = new System.Drawing.Size(60, 23);
|
||||||
|
this.btnColor.TabIndex = 5;
|
||||||
|
this.btnColor.Text = "색상";
|
||||||
|
this.btnColor.UseVisualStyleBackColor = false;
|
||||||
|
this.btnColor.Click += new System.EventHandler(this.BtnColor_Click);
|
||||||
|
//
|
||||||
|
// trackBrush
|
||||||
|
//
|
||||||
|
this.trackBrush.Location = new System.Drawing.Point(410, 10);
|
||||||
|
this.trackBrush.Maximum = 20;
|
||||||
|
this.trackBrush.Minimum = 1;
|
||||||
|
this.trackBrush.Name = "trackBrush";
|
||||||
|
this.trackBrush.Size = new System.Drawing.Size(100, 45);
|
||||||
|
this.trackBrush.TabIndex = 4;
|
||||||
|
this.trackBrush.Value = 3;
|
||||||
|
this.trackBrush.ValueChanged += new System.EventHandler(this.TrackBrush_ValueChanged);
|
||||||
|
//
|
||||||
|
// lblBrush
|
||||||
|
//
|
||||||
|
this.lblBrush.AutoSize = true;
|
||||||
|
this.lblBrush.Location = new System.Drawing.Point(350, 15);
|
||||||
|
this.lblBrush.Name = "lblBrush";
|
||||||
|
this.lblBrush.Size = new System.Drawing.Size(54, 12);
|
||||||
|
this.lblBrush.TabIndex = 3;
|
||||||
|
this.lblBrush.Text = "브러시:";
|
||||||
|
//
|
||||||
|
// btnSave
|
||||||
|
//
|
||||||
|
this.btnSave.Location = new System.Drawing.Point(230, 10);
|
||||||
|
this.btnSave.Name = "btnSave";
|
||||||
|
this.btnSave.Size = new System.Drawing.Size(100, 23);
|
||||||
|
this.btnSave.TabIndex = 2;
|
||||||
|
this.btnSave.Text = "저장 및 닫기";
|
||||||
|
this.btnSave.UseVisualStyleBackColor = true;
|
||||||
|
this.btnSave.Click += new System.EventHandler(this.BtnSave_Click);
|
||||||
|
//
|
||||||
|
// btnResize
|
||||||
|
//
|
||||||
|
this.btnResize.Location = new System.Drawing.Point(120, 10);
|
||||||
|
this.btnResize.Name = "btnResize";
|
||||||
|
this.btnResize.Size = new System.Drawing.Size(100, 23);
|
||||||
|
this.btnResize.TabIndex = 1;
|
||||||
|
this.btnResize.Text = "크기 조정";
|
||||||
|
this.btnResize.UseVisualStyleBackColor = true;
|
||||||
|
this.btnResize.Click += new System.EventHandler(this.BtnResize_Click);
|
||||||
|
//
|
||||||
|
// btnOpen
|
||||||
|
//
|
||||||
|
this.btnOpen.Location = new System.Drawing.Point(10, 10);
|
||||||
|
this.btnOpen.Name = "btnOpen";
|
||||||
|
this.btnOpen.Size = new System.Drawing.Size(100, 23);
|
||||||
|
this.btnOpen.TabIndex = 0;
|
||||||
|
this.btnOpen.Text = "이미지 열기";
|
||||||
|
this.btnOpen.UseVisualStyleBackColor = true;
|
||||||
|
this.btnOpen.Click += new System.EventHandler(this.BtnOpen_Click);
|
||||||
|
//
|
||||||
|
// canvasPanel
|
||||||
|
//
|
||||||
|
this.canvasPanel.AutoScroll = true;
|
||||||
|
this.canvasPanel.BackColor = System.Drawing.Color.White;
|
||||||
|
this.canvasPanel.Controls.Add(this.imageCanvas);
|
||||||
|
this.canvasPanel.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||||
|
this.canvasPanel.Location = new System.Drawing.Point(0, 50);
|
||||||
|
this.canvasPanel.Name = "canvasPanel";
|
||||||
|
this.canvasPanel.Size = new System.Drawing.Size(800, 550);
|
||||||
|
this.canvasPanel.TabIndex = 1;
|
||||||
|
//
|
||||||
|
// imageCanvas
|
||||||
|
//
|
||||||
|
this.imageCanvas.BackColor = System.Drawing.Color.White;
|
||||||
|
this.imageCanvas.BrushModeEnabled = false;
|
||||||
|
this.imageCanvas.BrushSize = 3;
|
||||||
|
this.imageCanvas.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||||
|
this.imageCanvas.DrawColor = System.Drawing.Color.Black;
|
||||||
|
this.imageCanvas.Location = new System.Drawing.Point(0, 0);
|
||||||
|
this.imageCanvas.Name = "imageCanvas";
|
||||||
|
this.imageCanvas.Size = new System.Drawing.Size(800, 550);
|
||||||
|
this.imageCanvas.TabIndex = 0;
|
||||||
|
//
|
||||||
|
// ImageEditorForm
|
||||||
|
//
|
||||||
|
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 12F);
|
||||||
|
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||||
|
this.ClientSize = new System.Drawing.Size(800, 600);
|
||||||
|
this.Controls.Add(this.canvasPanel);
|
||||||
|
this.Controls.Add(this.toolPanel);
|
||||||
|
this.Name = "ImageEditorForm";
|
||||||
|
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen;
|
||||||
|
this.Text = "이미지 편집기";
|
||||||
|
this.toolPanel.ResumeLayout(false);
|
||||||
|
this.toolPanel.PerformLayout();
|
||||||
|
((System.ComponentModel.ISupportInitialize)(this.trackBrush)).EndInit();
|
||||||
|
this.canvasPanel.ResumeLayout(false);
|
||||||
|
this.ResumeLayout(false);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
private System.Windows.Forms.Panel toolPanel;
|
||||||
|
private System.Windows.Forms.Button btnOpen;
|
||||||
|
private System.Windows.Forms.Button btnResize;
|
||||||
|
private System.Windows.Forms.Button btnSave;
|
||||||
|
private System.Windows.Forms.Label lblBrush;
|
||||||
|
private System.Windows.Forms.TrackBar trackBrush;
|
||||||
|
private System.Windows.Forms.Button btnColor;
|
||||||
|
private System.Windows.Forms.CheckBox chkBrushMode;
|
||||||
|
private System.Windows.Forms.Panel canvasPanel;
|
||||||
|
private AGVMapEditor.Controls.ImageEditorCanvas imageCanvas;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Drawing;
|
using System.Drawing;
|
||||||
using System.Drawing.Drawing2D;
|
using System.Drawing.Drawing2D;
|
||||||
using System.Linq;
|
|
||||||
using System.Windows.Forms;
|
using System.Windows.Forms;
|
||||||
using AGVNavigationCore.Models;
|
using AGVNavigationCore.Models;
|
||||||
using AGVNavigationCore.Utils;
|
using AGVNavigationCore.Utils;
|
||||||
@@ -14,17 +13,11 @@ namespace AGVMapEditor.Forms
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class ImageEditorForm : Form
|
public partial class ImageEditorForm : Form
|
||||||
{
|
{
|
||||||
private Bitmap _editingImage;
|
|
||||||
private Graphics _graphics;
|
|
||||||
private bool _isDrawing = false;
|
|
||||||
private Point _lastPoint = Point.Empty;
|
|
||||||
private Color _drawColor = Color.Black;
|
|
||||||
private int _brushSize = 3;
|
|
||||||
private MapNode _targetNode;
|
private MapNode _targetNode;
|
||||||
|
|
||||||
public ImageEditorForm(MapNode imageNode = null)
|
public ImageEditorForm(MapNode imageNode = null)
|
||||||
{
|
{
|
||||||
//InitializeComponent();
|
InitializeComponent();
|
||||||
_targetNode = imageNode;
|
_targetNode = imageNode;
|
||||||
SetupUI();
|
SetupUI();
|
||||||
|
|
||||||
@@ -36,64 +29,25 @@ namespace AGVMapEditor.Forms
|
|||||||
|
|
||||||
private void SetupUI()
|
private void SetupUI()
|
||||||
{
|
{
|
||||||
this.Text = "이미지 편집기";
|
// 캔버스 초기 설정
|
||||||
this.Size = new Size(800, 600);
|
imageCanvas.BrushSize = trackBrush.Value;
|
||||||
this.StartPosition = FormStartPosition.CenterScreen;
|
imageCanvas.BrushModeEnabled = chkBrushMode.Checked;
|
||||||
|
imageCanvas.BackColor = Color.FromArgb(32,32,32);
|
||||||
|
|
||||||
// 패널: 도구 모음
|
// 이벤트 연결
|
||||||
var toolPanel = new Panel { Dock = DockStyle.Top, Height = 50, BackColor = Color.LightGray };
|
chkBrushMode.CheckedChanged += (s, e) => imageCanvas.BrushModeEnabled = chkBrushMode.Checked;
|
||||||
|
}
|
||||||
|
|
||||||
// 버튼: 이미지 열기
|
private void TrackBrush_ValueChanged(object sender, EventArgs e)
|
||||||
var btnOpen = new Button { Text = "이미지 열기", Width = 100, Left = 10, Top = 10 };
|
{
|
||||||
btnOpen.Click += BtnOpen_Click;
|
imageCanvas.BrushSize = trackBrush.Value;
|
||||||
toolPanel.Controls.Add(btnOpen);
|
|
||||||
|
|
||||||
// 버튼: 크기 조정
|
|
||||||
var btnResize = new Button { Text = "크기 조정", Width = 100, Left = 120, Top = 10 };
|
|
||||||
btnResize.Click += BtnResize_Click;
|
|
||||||
toolPanel.Controls.Add(btnResize);
|
|
||||||
|
|
||||||
// 버튼: 저장
|
|
||||||
var btnSave = new Button { Text = "저장 및 닫기", Width = 100, Left = 230, Top = 10 };
|
|
||||||
btnSave.Click += BtnSave_Click;
|
|
||||||
toolPanel.Controls.Add(btnSave);
|
|
||||||
|
|
||||||
// 라벨: 브러시 크기
|
|
||||||
var lblBrush = new Label { Text = "브러시:", Left = 350, Top = 15, Width = 50 };
|
|
||||||
toolPanel.Controls.Add(lblBrush);
|
|
||||||
|
|
||||||
// 트랙바: 브러시 크기 조절
|
|
||||||
var trackBrush = new TrackBar { Left = 410, Top = 10, Width = 100, Minimum = 1, Maximum = 20, Value = 3 };
|
|
||||||
trackBrush.ValueChanged += (s, e) => _brushSize = trackBrush.Value;
|
|
||||||
toolPanel.Controls.Add(trackBrush);
|
|
||||||
|
|
||||||
// 버튼: 색상 선택
|
|
||||||
var btnColor = new Button { Text = "색상", Width = 60, Left = 520, Top = 10, BackColor = Color.Black, ForeColor = Color.White };
|
|
||||||
btnColor.Click += BtnColor_Click;
|
|
||||||
toolPanel.Controls.Add(btnColor);
|
|
||||||
|
|
||||||
// 패널: 캔버스
|
|
||||||
var canvasPanel = new Panel { Dock = DockStyle.Fill, AutoScroll = true, BackColor = Color.White };
|
|
||||||
var pictureBox = new PictureBox { SizeMode = PictureBoxSizeMode.AutoSize };
|
|
||||||
pictureBox.Name = "pictureBoxCanvas";
|
|
||||||
pictureBox.MouseDown += PictureBox_MouseDown;
|
|
||||||
pictureBox.MouseMove += PictureBox_MouseMove;
|
|
||||||
pictureBox.MouseUp += PictureBox_MouseUp;
|
|
||||||
canvasPanel.Controls.Add(pictureBox);
|
|
||||||
|
|
||||||
this.Controls.Add(canvasPanel);
|
|
||||||
this.Controls.Add(toolPanel);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void LoadImageFromNode(MapNode node)
|
private void LoadImageFromNode(MapNode node)
|
||||||
{
|
{
|
||||||
if (node.LoadedImage != null)
|
if (node.LoadedImage != null)
|
||||||
{
|
{
|
||||||
_editingImage?.Dispose();
|
imageCanvas.EditingImage = new Bitmap(node.LoadedImage);
|
||||||
_graphics?.Dispose();
|
|
||||||
_editingImage = new Bitmap(node.LoadedImage);
|
|
||||||
_graphics = Graphics.FromImage(_editingImage);
|
|
||||||
UpdateCanvas();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,25 +66,22 @@ namespace AGVMapEditor.Forms
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_editingImage?.Dispose();
|
|
||||||
_graphics?.Dispose();
|
|
||||||
|
|
||||||
var loadedImage = Image.FromFile(filePath);
|
var loadedImage = Image.FromFile(filePath);
|
||||||
|
|
||||||
// 이미지 크기가 크면 자동 축소 (최대 512x512)
|
// 이미지 크기가 크면 자동 축소 (최대 512x512)
|
||||||
|
Bitmap finalImage;
|
||||||
if (loadedImage.Width > 512 || loadedImage.Height > 512)
|
if (loadedImage.Width > 512 || loadedImage.Height > 512)
|
||||||
{
|
{
|
||||||
_editingImage = ResizeImage(loadedImage, 512, 512);
|
finalImage = ResizeImage(loadedImage, 512, 512);
|
||||||
loadedImage.Dispose();
|
loadedImage.Dispose();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_editingImage = new Bitmap(loadedImage);
|
finalImage = new Bitmap(loadedImage);
|
||||||
loadedImage.Dispose();
|
loadedImage.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
_graphics = Graphics.FromImage(_editingImage);
|
imageCanvas.EditingImage = finalImage;
|
||||||
UpdateCanvas();
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -140,12 +91,14 @@ namespace AGVMapEditor.Forms
|
|||||||
|
|
||||||
private void BtnResize_Click(object sender, EventArgs e)
|
private void BtnResize_Click(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
if (_editingImage == null)
|
if (imageCanvas.EditingImage == null)
|
||||||
{
|
{
|
||||||
MessageBox.Show("먼저 이미지를 로드하세요.");
|
MessageBox.Show("먼저 이미지를 로드하세요.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var currentSize = imageCanvas.ImageDisplaySize;
|
||||||
|
|
||||||
using (var form = new Form())
|
using (var form = new Form())
|
||||||
{
|
{
|
||||||
form.Text = "이미지 크기 조정";
|
form.Text = "이미지 크기 조정";
|
||||||
@@ -153,10 +106,10 @@ namespace AGVMapEditor.Forms
|
|||||||
form.StartPosition = FormStartPosition.CenterParent;
|
form.StartPosition = FormStartPosition.CenterParent;
|
||||||
|
|
||||||
var lblWidth = new Label { Text = "너비:", Left = 10, Top = 10, Width = 50 };
|
var lblWidth = new Label { Text = "너비:", Left = 10, Top = 10, Width = 50 };
|
||||||
var txtWidth = new TextBox { Left = 70, Top = 10, Width = 100, Text = _editingImage.Width.ToString() };
|
var txtWidth = new TextBox { Left = 70, Top = 10, Width = 100, Text = currentSize.Width.ToString() };
|
||||||
|
|
||||||
var lblHeight = new Label { Text = "높이:", Left = 10, Top = 40, Width = 50 };
|
var lblHeight = new Label { Text = "높이:", Left = 10, Top = 40, Width = 50 };
|
||||||
var txtHeight = new TextBox { Left = 70, Top = 40, Width = 100, Text = _editingImage.Height.ToString() };
|
var txtHeight = new TextBox { Left = 70, Top = 40, Width = 100, Text = currentSize.Height.ToString() };
|
||||||
|
|
||||||
var btnOk = new Button { Text = "적용", DialogResult = DialogResult.OK, Left = 70, Top = 70, Width = 100 };
|
var btnOk = new Button { Text = "적용", DialogResult = DialogResult.OK, Left = 70, Top = 70, Width = 100 };
|
||||||
var btnCancel = new Button { Text = "취소", DialogResult = DialogResult.Cancel, Left = 180, Top = 70, Width = 70 };
|
var btnCancel = new Button { Text = "취소", DialogResult = DialogResult.Cancel, Left = 180, Top = 70, Width = 70 };
|
||||||
@@ -174,12 +127,8 @@ namespace AGVMapEditor.Forms
|
|||||||
{
|
{
|
||||||
if (width > 0 && height > 0)
|
if (width > 0 && height > 0)
|
||||||
{
|
{
|
||||||
_graphics?.Dispose();
|
var resized = new Bitmap(imageCanvas.EditingImage, width, height);
|
||||||
var resized = new Bitmap(_editingImage, width, height);
|
imageCanvas.EditingImage = resized;
|
||||||
_editingImage.Dispose();
|
|
||||||
_editingImage = resized;
|
|
||||||
_graphics = Graphics.FromImage(_editingImage);
|
|
||||||
UpdateCanvas();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -188,19 +137,19 @@ namespace AGVMapEditor.Forms
|
|||||||
|
|
||||||
private void BtnColor_Click(object sender, EventArgs e)
|
private void BtnColor_Click(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
using (var cfd = new ColorDialog { Color = _drawColor })
|
using (var cfd = new ColorDialog { Color = imageCanvas.DrawColor })
|
||||||
{
|
{
|
||||||
if (cfd.ShowDialog() == DialogResult.OK)
|
if (cfd.ShowDialog() == DialogResult.OK)
|
||||||
{
|
{
|
||||||
_drawColor = cfd.Color;
|
imageCanvas.DrawColor = cfd.Color;
|
||||||
(sender as Button).BackColor = _drawColor;
|
(sender as Button).BackColor = cfd.Color;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void BtnSave_Click(object sender, EventArgs e)
|
private void BtnSave_Click(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
if (_editingImage == null)
|
if (imageCanvas.EditingImage == null)
|
||||||
{
|
{
|
||||||
MessageBox.Show("저장할 이미지가 없습니다.");
|
MessageBox.Show("저장할 이미지가 없습니다.");
|
||||||
return;
|
return;
|
||||||
@@ -208,10 +157,22 @@ namespace AGVMapEditor.Forms
|
|||||||
|
|
||||||
if (_targetNode != null && _targetNode.Type == NodeType.Image)
|
if (_targetNode != null && _targetNode.Type == NodeType.Image)
|
||||||
{
|
{
|
||||||
|
// 표시 크기로 리사이즈된 이미지 가져오기
|
||||||
|
var finalImage = imageCanvas.GetResizedImage();
|
||||||
|
|
||||||
|
if (finalImage == null)
|
||||||
|
{
|
||||||
|
MessageBox.Show("이미지 처리 중 오류가 발생했습니다.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var displaySize = imageCanvas.ImageDisplaySize;
|
||||||
|
MessageBox.Show($"이미지 크기: {displaySize.Width}x{displaySize.Height}로 저장됩니다.");
|
||||||
|
|
||||||
// 이미지를 Base64로 변환하여 저장
|
// 이미지를 Base64로 변환하여 저장
|
||||||
_targetNode.ImageBase64 = ImageConverterUtil.ImageToBase64(_editingImage, System.Drawing.Imaging.ImageFormat.Png);
|
_targetNode.ImageBase64 = ImageConverterUtil.ImageToBase64(finalImage, System.Drawing.Imaging.ImageFormat.Png);
|
||||||
_targetNode.LoadedImage?.Dispose();
|
_targetNode.LoadedImage?.Dispose();
|
||||||
_targetNode.LoadedImage = new Bitmap(_editingImage);
|
_targetNode.LoadedImage = finalImage;
|
||||||
|
|
||||||
MessageBox.Show("이미지가 저장되었습니다.");
|
MessageBox.Show("이미지가 저장되었습니다.");
|
||||||
this.DialogResult = DialogResult.OK;
|
this.DialogResult = DialogResult.OK;
|
||||||
@@ -219,41 +180,6 @@ namespace AGVMapEditor.Forms
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void PictureBox_MouseDown(object sender, MouseEventArgs e)
|
|
||||||
{
|
|
||||||
if (_editingImage != null && e.Button == MouseButtons.Left)
|
|
||||||
{
|
|
||||||
_isDrawing = true;
|
|
||||||
_lastPoint = e.Location;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void PictureBox_MouseMove(object sender, MouseEventArgs e)
|
|
||||||
{
|
|
||||||
if (_isDrawing && _lastPoint != Point.Empty)
|
|
||||||
{
|
|
||||||
_graphics.DrawLine(new Pen(_drawColor, _brushSize), _lastPoint, e.Location);
|
|
||||||
_lastPoint = e.Location;
|
|
||||||
UpdateCanvas();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void PictureBox_MouseUp(object sender, MouseEventArgs e)
|
|
||||||
{
|
|
||||||
_isDrawing = false;
|
|
||||||
_lastPoint = Point.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdateCanvas()
|
|
||||||
{
|
|
||||||
var pictureBox = this.Controls.Find("pictureBoxCanvas", true).FirstOrDefault() as PictureBox;
|
|
||||||
if (pictureBox != null)
|
|
||||||
{
|
|
||||||
pictureBox.Image?.Dispose();
|
|
||||||
pictureBox.Image = new Bitmap(_editingImage);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Bitmap ResizeImage(Image image, int maxWidth, int maxHeight)
|
private Bitmap ResizeImage(Image image, int maxWidth, int maxHeight)
|
||||||
{
|
{
|
||||||
double ratioX = (double)maxWidth / image.Width;
|
double ratioX = (double)maxWidth / image.Width;
|
||||||
@@ -273,15 +199,5 @@ namespace AGVMapEditor.Forms
|
|||||||
}
|
}
|
||||||
return resized;
|
return resized;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Dispose(bool disposing)
|
|
||||||
{
|
|
||||||
if (disposing)
|
|
||||||
{
|
|
||||||
_editingImage?.Dispose();
|
|
||||||
_graphics?.Dispose();
|
|
||||||
}
|
|
||||||
base.Dispose(disposing);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1186,14 +1186,25 @@ namespace AGVMapEditor.Forms
|
|||||||
|
|
||||||
if (fromNode != null && toNode != null)
|
if (fromNode != null && toNode != null)
|
||||||
{
|
{
|
||||||
// 단일 연결 삭제 (어느 방향에 저장되어 있는지 확인 후 삭제)
|
// 양방향 연결 삭제 (양쪽 방향 모두 제거)
|
||||||
|
bool removed = false;
|
||||||
|
|
||||||
if (fromNode.ConnectedNodes.Contains(toNode.NodeId))
|
if (fromNode.ConnectedNodes.Contains(toNode.NodeId))
|
||||||
{
|
{
|
||||||
fromNode.RemoveConnection(toNode.NodeId);
|
fromNode.RemoveConnection(toNode.NodeId);
|
||||||
|
removed = true;
|
||||||
}
|
}
|
||||||
else if (toNode.ConnectedNodes.Contains(fromNode.NodeId))
|
|
||||||
|
if (toNode.ConnectedNodes.Contains(fromNode.NodeId))
|
||||||
{
|
{
|
||||||
toNode.RemoveConnection(fromNode.NodeId);
|
toNode.RemoveConnection(fromNode.NodeId);
|
||||||
|
removed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!removed)
|
||||||
|
{
|
||||||
|
MessageBox.Show("연결을 찾을 수 없습니다.", "알림", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_hasChanges = true;
|
_hasChanges = true;
|
||||||
|
|||||||
@@ -101,11 +101,10 @@ namespace AGVNavigationCore.Controls
|
|||||||
|
|
||||||
foreach (var node in _nodes)
|
foreach (var node in _nodes)
|
||||||
{
|
{
|
||||||
if (node.ConnectedNodes == null) continue;
|
if (node.ConnectedMapNodes == null) continue;
|
||||||
|
|
||||||
foreach (var connectedNodeId in node.ConnectedNodes)
|
foreach (var targetNode in node.ConnectedMapNodes)
|
||||||
{
|
{
|
||||||
var targetNode = _nodes.FirstOrDefault(n => n.NodeId == connectedNodeId);
|
|
||||||
if (targetNode == null) continue;
|
if (targetNode == null) continue;
|
||||||
|
|
||||||
DrawConnection(g, node, targetNode);
|
DrawConnection(g, node, targetNode);
|
||||||
@@ -302,7 +301,7 @@ namespace AGVNavigationCore.Controls
|
|||||||
if (node == null) continue;
|
if (node == null) continue;
|
||||||
|
|
||||||
// 교차로 판정: 3개 이상의 노드가 연결된 경우
|
// 교차로 판정: 3개 이상의 노드가 연결된 경우
|
||||||
if (node.ConnectedNodes != null && node.ConnectedNodes.Count >= JUNCTION_CONNECTIONS)
|
if (node.ConnectedMapNodes != null && node.ConnectedMapNodes.Count >= JUNCTION_CONNECTIONS)
|
||||||
{
|
{
|
||||||
DrawJunctionHighlight(g, node);
|
DrawJunctionHighlight(g, node);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -567,15 +567,9 @@ namespace AGVNavigationCore.Controls
|
|||||||
toNode.ConnectedNodes.Contains(fromNode.NodeId))
|
toNode.ConnectedNodes.Contains(fromNode.NodeId))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// 단일 연결 생성 (사전순으로 정렬하여 일관성 유지)
|
// 양방향 연결 생성 (AGV가 양쪽 방향으로 이동 가능하도록)
|
||||||
if (string.Compare(fromNode.NodeId, toNode.NodeId, StringComparison.Ordinal) < 0)
|
fromNode.AddConnection(toNode.NodeId);
|
||||||
{
|
toNode.AddConnection(fromNode.NodeId);
|
||||||
fromNode.AddConnection(toNode.NodeId);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
toNode.AddConnection(fromNode.NodeId);
|
|
||||||
}
|
|
||||||
|
|
||||||
MapChanged?.Invoke(this, EventArgs.Empty);
|
MapChanged?.Invoke(this, EventArgs.Empty);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,12 +78,13 @@ namespace AGVNavigationCore.Models
|
|||||||
// 중복된 NodeId 정리
|
// 중복된 NodeId 정리
|
||||||
FixDuplicateNodeIds(result.Nodes);
|
FixDuplicateNodeIds(result.Nodes);
|
||||||
|
|
||||||
// 중복 연결 정리 (양방향 중복 제거)
|
|
||||||
CleanupDuplicateConnections(result.Nodes);
|
|
||||||
|
|
||||||
// 양방향 연결 자동 설정 (A→B가 있으면 B→A도 설정)
|
// 양방향 연결 자동 설정 (A→B가 있으면 B→A도 설정)
|
||||||
|
// 주의: CleanupDuplicateConnections()는 제거됨 - 양방향 연결을 단방향으로 변환하는 버그가 있었음
|
||||||
EnsureBidirectionalConnections(result.Nodes);
|
EnsureBidirectionalConnections(result.Nodes);
|
||||||
|
|
||||||
|
// ConnectedMapNodes 채우기 (string ID → MapNode 객체 참조)
|
||||||
|
ResolveConnectedMapNodes(result.Nodes);
|
||||||
|
|
||||||
// 이미지 노드들의 이미지 로드
|
// 이미지 노드들의 이미지 로드
|
||||||
LoadImageNodes(result.Nodes);
|
LoadImageNodes(result.Nodes);
|
||||||
|
|
||||||
@@ -145,6 +146,35 @@ namespace AGVNavigationCore.Models
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ConnectedMapNodes 채우기 (ConnectedNodes의 string ID → MapNode 객체 변환)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="mapNodes">맵 노드 목록</param>
|
||||||
|
private static void ResolveConnectedMapNodes(List<MapNode> mapNodes)
|
||||||
|
{
|
||||||
|
if (mapNodes == null || mapNodes.Count == 0) return;
|
||||||
|
|
||||||
|
// 빠른 조회를 위한 Dictionary 생성
|
||||||
|
var nodeDict = mapNodes.ToDictionary(n => n.NodeId, n => n);
|
||||||
|
|
||||||
|
foreach (var node in mapNodes)
|
||||||
|
{
|
||||||
|
// ConnectedMapNodes 초기화
|
||||||
|
node.ConnectedMapNodes.Clear();
|
||||||
|
|
||||||
|
if (node.ConnectedNodes != null && node.ConnectedNodes.Count > 0)
|
||||||
|
{
|
||||||
|
foreach (var connectedNodeId in node.ConnectedNodes)
|
||||||
|
{
|
||||||
|
if (nodeDict.TryGetValue(connectedNodeId, out var connectedNode))
|
||||||
|
{
|
||||||
|
node.ConnectedMapNodes.Add(connectedNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 기존 Description 데이터를 Name 필드로 마이그레이션
|
/// 기존 Description 데이터를 Name 필드로 마이그레이션
|
||||||
/// JSON 파일에서 Description 필드가 있는 경우 Name으로 이동
|
/// JSON 파일에서 Description 필드가 있는 경우 Name으로 이동
|
||||||
@@ -269,9 +299,12 @@ namespace AGVNavigationCore.Models
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 중복 연결을 정리합니다. 양방향 중복 연결을 단일 연결로 통합합니다.
|
/// [사용 중지됨] 중복 연결을 정리합니다. 양방향 중복 연결을 단일 연결로 통합합니다.
|
||||||
|
/// 주의: 이 함수는 버그가 있어 사용 중지됨 - 양방향 연결을 단방향으로 변환하여 경로 탐색 실패 발생
|
||||||
|
/// AGV 시스템에서는 모든 연결이 양방향이어야 하므로 EnsureBidirectionalConnections()만 사용
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="mapNodes">맵 노드 목록</param>
|
/// <param name="mapNodes">맵 노드 목록</param>
|
||||||
|
[Obsolete("이 함수는 양방향 연결을 단방향으로 변환하는 버그가 있습니다. 사용하지 마세요.")]
|
||||||
private static void CleanupDuplicateConnections(List<MapNode> mapNodes)
|
private static void CleanupDuplicateConnections(List<MapNode> mapNodes)
|
||||||
{
|
{
|
||||||
if (mapNodes == null || mapNodes.Count == 0) return;
|
if (mapNodes == null || mapNodes.Count == 0) return;
|
||||||
|
|||||||
@@ -44,6 +44,12 @@ namespace AGVNavigationCore.Models
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public List<string> ConnectedNodes { get; set; } = new List<string>();
|
public List<string> ConnectedNodes { get; set; } = new List<string>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 연결된 노드 객체 목록 (런타임 전용, JSON 무시)
|
||||||
|
/// </summary>
|
||||||
|
[Newtonsoft.Json.JsonIgnore]
|
||||||
|
public List<MapNode> ConnectedMapNodes { get; set; } = new List<MapNode>();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 회전 가능 여부 (180도 회전 가능한 지점)
|
/// 회전 가능 여부 (180도 회전 가능한 지점)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -92,15 +92,18 @@ namespace AGVNavigationCore.PathFinding.Analysis
|
|||||||
var connected = new HashSet<string>();
|
var connected = new HashSet<string>();
|
||||||
|
|
||||||
// 직접 연결된 노드들
|
// 직접 연결된 노드들
|
||||||
foreach (var connectedId in node.ConnectedNodes)
|
foreach (var connectedNode in node.ConnectedMapNodes)
|
||||||
{
|
{
|
||||||
connected.Add(connectedId);
|
if (connectedNode != null)
|
||||||
|
{
|
||||||
|
connected.Add(connectedNode.NodeId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 역방향 연결된 노드들 (다른 노드에서 이 노드로 연결)
|
// 역방향 연결된 노드들 (다른 노드에서 이 노드로 연결)
|
||||||
foreach (var otherNode in _mapNodes)
|
foreach (var otherNode in _mapNodes)
|
||||||
{
|
{
|
||||||
if (otherNode.NodeId != node.NodeId && otherNode.ConnectedNodes.Contains(node.NodeId))
|
if (otherNode.NodeId != node.NodeId && otherNode.ConnectedMapNodes.Any(n => n.NodeId == node.NodeId))
|
||||||
{
|
{
|
||||||
connected.Add(otherNode.NodeId);
|
connected.Add(otherNode.NodeId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,17 +66,17 @@ namespace AGVNavigationCore.PathFinding.Core
|
|||||||
{
|
{
|
||||||
var pathNode = _nodeMap[mapNode.NodeId];
|
var pathNode = _nodeMap[mapNode.NodeId];
|
||||||
|
|
||||||
foreach (var connectedNode in mapNode.ConnectedNodes)
|
foreach (var connectedNode in mapNode.ConnectedMapNodes)
|
||||||
{
|
{
|
||||||
if (_nodeMap.ContainsKey(connectedNode))
|
if (connectedNode != null && _nodeMap.ContainsKey(connectedNode.NodeId))
|
||||||
{
|
{
|
||||||
// 양방향 연결 생성 (단일 연결이 양방향을 의미)
|
// 양방향 연결 생성 (단일 연결이 양방향을 의미)
|
||||||
if (!pathNode.ConnectedNodes.Contains(connectedNode))
|
if (!pathNode.ConnectedNodes.Contains(connectedNode.NodeId))
|
||||||
{
|
{
|
||||||
pathNode.ConnectedNodes.Add(connectedNode);
|
pathNode.ConnectedNodes.Add(connectedNode.NodeId);
|
||||||
}
|
}
|
||||||
|
|
||||||
var connectedPathNode = _nodeMap[connectedNode];
|
var connectedPathNode = _nodeMap[connectedNode.NodeId];
|
||||||
if (!connectedPathNode.ConnectedNodes.Contains(mapNode.NodeId))
|
if (!connectedPathNode.ConnectedNodes.Contains(mapNode.NodeId))
|
||||||
{
|
{
|
||||||
connectedPathNode.ConnectedNodes.Add(mapNode.NodeId);
|
connectedPathNode.ConnectedNodes.Add(mapNode.NodeId);
|
||||||
|
|||||||
@@ -50,9 +50,13 @@ namespace AGVNavigationCore.PathFinding.Planning
|
|||||||
n.IsNavigationNode() &&
|
n.IsNavigationNode() &&
|
||||||
n.ConnectedNodes != null &&
|
n.ConnectedNodes != null &&
|
||||||
n.ConnectedNodes.Count >= 3 &&
|
n.ConnectedNodes.Count >= 3 &&
|
||||||
|
n.ConnectedMapNodes.Where(t => t.Type == NodeType.Docking || t.Type == NodeType.Charging).Any() == false &&
|
||||||
n.NodeId != startNode.NodeId
|
n.NodeId != startNode.NodeId
|
||||||
).ToList();
|
).ToList();
|
||||||
|
|
||||||
|
// docking 포인트가 연결된 노드는 제거한다.
|
||||||
|
|
||||||
|
|
||||||
if (junctions.Count == 0)
|
if (junctions.Count == 0)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
@@ -96,7 +100,8 @@ namespace AGVNavigationCore.PathFinding.Planning
|
|||||||
pathNode.IsActive &&
|
pathNode.IsActive &&
|
||||||
pathNode.IsNavigationNode() &&
|
pathNode.IsNavigationNode() &&
|
||||||
pathNode.ConnectedNodes != null &&
|
pathNode.ConnectedNodes != null &&
|
||||||
pathNode.ConnectedNodes.Count >= 3)
|
pathNode.ConnectedNodes.Count >= 3 &&
|
||||||
|
pathNode.ConnectedMapNodes.Where(t => t.Type == NodeType.Docking || t.Type == NodeType.Charging).Any() == false)
|
||||||
{
|
{
|
||||||
if (pathNode.NodeId.Equals(StartNode.NodeId) == false)
|
if (pathNode.NodeId.Equals(StartNode.NodeId) == false)
|
||||||
return pathNode;
|
return pathNode;
|
||||||
@@ -107,7 +112,7 @@ namespace AGVNavigationCore.PathFinding.Planning
|
|||||||
}
|
}
|
||||||
|
|
||||||
public AGVPathResult FindPath_test(MapNode startNode, MapNode targetNode,
|
public AGVPathResult FindPath_test(MapNode startNode, MapNode targetNode,
|
||||||
MapNode prevNode, AgvDirection prevDirection, AgvDirection currentDirection)
|
MapNode prevNode, AgvDirection prevDirection, AgvDirection currentDirection, bool crossignore = false)
|
||||||
{
|
{
|
||||||
// 입력 검증
|
// 입력 검증
|
||||||
if (startNode == null)
|
if (startNode == null)
|
||||||
@@ -117,7 +122,7 @@ namespace AGVNavigationCore.PathFinding.Planning
|
|||||||
if (prevNode == null)
|
if (prevNode == null)
|
||||||
return AGVPathResult.CreateFailure("이전위치 노드가 null입니다.", 0, 0);
|
return AGVPathResult.CreateFailure("이전위치 노드가 null입니다.", 0, 0);
|
||||||
if (startNode.NodeId == targetNode.NodeId && targetNode.DockDirection.MatchAGVDirection(prevDirection))
|
if (startNode.NodeId == targetNode.NodeId && targetNode.DockDirection.MatchAGVDirection(prevDirection))
|
||||||
return AGVPathResult.CreateFailure("목적지와 현재위치가 동일합니다.", 0, 0);
|
return AGVPathResult.CreateSuccess(new List<MapNode> { startNode,startNode }, new List<AgvDirection>(), 0, 0);
|
||||||
|
|
||||||
var ReverseDirection = (currentDirection == AgvDirection.Forward ? AgvDirection.Backward : AgvDirection.Forward);
|
var ReverseDirection = (currentDirection == AgvDirection.Forward ? AgvDirection.Backward : AgvDirection.Forward);
|
||||||
|
|
||||||
@@ -203,6 +208,39 @@ namespace AGVNavigationCore.PathFinding.Planning
|
|||||||
// return pathResult;
|
// return pathResult;
|
||||||
//}
|
//}
|
||||||
|
|
||||||
|
//현재 내 포인트가 교차로라면.. 무조건 왓던 방향 혹은 그 반대방향으로 이동해서 경로를 계산해야한다.
|
||||||
|
//교차로에 멈춰있을때에는 바로 방향전환을 할 수없으니. 정/역(straight)로 이동해서 다시 계산을 해야한다
|
||||||
|
if (crossignore == false && startNode.ConnectedNodes.Count > 2)
|
||||||
|
{
|
||||||
|
//진행방향으로 이동했을때 나오는 노드를 사용한다.
|
||||||
|
if (nextNodeForward != null)
|
||||||
|
{
|
||||||
|
var Path0 = _basicPathfinder.FindPath(startNode.NodeId, nextNodeForward.NodeId);
|
||||||
|
Path0.PrevNode = prevNode;
|
||||||
|
Path0.PrevDirection = prevDirection;
|
||||||
|
MakeDetailData(Path0, prevDirection);
|
||||||
|
|
||||||
|
var Path1 = FindPath_test(nextNodeForward, targetNode, startNode, prevDirection, currentDirection, true);
|
||||||
|
Path1.PrevNode = startNode;
|
||||||
|
Path1.PrevDirection = prevDirection;
|
||||||
|
//MakeDetailData(Path1, ReverseDirection);
|
||||||
|
|
||||||
|
var combinedResult0 = Path0;
|
||||||
|
combinedResult0 = _basicPathfinder.CombineResults(combinedResult0, Path1);
|
||||||
|
MakeMagnetDirection(combinedResult0);
|
||||||
|
return combinedResult0;
|
||||||
|
}
|
||||||
|
else if (nextNodeBackward != null)
|
||||||
|
{
|
||||||
|
return AGVPathResult.CreateFailure("backward 처리코드가 없습니다 오류", 0, 0);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return AGVPathResult.CreateFailure("교차로에서 시작하는 조건중 forward/backrad 노드 검색 실패", 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
//3. 도킹방향이 일치하지 않으니 교차로에서 방향을 회전시켜야 한다
|
//3. 도킹방향이 일치하지 않으니 교차로에서 방향을 회전시켜야 한다
|
||||||
//최단거리(=minpath)경로에 속하는 교차로가 있다면 그것을 사용하고 없다면 가장 가까운 교차로를 찾는다.
|
//최단거리(=minpath)경로에 속하는 교차로가 있다면 그것을 사용하고 없다면 가장 가까운 교차로를 찾는다.
|
||||||
var JunctionInPath = FindNearestJunctionOnPath(pathResult);
|
var JunctionInPath = FindNearestJunctionOnPath(pathResult);
|
||||||
@@ -224,19 +262,23 @@ namespace AGVNavigationCore.PathFinding.Planning
|
|||||||
path1.PrevNode = prevNode;
|
path1.PrevNode = prevNode;
|
||||||
path1.PrevDirection = prevDirection;
|
path1.PrevDirection = prevDirection;
|
||||||
|
|
||||||
//다음좌표를 보고 교차로가 진행방향인지 반대방향인지 체크한다.(!모터의 정/역방향을 말하는것이 아님)
|
//다음좌표를 보고 교차로가 진행방향인지 반대방향인지 체크한다.
|
||||||
bool ReverseCheck = false;
|
bool ReverseCheck = false;
|
||||||
if (path1.Path.Count > 1 && nextNodeForward != null && nextNodeForward.NodeId.Equals(path1.Path[1].NodeId))
|
//if (path1.Path.Count > 1 && nextNodeForward != null && nextNodeForward.NodeId.Equals(path1.Path[1].NodeId))
|
||||||
{
|
//{
|
||||||
ReverseCheck = false; //현재 진행 방향으로 이동해야한다
|
// ReverseCheck = false; //현재 진행 방향으로 이동해야한다
|
||||||
MakeDetailData(path1, currentDirection); // path1의 상세 경로 정보 채우기 (모터 방향 설정)
|
// MakeDetailData(path1, currentDirection); // path1의 상세 경로 정보 채우기 (모터 방향 설정)
|
||||||
}
|
//}
|
||||||
else if (path1.Path.Count > 1 && nextNodeBackward != null && nextNodeBackward.NodeId.Equals(path1.Path[1].NodeId))
|
if (path1.Path.Count > 1 && nextNodeBackward != null && nextNodeBackward.NodeId.Equals(path1.Path[1].NodeId))
|
||||||
{
|
{
|
||||||
ReverseCheck = true; //현재 방향의 반대방향으로 이동해야한다
|
ReverseCheck = true; //현재 방향의 반대방향으로 이동해야한다
|
||||||
MakeDetailData(path1, ReverseDirection); // path1의 상세 경로 정보 채우기 (모터 방향 설정)
|
MakeDetailData(path1, ReverseDirection); // path1의 상세 경로 정보 채우기 (모터 방향 설정)
|
||||||
}
|
}
|
||||||
else return AGVPathResult.CreateFailure("교차로까지 계산된 경로에 현재 위치정보로 추측을 할 수 없습니다", 0, 0);
|
else
|
||||||
|
{
|
||||||
|
ReverseCheck = false; //현재 진행 방향으로 이동해야한다
|
||||||
|
MakeDetailData(path1, currentDirection); // path1의 상세 경로 정보 채우기 (모터 방향 설정)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -232,15 +232,18 @@ namespace AGVNavigationCore.PathFinding.Planning
|
|||||||
var connected = new HashSet<string>();
|
var connected = new HashSet<string>();
|
||||||
|
|
||||||
// 직접 연결
|
// 직접 연결
|
||||||
foreach (var connectedId in node.ConnectedNodes)
|
foreach (var connectedNode in node.ConnectedMapNodes)
|
||||||
{
|
{
|
||||||
connected.Add(connectedId);
|
if (connectedNode != null)
|
||||||
|
{
|
||||||
|
connected.Add(connectedNode.NodeId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 역방향 연결
|
// 역방향 연결
|
||||||
foreach (var otherNode in _mapNodes)
|
foreach (var otherNode in _mapNodes)
|
||||||
{
|
{
|
||||||
if (otherNode.NodeId != nodeId && otherNode.ConnectedNodes.Contains(nodeId))
|
if (otherNode.NodeId != nodeId && otherNode.ConnectedMapNodes.Any(n => n.NodeId == nodeId))
|
||||||
{
|
{
|
||||||
connected.Add(otherNode.NodeId);
|
connected.Add(otherNode.NodeId);
|
||||||
}
|
}
|
||||||
@@ -728,9 +731,9 @@ namespace AGVNavigationCore.PathFinding.Planning
|
|||||||
string currentNode = path[i].NodeId;
|
string currentNode = path[i].NodeId;
|
||||||
string nextNode = path[i + 1].NodeId;
|
string nextNode = path[i + 1].NodeId;
|
||||||
|
|
||||||
// 두 노드간 직접 연결성 확인 (맵 노드의 ConnectedNodes 리스트 사용)
|
// 두 노드간 직접 연결성 확인 (맵 노드의 ConnectedMapNodes 리스트 사용)
|
||||||
var currentMapNode = _mapNodes.FirstOrDefault(n => n.NodeId == currentNode);
|
var currentMapNode = _mapNodes.FirstOrDefault(n => n.NodeId == currentNode);
|
||||||
if (currentMapNode == null || !currentMapNode.ConnectedNodes.Contains(nextNode))
|
if (currentMapNode == null || !currentMapNode.ConnectedMapNodes.Any(n => n.NodeId == nextNode))
|
||||||
{
|
{
|
||||||
return PathValidationResult.CreateInvalid(currentNode, nextNode, $"노드 {currentNode}와 {nextNode} 사이에 연결이 없음");
|
return PathValidationResult.CreateInvalid(currentNode, nextNode, $"노드 {currentNode}와 {nextNode} 사이에 연결이 없음");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,22 +69,18 @@ namespace AGVNavigationCore.Utils
|
|||||||
InitializeJunctionAnalyzer(allNodes);
|
InitializeJunctionAnalyzer(allNodes);
|
||||||
|
|
||||||
// 현재 노드에 연결된 노드들 중 이전 노드가 아닌 노드들만 필터링
|
// 현재 노드에 연결된 노드들 중 이전 노드가 아닌 노드들만 필터링
|
||||||
var connectedNodeIds = currentNode.ConnectedNodes;
|
var connectedMapNodes = currentNode.ConnectedMapNodes;
|
||||||
if (connectedNodeIds == null || connectedNodeIds.Count == 0)
|
if (connectedMapNodes == null || connectedMapNodes.Count == 0)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
List<MapNode> candidateNodes = new List<MapNode>();
|
List<MapNode> candidateNodes = new List<MapNode>();
|
||||||
if (prevDirection == direction)
|
if (prevDirection == direction)
|
||||||
{
|
{
|
||||||
candidateNodes = allNodes.Where(n => n.NodeId != prevNode.NodeId &&
|
candidateNodes = connectedMapNodes.Where(n => n.NodeId != prevNode.NodeId).ToList();
|
||||||
connectedNodeIds.Contains(n.NodeId)
|
|
||||||
).ToList();
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
candidateNodes = allNodes.Where(n =>
|
candidateNodes = connectedMapNodes.ToList();
|
||||||
connectedNodeIds.Contains(n.NodeId)
|
|
||||||
).ToList();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (candidateNodes.Count == 0)
|
if (candidateNodes.Count == 0)
|
||||||
@@ -375,13 +371,11 @@ namespace AGVNavigationCore.Utils
|
|||||||
if (currentNode == null || prevNode == null || allNodes == null)
|
if (currentNode == null || prevNode == null || allNodes == null)
|
||||||
return (null, 0, "입력 파라미터가 null입니다");
|
return (null, 0, "입력 파라미터가 null입니다");
|
||||||
|
|
||||||
var connectedNodeIds = currentNode.ConnectedNodes;
|
var connectedMapNodes = currentNode.ConnectedMapNodes;
|
||||||
if (connectedNodeIds == null || connectedNodeIds.Count == 0)
|
if (connectedMapNodes == null || connectedMapNodes.Count == 0)
|
||||||
return (null, 0, "연결된 노드가 없습니다");
|
return (null, 0, "연결된 노드가 없습니다");
|
||||||
|
|
||||||
var candidateNodes = allNodes.Where(n =>
|
var candidateNodes = connectedMapNodes.ToList();
|
||||||
connectedNodeIds.Contains(n.NodeId)
|
|
||||||
).ToList();
|
|
||||||
|
|
||||||
if (candidateNodes.Count == 0)
|
if (candidateNodes.Count == 0)
|
||||||
return (null, 0, "후보 노드가 없습니다");
|
return (null, 0, "후보 노드가 없습니다");
|
||||||
|
|||||||
@@ -28,6 +28,12 @@ namespace AGVNavigationCore.Utils
|
|||||||
System.Diagnostics.Debug.WriteLine($"[DockingValidator] 도킹 검증 불필요: 경로 없음");
|
System.Diagnostics.Debug.WriteLine($"[DockingValidator] 도킹 검증 불필요: 경로 없음");
|
||||||
return DockingValidationResult.CreateNotRequired();
|
return DockingValidationResult.CreateNotRequired();
|
||||||
}
|
}
|
||||||
|
if (pathResult.DetailedPath.Any() == false && pathResult.Path.Any() && pathResult.Path.Count == 2 &&
|
||||||
|
pathResult.Path[0].NodeId == pathResult.Path[1].NodeId)
|
||||||
|
{
|
||||||
|
System.Diagnostics.Debug.WriteLine($"[DockingValidator] 도킹 검증 불필요: 동일포인트");
|
||||||
|
return DockingValidationResult.CreateNotRequired();
|
||||||
|
}
|
||||||
|
|
||||||
// 목적지 노드 가져오기 (Path는 이제 List<MapNode>)
|
// 목적지 노드 가져오기 (Path는 이제 List<MapNode>)
|
||||||
var LastNode = pathResult.Path[pathResult.Path.Count - 1];
|
var LastNode = pathResult.Path[pathResult.Path.Count - 1];
|
||||||
|
|||||||
@@ -319,11 +319,16 @@ namespace AGVSimulator.Forms
|
|||||||
// 고급 경로 계획 사용 (노드 객체 직접 전달)
|
// 고급 경로 계획 사용 (노드 객체 직접 전달)
|
||||||
var advancedResult = _advancedPathfinder.FindPath_test(startNode, targetNode, prevNode, prevDir, currentDirection);
|
var advancedResult = _advancedPathfinder.FindPath_test(startNode, targetNode, prevNode, prevDir, currentDirection);
|
||||||
|
|
||||||
|
_simulatorCanvas.FitToNodes();
|
||||||
if (advancedResult.Success)
|
if (advancedResult.Success)
|
||||||
{
|
{
|
||||||
// 도킹 검증이 없는 경우 추가 검증 수행
|
// 도킹 검증이 없는 경우 추가 검증 수행
|
||||||
if (advancedResult.DockingValidation == null || !advancedResult.DockingValidation.IsValidationRequired)
|
if (advancedResult.DockingValidation == null || !advancedResult.DockingValidation.IsValidationRequired)
|
||||||
{
|
{
|
||||||
|
if(advancedResult.Path.Count < 1)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
advancedResult.DockingValidation = DockingValidator.ValidateDockingDirection(advancedResult, _mapNodes);
|
advancedResult.DockingValidation = DockingValidator.ValidateDockingDirection(advancedResult, _mapNodes);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -606,7 +611,7 @@ namespace AGVSimulator.Forms
|
|||||||
_rfidTextBox.Text = ""; // 입력 필드 초기화
|
_rfidTextBox.Text = ""; // 입력 필드 초기화
|
||||||
|
|
||||||
// 시뮬레이터 캔버스의 해당 노드로 이동
|
// 시뮬레이터 캔버스의 해당 노드로 이동
|
||||||
_simulatorCanvas.PanToNode(targetNode.NodeId);
|
//_simulatorCanvas.PanToNode(targetNode.NodeId);
|
||||||
|
|
||||||
// 시작 노드 콤보박스를 현재 위치로 자동 선택
|
// 시작 노드 콤보박스를 현재 위치로 자동 선택
|
||||||
SetStartNodeToCombo(targetNode.NodeId);
|
SetStartNodeToCombo(targetNode.NodeId);
|
||||||
@@ -1414,13 +1419,12 @@ namespace AGVSimulator.Forms
|
|||||||
|
|
||||||
foreach (var nodeA in _mapNodes)
|
foreach (var nodeA in _mapNodes)
|
||||||
{
|
{
|
||||||
if (nodeA.ConnectedNodes == null || nodeA.ConnectedNodes.Count == 0)
|
if (nodeA.ConnectedMapNodes == null || nodeA.ConnectedMapNodes.Count == 0)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
// 연결된 노드 ID를 실제 MapNode 객체로 변환
|
// 연결된 노드 객체 순회
|
||||||
foreach (var connectedNodeId in nodeA.ConnectedNodes)
|
foreach (var nodeB in nodeA.ConnectedMapNodes)
|
||||||
{
|
{
|
||||||
var nodeB = _mapNodes.FirstOrDefault(n => n.NodeId == connectedNodeId);
|
|
||||||
if (nodeB == null)
|
if (nodeB == null)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
@@ -1428,7 +1432,7 @@ namespace AGVSimulator.Forms
|
|||||||
var pairKey1 = $"{nodeA.NodeId}→{nodeB.NodeId}";
|
var pairKey1 = $"{nodeA.NodeId}→{nodeB.NodeId}";
|
||||||
var pairKey2 = $"{nodeB.NodeId}→{nodeA.NodeId}";
|
var pairKey2 = $"{nodeB.NodeId}→{nodeA.NodeId}";
|
||||||
|
|
||||||
if (!processedPairs.Contains(pairKey1) && !processedPairs.Contains(pairKey2))
|
if (nodeA.HasRfid() && nodeB.HasRfid() && !processedPairs.Contains(pairKey1) && !processedPairs.Contains(pairKey2))
|
||||||
{
|
{
|
||||||
pairs.Add((nodeA, nodeB));
|
pairs.Add((nodeA, nodeB));
|
||||||
processedPairs.Add(pairKey1);
|
processedPairs.Add(pairKey1);
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user