This commit is contained in:
backuppc
2026-02-10 14:53:54 +09:00
parent c2cc5d67ae
commit 471b8ff9c4
18 changed files with 786 additions and 743 deletions

View File

@@ -478,8 +478,32 @@ namespace AGVNavigationCore.Controls
// But this method draws normal first.
// So I should refactor to calculate path first, then draw?
// Or just draw highlight on top with alpha?
// Let's draw highlight on top with non-filled center? No, it's a line.
// I'll draw highlight on top for now, maybe with alpha.
// Let's draw highlight on top for now, maybe with alpha.
}
// 선택된 마그넷 핸들 그리기
if (magnet == _selectedNode && _canvasMode == CanvasMode.Edit)
{
using (var handleBrush = new SolidBrush(Color.White))
using (var handlePen = new Pen(Color.Black, 1))
{
float size = HANDLE_SIZE / _zoomFactor;
float half = size / 2;
// 시작점, 끝점 핸들
g.FillRectangle(handleBrush, startPoint.X - half, startPoint.Y - half, size, size);
g.DrawRectangle(handlePen, startPoint.X - half, startPoint.Y - half, size, size);
g.FillRectangle(handleBrush, endPoint.X - half, endPoint.Y - half, size, size);
g.DrawRectangle(handlePen, endPoint.X - half, endPoint.Y - half, size, size);
// 제어점 핸들 (곡선일 경우)
if (magnet.ControlPoint != null)
{
var cp = magnet.ControlPoint;
g.FillRectangle(handleBrush, (float)cp.X - half, (float)cp.Y - half, size, size);
g.DrawRectangle(handlePen, (float)cp.X - half, (float)cp.Y - half, size, size);
}
}
}
}
@@ -488,11 +512,10 @@ namespace AGVNavigationCore.Controls
if (_marks == null) return; // _marks 리스트 사용
int sensorSize = 12; // 크기 설정
int lineLength = 20; // 선 길이 설정
int halfLength = lineLength / 2;
foreach (var mark in _marks)
{
int lineLength = (int)mark.Length; // 저장된 길이 사용
int halfLength = lineLength / 2;
Point p = mark.Position;
double radians = mark.Rotation * Math.PI / 180.0;
@@ -514,6 +537,22 @@ namespace AGVNavigationCore.Controls
g.DrawLine(highlightPen, p1, p2);
}
}
// 선택된 마크 핸들 그리기
if (mark == _selectedNode && _canvasMode == CanvasMode.Edit)
{
using (var handleBrush = new SolidBrush(Color.White))
using (var handlePen = new Pen(Color.Black, 1))
{
float size = HANDLE_SIZE / _zoomFactor;
float half = size / 2;
g.FillRectangle(handleBrush, p1.X - half, p1.Y - half, size, size);
g.DrawRectangle(handlePen, p1.X - half, p1.Y - half, size, size);
g.FillRectangle(handleBrush, p2.X - half, p2.Y - half, size, size);
g.DrawRectangle(handlePen, p2.X - half, p2.Y - half, size, size);
}
}
}
}

View File

