fix: Add motor direction parameter to magnet direction calculation in pathfinding

- Fixed critical issue in ConvertToDetailedPath where motor direction was not passed to GetRequiredMagnetDirection
- Motor direction is essential for backward movement as Left/Right directions must be inverted
- Modified AGVPathfinder.cs line 280 to pass currentDirection parameter
- Ensures backward motor direction properly inverts magnet sensor directions

feat: Add waypoint support to pathfinding system

- Added FindPath overload with params string[] waypointNodeIds in AStarPathfinder
- Supports sequential traversal through multiple intermediate nodes
- Validates waypoints and prevents duplicates in sequence
- Returns combined path result with aggregated metrics

feat: Implement path result merging with DetailedPath preservation

- Added CombineResults method in AStarPathfinder for intelligent path merging
- Automatically deduplicates nodes when last of previous path equals first of current
- Preserves DetailedPath information including motor and magnet directions
- Essential for multi-segment path operations

feat: Integrate magnet direction with motor direction awareness

- Modified JunctionAnalyzer.GetRequiredMagnetDirection to accept AgvDirection parameter
- Inverts Left/Right magnet directions when moving Backward
- Properly handles motor direction context throughout pathfinding

feat: Add automatic start node selection in simulator

- Added SetStartNodeToCombo method to SimulatorForm
- Automatically selects start node combo box when AGV position is set via RFID
- Improves UI usability and workflow efficiency

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
backuppc
2025-10-24 15:46:16 +09:00
parent 3ddecf63ed
commit d932b8d332
47 changed files with 7473 additions and 1088 deletions

View File

