using System; using System.Collections.Generic; using System.ComponentModel.Design; using System.Drawing; using System.Drawing.Design; using System.Drawing.Drawing2D; using System.IO; using System.Linq; using System.Net.NetworkInformation; using System.Security.Cryptography; using System.Security.Permissions; using System.Text.RegularExpressions; using System.Windows.Forms; using AGVControl.Models; using AR; using static System.Net.Mime.MediaTypeNames; namespace AGVControl { public class RFIDConnection { public uint StartRFID { get; set; } public uint EndRFID { get; set; } public bool IsBidirectional { get; set; } public float Distance { get; set; } public List IntermediateRFIDs { get; set; } = new List(); public override bool Equals(object obj) { if (obj is RFIDConnection other) { return (StartRFID == other.StartRFID && EndRFID == other.EndRFID) || (IsBidirectional && other.IsBidirectional && StartRFID == other.EndRFID && EndRFID == other.StartRFID); } return false; } public override int GetHashCode() { return StartRFID.GetHashCode() ^ EndRFID.GetHashCode(); } } public partial class MapControl : Control { private List rfidPoints; private List mapTexts; private List customLines; private List rfidLines; private HashSet rfidConnections; public AGV agv; private float zoom = 1.0f; private PointF offset = PointF.Empty; private Point lastMousePosition; private bool isDragging = false; private Point? previewStartPoint = null; private Point currentMousePosition; private const int SNAP_DISTANCE = 10; // 점 근접 거리 private const int LINE_WIDTH = 20; // 선 굵기 private const int TOOLBAR_WIDTH = 58; private const int TOOLBAR_WIDTHR = 78; private const int TOOLBAR_BUTTON_HEIGHT = 40; private const int TOOLBAR_MARGIN = 5; private List toolbarRects; private bool isAddingText = false; private bool isAddingPoint = false; private bool isDrawingCustomLine = false; private bool isDrawingRFIDLine = false; private bool isDeletingRFIDLine = false; private bool isDrawingLine = false; private MapText selectedText = null; private CustomLine selectedLine = null; private RFIDPoint selectedRFID = null; private RFIDLine selectedRFIDLine = null; private Point? draggingPoint = null; private bool isDraggingPoint = false; private const int SELECTION_DISTANCE = 15; // 선택 가능 거리를 늘림 private bool isDraggingText = false; private Point dragOffset; // private List currentPath; public MapText SelectedText => selectedText; public CustomLine SelectedLine => selectedLine; public RFIDPoint SelectedRFID => selectedRFID; public RFIDLine SelectedRFIDLine => selectedRFIDLine; public float GetDistance(Point p1, Point p2) { float dx = p1.X - p2.X; float dy = p1.Y - p2.Y; return (float)Math.Sqrt(dx * dx + dy * dy); // double을 float로 명시적 캐스팅 } public MapControl() { this.DoubleBuffered = true; rfidPoints = new List(); mapTexts = new List(); customLines = new List(); rfidLines = new List(); rfidConnections = new HashSet(); agv = new AGV(); // 툴바 버튼 영역 초기화 UpdateToolbarRects(); } protected override void OnResize(EventArgs e) { base.OnResize(e); UpdateToolbarRects(); this.Invalidate(); } private void UpdateToolbarRects() { int x, y, c, row; var idx = 0; if (this.toolbarRects == null) this.toolbarRects = new List(); else this.toolbarRects.Clear(); //left toolbar x = TOOLBAR_MARGIN; y = 10; toolbarRects.Add(new ToolBarItem { Idx = idx++, Title = "+", Bounds = new Rectangle(x, y, TOOLBAR_WIDTH - 2 * TOOLBAR_MARGIN, TOOLBAR_BUTTON_HEIGHT) }); y += TOOLBAR_BUTTON_HEIGHT + TOOLBAR_MARGIN; toolbarRects.Add(new ToolBarItem { Idx = idx++, Title = "-", Bounds = new Rectangle(x, y, TOOLBAR_WIDTH - 2 * TOOLBAR_MARGIN, TOOLBAR_BUTTON_HEIGHT) }); y += TOOLBAR_BUTTON_HEIGHT + TOOLBAR_MARGIN; toolbarRects.Add(new ToolBarItem { Idx = idx++, Title = "1:1", Bounds = new Rectangle(x, y, TOOLBAR_WIDTH - 2 * TOOLBAR_MARGIN, TOOLBAR_BUTTON_HEIGHT) }); y += TOOLBAR_BUTTON_HEIGHT + TOOLBAR_MARGIN; toolbarRects.Add(new ToolBarItem { Idx = idx++, Title = "Cut", Bounds = new Rectangle(x, y, TOOLBAR_WIDTH - 2 * TOOLBAR_MARGIN, TOOLBAR_BUTTON_HEIGHT) }); //right toolbar y = 10; row = 0; x = DisplayRectangle.Right - TOOLBAR_WIDTHR - TOOLBAR_MARGIN; toolbarRects.Add(new ToolBarItem { Idx = idx++, Title = "Text", Bounds = new Rectangle(x, y, TOOLBAR_WIDTHR - 2 * TOOLBAR_MARGIN, TOOLBAR_BUTTON_HEIGHT) }); y += TOOLBAR_BUTTON_HEIGHT + TOOLBAR_MARGIN; toolbarRects.Add(new ToolBarItem { Idx = idx++, Title = "Line", Bounds = new Rectangle(x, y, TOOLBAR_WIDTHR - 2 * TOOLBAR_MARGIN, TOOLBAR_BUTTON_HEIGHT) }); y += TOOLBAR_BUTTON_HEIGHT + TOOLBAR_MARGIN; toolbarRects.Add(new ToolBarItem { Idx = idx++, Title = "Point", Bounds = new Rectangle(x, y, TOOLBAR_WIDTHR - 2 * TOOLBAR_MARGIN, TOOLBAR_BUTTON_HEIGHT) }); y += TOOLBAR_BUTTON_HEIGHT + TOOLBAR_MARGIN; toolbarRects.Add(new ToolBarItem { Idx = idx++, Title = "Magnet", Bounds = new Rectangle(x, y, TOOLBAR_WIDTHR - 2 * TOOLBAR_MARGIN, TOOLBAR_BUTTON_HEIGHT) }); y += TOOLBAR_BUTTON_HEIGHT + TOOLBAR_MARGIN; toolbarRects.Add(new ToolBarItem { Idx = idx++, Title = "Load", Bounds = new Rectangle(x, y, TOOLBAR_WIDTHR - 2 * TOOLBAR_MARGIN, TOOLBAR_BUTTON_HEIGHT) }); y += TOOLBAR_BUTTON_HEIGHT + TOOLBAR_MARGIN; toolbarRects.Add(new ToolBarItem { Idx = idx++, Title = "Save", Bounds = new Rectangle(x, y, TOOLBAR_WIDTHR - 2 * TOOLBAR_MARGIN, TOOLBAR_BUTTON_HEIGHT) }); y += TOOLBAR_BUTTON_HEIGHT + TOOLBAR_MARGIN; toolbarRects.Add(new ToolBarItem { Idx = idx++, Title = "Pos", Bounds = new Rectangle(x, y, TOOLBAR_WIDTHR - 2 * TOOLBAR_MARGIN, TOOLBAR_BUTTON_HEIGHT) }); y += TOOLBAR_BUTTON_HEIGHT + TOOLBAR_MARGIN; toolbarRects.Add(new ToolBarItem { Idx = idx++, Title = "Path", Bounds = new Rectangle(x, y, TOOLBAR_WIDTHR - 2 * TOOLBAR_MARGIN, TOOLBAR_BUTTON_HEIGHT) }); } public void SetPreviewStartPoint(Point? point) { previewStartPoint = point; this.Invalidate(); } public void UpdatePreviewLine(Point currentPosition) { currentMousePosition = currentPosition; this.Invalidate(); } private Point SnapToPoint(Point point) { // RFID 포인트와 근접한지 확인 foreach (var rfid in rfidPoints) { if (GetDistance(point, rfid.Location) <= SNAP_DISTANCE) { return rfid.Location; } } return point; } // 화면 좌표를 실제 맵 좌표로 변환 public Point ScreenToMap(Point screenPoint) { int adjustedX = screenPoint.X; return new Point( (int)((adjustedX - offset.X) / zoom), (int)((screenPoint.Y - offset.Y) / zoom) ); } public RFIDPoint FindRFIDPoint(uint rfidValue) { if (rfidPoints == null || rfidPoints.Any() == false) return null; return rfidPoints.FirstOrDefault(r => r.RFIDValue == rfidValue); } public bool SetCurrentPosition(uint rfidValue) { var rfidPoint = FindRFIDPoint(rfidValue); if (rfidPoint != null) { agv.CurrentPosition = rfidPoint.Location; agv.CurrentRFID = rfidValue; // 목적지가 설정되어 있고 경로가 있는 경우 검증 if (agv.TargetPosition != Point.Empty && agv.CurrentPath.Count > 0) { // 현재 위치가 경로에 있는지 확인 bool isOnPath = agv.CurrentPath.Contains(agv.CurrentPosition); if (!isOnPath) { // 경로를 벗어났으므로 새로운 경로 계산 var pathResult = CalculatePath(agv.CurrentRFID, agv.TargetRFID); if (pathResult.Success) { SetCurrentPath(pathResult.Path); } } // AGV의 방향 결정 if (agv.CurrentPath.Count > 0) { // 다음 목적지 찾기 var nextPoint = agv.CurrentPath[0]; // 회전 가능 여부를 고려하여 방향 결정 agv.TargetDirection = DetermineDirection(agv.CurrentPosition, nextPoint, agv.TargetPosition); } } // 목적지 RFID에 도착했고, 해당 RFID에 고정방향이 있으면 TargetDirection을 강제 설정 if (agv.TargetRFID == rfidValue) { var destRFID = FindRFIDPoint(rfidValue); if (destRFID != null && destRFID.FixedDirection.HasValue) { agv.TargetDirection = destRFID.FixedDirection.Value; } } this.Invalidate(); return true; } return false; } public bool SetTargetPosition(uint rfidValue) { var rfidPoint = FindRFIDPoint(rfidValue); if (rfidPoint != null) { agv.TargetPosition = rfidPoint.Location; agv.TargetRFID = rfidValue; this.Invalidate(); return true; } return false; } protected override void OnMouseWheel(MouseEventArgs e) { base.OnMouseWheel(e); if (e.Delta > 0) { zoom *= 1.1f; } else { zoom /= 1.1f; } zoom = Math.Max(0.1f, Math.Min(10.0f, zoom)); this.Invalidate(); } protected override void OnMouseDown(MouseEventArgs e) { base.OnMouseDown(e); lastMousePosition = e.Location; var mapPoint = ScreenToMap(e.Location); if (e.Button == MouseButtons.Middle) { isDragging = true; this.Cursor = Cursors.SizeAll; } else if (e.Button == MouseButtons.Left && !isAddingText && !isDrawingCustomLine && !isDrawingRFIDLine) { isDragging = true; // 텍스트 선택 및 드래그 시작 foreach (var text in mapTexts) { var textSize = CreateGraphics().MeasureString(text.Text, text.Font); var rect = new RectangleF( text.Location.X - SELECTION_DISTANCE, text.Location.Y - SELECTION_DISTANCE, textSize.Width + SELECTION_DISTANCE * 2, textSize.Height + SELECTION_DISTANCE * 2 ); if (rect.Contains(mapPoint)) { selectedText = text; isDraggingText = true; // 드래그 시작점과 텍스트 위치의 차이를 저장 dragOffset = new Point( text.Location.X - mapPoint.X, text.Location.Y - mapPoint.Y ); return; } } // 커스텀 라인 선택 및 드래그 포인트 설정 foreach (var line in customLines) { float startDistance = GetDistance(mapPoint, line.StartPoint); float endDistance = GetDistance(mapPoint, line.EndPoint); if (startDistance < SELECTION_DISTANCE) { selectedLine = line; draggingPoint = line.StartPoint; isDraggingPoint = true; return; } else if (endDistance < SELECTION_DISTANCE) { selectedLine = line; draggingPoint = line.EndPoint; isDraggingPoint = true; return; } } // RFID 포인트 선택 var clickedRFID = rfidPoints.FirstOrDefault(r => GetDistance(mapPoint, r.Location) <= SNAP_DISTANCE); if (clickedRFID != null) { selectedRFID = clickedRFID; draggingPoint = clickedRFID.Location; isDraggingPoint = true; return; } } } Point? lineStartPoint = null; Point? branchPoint = null; protected override void OnMouseMove(MouseEventArgs e) { base.OnMouseMove(e); currentMousePosition = e.Location; var mapPoint = ScreenToMap(e.Location); if ((mousemode == eMouseMode.addcustomline || mousemode == eMouseMode.addrfidline) && branchPoint.HasValue) { UpdatePreviewLine(e.Location); } // 툴바 버튼 호버 상태 업데이트 var oldHovering = toolbarRects.OrderBy(t => t.Idx).Select(t => t.isHovering).ToArray(); //toolbar check toolbarRects.ForEach(t => t.isHovering = t.Bounds.Contains(e.Location)); //hovering check if (toolbarRects.Where(t => t.isHovering).Any()) { this.Cursor = Cursors.Hand; } else if (isDeletingRFIDLine) { this.Cursor = GetScissorsCursor(); } else { this.Cursor = Cursors.Default; } //hovering display update if (toolbarRects.Where(t => t.Dirty).Any()) { this.Invalidate(); } if (isDragging) { if (e.Button == MouseButtons.Middle) { offset = new PointF( offset.X + (e.Location.X - lastMousePosition.X), offset.Y + (e.Location.Y - lastMousePosition.Y) ); } else if (isDraggingText && selectedText != null) { // 텍스트 이동 - 드래그 오프셋 적용 selectedText.Location = new Point( mapPoint.X + dragOffset.X, mapPoint.Y + dragOffset.Y ); } else if (isDraggingPoint && draggingPoint.HasValue) { // 포인트 이동 var delta = new Point( mapPoint.X - ScreenToMap(lastMousePosition).X, mapPoint.Y - ScreenToMap(lastMousePosition).Y ); if (selectedLine != null) { // 커스텀 라인 포인트 이동 if (draggingPoint.Value == selectedLine.StartPoint) { selectedLine.StartPoint = new Point( selectedLine.StartPoint.X + delta.X, selectedLine.StartPoint.Y + delta.Y ); } else if (draggingPoint.Value == selectedLine.EndPoint) { selectedLine.EndPoint = new Point( selectedLine.EndPoint.X + delta.X, selectedLine.EndPoint.Y + delta.Y ); } } else if (selectedRFID != null) // RFID 포인트 이동 { // RFID 포인트 위치 업데이트 selectedRFID.Location = new Point( selectedRFID.Location.X + delta.X, selectedRFID.Location.Y + delta.Y ); // 연결된 RFID 라인 업데이트 foreach (var line in rfidLines) { if (line.StartPoint == draggingPoint.Value) { line.StartPoint = selectedRFID.Location; } if (line.EndPoint == draggingPoint.Value) { line.EndPoint = selectedRFID.Location; } } } } lastMousePosition = e.Location; this.Invalidate(); } // 미리보기 라인 업데이트를 위한 마우스 위치 저장 if (isDrawingRFIDLine || isDrawingCustomLine || previewStartPoint.HasValue) { currentMousePosition = mapPoint; } this.Invalidate(); } private List CalculatePath(Point start, Point end) { var openList = new List { start }; var closedList = new List(); var cameFrom = new Dictionary(); var gScore = new Dictionary { { start, 0 } }; var fScore = new Dictionary { { start, Heuristic(start, end) } }; while (openList.Count > 0) { var current = openList.OrderBy(p => fScore.ContainsKey(p) ? fScore[p] : float.MaxValue).First(); if (current == end) { return ReconstructPath(cameFrom, current); } openList.Remove(current); closedList.Add(current); foreach (var neighbor in GetNeighbors(current)) { if (closedList.Contains(neighbor)) continue; float tentativeGScore = gScore[current] + Distance(current, neighbor); if (!openList.Contains(neighbor)) openList.Add(neighbor); else if (tentativeGScore >= gScore[neighbor]) continue; cameFrom[neighbor] = current; gScore[neighbor] = tentativeGScore; fScore[neighbor] = gScore[neighbor] + Heuristic(neighbor, end); } } return null; } private List ReconstructPath(Dictionary cameFrom, Point current) { var path = new List { current }; while (cameFrom.ContainsKey(current)) { current = cameFrom[current]; path.Insert(0, current); } return path; } private float Heuristic(Point a, Point b) { return (float)Math.Sqrt(Math.Pow(a.X - b.X, 2) + Math.Pow(a.Y - b.Y, 2)); } protected override void OnMouseUp(MouseEventArgs e) { base.OnMouseUp(e); if (e.Button == MouseButtons.Middle) { isDragging = false; this.Cursor = Cursors.Default; } else if (e.Button == MouseButtons.Left) { isDragging = false; isDraggingPoint = false; isDraggingText = false; } } private float Distance(Point a, Point b) { // RFID 라인을 통한 연결 확인 var rfidLines = GetRFIDLines(); var directConnection = rfidLines.FirstOrDefault(line => (line.StartPoint == a && line.EndPoint == b) || (line.StartPoint == b && line.EndPoint == a)); if (directConnection != null) { return directConnection.Distance; } // 직접 연결되지 않은 경우 매우 큰 값 반환 return float.MaxValue; } private List GetNeighbors(Point point) { var neighbors = new List(); var rfidLines = GetRFIDLines(); // RFID 라인에서 이웃 노드 찾기 foreach (var line in rfidLines) { if (line.StartPoint == point) { neighbors.Add(line.EndPoint); } else if (line.EndPoint == point) { // 양방향 연결인 경우에만 시작점을 이웃으로 추가 neighbors.Add(line.StartPoint); } } return neighbors.Distinct().ToList(); } public void SetPath(List rfids) { if (rfids == null || rfids.Count == 0) return; var path = new List(); foreach (var rfid in rfids) { var point = GetRFIDPoints().FirstOrDefault(p => p.RFIDValue == rfid); if (point != null) { path.Add(point.Location); } } if (path.Count > 0) { SetCurrentPath(path); } } public string RFIDStartNo { get; set; } = string.Empty; public int RFIDLastNumber = 0; string filename = string.Empty; public struct PathResult { public bool Success { get; set; } public string Message { get; set; } public List Path { get; set; } } public PathResult CalculatePath(uint tagStrt, uint tagEnd) { var retval = new PathResult { Message = string.Empty, Success = false, Path = new List(), }; var sp = tagStrt; //만약시작위치가 없다면 항상 충전기 기준으로 한다 var ep = tagEnd; var startPoint = FindRFIDPoint(sp); var endPoint = FindRFIDPoint(ep); if (startPoint == null || endPoint == null) { retval.Message = "유효한 RFID 값을 입력해주세요."; return retval; } retval.Path = CalculatePath(startPoint.Location, endPoint.Location); if (retval.Path != null && retval.Path.Any()) { //SetCurrentPath(retval.Path); //// 경로 상의 모든 RFID 값을 가져옴 //var rfidPath = new List(); //foreach (var point in path) //{ // var rfid = GetRFIDPoints() // .FirstOrDefault(r => r.Location == point); // if (rfid != null) // { // rfidPath.Add(rfid.RFIDValue); // } //} retval.Success = true; } else { retval.Message = "경로를 찾을 수 없습니다"; } return retval; } protected override void OnMouseClick(MouseEventArgs e) { base.OnMouseClick(e); var mapPoint = ScreenToMap(e.Location); if (e.Button == MouseButtons.Right) { OnRightClick?.Invoke(this, e); this.MouseMode = eMouseMode.Default; } else if (e.Button == MouseButtons.Left) { // 툴바 버튼 클릭 처리 var toolbar = toolbarRects.FirstOrDefault(t => t.Bounds.Contains(e.Location)); if (toolbar != null) { switch (toolbar.Title.ToLower()) { case "+": ZoomIn(); break; case "-": ZoomOut(); break; case "1:1": ResetZoom(); break; case "cut": MouseMode = (eMouseMode.rfidcut); break; case "text": MouseMode = (eMouseMode.addtext); break; case "line": MouseMode = (eMouseMode.addrfidline); break; case "cline": MouseMode = (eMouseMode.addcustomline); break; case "point": MouseMode = (eMouseMode.addrfidpoint); break; case "path": var input1 = AR.UTIL.InputBox("input start"); if (input1.Item1 == false) return; var input2 = AR.UTIL.InputBox("input end"); if (input2.Item1 == false) return; var startRFID = input1.Item2; var endRFID = input2.Item2; var valid1 = uint.TryParse(input1.Item2, out uint vstart); var valid2 = uint.TryParse(input2.Item2, out uint vend); if (valid1 == false || valid2 == false) { MessageBox.Show("RFID값은 정수로 입력하세요", "경로 계산", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } var rlt = CalculatePath(vstart, vend); if (rlt.Success == false) { MessageBox.Show(rlt.Message, "경로 계산", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } else { SetCurrentPath(rlt.Path); //현재 경로로 설정함 MessageBox.Show($"경로가 계산되었습니다.\nRFID 순서: {string.Join(" -> ", rlt.Path)}", "경로 계산", MessageBoxButtons.OK, MessageBoxIcon.Information); if (SetTargetPosition(vend) == false) { MessageBox.Show("목적지 설정 실패"); } } break; case "pos": var tag = AR.UTIL.InputBox("input rfid tag value"); if (tag.Item1 && tag.Item2 != "" && uint.TryParse(tag.Item2, out uint val) == true) { var targetRFID = SetCurrentPosition(val); } break; case "save": using (var od = new SaveFileDialog()) { od.Filter = "path data|*.route"; od.FilterIndex = 0; od.RestoreDirectory = true; if (filename.isEmpty() == false) { od.FileName = System.IO.Path.GetFileName(filename); od.InitialDirectory = System.IO.Path.GetDirectoryName(filename); } if (od.ShowDialog() == DialogResult.OK) { filename = od.FileName; this.SaveToFile(filename); this.Invalidate(); } } break; case "load": using (var od = new OpenFileDialog()) { od.Filter = "path data|*.route"; od.FilterIndex = 0; if (string.IsNullOrEmpty(this.filename) == false) { od.FileName = System.IO.Path.GetFileName(this.filename); od.InitialDirectory = System.IO.Path.GetDirectoryName(this.filename); } else { od.RestoreDirectory = true; } if (od.ShowDialog() == DialogResult.OK) { this.LoadFromFile(filename, out string errmsg); if (errmsg.isEmpty() == false) UTIL.MsgE(errmsg); this.Invalidate(); } } break; } return; } // RFID 포인트 선택 var clickedRFID = rfidPoints.FirstOrDefault(r => GetDistance(mapPoint, r.Location) <= SNAP_DISTANCE); switch (mousemode) { case eMouseMode.rfidcut: DeleteNearbyRFIDLine(mapPoint); break; case eMouseMode.addrfidpoint: if (string.IsNullOrEmpty(this.RFIDStartNo) == false) { if (uint.TryParse(this.RFIDStartNo, out uint rfidvalue)) { AddRFIDPoint(mapPoint, rfidvalue); // 숫자로 끝나는 RFID 값인 경우 자동 증가 if (Regex.IsMatch(RFIDStartNo, @"^[A-Za-z]+\d+$")) { // 마지막 숫자 부분 추출 Match match = Regex.Match(RFIDStartNo, @"\d+$"); if (match.Success) { int currentNumber = int.Parse(match.Value); if (currentNumber > this.RFIDLastNumber) { RFIDLastNumber = currentNumber; } RFIDLastNumber++; // 숫자 부분을 새로운 번호로 교체 RFIDStartNo = RFIDStartNo.Substring(0, match.Index) + RFIDLastNumber.ToString("D4"); } } } } break; case eMouseMode.addtext: var text = new MapText { Location = mapPoint, Text = "새 텍스트", TextColor = Color.Black, BackgroundColor = Color.Transparent, Font = new Font("Arial", 12) }; mapTexts.Add(text); selectedText = text; this.Invalidate(); break; case eMouseMode.addcustomline: if (previewStartPoint == null) { previewStartPoint = mapPoint; } else { var line = new CustomLine { StartPoint = previewStartPoint.Value, EndPoint = mapPoint, LineColor = Color.Red, LineWidth = 2 }; customLines.Add(line); selectedLine = line; previewStartPoint = null; this.Invalidate(); } break; case eMouseMode.addrfidline: if (clickedRFID != null) { if (previewStartPoint == null) { previewStartPoint = clickedRFID.Location; } else { var startRFID = rfidPoints.FirstOrDefault(r => r.Location == previewStartPoint); if (startRFID != null) { var line = new RFIDLine { StartPoint = previewStartPoint.Value, EndPoint = clickedRFID.Location, }; rfidLines.Add(line); selectedRFIDLine = line; } // 다음 라인을 위해 현재 클릭한 RFID를 시작점으로 설정 previewStartPoint = clickedRFID.Location; } this.Invalidate(); } break; } } } private void AddNearbyRFIDPoints(RFIDLine line) { const float NEARBY_DISTANCE = 20.0f; // 근처로 간주하는 거리를 50에서 20으로 줄임 // 선 근처의 RFID 포인트들을 찾아서 거리에 따라 정렬 var nearbyPoints = new List<(RFIDPoint Point, float Distance, float ProjectionRatio)>(); foreach (var rfid in rfidPoints) { if (rfid.Location == line.StartPoint || rfid.Location == line.EndPoint) continue; // 선분과 RFID 포인트 사이의 최단 거리 계산 float distance = GetDistanceToLine(rfid.Location, line.StartPoint, line.EndPoint); if (distance <= NEARBY_DISTANCE) { // 시작점으로부터의 투영 비율 계산 (0~1 사이 값) float projectionRatio = GetProjectionRatio(rfid.Location, line.StartPoint, line.EndPoint); if (projectionRatio >= 0 && projectionRatio <= 1) // 선분 위에 있는 점만 포함 { nearbyPoints.Add((rfid, distance, projectionRatio)); } } } // 시작점에서 끝점 방향으로 정렬 nearbyPoints.Sort((a, b) => a.ProjectionRatio.CompareTo(b.ProjectionRatio)); // 이전 RFID 값과 위치를 저장 //uint prevRFID = line.StartRFID; Point prevPoint = line.StartPoint; // 정렬된 포인트들을 순차적으로 연결 foreach (var item in nearbyPoints) { var rfidLine = new RFIDLine { StartPoint = prevPoint, EndPoint = item.Point.Location, }; rfidLines.Add(rfidLine); // 현재 포인트를 다음 선분의 시작점으로 설정 prevPoint = item.Point.Location; } // 마지막 포인트와 원래 선의 끝점을 연결 var finalLine = new RFIDLine { StartPoint = prevPoint, EndPoint = line.EndPoint, }; rfidLines.Add(finalLine); // 원래 선은 제거 rfidLines.Remove(line); } private float GetProjectionRatio(Point point, Point lineStart, Point lineEnd) { float lineLength = GetDistance(lineStart, lineEnd); if (lineLength == 0) return 0; return ((point.X - lineStart.X) * (lineEnd.X - lineStart.X) + (point.Y - lineStart.Y) * (lineEnd.Y - lineStart.Y)) / (lineLength * lineLength); } private float GetDistanceToLine(Point point, Point lineStart, Point lineEnd) { float lineLength = GetDistance(lineStart, lineEnd); if (lineLength == 0) return GetDistance(point, lineStart); float t = ((point.X - lineStart.X) * (lineEnd.X - lineStart.X) + (point.Y - lineStart.Y) * (lineEnd.Y - lineStart.Y)) / (lineLength * lineLength); t = Math.Max(0, Math.Min(1, t)); float projectionX = lineStart.X + t * (lineEnd.X - lineStart.X); float projectionY = lineStart.Y + t * (lineEnd.Y - lineStart.Y); return GetDistance(point, new Point((int)projectionX, (int)projectionY)); } protected override void OnMouseDoubleClick(MouseEventArgs e) { base.OnMouseDoubleClick(e); if (e.Button == MouseButtons.Left) { var mapPoint = ScreenToMap(e.Location); //RFID 포인트 찾기 var selected_rfid = rfidPoints.Where(t => t.Bounds.Expand(SELECTION_DISTANCE, SELECTION_DISTANCE).Contains(mapPoint)).FirstOrDefault(); if (selected_rfid != null) { // 고정방향 순환 변경: 없음→전진→후진→없음 if (!selected_rfid.FixedDirection.HasValue) selected_rfid.FixedDirection = Direction.Forward; else if (selected_rfid.FixedDirection == Direction.Forward) selected_rfid.FixedDirection = Direction.Backward; else selected_rfid.FixedDirection = null; this.Invalidate(); return; } // 텍스트 객체 찾기 var selected_txt = mapTexts.Where(t => t.Bounds.Expand(SELECTION_DISTANCE, SELECTION_DISTANCE).Contains(mapPoint)).FirstOrDefault(); if (selected_txt != null) { var rlt = AR.UTIL.InputBox("input", selected_txt.Text); if (rlt.Item1 == true) { selected_txt.Text = rlt.Item2; this.Invalidate(); } return; } } } public event EventHandler OnRightClick; public void SetRFIDPoints(List points) { rfidPoints = points; this.Invalidate(); } public void SetAGV(AGV vehicle) { agv = vehicle; this.Invalidate(); } public void SetMapTexts(List texts) { mapTexts = texts; if (mapTexts == null) mapTexts = new List(); this.Invalidate(); } public void SetCustomLines(List lines) { customLines = lines; if (customLines == null) customLines = new List(); this.Invalidate(); } public void SetIsAddingText(bool value) { isAddingText = value; isDrawingCustomLine = false; this.Cursor = value ? Cursors.IBeam : Cursors.Default; } public void SetIsDrawingCustomLine(bool value) { isDrawingCustomLine = value; isAddingText = false; this.Cursor = value ? Cursors.Cross : Cursors.Default; } public void SetIsDrawingRFIDLine(bool value) { isDrawingRFIDLine = value; isDrawingCustomLine = false; isAddingText = false; this.Cursor = value ? Cursors.Cross : Cursors.Default; } public void SetIsDeletingRFIDLine(bool value) { isDeletingRFIDLine = value; isDrawingCustomLine = false; isAddingText = false; isDrawingRFIDLine = false; this.Cursor = value ? GetScissorsCursor() : Cursors.Default; } public void SetIsDrawingLine(bool value) { isDrawingLine = value; isDrawingCustomLine = false; isAddingText = false; isDrawingRFIDLine = false; this.Cursor = value ? Cursors.Cross : Cursors.Default; } public enum eMouseMode : byte { Default = 0, pan, rfidcut, addtext, addcustomline, addrfidpoint, addrfidline, } private eMouseMode mousemode = eMouseMode.Default; public eMouseMode MouseMode { get { return mousemode; } set { if (this.mousemode == value) mousemode = eMouseMode.Default; else this.mousemode = value; switch (this.mousemode) { case eMouseMode.pan: this.Cursor = Cursors.Hand; break; case eMouseMode.addrfidline: this.Cursor = Cursors.Default; break; case eMouseMode.addrfidpoint: this.Cursor = Cursors.Default; break; case eMouseMode.addtext: this.Cursor = Cursors.Default; break; case eMouseMode.addcustomline: this.Cursor = Cursors.Default; break; default: this.Cursor = Cursors.Default; break; } previewStartPoint = null; Invalidate(); } } public void SetIsAddingMagnet(bool value) { } public void SetIsAddingPoint(bool value) { isDrawingCustomLine = false; isDrawingLine = false; isDrawingRFIDLine = false; isAddingPoint = value; this.Cursor = value ? Cursors.Cross : Cursors.Default; } private Cursor GetScissorsCursor() { // 가위 커서 아이콘 생성 using (var bitmap = new Bitmap(32, 32)) using (var g = Graphics.FromImage(bitmap)) { g.Clear(Color.Transparent); // 가위 모양 그리기 using (var pen = new Pen(Color.Black, 2)) { // 가위 손잡이 g.DrawEllipse(pen, 12, 20, 8, 8); g.DrawLine(pen, 16, 20, 16, 16); // 가위 날 g.DrawLine(pen, 16, 16, 8, 8); g.DrawLine(pen, 16, 16, 24, 8); } return new Cursor(bitmap.GetHicon()); } } public void SetCurrentPath(List path) { agv.CurrentPath = path; this.Invalidate(); } protected override void OnPaint(PaintEventArgs e) { //base.OnPaint(e); e.Graphics.TranslateTransform(offset.X, offset.Y); e.Graphics.ScaleTransform(zoom, zoom); DrawRFIDLines(e.Graphics); DrawRFIDPoints(e.Graphics); //DrawCustomLines(e.Graphics); DrawMapTexts(e.Graphics); DrawPath(e.Graphics); DrawAGV(e.Graphics); DrawTargetFlag(e.Graphics); // 목적지 깃발 그리기 추가 // 선택된 개체 강조 표시 if (selectedRFID != null) { using (Pen pen = new Pen(Color.Magenta, 2)) { pen.DashStyle = System.Drawing.Drawing2D.DashStyle.Dash; e.Graphics.DrawEllipse(pen, selectedRFID.Location.X - 10, selectedRFID.Location.Y - 10, 20, 20); } } if (selectedRFIDLine != null) { using (Pen pen = new Pen(Color.Magenta, 2)) { pen.DashStyle = System.Drawing.Drawing2D.DashStyle.Dash; e.Graphics.DrawLine(pen, selectedRFIDLine.StartPoint, selectedRFIDLine.EndPoint); } } // 미리보기 라인 그리기 if (previewStartPoint.HasValue) { using (Pen previewPen = new Pen(Color.FromArgb(180, Color.Yellow), LINE_WIDTH)) { previewPen.DashStyle = System.Drawing.Drawing2D.DashStyle.Dash; e.Graphics.DrawLine(previewPen, previewStartPoint.Value, currentMousePosition); } } // 그래픽스 변환 초기화 e.Graphics.ResetTransform(); // 툴바 버튼 그리기 foreach (var item in this.toolbarRects) DrawToolbarButton(e.Graphics, item.Bounds, item.Title, item.isHovering); } private void DrawRFIDPoints(Graphics g) { // RFID 포인트 그리기 foreach (var rfid in rfidPoints) { var MarkerSize = 5; var half = MarkerSize / 2f; rfid.Bounds = new RectangleF(rfid.Location.X - half, rfid.Location.Y - half, MarkerSize, MarkerSize); // 회전 가능 여부에 따라 다른 색상 사용 using (var brush = new SolidBrush(rfid.IsRotatable ? Color.Yellow : Color.Green)) { g.FillEllipse(brush, rfid.Bounds); } // 고정방향이 있으면 테두리 색상 표시 if (rfid.FixedDirection.HasValue) { Color borderColor = rfid.FixedDirection.Value == Direction.Forward ? Color.DeepSkyBlue : Color.Gold; using (var pen = new Pen(borderColor, 2)) { g.DrawEllipse(pen, rfid.Bounds.Expand(5,5)); } } } // RFID 값 표시 foreach (var rfid in rfidPoints) { var tagstr = $"{rfid.RFIDValue}"; var fsize = g.MeasureString(tagstr, Font); var rect = new RectangleF(rfid.Bounds.X - (fsize.Width / 2f) + 3, rfid.Bounds.Y + 6, fsize.Width, fsize.Height); g.DrawString(tagstr, Font, Brushes.WhiteSmoke, rect, new StringFormat { Alignment = StringAlignment.Center, LineAlignment = StringAlignment.Center, }); } } private void DrawAGV(Graphics g) { var agvsize = 30; var halfsize = (int)(agvsize / 2); // AGV의 현재 위치를 중심으로 하는 원 var circleRect = new Rectangle( agv.CurrentPosition.X - halfsize, agv.CurrentPosition.Y - halfsize, agvsize, agvsize); // 삼각형 화살표를 위한 포인트 배열 Point[] trianglePoints = new Point[3]; var arrowSize = halfsize - 5; // 삼각형 크기 // AGV의 방향에 따라 삼각형 포인트 계산 switch (agv.CurrentDirection) { case Direction.Forward: trianglePoints[0] = new Point(agv.CurrentPosition.X, agv.CurrentPosition.Y - arrowSize); trianglePoints[1] = new Point(agv.CurrentPosition.X - arrowSize, agv.CurrentPosition.Y + arrowSize); trianglePoints[2] = new Point(agv.CurrentPosition.X + arrowSize, agv.CurrentPosition.Y + arrowSize); break; case Direction.Backward: trianglePoints[0] = new Point(agv.CurrentPosition.X, agv.CurrentPosition.Y + arrowSize); trianglePoints[1] = new Point(agv.CurrentPosition.X - arrowSize, agv.CurrentPosition.Y - arrowSize); trianglePoints[2] = new Point(agv.CurrentPosition.X + arrowSize, agv.CurrentPosition.Y - arrowSize); break; } // 원 그리기 Color bgcolor = agv.BatteryLevel > 80 ? Color.Lime : ( agv.BatteryLevel > 60 ? Color.Gold : Color.Tomato ); using (var circleBrush = new SolidBrush(Color.FromArgb(150, bgcolor))) g.FillEllipse(circleBrush, circleRect); using (var circlePen = new Pen(Color.Black, 2)) g.DrawEllipse(circlePen, circleRect); // 삼각형 화살표 그리기 using (var arrowBrush = new SolidBrush(Color.FromArgb(200, Color.White))) g.FillPolygon(arrowBrush, trianglePoints); using (var arrowPen = new Pen(Color.Black, 2)) g.DrawPolygon(arrowPen, trianglePoints); //g.DrawImage(Properties.Resources.ico_navi_40, circleRect); } private void DrawCustomLines(Graphics g) { if (customLines == null) return; foreach (var line in customLines) { using (Pen linePen = new Pen(line.LineColor, line.LineWidth)) { g.DrawLine(linePen, line.StartPoint, line.EndPoint); } } } private void DrawMapTexts(Graphics g) { if (mapTexts == null) return; foreach (var text in mapTexts) { var textSize = g.MeasureString(text.Text, text.Font); if (text.Dirty) { text.Bounds = new RectangleF( text.Location.X, text.Location.Y, textSize.Width, textSize.Height ); text.Dirty = false; } if (text.BackgroundColor != Color.Transparent) { using (var brush = new SolidBrush(text.BackgroundColor)) { g.FillRectangle(brush, text.Bounds); } } using (var brush = new SolidBrush(text.TextColor)) { g.DrawString(text.Text, text.Font, brush, text.Location); } if (text == selectedText) { using (Pen pen = new Pen(Color.Blue, 1)) { pen.DashStyle = System.Drawing.Drawing2D.DashStyle.Dash; g.DrawRectangle(pen, text.Bounds.X, text.Bounds.Y, text.Bounds.Width, text.Bounds.Height); } } } } private void DrawRFIDLines(Graphics g) { var idx = 0; using (Font f = new Font("arial", 4)) { foreach (var item in rfidLines) { var sp = item.StartPoint; var ep = item.EndPoint; using (var p = new Pen(Color.FromArgb(50, Color.White), 10)) { g.DrawLine(p, sp, ep); var x = sp.X; var y = sp.Y; g.DrawString($"{idx}", f, Brushes.Gold, x, y); x = ep.X; y = ep.Y; g.DrawString($"{idx}", f, Brushes.Pink, x, y); idx++; } } } foreach (var connection in rfidConnections) { var startPoint = rfidPoints.First(p => p.RFIDValue == connection.StartRFID).Location; var endPoint = rfidPoints.First(p => p.RFIDValue == connection.EndRFID).Location; using (Pen linePen = new Pen(Color.FromArgb(50, Color.Wheat), 2)) { if (connection.IsBidirectional) { // 단방향 화살표 그리기 var arrowSize = 10; var angle = Math.Atan2(endPoint.Y - startPoint.Y, endPoint.X - startPoint.X); var arrowPoint = new PointF( endPoint.X - (float)(arrowSize * Math.Cos(angle)), endPoint.Y - (float)(arrowSize * Math.Sin(angle)) ); g.DrawLine(linePen, startPoint, arrowPoint); // 화살표 머리 그리기 var arrowAngle = Math.PI / 6; var arrowLength = 15; var arrow1 = new PointF( arrowPoint.X - (float)(arrowLength * Math.Cos(angle - arrowAngle)), arrowPoint.Y - (float)(arrowLength * Math.Sin(angle - arrowAngle)) ); var arrow2 = new PointF( arrowPoint.X - (float)(arrowLength * Math.Cos(angle + arrowAngle)), arrowPoint.Y - (float)(arrowLength * Math.Sin(angle + arrowAngle)) ); g.DrawLine(linePen, arrowPoint, arrow1); g.DrawLine(linePen, arrowPoint, arrow2); } else { g.DrawLine(linePen, startPoint, endPoint); } } } // 미리보기 라인 그리기 if (previewStartPoint.HasValue && isDrawingRFIDLine) { using (Pen previewPen = new Pen(Color.FromArgb(180, Color.Green), 2)) { previewPen.DashStyle = System.Drawing.Drawing2D.DashStyle.Dash; var currentMapPosition = ScreenToMap(currentMousePosition); g.DrawLine(previewPen, previewStartPoint.Value, currentMapPosition); } } } private void DrawPath(Graphics g) { if (agv.CurrentPath == null || agv.CurrentPath.Count < 2) return; Color pathColor = Color.FromArgb(100, Color.Lime); int pathWidth = 10; using (Pen pathPen = new Pen(pathColor, pathWidth)) { pathPen.DashStyle = System.Drawing.Drawing2D.DashStyle.Dash; for (int i = 0; i < agv.CurrentPath.Count - 1; i++) { g.DrawLine(pathPen, agv.CurrentPath[i], agv.CurrentPath[i + 1]); } } } public List GetRFIDPoints() { return rfidPoints; } public List GetRFIDLines() { return rfidLines; } public void SetRFIDLines(List lines) { rfidLines = lines; this.Invalidate(); } public void AddRFIDLine(Point startPoint, Point endPoint) { // 시작점과 끝점 사이의 모든 RFID 포인트 찾기 var allPoints = new List<(RFIDPoint Point, float Distance)>(); var lineVector = new Point(endPoint.X - startPoint.X, endPoint.Y - startPoint.Y); var lineLength = (float)Math.Sqrt(lineVector.X * lineVector.X + lineVector.Y * lineVector.Y); foreach (var rfid in rfidPoints) { if (rfid.Location == startPoint || rfid.Location == endPoint) continue; // RFID 포인트가 선 위에 있는지 확인 var pointVector = new Point(rfid.Location.X - startPoint.X, rfid.Location.Y - startPoint.Y); var dotProduct = pointVector.X * lineVector.X + pointVector.Y * lineVector.Y; var projectionLength = dotProduct / (lineLength * lineLength); if (projectionLength >= 0 && projectionLength <= 1) { // 선과 RFID 포인트 사이의 거리 계산 var distance = GetDistanceToLine(rfid.Location, startPoint, endPoint); if (distance <= SNAP_DISTANCE) { allPoints.Add((rfid, projectionLength)); } } } // 시작점에서 끝점 방향으로 정렬 allPoints.Sort((a, b) => a.Distance.CompareTo(b.Distance)); // 모든 연결 정보를 포함하는 RFID 라인 생성 var line = new RFIDLine { StartPoint = startPoint, EndPoint = endPoint, }; rfidLines.Add(line); this.Invalidate(); } public void ClearMap() { rfidPoints.Clear(); mapTexts.Clear(); customLines.Clear(); rfidLines.Clear(); // 선택 상태도 초기화 selectedText = null; selectedLine = null; selectedRFID = null; selectedRFIDLine = null; draggingPoint = null; // 미리보기 상태도 초기화 previewStartPoint = null; // 화면 갱신 this.Invalidate(); } public void AddRFIDPoint(Point mapLocation, uint rfidValue) { // 이미 맵 좌표로 변환된 위치를 사용 var rfidPoint = new RFIDPoint { Location = mapLocation, RFIDValue = rfidValue }; rfidPoints.Add(rfidPoint); this.Invalidate(); } public void SaveToFile(string filename) { var lines = new List(); // RFID 포인트 저장 lines.Add("[RFID_POINTS]"); foreach (var point in rfidPoints) { lines.Add($"{point.Location.X},{point.Location.Y},{point.RFIDValue}"); } // RFID 라인 저장 lines.Add("[RFID_LINES]"); foreach (var connection in rfidConnections) { var startPoint = rfidPoints.First(p => p.RFIDValue == connection.StartRFID).Location; var endPoint = rfidPoints.First(p => p.RFIDValue == connection.EndRFID).Location; lines.Add($"{startPoint.X},{startPoint.Y},{endPoint.X},{endPoint.Y}," + $"{connection.StartRFID},{connection.EndRFID},{connection.IsBidirectional},{connection.Distance}"); } // 텍스트 저장 lines.Add("[MAP_TEXTS]"); foreach (var text in mapTexts) { lines.Add($"{text.Location.X},{text.Location.Y},{text.TextColor.ToArgb()},{text.BackgroundColor.ToArgb()},{text.Font.Name},{text.Font.Size},{text.Text}"); } // 커스텀 라인 저장 lines.Add("[CUSTOM_LINES]"); foreach (var line in customLines) { lines.Add($"{line.StartPoint.X},{line.StartPoint.Y},{line.EndPoint.X},{line.EndPoint.Y},{line.LineColor.ToArgb()},{line.LineWidth}"); } File.WriteAllLines(filename, lines); this.filename = filename; } public bool LoadFromFile(string filename, out string message) { this.filename = filename; message = string.Empty; ClearMap(); var lines = File.ReadAllLines(filename); var section = ""; var sb = new System.Text.StringBuilder(); foreach (var line in lines) { if (line.StartsWith("[") && line.EndsWith("]")) { section = line; continue; } switch (section) { case "[RFID_POINTS]": var rfidParts = line.Split(','); if (rfidParts.Length >= 3) { var validX = int.TryParse(rfidParts[0], out int valX); var validY = int.TryParse(rfidParts[1], out int valY); var validN = uint.TryParse(rfidParts[2], out uint valRfid); if (validX && validY && validN) { if (rfidPoints.Where(t => t.RFIDValue == valRfid).Any()) { //이미존재한다 var newvalue = sb.AppendLine($"rfid중복{valRfid}"); } AddRFIDPoint(new Point(valX, valY), valRfid); } else sb.AppendLine($"[{section}] {line}"); } break; case "[RFID_LINES]": var rfidLineParts = line.Split(','); if (rfidLineParts.Length >= 8) { AddRFIDLine( new Point(int.Parse(rfidLineParts[0]), int.Parse(rfidLineParts[1])), new Point(int.Parse(rfidLineParts[2]), int.Parse(rfidLineParts[3])) ); } break; case "[MAP_TEXTS]": var textParts = line.Split(','); if (textParts.Length >= 7) { var text = new MapText { Location = new Point(int.Parse(textParts[0]), int.Parse(textParts[1])), TextColor = Color.FromArgb(int.Parse(textParts[2])), BackgroundColor = Color.FromArgb(int.Parse(textParts[3])), Font = new Font(textParts[4], float.Parse(textParts[5])), Text = string.Join(",", textParts.Skip(6)) // 텍스트에 쉼표가 포함될 수 있으므로 }; mapTexts.Add(text); } break; case "[CUSTOM_LINES]": var customLineParts = line.Split(','); if (customLineParts.Length >= 6) { var customLine = new CustomLine { StartPoint = new Point(int.Parse(customLineParts[0]), int.Parse(customLineParts[1])), EndPoint = new Point(int.Parse(customLineParts[2]), int.Parse(customLineParts[3])), LineColor = Color.FromArgb(int.Parse(customLineParts[4])), LineWidth = int.Parse(customLineParts[5]) }; customLines.Add(customLine); } break; } } // RFID 연결 정보 처리 ProcessRFIDConnections(); this.Invalidate(); message = sb.ToString(); return true; } private void ProcessRFIDConnections() { rfidConnections.Clear(); var connectionSet = new HashSet(); foreach (var line in rfidLines) { var start = line.StartPoint; var end = line.EndPoint; // 1. 선 위의 모든 RFID 포인트(시작, 끝 포함)를 projectionRatio로 정렬 var pointsOnThisLine = rfidPoints .Where(p => IsPointOnLine(p.Location, start, end, 10f)) // 오차 허용치 넉넉히 .Select(p => new { RFID = p.RFIDValue, Ratio = GetProjectionRatio(p.Location, start, end) }) .ToList(); //// 2. 시작/끝 RFID가 목록에 없으면 강제로 추가 //if (!pointsOnThisLine.Any(p => p.RFID == line.StartRFID)) // pointsOnThisLine.Add(new { RFID = line.StartRFID, Ratio = 0f }); //if (!pointsOnThisLine.Any(p => p.RFID == line.EndRFID)) // pointsOnThisLine.Add(new { RFID = line.EndRFID, Ratio = 1f }); // 3. 정렬 pointsOnThisLine = pointsOnThisLine.OrderBy(p => p.Ratio).ToList(); // 4. 순서대로 1:1 연결 for (int i = 0; i < pointsOnThisLine.Count - 1; i++) { var from = pointsOnThisLine[i].RFID; var to = pointsOnThisLine[i + 1].RFID; var key = $"{Math.Min(from, to)}_{Math.Max(from, to)}"; if (connectionSet.Contains(key)) continue; var fromPt = rfidPoints.FirstOrDefault(p => p.RFIDValue == from)?.Location ?? line.StartPoint; var toPt = rfidPoints.FirstOrDefault(p => p.RFIDValue == to)?.Location ?? line.EndPoint; rfidConnections.Add(new RFIDConnection { StartRFID = from, EndRFID = to, Distance = GetDistance(fromPt, toPt) }); connectionSet.Add(key); } } } // tolerance 인자를 받는 IsPointOnLine private bool IsPointOnLine(Point point, Point lineStart, Point lineEnd, float tolerance = 10.0f) { var distance = GetDistanceToLine(point, lineStart, lineEnd); if (distance > tolerance) return false; var projectionRatio = Math.Round(GetProjectionRatio(point, lineStart, lineEnd), 2); return projectionRatio >= 0 && projectionRatio <= 1.0; } private void DeleteNearbyRFIDLine(Point clickPoint) { const float DELETE_DISTANCE = 10.0f; // 클릭 지점으로부터의 허용 거리 RFIDLine lineToDelete = null; float minDistance = float.MaxValue; foreach (var line in rfidLines) { float distance = GetDistanceToLine(clickPoint, line.StartPoint, line.EndPoint); if (distance < DELETE_DISTANCE && distance < minDistance) { minDistance = distance; lineToDelete = line; } } if (lineToDelete != null) { rfidLines.Remove(lineToDelete); this.Invalidate(); } } private void ZoomIn() { zoom *= 1.2f; zoom = Math.Min(10.0f, zoom); this.Invalidate(); } private void ZoomOut() { zoom /= 1.2f; zoom = Math.Max(0.1f, zoom); this.Invalidate(); } private void ResetZoom() { zoom = 1.0f; offset = PointF.Empty; this.Invalidate(); } private void DrawToolbarButton(Graphics g, Rectangle rect, string text, bool isHovering) { var color1 = isHovering ? Color.LightSkyBlue : Color.White; var color2 = isHovering ? Color.DeepSkyBlue : Color.WhiteSmoke; using (var brush = new LinearGradientBrush(rect, color1, color2, LinearGradientMode.Vertical)) using (var pen = new Pen(Color.Gray)) using (var font = new Font("Tahoma", 9, FontStyle.Bold)) using (var format = new StringFormat { Alignment = StringAlignment.Center, LineAlignment = StringAlignment.Center }) { g.FillRectangle(Brushes.LightGray, rect.X + 2, rect.Y + 2, rect.Width, rect.Height); g.FillRectangle(brush, rect); g.DrawRectangle(pen, rect); g.DrawString(text, font, Brushes.Black, rect, format); } } private void DrawTargetFlag(Graphics g) { if (agv.TargetPosition == Point.Empty) return; // 바닥에 흰색 원 그리기 using (var baseBrush = new SolidBrush(Color.Red)) using (var basePen = new Pen(Color.Black, 1)) { var baseSize = 8; g.FillEllipse(baseBrush, agv.TargetPosition.X - baseSize / 2, agv.TargetPosition.Y - baseSize / 2, baseSize, baseSize); g.DrawEllipse(basePen, agv.TargetPosition.X - baseSize / 2, agv.TargetPosition.Y - baseSize / 2, baseSize, baseSize); } // 깃대 그리기 (길이를 2/3로 줄임) using (var polePen = new Pen(Color.Brown, 3)) { var poleLength = 27; // 40 * 2/3 ≈ 27 g.DrawLine(polePen, agv.TargetPosition.X, agv.TargetPosition.Y, agv.TargetPosition.X, agv.TargetPosition.Y - poleLength); } // 깃발 그리기 Point[] flagPoints = new Point[3]; flagPoints[0] = new Point(agv.TargetPosition.X, agv.TargetPosition.Y - 27); // 깃대 길이에 맞춤 flagPoints[1] = new Point(agv.TargetPosition.X + 20, agv.TargetPosition.Y - 22); flagPoints[2] = new Point(agv.TargetPosition.X, agv.TargetPosition.Y - 17); using (var flagBrush = new SolidBrush(Color.Red)) using (var flagPen = new Pen(Color.DarkRed, 1)) { g.FillPolygon(flagBrush, flagPoints); g.DrawPolygon(flagPen, flagPoints); } } // 회전 가능 여부 토글 함수 public void ToggleRotatable(uint rfidValue) { var rfidPoint = FindRFIDPoint(rfidValue); if (rfidPoint != null) { rfidPoint.IsRotatable = !rfidPoint.IsRotatable; this.Invalidate(); } } // 회전 가능 여부 확인 함수 public bool IsPointRotatable(uint rfidValue) { var rfidPoint = FindRFIDPoint(rfidValue); return rfidPoint?.IsRotatable ?? false; } // 경로 계산 시 회전 가능 여부를 고려하여 방향 결정 private Direction DetermineDirection(Point current, Point next, Point target) { // 현재 위치가 회전 가능한 구간인 경우 var currentRFID = rfidPoints.FirstOrDefault(p => p.Location == current); if (currentRFID?.IsRotatable ?? false) { // 목적지 방향으로 직접 방향 결정 if (target.X > current.X) return Direction.Forward; if (target.X < current.X) return Direction.Backward; if (target.Y > current.Y) return Direction.Forward; if (target.Y < current.Y) return Direction.Backward; } // 회전 불가능한 구간인 경우 현재 진행 방향 유지 return agv.CurrentDirection; } } }