@@ -220,12 +220,28 @@ namespace AGVNavigationCore.Controls
{
if (_editMode == EditMode.Move)
{
// 0. 핸들 선택 확인 (이미 선택된 노드가 있을 때)
if (_selectedNode != null)
{
int handleIdx = GetHandleAt(worldPoint);
if (handleIdx != -1)
{
_dragHandleIndex = handleIdx;
_isDragging = true;
_isPanning = false;
Capture = true;
Invalidate();
return;
}
}
// 1. 노드 선택 확인
var hitNode = GetItemAt(worldPoint);
if (hitNode != null)
{
_isDragging = true;
_isPanning = false;
_dragHandleIndex = -1; // 노드 전체 드래그
_selectedNode = hitNode;
_dragStartPosition = hitNode.Position;
_dragOffset = new Point(worldPoint.X - hitNode.Position.X, worldPoint.Y - hitNode.Position.Y);
@@ -322,8 +338,38 @@ namespace AGVNavigationCore.Controls
// 노드 드래그
if (_selectedNode != null)
{
_selectedNode.Position = newPosition;
NodeMoved?.Invoke(this, _selectedNode);
if (_dragHandleIndex != -1)
{
// 핸들 드래그 (포인트별 수정)
if (_selectedNode is MapMagnet magnet)
{
if (_dragHandleIndex == 0) magnet.StartPoint = newPosition;
else if (_dragHandleIndex == 1) magnet.EndPoint = newPosition;
else if (_dragHandleIndex == 2 && magnet.ControlPoint != null)
{
magnet.ControlPoint.X = newPosition.X;
magnet.ControlPoint.Y = newPosition.Y;
}
}
else if (_selectedNode is MapMark mark)
{
// 마크는 중심점 대비 각도와 길이를 계산하여 수정
var dx = newPosition.X - mark.Position.X;
var dy = newPosition.Y - mark.Position.Y;
// 핸들 인덱스에 따라 각도 반전 (p1 vs p2)
if (_dragHandleIndex == 0) { dx = -dx; dy = -dy; }
mark.Rotation = Math.Atan2(dy, dx) * 180.0 / Math.PI;
mark.Length = Math.Sqrt(dx * dx + dy * dy) * 2;
}
}
else
{
// 노드 전체 드래그
_selectedNode.Position = newPosition;
NodeMoved?.Invoke(this, _selectedNode);
}
moved = true;
}
@@ -352,6 +398,7 @@ namespace AGVNavigationCore.Controls
if (_isDragging && _canvasMode == CanvasMode.Edit)
{
_isDragging = false;
_dragHandleIndex = -1;
Capture = false; // 🔥 마우스 캡처 해제
Cursor = GetCursorForMode(_editMode);
}
@@ -463,6 +510,25 @@ namespace AGVNavigationCore.Controls
}
}
if (_marks != null)
{
for (int i = _marks.Count - 1; i >= 0; i--)
{
var node = _marks[i];
if (IsPointInNode(worldPoint, node))
return node;
}
}
if (_magnets != null)
{
for (int i = _magnets.Count - 1; i >= 0; i--)
{
var node = _magnets[i];
if (IsPointInNode(worldPoint, node))
return node;
}
}
return null;
}
@@ -477,6 +543,14 @@ namespace AGVNavigationCore.Controls
{
return IsPointInImage(point, image);
}
if (node is MapMark mark)
{
return IsPointInMark(point, mark);
}
if (node is MapMagnet magnet)
{
return IsPointInMagnet(point, magnet);
}
// 라벨과 이미지는 별도 리스트로 관리되므로 여기서 처리하지 않음
// 하지만 혹시 모를 하위 호환성을 위해 타입 체크는 유지하되,
// 실제 로직은 CircularNode 등으로 분기
@@ -648,6 +722,55 @@ namespace AGVNavigationCore.Controls
return imageRect.Contains(point);
}
private bool IsPointInMark(Point point, MapMark mark)
{
int lineLength = (int)mark.Length;
int halfLength = lineLength / 2;
double radians = mark.Rotation * Math.PI / 180.0;
int dx = (int)(halfLength * Math.Cos(radians));
int dy = (int)(halfLength * Math.Sin(radians));
Point p1 = new Point(mark.Position.X - dx, mark.Position.Y - dy);
Point p2 = new Point(mark.Position.X + dx, mark.Position.Y + dy);
// 마크 선택을 위해 약간 넉넉한 히트 영역 (7픽셀)
return CalculatePointToLineDistance(point, p1, p2) <= 7 / _zoomFactor;
}
private bool IsPointInMagnet(Point point, MapMagnet magnet)
{
// 마그넷은 두꺼우므로 (Pen Width 15) 절반인 7.5 정도를 히트 영역으로 잡음
float hitThreshold = Math.Max(8f, 12f / _zoomFactor);
if (magnet.ControlPoint != null)
{
// 베지어 곡선 정밀 샘플링 (10개 세그먼트)
Point prevPoint = magnet.StartPoint;
for (int i = 1; i <= 10; i++)
{
float t = i / 10f;
// Quadratic Bezier: (1-t)^2*P0 + 2(1-t)t*P1 + t^2*P2
float u = 1 - t;
float tt = t * t;
float uu = u * u;
float x = uu * magnet.StartPoint.X + 2 * u * t * (float)magnet.ControlPoint.X + tt * magnet.EndPoint.X;
float y = uu * magnet.StartPoint.Y + 2 * u * t * (float)magnet.ControlPoint.Y + tt * magnet.EndPoint.Y;
Point currentPoint = new Point((int)x, (int)y);
if (CalculatePointToLineDistance(point, prevPoint, currentPoint) <= hitThreshold)
return true;
prevPoint = currentPoint;
}
return false;
}
else
{
return CalculatePointToLineDistance(point, magnet.StartPoint, magnet.EndPoint) <= hitThreshold;
}
}
//private MapLabel GetLabelAt(Point worldPoint)
//{
// if (_labels == null) return null;
@@ -833,7 +956,7 @@ namespace AGVNavigationCore.Controls
/// <summary>
/// 중복되지 않는 고유한 NodeId 생성
/// </summary>
private string GenerateUniqueNodeId()
public string GenerateUniqueNodeId()
{
string nodeId;
int counter = _nodeCounter;
@@ -1053,8 +1176,8 @@ namespace AGVNavigationCore.Controls
var C = lineEnd.X - lineStart.X;
var D = lineEnd.Y - lineStart.Y;
var dot = A * C + B * D;
var lenSq = C * C + D * D;
var dot = (double)A * C + (double)B * D;
var lenSq = (double)C * C + (double)D * D;
if (lenSq == 0) return CalculateDistance(point, lineStart);
@@ -1102,6 +1225,39 @@ namespace AGVNavigationCore.Controls
}
}
private int GetHandleAt(Point worldPoint)
{
if (_selectedNode == null) return -1;
float hitTolerance = (HANDLE_SIZE + 4) / _zoomFactor;
if (_selectedNode is MapMagnet magnet)
{
if (CalculateDistance(worldPoint, magnet.StartPoint) <= hitTolerance) return 0;
if (CalculateDistance(worldPoint, magnet.EndPoint) <= hitTolerance) return 1;
if (magnet.ControlPoint != null)
{
if (CalculateDistance(worldPoint, new Point((int)magnet.ControlPoint.X, (int)magnet.ControlPoint.Y)) <= hitTolerance) return 2;
}
}
else if (_selectedNode is MapMark mark)
{
int lineLength = (int)mark.Length;
int halfLength = lineLength / 2;
double radians = mark.Rotation * Math.PI / 180.0;
int dx = (int)(halfLength * Math.Cos(radians));
int dy = (int)(halfLength * Math.Sin(radians));
Point p1 = new Point(mark.Position.X - dx, mark.Position.Y - dy);
Point p2 = new Point(mark.Position.X + dx, mark.Position.Y + dy);
if (CalculateDistance(worldPoint, p1) <= hitTolerance) return 0;
if (CalculateDistance(worldPoint, p2) <= hitTolerance) return 1;
}
return -1;
}
#endregion
#region View Control Methods