@@ -0,0 +1,287 @@
using System;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Linq;
using System.Windows.Forms;
using AGVNavigationCore.Models;
using AGVNavigationCore.Utils;
namespace AGVMapEditor.Forms
{
/// <summary>
/// 이미지 노드의 이미지를 편집하기 위한 간단한 그림판
/// 불러오기, 저장, 크기 조정, 기본 드로잉 기능 제공
/// </summary>
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;
public ImageEditorForm(MapNode imageNode = null)
{
//InitializeComponent();
_targetNode = imageNode;
SetupUI();
if (imageNode != null && imageNode.LoadedImage != null)
{
LoadImageFromNode(imageNode);
}
}
private void SetupUI()
{
this.Text = "이미지 편집기";
this.Size = new Size(800, 600);
this.StartPosition = FormStartPosition.CenterScreen;
// 패널: 도구 모음
var toolPanel = new Panel { Dock = DockStyle.Top, Height = 50, BackColor = Color.LightGray };
// 버튼: 이미지 열기
var btnOpen = new Button { Text = "이미지 열기", Width = 100, Left = 10, Top = 10 };
btnOpen.Click += BtnOpen_Click;
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)
{
if (node.LoadedImage != null)
{
_editingImage?.Dispose();
_graphics?.Dispose();
_editingImage = new Bitmap(node.LoadedImage);
_graphics = Graphics.FromImage(_editingImage);
UpdateCanvas();
}
}
private void BtnOpen_Click(object sender, EventArgs e)
{
using (var ofd = new OpenFileDialog { Filter = "이미지|*.jpg;*.png;*.bmp|모든 파일|*.*" })
{
if (ofd.ShowDialog() == DialogResult.OK)
{
LoadImageFromFile(ofd.FileName);
}
}
}
private void LoadImageFromFile(string filePath)
{
try
{
_editingImage?.Dispose();
_graphics?.Dispose();
var loadedImage = Image.FromFile(filePath);
// 이미지 크기가 크면 자동 축소 (최대 512x512)
if (loadedImage.Width > 512 || loadedImage.Height > 512)
{
_editingImage = ResizeImage(loadedImage, 512, 512);
loadedImage.Dispose();
}
else
{
_editingImage = new Bitmap(loadedImage);
loadedImage.Dispose();
}
_graphics = Graphics.FromImage(_editingImage);
UpdateCanvas();
}
catch (Exception ex)
{
MessageBox.Show($"이미지 로드 실패: {ex.Message}");
}
}
private void BtnResize_Click(object sender, EventArgs e)
{
if (_editingImage == null)
{
MessageBox.Show("먼저 이미지를 로드하세요.");
return;
}
using (var form = new Form())
{
form.Text = "이미지 크기 조정";
form.Size = new Size(300, 150);
form.StartPosition = FormStartPosition.CenterParent;
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 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 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 };
form.Controls.Add(lblWidth);
form.Controls.Add(txtWidth);
form.Controls.Add(lblHeight);
form.Controls.Add(txtHeight);
form.Controls.Add(btnOk);
form.Controls.Add(btnCancel);
if (form.ShowDialog(this) == DialogResult.OK)
{
if (int.TryParse(txtWidth.Text, out int width) && int.TryParse(txtHeight.Text, out int height))
{
if (width > 0 && height > 0)
{
_graphics?.Dispose();
var resized = new Bitmap(_editingImage, width, height);
_editingImage.Dispose();
_editingImage = resized;
_graphics = Graphics.FromImage(_editingImage);
UpdateCanvas();
}
}
}
}
}
private void BtnColor_Click(object sender, EventArgs e)
{
using (var cfd = new ColorDialog { Color = _drawColor })
{
if (cfd.ShowDialog() == DialogResult.OK)
{
_drawColor = cfd.Color;
(sender as Button).BackColor = _drawColor;
}
}
}
private void BtnSave_Click(object sender, EventArgs e)
{
if (_editingImage == null)
{
MessageBox.Show("저장할 이미지가 없습니다.");
return;
}
if (_targetNode != null && _targetNode.Type == NodeType.Image)
{
// 이미지를 Base64로 변환하여 저장
_targetNode.ImageBase64 = ImageConverterUtil.ImageToBase64(_editingImage, System.Drawing.Imaging.ImageFormat.Png);
_targetNode.LoadedImage?.Dispose();
_targetNode.LoadedImage = new Bitmap(_editingImage);
MessageBox.Show("이미지가 저장되었습니다.");
this.DialogResult = DialogResult.OK;
this.Close();
}
}
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)
{
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 resized = new Bitmap(newWidth, newHeight);
using (var g = Graphics.FromImage(resized))
{
g.CompositingQuality = CompositingQuality.HighQuality;
g.InterpolationMode = InterpolationMode.HighQualityBicubic;
g.SmoothingMode = SmoothingMode.HighQuality;
g.DrawImage(image, 0, 0, newWidth, newHeight);
}
return resized;
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
_editingImage?.Dispose();
_graphics?.Dispose();
}
base.Dispose(disposing);
}
}
}

View File

