MapNode.cs: - Padding 속성 추가 (기본값 8px, 텍스트 주변 여백) - Clone 메서드에 Padding 복사 추가 NodePropertyWrapper.cs: - LabelNodePropertyWrapper에 Padding 속성 추가 - PropertyGrid에서 0~50px 범위로 조정 가능 UnifiedAGVCanvas.Events.cs: - DrawLabelNode: 하드코딩된 패딩을 node.Padding 사용 - GetNodeBrush: RFID 없는 노드를 회색 계통으로 표시 * Normal: Blue → LightGray * Rotation: Orange → DarkGray * Docking: Green → Gray * Charging: Red → Silver UnifiedAGVCanvas.Mouse.cs: - HandleLabelNodeDoubleClick: node.Name → node.LabelText 사용 이제 라벨 노드 패딩을 속성창에서 조정 가능하고, RFID 미할당 노드를 시각적으로 쉽게 구분 가능 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
416 lines
13 KiB
C#
416 lines
13 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|