From f13a33f82e3c8a022bba5e4099d6bff6c88e4da5 Mon Sep 17 00:00:00 2001 From: backuppc Date: Thu, 30 Oct 2025 11:24:04 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20=EC=A4=8C/=ED=8C=AC=20=EC=8B=9C=20?= =?UTF-8?q?=EB=A7=88=EC=9A=B0=EC=8A=A4=20=EC=A2=8C=ED=91=9C=20=EB=B3=80?= =?UTF-8?q?=ED=99=98=20=EB=B2=84=EA=B7=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 주요 변경사항: - Graphics Transform 순서 수정 (Translate → Scale) - ScreenToWorld/WorldToScreen 공식 통일 - 마우스 휠 줌 계산 수정 - FitToNodes, PanTo 계산 수정 - GetVisibleBounds 공식 수정 - 이제 줌 상태에서 노드 선택이 정확하게 작동 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../Controls/UnifiedAGVCanvas.Events.cs | 13 ++++--- .../Controls/UnifiedAGVCanvas.Mouse.cs | 39 ++++++++++++------- .../AGVSimulator/Forms/SimulatorForm.cs | 2 - 3 files changed, 33 insertions(+), 21 deletions(-) diff --git a/Cs_HMI/AGVLogic/AGVNavigationCore/Controls/UnifiedAGVCanvas.Events.cs b/Cs_HMI/AGVLogic/AGVNavigationCore/Controls/UnifiedAGVCanvas.Events.cs index 2a29756..6b3290b 100644 --- a/Cs_HMI/AGVLogic/AGVNavigationCore/Controls/UnifiedAGVCanvas.Events.cs +++ b/Cs_HMI/AGVLogic/AGVNavigationCore/Controls/UnifiedAGVCanvas.Events.cs @@ -21,9 +21,10 @@ namespace AGVNavigationCore.Controls g.InterpolationMode = InterpolationMode.High; // 변환 행렬 설정 (줌 및 팬) + // 순서: Translate 먼저, Scale 나중 (Append로 순서 보장) var transform = new Matrix(); - transform.Scale(_zoomFactor, _zoomFactor); transform.Translate(_panOffset.X, _panOffset.Y); + transform.Scale(_zoomFactor, _zoomFactor, System.Drawing.Drawing2D.MatrixOrder.Append); g.Transform = transform; try @@ -1716,10 +1717,12 @@ namespace AGVNavigationCore.Controls private Rectangle GetVisibleBounds() { - var left = (int)(-_panOffset.X / _zoomFactor); - var top = (int)(-_panOffset.Y / _zoomFactor); - var right = (int)((Width - _panOffset.X) / _zoomFactor); - var bottom = (int)((Height - _panOffset.Y) / _zoomFactor); + // Graphics Transform: Screen = (World + Pan) * Zoom + // World = Screen / Zoom - Pan + var left = (int)(0 / _zoomFactor - _panOffset.X); + var top = (int)(0 / _zoomFactor - _panOffset.Y); + var right = (int)(Width / _zoomFactor - _panOffset.X); + var bottom = (int)(Height / _zoomFactor - _panOffset.Y); return new Rectangle(left, top, right - left, bottom - top); } diff --git a/Cs_HMI/AGVLogic/AGVNavigationCore/Controls/UnifiedAGVCanvas.Mouse.cs b/Cs_HMI/AGVLogic/AGVNavigationCore/Controls/UnifiedAGVCanvas.Mouse.cs index 1499fca..b400602 100644 --- a/Cs_HMI/AGVLogic/AGVNavigationCore/Controls/UnifiedAGVCanvas.Mouse.cs +++ b/Cs_HMI/AGVLogic/AGVNavigationCore/Controls/UnifiedAGVCanvas.Mouse.cs @@ -190,8 +190,10 @@ namespace AGVNavigationCore.Controls Point mouseScreenPos = e.Location; // 줌 전 마우스가 가리키는 월드 좌표 - float worldX_before = (mouseScreenPos.X - _panOffset.X) / _zoomFactor; - float worldY_before = (mouseScreenPos.Y - _panOffset.Y) / _zoomFactor; + // Graphics Transform: Screen = (World + Pan) * Zoom + // 역변환: World = Screen / Zoom - Pan + float worldX_before = mouseScreenPos.X / _zoomFactor - _panOffset.X; + float worldY_before = mouseScreenPos.Y / _zoomFactor - _panOffset.Y; // 이전 줌 팩터 저장 float oldZoom = _zoomFactor; @@ -203,8 +205,10 @@ namespace AGVNavigationCore.Controls _zoomFactor = Math.Max(_zoomFactor / 1.15f, 0.1f); // 축소 // 줌 후에도 마우스가 같은 월드 좌표를 가리키도록 팬 오프셋 조정 - _panOffset.X = (int)(mouseScreenPos.X - worldX_before * _zoomFactor); - _panOffset.Y = (int)(mouseScreenPos.Y - worldY_before * _zoomFactor); + // mouseScreen = (worldBefore + newPan) * newZoom + // newPan = mouseScreen / newZoom - worldBefore + _panOffset.X = (int)(mouseScreenPos.X / _zoomFactor - worldX_before); + _panOffset.Y = (int)(mouseScreenPos.Y / _zoomFactor - worldY_before); Invalidate(); } @@ -216,9 +220,10 @@ namespace AGVNavigationCore.Controls private Point ScreenToWorld(Point screenPoint) { // 스크린 좌표를 월드 좌표로 변환 - // 역순으로: 팬 오프셋 제거 → 줌 적용 - float worldX = (screenPoint.X - _panOffset.X) / _zoomFactor; - float worldY = (screenPoint.Y - _panOffset.Y) / _zoomFactor; + // Graphics Transform: Screen = (World + Pan) * Zoom + // 역변환: World = Screen / Zoom - Pan + float worldX = screenPoint.X / _zoomFactor - _panOffset.X; + float worldY = screenPoint.Y / _zoomFactor - _panOffset.Y; return new Point((int)worldX, (int)worldY); } @@ -226,10 +231,10 @@ namespace AGVNavigationCore.Controls private Point WorldToScreen(Point worldPoint) { // 월드 좌표를 스크린 좌표로 변환 - // 순서: 줌 적용 → 팬 오프셋 추가 + // Graphics Transform: Screen = (World + Pan) * Zoom return new Point( - (int)(worldPoint.X * _zoomFactor + _panOffset.X), - (int)(worldPoint.Y * _zoomFactor + _panOffset.Y) + (int)((worldPoint.X + _panOffset.X) * _zoomFactor), + (int)((worldPoint.Y + _panOffset.Y) * _zoomFactor) ); } @@ -744,8 +749,11 @@ namespace AGVNavigationCore.Controls var centerX = (minX + maxX) / 2; var centerY = (minY + maxY) / 2; - _panOffset.X = (int)(Width / 2 - centerX * _zoomFactor); - _panOffset.Y = (int)(Height / 2 - centerY * _zoomFactor); + // Graphics Transform: Screen = (World + Pan) * Zoom + // 중앙에 위치: Width/2 = (centerX + Pan) * Zoom + // Pan = Width/2 / Zoom - centerX + _panOffset.X = (int)(Width / 2 / _zoomFactor - centerX); + _panOffset.Y = (int)(Height / 2 / _zoomFactor - centerY); Invalidate(); } @@ -765,8 +773,11 @@ namespace AGVNavigationCore.Controls /// public void PanTo(Point worldPoint) { - _panOffset.X = (int)(Width / 2 - worldPoint.X * _zoomFactor); - _panOffset.Y = (int)(Height / 2 - worldPoint.Y * _zoomFactor); + // Graphics Transform: Screen = (World + Pan) * Zoom + // 중앙에 위치: Width/2 = (worldPoint + Pan) * Zoom + // Pan = Width/2 / Zoom - worldPoint + _panOffset.X = (int)(Width / 2 / _zoomFactor - worldPoint.X); + _panOffset.Y = (int)(Height / 2 / _zoomFactor - worldPoint.Y); Invalidate(); } diff --git a/Cs_HMI/AGVLogic/AGVSimulator/Forms/SimulatorForm.cs b/Cs_HMI/AGVLogic/AGVSimulator/Forms/SimulatorForm.cs index 5d0bd64..c3b5c05 100644 --- a/Cs_HMI/AGVLogic/AGVSimulator/Forms/SimulatorForm.cs +++ b/Cs_HMI/AGVLogic/AGVSimulator/Forms/SimulatorForm.cs @@ -1382,14 +1382,12 @@ namespace AGVSimulator.Forms { logItem.Success = true; logItem.Message = "성공"; - //logItem.DetailedPath = string.Join(" → ", currentPath.GetDetailedInfo()); logItem.DetailedPath = currentPath.GetDetailedPathInfo(); } else { logItem.Success = false; logItem.Message = $"도킹 검증 실패: {dockingValidation.ValidationError}"; - //logItem.DetailedPath = string.Join(" → ", currentPath.GetDetailedInfo()); logItem.DetailedPath = currentPath.GetDetailedPathInfo(); } }