@@ -4,6 +4,7 @@ using System.Drawing;
using System.IO;
using System.Linq;
using System.Windows.Forms;
using AGVMapEditor.Models;
using AGVNavigationCore.Controls;
using AGVNavigationCore.Models;
using Newtonsoft.Json;
@@ -165,52 +166,61 @@ namespace AGVMapEditor.Forms
btnAddImage.Location = new Point(325, 3);
btnAddImage.Click += (s, e) => _mapCanvas.CurrentEditMode = UnifiedAGVCanvas.EditMode.AddImage;
// 이미지 편집 버튼
var btnEditImage = new Button();
btnEditImage.Name = "btnToolbarEditImage";
btnEditImage.Text = "이미지 편집";
btnEditImage.Size = new Size(80, 28);
btnEditImage.Location = new Point(420, 3);
btnEditImage.Enabled = false; // 처음에는 비활성화
btnEditImage.Click += BtnToolbarEditImage_Click;
// 연결 모드 버튼
var btnConnect = new Button();
btnConnect.Text = "연결 (C)";
btnConnect.Size = new Size(70, 28);
btnConnect.Location = new Point(420, 3);
btnConnect.Location = new Point(505, 3);
btnConnect.Click += (s, e) => _mapCanvas.CurrentEditMode = UnifiedAGVCanvas.EditMode.Connect;
// 삭제 모드 버튼
var btnDelete = new Button();
btnDelete.Text = "삭제 (D)";
btnDelete.Size = new Size(70, 28);
btnDelete.Location = new Point(495, 3);
btnDelete.Location = new Point(580, 3);
btnDelete.Click += (s, e) => _mapCanvas.CurrentEditMode = UnifiedAGVCanvas.EditMode.Delete;
// 연결 삭제 버튼
var btnDeleteConnection = new Button();
btnDeleteConnection.Text = "연결삭제 (X)";
btnDeleteConnection.Size = new Size(80, 28);
btnDeleteConnection.Location = new Point(570, 3);
btnDeleteConnection.Location = new Point(655, 3);
btnDeleteConnection.Click += (s, e) => _mapCanvas.CurrentEditMode = UnifiedAGVCanvas.EditMode.DeleteConnection;
// 구분선
var separator1 = new Label();
separator1.Text = "|";
separator1.Size = new Size(10, 28);
separator1.Location = new Point(655, 3);
separator1.Location = new Point(740, 3);
separator1.TextAlign = ContentAlignment.MiddleCenter;
// 그리드 토글 버튼
var btnToggleGrid = new Button();
btnToggleGrid.Text = "그리드";
btnToggleGrid.Size = new Size(60, 28);
btnToggleGrid.Location = new Point(670, 3);
btnToggleGrid.Location = new Point(755, 3);
btnToggleGrid.Click += (s, e) => _mapCanvas.ShowGrid = !_mapCanvas.ShowGrid;
// 맵 맞춤 버튼
var btnFitMap = new Button();
btnFitMap.Text = "맵 맞춤";
btnFitMap.Size = new Size(70, 28);
btnFitMap.Location = new Point(735, 3);
btnFitMap.Location = new Point(820, 3);
btnFitMap.Click += (s, e) => _mapCanvas.FitToNodes();
// 툴바에 버튼들 추가
toolbarPanel.Controls.AddRange(new Control[]
{
btnSelect, btnMove, btnAddNode, btnAddLabel, btnAddImage, btnConnect, btnDelete, btnDeleteConnection, separator1, btnToggleGrid, btnFitMap
btnSelect, btnMove, btnAddNode, btnAddLabel, btnAddImage, btnEditImage, btnConnect, btnDelete, btnDeleteConnection, separator1, btnToggleGrid, btnFitMap
});
// 스플리터 패널에 툴바 추가 (맨 위에)
@@ -946,11 +956,59 @@ namespace AGVMapEditor.Forms
var nodeWrapper = NodePropertyWrapperFactory.CreateWrapper(_selectedNode, _mapNodes);
_propertyGrid.SelectedObject = nodeWrapper;
_propertyGrid.Focus();
// 이미지 노드인 경우 편집 버튼 활성화
UpdateImageEditButton();
}
private void ClearNodeProperties()
{
_propertyGrid.SelectedObject = null;
DisableImageEditButton();
}
/// <summary>
/// 선택된 노드가 이미지 노드이면 편집 버튼 활성화
/// </summary>
private void UpdateImageEditButton()
{
var btn = this.Controls.Find("btnToolbarEditImage", true).FirstOrDefault() as Button;
if (btn != null)
{
btn.Enabled = (_selectedNode != null && _selectedNode.Type == NodeType.Image);
}
}
/// <summary>
/// 이미지 편집 버튼 비활성화
/// </summary>
private void DisableImageEditButton()
{
var btn = this.Controls.Find("btnToolbarEditImage", true).FirstOrDefault() as Button;
if (btn != null)
{
btn.Enabled = false;
}
}
/// <summary>
/// 상단 툴바의 이미지 편집 버튼 클릭 이벤트
/// </summary>
private void BtnToolbarEditImage_Click(object sender, EventArgs e)
{
if (_selectedNode != null && _selectedNode.Type == NodeType.Image)
{
using (var editor = new ImageEditorForm(_selectedNode))
{
if (editor.ShowDialog(this) == DialogResult.OK)
{
_hasChanges = true;
UpdateTitle();
_mapCanvas.Invalidate(); // 캔버스 다시 그리기
UpdateNodeProperties(); // 속성 업데이트
}
}
}
}
private void UpdateTitle()