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:
backuppc
2025-10-30 09:34:10 +09:00
parent 0b59479d34
commit 3f6db7113f
18 changed files with 838 additions and 1303 deletions

View 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;
}
}

View File

@@ -1,7 +1,6 @@
using System;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Linq;
using System.Windows.Forms;
using AGVNavigationCore.Models;
using AGVNavigationCore.Utils;
@@ -14,17 +13,11 @@ namespace AGVMapEditor.Forms
/// </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();
InitializeComponent();
_targetNode = imageNode;
SetupUI();
@@ -36,64 +29,25 @@ namespace AGVMapEditor.Forms
private void SetupUI()
{
this.Text = "이미지 편집기";
this.Size = new Size(800, 600);
this.StartPosition = FormStartPosition.CenterScreen;
// 캔버스 초기 설정
imageCanvas.BrushSize = trackBrush.Value;
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;
}
// 버튼: 이미지 열기
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 TrackBrush_ValueChanged(object sender, EventArgs e)
{
imageCanvas.BrushSize = trackBrush.Value;
}
private void LoadImageFromNode(MapNode node)
{
if (node.LoadedImage != null)
{
_editingImage?.Dispose();
_graphics?.Dispose();
_editingImage = new Bitmap(node.LoadedImage);
_graphics = Graphics.FromImage(_editingImage);
UpdateCanvas();
imageCanvas.EditingImage = new Bitmap(node.LoadedImage);
}
}
@@ -112,25 +66,22 @@ namespace AGVMapEditor.Forms
{
try
{
_editingImage?.Dispose();
_graphics?.Dispose();
var loadedImage = Image.FromFile(filePath);
// 이미지 크기가 크면 자동 축소 (최대 512x512)
Bitmap finalImage;
if (loadedImage.Width > 512 || loadedImage.Height > 512)
{
_editingImage = ResizeImage(loadedImage, 512, 512);
finalImage = ResizeImage(loadedImage, 512, 512);
loadedImage.Dispose();
}
else
{
_editingImage = new Bitmap(loadedImage);
finalImage = new Bitmap(loadedImage);
loadedImage.Dispose();
}
_graphics = Graphics.FromImage(_editingImage);
UpdateCanvas();
imageCanvas.EditingImage = finalImage;
}
catch (Exception ex)
{
@@ -140,12 +91,14 @@ namespace AGVMapEditor.Forms
private void BtnResize_Click(object sender, EventArgs e)
{
if (_editingImage == null)
if (imageCanvas.EditingImage == null)
{
MessageBox.Show("먼저 이미지를 로드하세요.");
return;
}
var currentSize = imageCanvas.ImageDisplaySize;
using (var form = new Form())
{
form.Text = "이미지 크기 조정";
@@ -153,10 +106,10 @@ namespace AGVMapEditor.Forms
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 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 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 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)
{
_graphics?.Dispose();
var resized = new Bitmap(_editingImage, width, height);
_editingImage.Dispose();
_editingImage = resized;
_graphics = Graphics.FromImage(_editingImage);
UpdateCanvas();
var resized = new Bitmap(imageCanvas.EditingImage, width, height);
imageCanvas.EditingImage = resized;
}
}
}
@@ -188,19 +137,19 @@ namespace AGVMapEditor.Forms
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)
{
_drawColor = cfd.Color;
(sender as Button).BackColor = _drawColor;
imageCanvas.DrawColor = cfd.Color;
(sender as Button).BackColor = cfd.Color;
}
}
}
private void BtnSave_Click(object sender, EventArgs e)
{
if (_editingImage == null)
if (imageCanvas.EditingImage == null)
{
MessageBox.Show("저장할 이미지가 없습니다.");
return;
@@ -208,10 +157,22 @@ namespace AGVMapEditor.Forms
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로 변환하여 저장
_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 = new Bitmap(_editingImage);
_targetNode.LoadedImage = finalImage;
MessageBox.Show("이미지가 저장되었습니다.");
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)
{
double ratioX = (double)maxWidth / image.Width;
@@ -273,15 +199,5 @@ namespace AGVMapEditor.Forms
}
return resized;
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
_editingImage?.Dispose();
_graphics?.Dispose();
}
base.Dispose(disposing);
}
}
}

View File

@@ -1186,14 +1186,25 @@ namespace AGVMapEditor.Forms
if (fromNode != null && toNode != null)
{
// 단일 연결 삭제 (어느 방향에 저장되어 있는지 확인 후 삭제)
// 양방향 연결 삭제 (양쪽 방향 모두 제거)
bool removed = false;
if (fromNode.ConnectedNodes.Contains(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);
removed = true;
}
if (!removed)
{
MessageBox.Show("연결을 찾을 수 없습니다.", "알림", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
_hasChanges = true;