View File

@@ -109,6 +109,8 @@ namespace AGVNavigationCore.Controls
private MapNode _connectionStartNode;
private Point _connectionEndPoint;
private int _mouseMoveCounter = 0; // 디버그용: MouseMove 실행 횟수
private int _dragHandleIndex = -1; // 드래그 중인 핸들 인덱스
private const int HANDLE_SIZE = 8; // 편집 핸들 크기
// 영역 선택 관련
private bool _isAreaSelecting;
@@ -341,6 +343,17 @@ namespace AGVNavigationCore.Controls
Invalidate();
}
[Browsable(false)]
public PointF PanOffset
{
get => _panOffset;
set
{
_panOffset = value;
Invalidate();
}
}
/// <summary>
/// 그리드 표시 여부
/// </summary>

View File

@@ -56,17 +56,25 @@ namespace AGVNavigationCore.Models
}
/// <summary>
/// 시작점 Point 반환
/// 시작점 Point 반환 및 설정
/// </summary>
[Browsable(false)]
[JsonIgnore]
public Point StartPoint => new Point((int)P1.X, (int)P1.Y);
public Point StartPoint
{
get => new Point((int)P1.X, (int)P1.Y);
set { P1.X = value.X; P1.Y = value.Y; }
}
/// <summary>
/// 끝점 Point 반환
/// 끝점 Point 반환 및 설정
/// </summary>
[Browsable(false)]
[JsonIgnore]
public Point EndPoint => new Point((int)P2.X, (int)P2.Y);
public Point EndPoint
{
get => new Point((int)P2.X, (int)P2.Y);
set { P2.X = value.X; P2.Y = value.Y; }
}
}
}

View File

@@ -33,5 +33,9 @@ namespace AGVNavigationCore.Models
[Category("위치 정보")]
[Description("마크의 회전 각도")]
public double Rotation { get; set; }
[Category("위치 정보")]
[Description("마크의 길이")]
public double Length { get; set; } = 20.0;
}
}