From 93097412b0ffe07cfeb5dfc1fca4534c87bbdf19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=90=C4=83ng=20Nguy=E1=BB=85n?= Date: Thu, 2 Oct 2025 09:49:16 +0700 Subject: [PATCH] update move. scale mapping --- .../Pages/Components/Mapping/MapTable.razor | 138 +++----- .../Pages/Components/Mapping/MapView.razor | 314 +++++++++++++++++- .../Components/Mapping/MapView.razor.css | 173 ++++++++++ RobotApp.Client/Pages/MapsManager.razor | 2 +- RobotApp.Client/Pages/_Imports.razor | 2 + RobotApp.Client/RobotApp.Client.csproj | 2 +- RobotApp.Client/wwwroot/js/canvas.js | 11 + RobotApp/Components/App.razor | 1 + RobotApp/Controllers/ImagesController.cs | 10 + RobotApp/robot.db | Bin 167936 -> 167936 bytes 10 files changed, 560 insertions(+), 93 deletions(-) create mode 100644 RobotApp.Client/Pages/Components/Mapping/MapView.razor.css create mode 100644 RobotApp.Client/wwwroot/js/canvas.js create mode 100644 RobotApp/Controllers/ImagesController.cs diff --git a/RobotApp.Client/Pages/Components/Mapping/MapTable.razor b/RobotApp.Client/Pages/Components/Mapping/MapTable.razor index 1a306cd..b1851bd 100644 --- a/RobotApp.Client/Pages/Components/Mapping/MapTable.razor +++ b/RobotApp.Client/Pages/Components/Mapping/MapTable.razor @@ -1,72 +1,55 @@ @inject HttpClient Http - - -
- - - Maps - - - Nr - Name - Width (m) - Height (m) - Resolution (m/px) - OriginX - OriginY - - - - - @(Table?.CurrentPage * Table?.RowsPerPage + MapsShow.IndexOf(context) + 1) - - - @context.Name - - - @context.Width - - - @context.Height - - - @context.Resolution - - - @context.OriginX - - - @context.OriginY - - - Active - Delete - - - -
- -
-
-
-
+ + + Maps + + + Nr + Name + Width (m) + Height (m) + Resolution (m/px) + OriginX + OriginY + + + + + @(Table?.CurrentPage * Table?.RowsPerPage + MapsShow.IndexOf(context) + 1) + + + @context.Name + + + @context.Width + + + @context.Height + + + @context.Resolution + + + @context.OriginX + + + @context.OriginY + + + Active + Delete + + + +
+ +
+
+
@code { - private string txtSearch = ""; private bool IsLoading = false; @@ -137,29 +120,4 @@ MapsShow = tasks.Skip(state.Page * state.PageSize).Take(state.PageSize).ToList(); return Task.FromResult(new TableData() { TotalItems = tasks.Count, Items = MapsShow }); } - - private void RowClickEvent(TableRowClickEventArgs tableRowClickEventArgs) { } - - private string SelectedRowClassFunc(MapDto element, int rowNumber) - { - if (selectedRowNumber == rowNumber && Table?.SelectedItem != null && !Table.SelectedItem.Equals(element)) - { - return string.Empty; - } - else if (selectedRowNumber == rowNumber && Table?.SelectedItem != null && Table.SelectedItem.Equals(element)) - { - return "selected"; - } - else if (Table?.SelectedItem != null && Table.SelectedItem.Equals(element)) - { - selectedRowNumber = rowNumber; - MapSelected = element; - // MapPreviewRef?.SetMapPreview(MapSelected); - return "selected"; - } - else - { - return string.Empty; - } - } } diff --git a/RobotApp.Client/Pages/Components/Mapping/MapView.razor b/RobotApp.Client/Pages/Components/Mapping/MapView.razor index ba25d3c..afe0338 100644 --- a/RobotApp.Client/Pages/Components/Mapping/MapView.razor +++ b/RobotApp.Client/Pages/Components/Mapping/MapView.razor @@ -1,5 +1,317 @@ -

MapView

+@inject IJSRuntime JS +@using Excubo.Blazor.Canvas.Contexts + +
+
+ + + + + + + + + +
+
+ + + + Zoom: @($"{ZoomScale:F2}x") + + | + + + Mouse: (@($"{MouseX:F0}"), @($"{MouseY:F0}")) + + | + + + World: (@($"{WorldMouseX:F2}m"), @($"{WorldMouseY:F2}m")) + + | + + + Translate: (@($"{CanvasTranslateX:F2}"), @($"{CanvasTranslateY:F2}")) + + +
+
+
+
+ +
+
@code { + private ElementReference CanvasRef; + private ElementReference ViewContainerRef; + private double ZoomScale = 1.0; + private const double MIN_ZOOM = 0.1; + private const double MAX_ZOOM = 5.0; + + private const double BASE_PIXELS_PER_METER = 50.0; + + private bool IsMouseInCanvas = false; + private double MouseX; + private double MouseY; + + private double OriginX = 0; + private double OriginY = 0; + + private double WorldMouseX; + private double WorldMouseY; + + private double CanvasWidth; + private double CanvasHeight; + + private double CanvasTranslateX = 0; + private double CanvasTranslateY = 0; + + protected override async Task OnAfterRenderAsync(bool first_render) + { + await base.OnAfterRenderAsync(first_render); + if (!first_render) return; + + var parentSize = await JS.InvokeAsync("getElementSize", ViewContainerRef); + + CanvasWidth = parentSize.Width; + CanvasHeight = parentSize.Height; + + await JS.InvokeVoidAsync("setCanvasSize", CanvasRef, CanvasWidth, CanvasHeight); + + CanvasTranslateX = CanvasWidth / 2; + CanvasTranslateY = CanvasHeight / 2; + + await DrawCanvas(); + } + + private async Task DrawCanvas() + { + await using var ctx = await JS.GetContext2DAsync(CanvasRef); + + await ctx.ClearRectAsync(0, 0, CanvasWidth, CanvasHeight); + await ctx.SaveAsync(); + + await ctx.TranslateAsync(CanvasTranslateX, CanvasTranslateY); + await ctx.ScaleAsync(ZoomScale, ZoomScale); + + await DrawGrid(ctx); + await DrawAxes(ctx); + + await ctx.RestoreAsync(); + } + + private async Task DrawAxes(Context2D ctx) + { + double originCanvasX = OriginX * BASE_PIXELS_PER_METER; + double originCanvasY = OriginY * BASE_PIXELS_PER_METER; + + await ctx.FillStyleAsync("red"); + await ctx.BeginPathAsync(); + await ctx.ArcAsync(originCanvasX, originCanvasY, 10 / ZoomScale, 0, Math.PI * 2); + await ctx.FillAsync(FillRule.NonZero); + + await ctx.LineWidthAsync(2 / ZoomScale); + + await ctx.StrokeStyleAsync("blue"); + await ctx.BeginPathAsync(); + await ctx.MoveToAsync(-CanvasWidth / ZoomScale, originCanvasY); + await ctx.LineToAsync(CanvasWidth / ZoomScale, originCanvasY); + await ctx.StrokeAsync(); + + await ctx.StrokeStyleAsync("red"); + await ctx.BeginPathAsync(); + await ctx.MoveToAsync(originCanvasX, -CanvasHeight / ZoomScale); + await ctx.LineToAsync(originCanvasX, CanvasHeight / ZoomScale); + await ctx.StrokeAsync(); + + double gridSpacingMeters = GetGridSpacingMeters(); + double arrowLength = gridSpacingMeters * BASE_PIXELS_PER_METER; + double arrowHeadSize = 16 / ZoomScale; + + await ctx.FillStyleAsync("blue"); + await ctx.StrokeStyleAsync("blue"); + await ctx.LineWidthAsync(4 / ZoomScale); + + await ctx.BeginPathAsync(); + await ctx.MoveToAsync(originCanvasX, originCanvasY); + await ctx.LineToAsync(originCanvasX + arrowLength, originCanvasY); + await ctx.StrokeAsync(); + + await ctx.BeginPathAsync(); + await ctx.MoveToAsync(originCanvasX + arrowLength, originCanvasY); + await ctx.LineToAsync(originCanvasX + arrowLength - arrowHeadSize, originCanvasY - arrowHeadSize / 2); + await ctx.LineToAsync(originCanvasX + arrowLength - arrowHeadSize, originCanvasY + arrowHeadSize / 2); + await ctx.ClosePathAsync(); + await ctx.FillAsync(FillRule.NonZero); + + await ctx.FillStyleAsync("red"); + await ctx.StrokeStyleAsync("red"); + await ctx.LineWidthAsync(4 / ZoomScale); + + await ctx.BeginPathAsync(); + await ctx.MoveToAsync(originCanvasX, originCanvasY); + await ctx.LineToAsync(originCanvasX, originCanvasY + arrowLength); + await ctx.StrokeAsync(); + + await ctx.BeginPathAsync(); + await ctx.MoveToAsync(originCanvasX, originCanvasY + arrowLength); + await ctx.LineToAsync(originCanvasX - arrowHeadSize / 2, originCanvasY + arrowLength - arrowHeadSize); + await ctx.LineToAsync(originCanvasX + arrowHeadSize / 2, originCanvasY + arrowLength - arrowHeadSize); + await ctx.ClosePathAsync(); + await ctx.FillAsync(FillRule.NonZero); + } + + private async Task DrawGrid(Context2D ctx) + { + await ctx.StrokeStyleAsync("rgba(200, 200, 200, 0.4)"); + await ctx.LineWidthAsync(1 / ZoomScale); + await ctx.SetLineDashAsync(new double[] { 5 / ZoomScale, 5 / ZoomScale }); + + double gridSpacingMeters = GetGridSpacingMeters(); + double gridSpacingPixels = gridSpacingMeters * BASE_PIXELS_PER_METER; + + double visibleLeft = -CanvasTranslateX / ZoomScale; + double visibleRight = (CanvasWidth - CanvasTranslateX) / ZoomScale; + double visibleTop = -CanvasTranslateY / ZoomScale; + double visibleBottom = (CanvasHeight - CanvasTranslateY) / ZoomScale; + + double startX = Math.Floor(visibleLeft / gridSpacingPixels) * gridSpacingPixels; + double startY = Math.Floor(visibleTop / gridSpacingPixels) * gridSpacingPixels; + + for (double x = startX; x <= visibleRight; x += gridSpacingPixels) + { + await ctx.BeginPathAsync(); + await ctx.MoveToAsync(x, visibleTop); + await ctx.LineToAsync(x, visibleBottom); + await ctx.StrokeAsync(); + } + + for (double y = startY; y <= visibleBottom; y += gridSpacingPixels) + { + await ctx.BeginPathAsync(); + await ctx.MoveToAsync(visibleLeft, y); + await ctx.LineToAsync(visibleRight, y); + await ctx.StrokeAsync(); + } + + await ctx.SetLineDashAsync(new double[] { }); + } + + private double GetGridSpacingMeters() + { + if (BASE_PIXELS_PER_METER * ZoomScale >= 300) return 0.2; + else if (BASE_PIXELS_PER_METER * ZoomScale >= 150) return 0.5; + else if (BASE_PIXELS_PER_METER * ZoomScale >= 75) return 1.0; + else if (BASE_PIXELS_PER_METER * ZoomScale >= 40) return 2.0; + else if (BASE_PIXELS_PER_METER * ZoomScale >= 20) return 5.0; + else if (BASE_PIXELS_PER_METER * ZoomScale >= 10) return 10.0; + else return 20.0; + } + + private double CanvasToWorldX(double canvasX) + { + return (canvasX - CanvasTranslateX) / ZoomScale / BASE_PIXELS_PER_METER - OriginX; + } + + private double CanvasToWorldY(double canvasY) + { + return (canvasY - CanvasTranslateY) / ZoomScale / BASE_PIXELS_PER_METER - OriginY; + } + + private double WorldToCanvasX(double worldX) + { + return (worldX + OriginX) * BASE_PIXELS_PER_METER * ZoomScale + CanvasTranslateX; + } + + private double WorldToCanvasY(double worldY) + { + return (worldY + OriginY) * BASE_PIXELS_PER_METER * ZoomScale + CanvasTranslateY; + } + + private async Task HandleMouseMove(MouseEventArgs e) + { + MouseX = e.OffsetX; + MouseY = e.OffsetY; + IsMouseInCanvas = true; + + WorldMouseX = CanvasToWorldX(MouseX); + WorldMouseY = CanvasToWorldY(MouseY); + + StateHasChanged(); + + if (e.Buttons == 4) + { + CanvasTranslateX += e.MovementX; + CanvasTranslateY -= e.MovementY; + await DrawCanvas(); + } + } + + private async Task HandleMouseLeave(MouseEventArgs e) + { + IsMouseInCanvas = false; + MouseX = 0; + MouseY = 0; + StateHasChanged(); + await DrawCanvas(); + } + + private async Task HandleWheel(WheelEventArgs e) + { + if (e.Buttons == 4) return; + const double zoomFactor = 0.1; + double oldZoom = ZoomScale; + + if (e.DeltaY < 0) ZoomScale = Math.Min(MAX_ZOOM, ZoomScale * (1 + zoomFactor)); + else ZoomScale = Math.Max(MIN_ZOOM, ZoomScale * (1 - zoomFactor)); + + if (Math.Abs(ZoomScale - oldZoom) < 0.001) return; + + MouseX = e.OffsetX; + MouseY = e.OffsetY; + + WorldMouseX = CanvasToWorldX(MouseX); + WorldMouseY = CanvasToWorldY(MouseY); + + double zoomPointWorldX = (MouseX - CanvasTranslateX) / oldZoom / BASE_PIXELS_PER_METER - OriginX; + double zoomPointWorldY = (MouseY - CanvasTranslateY) / oldZoom / BASE_PIXELS_PER_METER - OriginY; + + double newZoomPointCanvasX = (zoomPointWorldX + OriginX) * BASE_PIXELS_PER_METER * ZoomScale; + double newZoomPointCanvasY = (zoomPointWorldY + OriginY) * BASE_PIXELS_PER_METER * ZoomScale; + + CanvasTranslateX = MouseX - newZoomPointCanvasX; + CanvasTranslateY = MouseY - newZoomPointCanvasY; + + StateHasChanged(); + await DrawCanvas(); + } + + private async Task ResetView() + { + CanvasTranslateX = CanvasWidth / 2; + CanvasTranslateY = CanvasHeight / 2; + ZoomScale = 1.0; + StateHasChanged(); + await DrawCanvas(); + } + + public class DomRect + { + public double Width { get; set; } + public double Height { get; set; } + } } diff --git a/RobotApp.Client/Pages/Components/Mapping/MapView.razor.css b/RobotApp.Client/Pages/Components/Mapping/MapView.razor.css new file mode 100644 index 0000000..2d9e089 --- /dev/null +++ b/RobotApp.Client/Pages/Components/Mapping/MapView.razor.css @@ -0,0 +1,173 @@ +.view { + height: 100%; + width: 100%; + overflow: hidden; + display: flex; + flex-direction: column; + position: relative; + background-color: var(--mud-palette-surface); + border-radius: var(--mud-default-borderradius); + transition: box-shadow 300ms cubic-bezier(0.4, 0, 0.2, 1) 0ms; + box-shadow: var(--mud-elevation-10); +} + + .view .toolbar { + width: 100%; + height: 3rem; + flex: 0 0 auto; + display: flex; + align-items: center; + padding: 0.5rem; + border-bottom: 1px solid var(--mud-palette-divider); + background-color: var(--mud-palette-background); + } + + .view .toolbar .action-button { + padding: 0rem 0.4rem; + margin-right: 0.5rem; + border-radius: var(--mud-default-borderradius); + transition: all 0.3s ease; + background-color: var(--bs-gray-200); + } + + .view .toolbar .action-button:hover { + background-color: var(--mud-palette-action-hover); + transform: translateY(-1px); + } + + .view .toolbar .action-button:disabled { + opacity: 0.6; + cursor: not-allowed; + } + + .view .toolbar .action-button:disabled:hover { + transform: none; + background-color: transparent; + } + + .view .toolbar .icon-button { + font-size: 1.2rem; + color: var(--mud-palette-primary); + } + + .view canvas { + transition: cursor 0.2s ease; + } + + .view canvas:hover { + cursor: crosshair; + } + +/* Enhanced zoom and coordinate info styling */ +.zoom-info-container { + display: flex; + align-items: center; + background-color: var(--mud-palette-background); + border: 1px solid var(--mud-palette-divider); + border-radius: 6px; + padding: 0.5rem 0.75rem; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); +} + +.zoom-info { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + font-size: 0.8rem; + color: var(--mud-palette-text-primary); + display: flex; + align-items: center; + gap: 0.5rem; + white-space: nowrap; +} + +.info-item { + display: flex; + align-items: center; + gap: 0.25rem; + font-weight: 500; +} + + .info-item i { + font-size: 0.9rem; + color: var(--mud-palette-primary); + } + +.info-separator { + color: var(--mud-palette-divider); + font-weight: 300; + margin: 0 0.25rem; +} + +/* Responsive design for info container */ +@media (max-width: 768px) { + .zoom-info-container { + padding: 0.25rem 0.5rem; + } + + .zoom-info { + font-size: 0.7rem; + gap: 0.25rem; + } + + .info-item { + gap: 0.15rem; + } + + .info-item i { + font-size: 0.8rem; + } +} + +/* Very small screens - stack vertically */ +@media (max-width: 480px) { + .zoom-info { + flex-direction: column; + gap: 0.15rem; + text-align: center; + } + + .info-separator { + display: none; + } + + .zoom-info-container { + padding: 0.35rem; + } +} + +/* Hover effects for info container */ +.zoom-info-container:hover { + background-color: var(--mud-palette-action-hover); + box-shadow: 0 4px 8px rgba(0,0,0,0.15); +} + +/* Animation for coordinate updates */ +.info-item { + transition: color 0.2s ease; +} + +.info-item:hover { + color: var(--mud-palette-primary); +} + +/* High contrast mode support */ +@media (prefers-contrast: high) { + .zoom-info-container { + border-width: 2px; + background-color: var(--mud-palette-surface); + } + + .info-item { + font-weight: 600; + } +} + +/* Dark mode adjustments */ +@media (prefers-color-scheme: dark) { + .zoom-info-container { + box-shadow: 0 2px 4px rgba(0,0,0,0.3); + } + + .zoom-info-container:hover { + box-shadow: 0 4px 8px rgba(0,0,0,0.4); + } +} diff --git a/RobotApp.Client/Pages/MapsManager.razor b/RobotApp.Client/Pages/MapsManager.razor index cff3a98..819c5e6 100644 --- a/RobotApp.Client/Pages/MapsManager.razor +++ b/RobotApp.Client/Pages/MapsManager.razor @@ -7,7 +7,7 @@ Map Manager
-
+
diff --git a/RobotApp.Client/Pages/_Imports.razor b/RobotApp.Client/Pages/_Imports.razor index d8c136a..112934e 100644 --- a/RobotApp.Client/Pages/_Imports.razor +++ b/RobotApp.Client/Pages/_Imports.razor @@ -14,3 +14,5 @@ @using MudBlazor @using RobotApp.Common.Shares @using RobotApp.Common.Shares.Dtos +@using Excubo.Blazor.Canvas +@using Excubo.Blazor.Canvas.Contexts \ No newline at end of file diff --git a/RobotApp.Client/RobotApp.Client.csproj b/RobotApp.Client/RobotApp.Client.csproj index c65ed05..cf5879a 100644 --- a/RobotApp.Client/RobotApp.Client.csproj +++ b/RobotApp.Client/RobotApp.Client.csproj @@ -9,6 +9,7 @@ + @@ -20,7 +21,6 @@ - diff --git a/RobotApp.Client/wwwroot/js/canvas.js b/RobotApp.Client/wwwroot/js/canvas.js new file mode 100644 index 0000000..5b34e27 --- /dev/null +++ b/RobotApp.Client/wwwroot/js/canvas.js @@ -0,0 +1,11 @@ +window.getElementSize = (element) => { + return { + width: element.clientWidth, + height: element.clientHeight, + }; +} + +window.setCanvasSize = (canvas, width, height) => { + canvas.width = width; + canvas.height = height; +} \ No newline at end of file diff --git a/RobotApp/Components/App.razor b/RobotApp/Components/App.razor index 7c4ad1b..d54d1b7 100644 --- a/RobotApp/Components/App.razor +++ b/RobotApp/Components/App.razor @@ -40,6 +40,7 @@ + diff --git a/RobotApp/Controllers/ImagesController.cs b/RobotApp/Controllers/ImagesController.cs new file mode 100644 index 0000000..875c9ca --- /dev/null +++ b/RobotApp/Controllers/ImagesController.cs @@ -0,0 +1,10 @@ +using Microsoft.AspNetCore.Mvc; + +namespace RobotApp.Controllers +{ + [Route("api/[controller]")] + [ApiController] + public class ImagesController : ControllerBase + { + } +} diff --git a/RobotApp/robot.db b/RobotApp/robot.db index 27b6947f45c630d4aaf81c65482cceb4a4a85828..b7dd223dac6b03db7e2ee7a2f71bf62e46c9a34b 100644 GIT binary patch delta 74 zcmZozz}2vTYl0LLv*Sb=Cm^{oVI@D~?ahJ#7x)!alo^>Be2ol@Omz(nbPbFY42-Od d&8-Y9^^6P*49qN>f62H1l4sohOP-0X008l?6kPxS delta 74 zcmZozz}2vTYl0LL!}p0YPC#;F!b*O|Tbl&~F7PX;Ffudv8yOgx>Ka(;8ks2=8dw>c dSecsWnHpId7+YF2|B`S2CC|A1mpl_&0RT&x73=^2