diff --git a/RobotApp.Client/Pages/Components/Monitor/RobotMonitorView.razor b/RobotApp.Client/Pages/Components/Monitor/RobotMonitorView.razor index 501eb37..826bb53 100644 --- a/RobotApp.Client/Pages/Components/Monitor/RobotMonitorView.razor +++ b/RobotApp.Client/Pages/Components/Monitor/RobotMonitorView.razor @@ -17,6 +17,13 @@ +
+ + + Follow Robot + + +
@if (MonitorData?.RobotPosition != null) { @@ -58,6 +65,9 @@ @* Background Map Image *@ @if (MapImageLoaded && MapImageWidth > 0 && MapImageHeight > 0) { + @* Image origin is at bottom-left corner (MapImageOriginX, MapImageOriginY) in world coordinates + In SVG (after Y flip), image top-left corner is at (MapImageOriginX, -MapImageOriginY - MapImageHeight) + So we render image at: x = MapImageOriginX, y = -MapImageOriginY - MapImageHeight *@ } - @* Origin Marker (2 arrows: X+ and Y+) *@ + @* Origin Marker (2 arrows: X+ and Y+) at (MapImageOriginX, MapImageOriginY) *@ @* X+ Arrow (pointing right) *@ @* Origin point *@ - @@ -144,7 +154,7 @@ private ElementReference SvgRef; private ElementReference SvgContainerRef; - private double ZoomScale = 2.0; // Zoom vào robot hơn khi mở + private double ZoomScale = 1.5; // Zoom vào robot hơn khi mở private const double MIN_ZOOM = 0.1; private const double MAX_ZOOM = 5.0; private const double BASE_PIXELS_PER_METER = 50.0; @@ -166,6 +176,19 @@ private double? MouseWorldX = null; private double? MouseWorldY = null; + // Auto-follow robot + private bool AutoFollowRobot = false; + + private void OnAutoFollowRobotChanged(bool value) + { + AutoFollowRobot = value; + if (AutoFollowRobot && MonitorData?.RobotPosition != null) + { + UpdateViewToFollowRobot(); + StateHasChanged(); + } + } + // Map image properties private const double MapImageOriginX = -20.0; // OriginX in world coordinates (meters) private const double MapImageOriginY = -20.0; // OriginY in world coordinates (meters) @@ -215,8 +238,6 @@ MapImageWidth = imageDimensions.Width * MapImageResolution; MapImageHeight = imageDimensions.Height * MapImageResolution; - Console.WriteLine($"Map image loaded: {imageDimensions.Width}x{imageDimensions.Height} pixels, {MapImageWidth}x{MapImageHeight} meters"); - if (MapImageWidth > 0 && MapImageHeight > 0) { MapImageLoaded = true; @@ -232,6 +253,12 @@ private string GetTransform() { + // Transform applies: first translate (in screen pixels), then scale (pixels per meter) + // World coordinates are in meters + // After transform: screenX = TranslateX + worldX * (ZoomScale * BASE_PIXELS_PER_METER) + // screenY = TranslateY + worldY * (ZoomScale * BASE_PIXELS_PER_METER) + // But we need to flip Y: screenY = TranslateY - worldY * (ZoomScale * BASE_PIXELS_PER_METER) + // This is handled by WorldToSvgY which flips Y before applying transform return $"translate({TranslateX}, {TranslateY}) scale({ZoomScale * BASE_PIXELS_PER_METER})"; } @@ -254,7 +281,7 @@ // Điều chỉnh kích thước dựa trên ZoomScale // Tăng kích thước lên 1.5x để robot to hơn - double scaleFactor = 3 / ZoomScale; // Tăng kích thước hiển thị + double scaleFactor = 2 / ZoomScale; // Tăng kích thước hiển thị double width = RobotWidthMeters * scaleFactor; double height = RobotLengthMeters * scaleFactor; @@ -299,17 +326,20 @@ private string GetOriginMarkerTransform() { - // Origin is at (MapImageOriginX, MapImageOriginY) in world coordinates - // In SVG: (MapImageOriginX, -MapImageOriginY) - var x = WorldToSvgX(MapImageOriginX); - var y = WorldToSvgY(MapImageOriginY); + // Origin is at (MapImageOriginX, MapImageOriginY) in world coordinates (bottom-left corner of image) + // In SVG coordinates (after Y flip): (MapImageOriginX, -MapImageOriginY) + // Note: Image is rendered at (MapImageOriginX, -MapImageOriginY - MapImageHeight) in SVG + // So origin marker should be at (MapImageOriginX, -MapImageOriginY) in SVG + var x = WorldToSvgX(0); + var y = WorldToSvgY(0); + return $"translate({x}, {y})"; } private double GetOriginMarkerSize() { // Marker size in world coordinates (meters) - const double BaseMarkerSize = 0.5; // 1 meter + const double BaseMarkerSize = 1; // 1 meter double scaleFactor = 1.0 / ZoomScale; // Keep visual size constant return BaseMarkerSize * scaleFactor; } @@ -322,6 +352,25 @@ return BaseStrokeWidth * scaleFactor; } + public void OnMonitorDataUpdated() + { + // Auto-follow robot when MonitorData changes + if (AutoFollowRobot && !IsPanning && MonitorData?.RobotPosition != null) + { + UpdateViewToFollowRobot(); + } + } + + private void UpdateViewToFollowRobot() + { + if (MonitorData?.RobotPosition == null) return; + + // Center view on robot + TranslateX = SvgWidth / 2 - MonitorData.RobotPosition.X * BASE_PIXELS_PER_METER * ZoomScale; + TranslateY = SvgHeight / 2 + MonitorData.RobotPosition.Y * BASE_PIXELS_PER_METER * ZoomScale; + StateHasChanged(); + } + public void UpdatePath() { if (MonitorData is not null && MonitorData.EdgeStates.Length > 0) diff --git a/RobotApp.Client/Pages/Components/Monitor/RobotMonitorView.razor.css b/RobotApp.Client/Pages/Components/Monitor/RobotMonitorView.razor.css index 21240f7..edb6c03 100644 --- a/RobotApp.Client/Pages/Components/Monitor/RobotMonitorView.razor.css +++ b/RobotApp.Client/Pages/Components/Monitor/RobotMonitorView.razor.css @@ -66,19 +66,27 @@ gap: 8px; } +.auto-follow-control { + display: flex; + align-items: center; + padding: 4px 8px; + background-color: #3d3d3d; + border-radius: 4px; +} + .svg-container { flex: 1; overflow: hidden; position: relative; - background-color: #fafafa; + background-color: #808080; } -.svg-container svg { - width: 100%; - height: 100%; - cursor: grab; - background-color: dimgray; -} + .svg-container svg { + width: 100%; + height: 100%; + cursor: grab; + background-color: #808080; + } .svg-container svg:active { cursor: grabbing; diff --git a/RobotApp.Client/Pages/RobotMonitor.razor b/RobotApp.Client/Pages/RobotMonitor.razor index f1f857b..6403ddb 100644 --- a/RobotApp.Client/Pages/RobotMonitor.razor +++ b/RobotApp.Client/Pages/RobotMonitor.razor @@ -29,6 +29,7 @@ { _monitorData = data; RobotMonitorViewRef?.UpdatePath(); + RobotMonitorViewRef?.OnMonitorDataUpdated(); StateHasChanged(); }