update
This commit is contained in:
parent
2853340856
commit
c5686e4ecf
|
|
@ -1,53 +1,57 @@
|
||||||
@inject HttpClient Http
|
@inject HttpClient Http
|
||||||
|
@inject IJSRuntime JS
|
||||||
|
|
||||||
|
<div @ref="ViewContainerRef" class="w-100 h-100">
|
||||||
|
<MudTable Class="h-100 w-100" @ref="Table" Items="@MapsShow" T="MapDto" Dense Hover ReadOnly FixedHeader RowClass="cursor-pointer" Striped Elevation="10"
|
||||||
|
ServerData="ReloadData" Loading=@IsLoading Height="@($"{TableHeight}px")" HorizontalScrollbar=true>
|
||||||
|
<ToolBarContent>
|
||||||
|
<h4>Maps</h4>
|
||||||
|
</ToolBarContent>
|
||||||
|
<HeaderContent>
|
||||||
|
<MudTh>Nr</MudTh>
|
||||||
|
<MudTh>Name</MudTh>
|
||||||
|
<MudTh>Width (m)</MudTh>
|
||||||
|
<MudTh>Height (m)</MudTh>
|
||||||
|
<MudTh>Resolution (m/px)</MudTh>
|
||||||
|
<MudTh>OriginX</MudTh>
|
||||||
|
<MudTh>OriginY</MudTh>
|
||||||
|
<MudTh></MudTh>
|
||||||
|
</HeaderContent>
|
||||||
|
<RowTemplate>
|
||||||
|
<MudTd DataLabel="Nr">
|
||||||
|
@(Table?.CurrentPage * Table?.RowsPerPage + MapsShow.IndexOf(context) + 1)
|
||||||
|
</MudTd>
|
||||||
|
<MudTd DataLabel="Name">
|
||||||
|
@context.Name
|
||||||
|
</MudTd>
|
||||||
|
<MudTd DataLabel="Width">
|
||||||
|
@context.Width
|
||||||
|
</MudTd>
|
||||||
|
<MudTd DataLabel="Height">
|
||||||
|
@context.Height
|
||||||
|
</MudTd>
|
||||||
|
<MudTd DataLabel="Resolution">
|
||||||
|
@context.Resolution
|
||||||
|
</MudTd>
|
||||||
|
<MudTd DataLabel="OriginX">
|
||||||
|
@context.OriginX
|
||||||
|
</MudTd>
|
||||||
|
<MudTd DataLabel="OriginY">
|
||||||
|
@context.OriginY
|
||||||
|
</MudTd>
|
||||||
|
<MudTd>
|
||||||
|
<MudMenuItem Icon="@Icons.Material.Filled.Edit" IconColor="Color.Info">Active</MudMenuItem>
|
||||||
|
<MudMenuItem Icon="@Icons.Material.Filled.Delete" IconColor="Color.Error">Delete</MudMenuItem>
|
||||||
|
</MudTd>
|
||||||
|
</RowTemplate>
|
||||||
|
<PagerContent>
|
||||||
|
<div class="d-flex w-100 flex-row-reverse">
|
||||||
|
<MudTablePager Style="width: 100%;" PageSizeOptions="new[] { 25, 100, 200 }" />
|
||||||
|
</div>
|
||||||
|
</PagerContent>
|
||||||
|
</MudTable>
|
||||||
|
</div>
|
||||||
|
|
||||||
<MudTable Class="h-100 w-100" @ref="Table" Items="@MapsShow" T="MapDto" Dense Hover ReadOnly FixedHeader RowClass="cursor-pointer" Striped Elevation="10"
|
|
||||||
ServerData="ReloadData" Loading=@IsLoading Height="88%" HorizontalScrollbar=true>
|
|
||||||
<ToolBarContent >
|
|
||||||
<MudText Typo="Typo.h6">Maps</MudText>
|
|
||||||
</ToolBarContent>
|
|
||||||
<HeaderContent>
|
|
||||||
<MudTh>Nr</MudTh>
|
|
||||||
<MudTh>Name</MudTh>
|
|
||||||
<MudTh>Width (m)</MudTh>
|
|
||||||
<MudTh>Height (m)</MudTh>
|
|
||||||
<MudTh>Resolution (m/px)</MudTh>
|
|
||||||
<MudTh>OriginX</MudTh>
|
|
||||||
<MudTh>OriginY</MudTh>
|
|
||||||
<MudTh></MudTh>
|
|
||||||
</HeaderContent>
|
|
||||||
<RowTemplate>
|
|
||||||
<MudTd DataLabel="Nr">
|
|
||||||
@(Table?.CurrentPage * Table?.RowsPerPage + MapsShow.IndexOf(context) + 1)
|
|
||||||
</MudTd>
|
|
||||||
<MudTd DataLabel="Name">
|
|
||||||
@context.Name
|
|
||||||
</MudTd>
|
|
||||||
<MudTd DataLabel="Width">
|
|
||||||
@context.Width
|
|
||||||
</MudTd>
|
|
||||||
<MudTd DataLabel="Height">
|
|
||||||
@context.Height
|
|
||||||
</MudTd>
|
|
||||||
<MudTd DataLabel="Resolution">
|
|
||||||
@context.Resolution
|
|
||||||
</MudTd>
|
|
||||||
<MudTd DataLabel="OriginX">
|
|
||||||
@context.OriginX
|
|
||||||
</MudTd>
|
|
||||||
<MudTd DataLabel="OriginY">
|
|
||||||
@context.OriginY
|
|
||||||
</MudTd>
|
|
||||||
<MudTd>
|
|
||||||
<MudMenuItem Icon="@Icons.Material.Filled.Edit" IconColor="Color.Info">Active</MudMenuItem>
|
|
||||||
<MudMenuItem Icon="@Icons.Material.Filled.Delete" IconColor="Color.Error">Delete</MudMenuItem>
|
|
||||||
</MudTd>
|
|
||||||
</RowTemplate>
|
|
||||||
<PagerContent>
|
|
||||||
<div class="d-flex w-100 flex-row-reverse">
|
|
||||||
<MudTablePager Style="width: 100%;" PageSizeOptions="new[] { 25, 100, 200 }" />
|
|
||||||
</div>
|
|
||||||
</PagerContent>
|
|
||||||
</MudTable>
|
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
private string txtSearch = "";
|
private string txtSearch = "";
|
||||||
|
|
@ -60,15 +64,21 @@
|
||||||
private MapDto MapSelected = new();
|
private MapDto MapSelected = new();
|
||||||
|
|
||||||
private MudTable<MapDto>? Table;
|
private MudTable<MapDto>? Table;
|
||||||
|
private ElementReference ViewContainerRef;
|
||||||
|
private ElementReference ToolbarRef;
|
||||||
|
|
||||||
private MapPreview? MapPreviewRef;
|
private MapPreview? MapPreviewRef;
|
||||||
|
private double TableHeight = 105;
|
||||||
|
|
||||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
{
|
{
|
||||||
await base.OnAfterRenderAsync(firstRender);
|
await base.OnAfterRenderAsync(firstRender);
|
||||||
if (!firstRender) return;
|
if (!firstRender) return;
|
||||||
|
|
||||||
|
var containerSize = await JS.InvokeAsync<DomRect>("getElementSize", ViewContainerRef);
|
||||||
|
TableHeight = containerSize.Height - 105;
|
||||||
// await LoadMaps();
|
// await LoadMaps();
|
||||||
|
StateHasChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task LoadMaps()
|
private async Task LoadMaps()
|
||||||
|
|
@ -120,4 +130,10 @@
|
||||||
MapsShow = tasks.Skip(state.Page * state.PageSize).Take(state.PageSize).ToList();
|
MapsShow = tasks.Skip(state.Page * state.PageSize).Take(state.PageSize).ToList();
|
||||||
return Task.FromResult(new TableData<MapDto>() { TotalItems = tasks.Count, Items = MapsShow });
|
return Task.FromResult(new TableData<MapDto>() { TotalItems = tasks.Count, Items = MapsShow });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class DomRect
|
||||||
|
{
|
||||||
|
public double Width { get; set; }
|
||||||
|
public double Height { get; set; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,15 @@
|
||||||
@inject IJSRuntime JS
|
@inject IJSRuntime JS
|
||||||
@inject HttpClient Http
|
|
||||||
@using Excubo.Blazor.Canvas.Contexts
|
@using Excubo.Blazor.Canvas.Contexts
|
||||||
|
|
||||||
<div class="view">
|
<div class="view">
|
||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
<MudTooltip Text="Zoom In" role="button" Placement="Placement.Bottom" Color="Color.Info">
|
<MudTooltip Text="Zoom In" role="button" Placement="Placement.Bottom" Color="Color.Info">
|
||||||
<button type="button" class="btn btn-secondary action-button" @onclick="ResetView">
|
<button type="button" class="btn btn-secondary action-button" @onclick="ZoomIn">
|
||||||
<i class="mdi mdi-magnify-plus-outline icon-button"></i>
|
<i class="mdi mdi-magnify-plus-outline icon-button"></i>
|
||||||
</button>
|
</button>
|
||||||
</MudTooltip>
|
</MudTooltip>
|
||||||
<MudTooltip Text="Zoom Out" role="button" Placement="Placement.Bottom" Color="Color.Info">
|
<MudTooltip Text="Zoom Out" role="button" Placement="Placement.Bottom" Color="Color.Info">
|
||||||
<button type="button" class="btn btn-secondary action-button" @onclick="ResetView">
|
<button type="button" class="btn btn-secondary action-button" @onclick="ZoomOut">
|
||||||
<i class="mdi mdi-magnify-minus-outline icon-button"></i>
|
<i class="mdi mdi-magnify-minus-outline icon-button"></i>
|
||||||
</button>
|
</button>
|
||||||
</MudTooltip>
|
</MudTooltip>
|
||||||
|
|
@ -32,27 +31,23 @@
|
||||||
</MudTooltip>
|
</MudTooltip>
|
||||||
<MudTooltip Text="Start Mapping" role="button" Placement="Placement.Bottom" Color="Color.Info">
|
<MudTooltip Text="Start Mapping" role="button" Placement="Placement.Bottom" Color="Color.Info">
|
||||||
<button type="button" class="btn btn-secondary action-button" disabled="@(false)">
|
<button type="button" class="btn btn-secondary action-button" disabled="@(false)">
|
||||||
<i class="mdi mdi-play icon-button"></i>
|
<i class="mdi mdi-plus icon-button"></i>
|
||||||
</button>
|
</button>
|
||||||
</MudTooltip>
|
</MudTooltip>
|
||||||
<MudTooltip Text="Stop Mapping" role="button" Placement="Placement.Bottom" Color="Color.Info">
|
<MudTooltip Text="Stop Mapping" role="button" Placement="Placement.Bottom" Color="Color.Info">
|
||||||
<button type="button" class="btn btn-secondary action-button" disabled="@(true)">
|
<button type="button" class="btn btn-secondary action-button" disabled="@(true)">
|
||||||
<i class="mdi mdi-pause icon-button"></i>
|
<i class="mdi mdi-stop icon-button"></i>
|
||||||
</button>
|
|
||||||
</MudTooltip>
|
|
||||||
<MudSpacer />
|
|
||||||
<MudTooltip Text="Reload Map" role="button" Placement="Placement.Bottom" Color="Color.Info">
|
|
||||||
<button type="button" class="btn btn-secondary action-button" @onclick="ReloadMapImage">
|
|
||||||
<i class="mdi mdi-map-outline icon-button"></i>
|
|
||||||
</button>
|
</button>
|
||||||
</MudTooltip>
|
</MudTooltip>
|
||||||
</div>
|
</div>
|
||||||
<div @ref="ViewContainerRef">
|
<div @ref="ViewContainerRef">
|
||||||
<canvas @ref="CanvasRef"
|
<canvas @ref="CanvasRef"
|
||||||
@onwheel="HandleWheel"
|
@onwheel="HandleWheel"
|
||||||
@onwheel:preventDefault="true"
|
|
||||||
@onmousemove="HandleMouseMove"
|
@onmousemove="HandleMouseMove"
|
||||||
@onmouseleave="HandleMouseLeave"></canvas>
|
@onmouseleave="HandleMouseLeave"
|
||||||
|
@ontouchstart="HandleTouchStart"
|
||||||
|
@ontouchmove="HandleTouchMove"
|
||||||
|
@ontouchend="HandleTouchEnd"></canvas>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -91,22 +86,27 @@
|
||||||
private bool MapImageLoaded = false;
|
private bool MapImageLoaded = false;
|
||||||
|
|
||||||
private const double ImageX = -10;
|
private const double ImageX = -10;
|
||||||
private const double ImageY = -10;
|
private const double ImageY = -5;
|
||||||
private const double ImageResolution = 0.05;
|
private const double ImageResolution = 0.05;
|
||||||
|
|
||||||
private double MapImageWidth = 0;
|
private double MapImageWidth = 0;
|
||||||
private double MapImageHeight = 0;
|
private double MapImageHeight = 0;
|
||||||
private const string MAP_CACHE_KEY = "map_image";
|
private const string MAP_CACHE_KEY = "map_image";
|
||||||
|
|
||||||
|
private TouchPoint? LastTouchPoint;
|
||||||
|
private TouchPoint? LastSecondTouchPoint;
|
||||||
|
private double LastTouchDistance = 0;
|
||||||
|
private bool IsTouching = false;
|
||||||
|
|
||||||
protected override async Task OnAfterRenderAsync(bool first_render)
|
protected override async Task OnAfterRenderAsync(bool first_render)
|
||||||
{
|
{
|
||||||
await base.OnAfterRenderAsync(first_render);
|
await base.OnAfterRenderAsync(first_render);
|
||||||
if (!first_render) return;
|
if (!first_render) return;
|
||||||
|
|
||||||
var parentSize = await JS.InvokeAsync<DomRect>("getElementSize", ViewContainerRef);
|
var containerSize = await JS.InvokeAsync<DomRect>("getElementSize", ViewContainerRef);
|
||||||
|
|
||||||
CanvasWidth = parentSize.Width;
|
CanvasWidth = containerSize.Width;
|
||||||
CanvasHeight = parentSize.Height;
|
CanvasHeight = containerSize.Height;
|
||||||
|
|
||||||
await JS.InvokeVoidAsync("setCanvasSize", CanvasRef, CanvasWidth, CanvasHeight);
|
await JS.InvokeVoidAsync("setCanvasSize", CanvasRef, CanvasWidth, CanvasHeight);
|
||||||
|
|
||||||
|
|
@ -137,11 +137,10 @@
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
MapImageLoaded = false;
|
MapImageLoaded = false;
|
||||||
string baseUrl = Http.BaseAddress?.ToString() ?? "";
|
string apiUrl = "api/images/mapping";
|
||||||
string apiUrl = $"{baseUrl}api/images/map";
|
|
||||||
await JS.InvokeVoidAsync("preloadImageFromUrl", apiUrl, MAP_CACHE_KEY);
|
await JS.InvokeVoidAsync("preloadImageFromUrl", apiUrl, MAP_CACHE_KEY);
|
||||||
|
|
||||||
var imageDimensions = await JS.InvokeAsync<ImageDimensions>("getImageDimensions", MAP_CACHE_KEY);
|
var imageDimensions = await JS.InvokeAsync<DomRect>("getImageDimensions", MAP_CACHE_KEY);
|
||||||
MapImageWidth = imageDimensions.Width * ImageResolution;
|
MapImageWidth = imageDimensions.Width * ImageResolution;
|
||||||
MapImageHeight = imageDimensions.Height * ImageResolution;
|
MapImageHeight = imageDimensions.Height * ImageResolution;
|
||||||
if (MapImageWidth > 0 && MapImageHeight > 0) MapImageLoaded = true;
|
if (MapImageWidth > 0 && MapImageHeight > 0) MapImageLoaded = true;
|
||||||
|
|
@ -152,545 +151,61 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task DrawCanvas()
|
private async Task ResetView()
|
||||||
{
|
{
|
||||||
await using var ctx = await JS.GetContext2DAsync(CanvasRef);
|
CanvasTranslateX = CanvasWidth / 2;
|
||||||
|
CanvasTranslateY = CanvasHeight / 2;
|
||||||
|
ZoomScale = 1.0;
|
||||||
|
StateHasChanged();
|
||||||
|
await DrawCanvas();
|
||||||
|
}
|
||||||
|
|
||||||
await ctx.ClearRectAsync(0, 0, CanvasWidth, CanvasHeight);
|
private async Task ZoomIn()
|
||||||
|
{
|
||||||
await DrawRulers(ctx);
|
const double zoomFactor = 0.15;
|
||||||
|
double oldZoom = ZoomScale;
|
||||||
await ctx.SaveAsync();
|
|
||||||
|
|
||||||
await ctx.TranslateAsync(CanvasTranslateX, CanvasTranslateY);
|
|
||||||
await ctx.ScaleAsync(ZoomScale, ZoomScale);
|
|
||||||
|
|
||||||
await DrawMapImage(ctx);
|
ZoomScale = Math.Min(MAX_ZOOM, ZoomScale * (1 + zoomFactor));
|
||||||
await DrawGrid(ctx);
|
|
||||||
await DrawAxes(ctx);
|
if (Math.Abs(ZoomScale - oldZoom) < 0.001) return;
|
||||||
await DrawLaserScannerPoints(ctx);
|
|
||||||
await ctx.RestoreAsync();
|
await ZoomAtCenter(oldZoom);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ZoomOut()
|
||||||
|
{
|
||||||
|
const double zoomFactor = 0.15;
|
||||||
|
double oldZoom = ZoomScale;
|
||||||
|
|
||||||
|
ZoomScale = Math.Max(MIN_ZOOM, ZoomScale * (1 - zoomFactor));
|
||||||
|
|
||||||
|
if (Math.Abs(ZoomScale - oldZoom) < 0.001) return;
|
||||||
|
|
||||||
|
await ZoomAtCenter(oldZoom);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ZoomAtCenter(double oldZoom)
|
||||||
|
{
|
||||||
|
double centerX = CanvasWidth / 2;
|
||||||
|
double centerY = CanvasHeight / 2;
|
||||||
|
|
||||||
|
double centerWorldX = (centerX - CanvasTranslateX) / oldZoom / BASE_PIXELS_PER_METER - OriginX;
|
||||||
|
double centerWorldY = (centerY - CanvasTranslateY) / oldZoom / BASE_PIXELS_PER_METER - OriginY;
|
||||||
|
|
||||||
|
double newCenterCanvasX = (centerWorldX + OriginX) * BASE_PIXELS_PER_METER * ZoomScale;
|
||||||
|
double newCenterCanvasY = (centerWorldY + OriginY) * BASE_PIXELS_PER_METER * ZoomScale;
|
||||||
|
|
||||||
|
CanvasTranslateX = centerX - newCenterCanvasX;
|
||||||
|
CanvasTranslateY = centerY - newCenterCanvasY;
|
||||||
|
|
||||||
if (IsMouseInCanvas)
|
if (IsMouseInCanvas)
|
||||||
{
|
{
|
||||||
await DrawMouseIndicator(ctx);
|
WorldMouseX = CanvasToWorldX(MouseX);
|
||||||
|
WorldMouseY = CanvasToWorldY(MouseY);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
StateHasChanged();
|
||||||
private async Task DrawMapImage(Context2D ctx)
|
await DrawCanvas();
|
||||||
{
|
|
||||||
if (!MapImageLoaded)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await ctx.SaveAsync();
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
|
|
||||||
double imageWidthCanvas = MapImageWidth * BASE_PIXELS_PER_METER;
|
|
||||||
double imageHeightCanvas = MapImageHeight * BASE_PIXELS_PER_METER;
|
|
||||||
|
|
||||||
double mapCanvasX = ImageX * BASE_PIXELS_PER_METER;
|
|
||||||
double mapCanvasY = (ImageY + MapImageHeight) * BASE_PIXELS_PER_METER;
|
|
||||||
|
|
||||||
bool success = await JS.InvokeAsync<bool>("drawCachedImageOnCanvas",
|
|
||||||
CanvasRef,
|
|
||||||
MAP_CACHE_KEY,
|
|
||||||
mapCanvasX,
|
|
||||||
mapCanvasY - imageHeightCanvas,
|
|
||||||
imageWidthCanvas,
|
|
||||||
imageHeightCanvas);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
await ctx.RestoreAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task DrawLaserScannerPoints(Context2D ctx)
|
|
||||||
{
|
|
||||||
var scanData = GenerateLaserScanData();
|
|
||||||
|
|
||||||
double robotCanvasX = scanData.RobotX * BASE_PIXELS_PER_METER;
|
|
||||||
double robotCanvasY = scanData.RobotY * BASE_PIXELS_PER_METER;
|
|
||||||
|
|
||||||
await ctx.SaveAsync();
|
|
||||||
|
|
||||||
if (scanData.Points.Count > 0)
|
|
||||||
{
|
|
||||||
await ctx.BeginPathAsync();
|
|
||||||
|
|
||||||
for (int i = 0; i < scanData.Points.Count; i++)
|
|
||||||
{
|
|
||||||
var point = scanData.Points[i];
|
|
||||||
double pointCanvasX = point.X * BASE_PIXELS_PER_METER;
|
|
||||||
double pointCanvasY = point.Y * BASE_PIXELS_PER_METER;
|
|
||||||
|
|
||||||
if (i == 0)
|
|
||||||
{
|
|
||||||
await ctx.MoveToAsync(pointCanvasX, pointCanvasY);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
await ctx.LineToAsync(pointCanvasX, pointCanvasY);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await ctx.StrokeStyleAsync("rgba(255, 100, 100, 0.8)");
|
|
||||||
await ctx.LineWidthAsync(2 / ZoomScale);
|
|
||||||
await ctx.StrokeAsync();
|
|
||||||
|
|
||||||
await ctx.LineToAsync(robotCanvasX, robotCanvasY);
|
|
||||||
await ctx.ClosePathAsync();
|
|
||||||
await ctx.FillStyleAsync("rgba(255, 100, 100, 0.1)");
|
|
||||||
await ctx.FillAsync(FillRule.NonZero);
|
|
||||||
}
|
|
||||||
|
|
||||||
await DrawRobotImage(ctx, robotCanvasX, robotCanvasY, scanData.RobotOrientation);
|
|
||||||
await DrawRobotOrientationArrows(ctx, robotCanvasX, robotCanvasY, scanData.RobotOrientation);
|
|
||||||
|
|
||||||
await ctx.RestoreAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task DrawRobotImage(Context2D ctx, double robotCanvasX, double robotCanvasY, double robotOrientation)
|
|
||||||
{
|
|
||||||
if (!RobotImageLoaded)
|
|
||||||
{
|
|
||||||
// Fallback to circle if image not loaded
|
|
||||||
await ctx.FillStyleAsync("rgba(0, 255, 0, 0.8)");
|
|
||||||
await ctx.BeginPathAsync();
|
|
||||||
await ctx.ArcAsync(robotCanvasX, robotCanvasY, 8 / ZoomScale, 0, Math.PI * 2);
|
|
||||||
await ctx.FillAsync(FillRule.NonZero);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await ctx.SaveAsync();
|
|
||||||
|
|
||||||
double robotWidthPixels = RobotWidth * BASE_PIXELS_PER_METER;
|
|
||||||
double robotLengthPixels = RobotLength * BASE_PIXELS_PER_METER;
|
|
||||||
|
|
||||||
double scaledWidth = ZoomScale < 1 ? robotWidthPixels / ZoomScale : robotWidthPixels;
|
|
||||||
double scaledLength = ZoomScale < 1 ? robotLengthPixels / ZoomScale : robotLengthPixels;
|
|
||||||
|
|
||||||
await ctx.TranslateAsync(robotCanvasX, robotCanvasY);
|
|
||||||
await ctx.RotateAsync(robotOrientation);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
bool success = await JS.InvokeAsync<bool>("drawImageOnCanvas",
|
|
||||||
CanvasRef,
|
|
||||||
"images/AMR-250.png",
|
|
||||||
-scaledLength / 2,
|
|
||||||
-scaledWidth / 2,
|
|
||||||
scaledLength,
|
|
||||||
scaledWidth);
|
|
||||||
|
|
||||||
if (!success)
|
|
||||||
{
|
|
||||||
await ctx.FillStyleAsync("rgba(0, 255, 0, 0.8)");
|
|
||||||
await ctx.FillRectAsync(-scaledLength / 2, -scaledWidth / 2, scaledLength, scaledWidth);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
await ctx.FillStyleAsync("rgba(0, 255, 0, 0.8)");
|
|
||||||
await ctx.FillRectAsync(-scaledLength / 2, -scaledWidth / 2, scaledLength, scaledWidth);
|
|
||||||
}
|
|
||||||
|
|
||||||
await ctx.RestoreAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task DrawRobotOrientationArrows(Context2D ctx, double robotCanvasX, double robotCanvasY, double robotOrientation)
|
|
||||||
{
|
|
||||||
double arrowLength = 30 / ZoomScale;
|
|
||||||
double arrowHeadSize = 10 / ZoomScale;
|
|
||||||
|
|
||||||
await ctx.StrokeStyleAsync("rgba(0, 100, 255, 1.0)");
|
|
||||||
await ctx.FillStyleAsync("rgba(0, 100, 255, 1.0)");
|
|
||||||
await ctx.LineWidthAsync(3 / ZoomScale);
|
|
||||||
|
|
||||||
await ctx.BeginPathAsync();
|
|
||||||
await ctx.MoveToAsync(robotCanvasX, robotCanvasY);
|
|
||||||
double xAxisEndX = robotCanvasX + Math.Cos(robotOrientation) * (arrowLength - arrowHeadSize + 1);
|
|
||||||
double xAxisEndY = robotCanvasY + Math.Sin(robotOrientation) * (arrowLength - arrowHeadSize + 1);
|
|
||||||
await ctx.LineToAsync(xAxisEndX, xAxisEndY);
|
|
||||||
await ctx.StrokeAsync();
|
|
||||||
|
|
||||||
await ctx.BeginPathAsync();
|
|
||||||
double xArrowTipX = robotCanvasX + Math.Cos(robotOrientation) * arrowLength;
|
|
||||||
double xArrowTipY = robotCanvasY + Math.Sin(robotOrientation) * arrowLength;
|
|
||||||
await ctx.MoveToAsync(xArrowTipX, xArrowTipY);
|
|
||||||
double xArrowAngle = robotOrientation + Math.PI;
|
|
||||||
await ctx.LineToAsync(xArrowTipX + Math.Cos(xArrowAngle + Math.PI / 6) * arrowHeadSize, xArrowTipY + Math.Sin(xArrowAngle + Math.PI / 6) * arrowHeadSize);
|
|
||||||
await ctx.LineToAsync(xArrowTipX + Math.Cos(xArrowAngle - Math.PI / 6) * arrowHeadSize, xArrowTipY + Math.Sin(xArrowAngle - Math.PI / 6) * arrowHeadSize);
|
|
||||||
await ctx.ClosePathAsync();
|
|
||||||
await ctx.FillAsync(FillRule.NonZero);
|
|
||||||
|
|
||||||
await ctx.StrokeStyleAsync("rgba(255, 50, 50, 1.0)");
|
|
||||||
await ctx.FillStyleAsync("rgba(255, 50, 50, 1.0)");
|
|
||||||
await ctx.LineWidthAsync(3 / ZoomScale);
|
|
||||||
|
|
||||||
double yAxisAngle = robotOrientation + Math.PI / 2;
|
|
||||||
|
|
||||||
await ctx.BeginPathAsync();
|
|
||||||
await ctx.MoveToAsync(robotCanvasX, robotCanvasY);
|
|
||||||
double yAxisEndX = robotCanvasX + Math.Cos(yAxisAngle) * (arrowLength - arrowHeadSize + 1);
|
|
||||||
double yAxisEndY = robotCanvasY + Math.Sin(yAxisAngle) * (arrowLength - arrowHeadSize + 1);
|
|
||||||
await ctx.LineToAsync(yAxisEndX, yAxisEndY);
|
|
||||||
await ctx.StrokeAsync();
|
|
||||||
|
|
||||||
await ctx.BeginPathAsync();
|
|
||||||
double yArrowTipX = robotCanvasX + Math.Cos(yAxisAngle) * arrowLength;
|
|
||||||
double yArrowTipY = robotCanvasY + Math.Sin(yAxisAngle) * arrowLength;
|
|
||||||
await ctx.MoveToAsync(yArrowTipX, yArrowTipY);
|
|
||||||
double yArrowAngle = yAxisAngle + Math.PI;
|
|
||||||
await ctx.LineToAsync(yArrowTipX + Math.Cos(yArrowAngle + Math.PI / 6) * arrowHeadSize, yArrowTipY + Math.Sin(yArrowAngle + Math.PI / 6) * arrowHeadSize);
|
|
||||||
await ctx.LineToAsync(yArrowTipX + Math.Cos(yArrowAngle - Math.PI / 6) * arrowHeadSize, yArrowTipY + Math.Sin(yArrowAngle - Math.PI / 6) * arrowHeadSize);
|
|
||||||
await ctx.ClosePathAsync();
|
|
||||||
await ctx.FillAsync(FillRule.NonZero);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task DrawMouseIndicator(Context2D ctx)
|
|
||||||
{
|
|
||||||
await ctx.SaveAsync();
|
|
||||||
|
|
||||||
await ctx.StrokeStyleAsync("rgba(255, 50, 50, 0.8)");
|
|
||||||
await ctx.LineWidthAsync(1);
|
|
||||||
await ctx.SetLineDashAsync(new double[] { 3, 3 });
|
|
||||||
|
|
||||||
await ctx.BeginPathAsync();
|
|
||||||
await ctx.MoveToAsync(MouseX, RulerHeight);
|
|
||||||
await ctx.LineToAsync(MouseX, CanvasHeight);
|
|
||||||
await ctx.StrokeAsync();
|
|
||||||
|
|
||||||
await ctx.BeginPathAsync();
|
|
||||||
await ctx.MoveToAsync(RulerHeight, MouseY);
|
|
||||||
await ctx.LineToAsync(CanvasWidth, MouseY);
|
|
||||||
await ctx.StrokeAsync();
|
|
||||||
|
|
||||||
await ctx.SetLineDashAsync(new double[] { });
|
|
||||||
|
|
||||||
const double labelPadding = 7;
|
|
||||||
const double labelMargin = 8;
|
|
||||||
|
|
||||||
string coordinateText = $"({WorldMouseX:F2}m, {WorldMouseY:F2}m)";
|
|
||||||
|
|
||||||
await ctx.FontAsync("bold 12px Arial");
|
|
||||||
var textMetrics = await ctx.MeasureTextAsync(coordinateText);
|
|
||||||
double textWidth = textMetrics.Width;
|
|
||||||
double textHeight = 16;
|
|
||||||
|
|
||||||
double labelX = MouseX + labelMargin;
|
|
||||||
double labelY = MouseY - textHeight - labelPadding * 2 - labelMargin;
|
|
||||||
|
|
||||||
if (labelX + textWidth + labelPadding * 2 > CanvasWidth)
|
|
||||||
{
|
|
||||||
labelX = MouseX - textWidth - labelPadding * 2 - labelMargin;
|
|
||||||
}
|
|
||||||
if (labelY - textHeight - labelPadding * 2 < RulerHeight)
|
|
||||||
{
|
|
||||||
labelY = MouseY + labelMargin;
|
|
||||||
}
|
|
||||||
|
|
||||||
await ctx.FillStyleAsync("rgba(0, 0, 0, 0.8)");
|
|
||||||
await ctx.FillRectAsync(labelX, labelY, textWidth + labelPadding * 2, textHeight + labelPadding * 2);
|
|
||||||
|
|
||||||
await ctx.StrokeStyleAsync("rgba(255,255,255,0.6)");
|
|
||||||
await ctx.LineWidthAsync(1);
|
|
||||||
await ctx.StrokeRectAsync(labelX, labelY, textWidth + labelPadding * 2, textHeight + labelPadding * 2);
|
|
||||||
|
|
||||||
await ctx.FillStyleAsync("rgba(255, 50, 50, 0.9)");
|
|
||||||
await ctx.BeginPathAsync();
|
|
||||||
await ctx.ArcAsync(MouseX, MouseY, 3, 0, Math.PI * 2);
|
|
||||||
await ctx.FillAsync(FillRule.NonZero);
|
|
||||||
|
|
||||||
await ctx.StrokeStyleAsync("rgba(255, 255, 255, 0.8)");
|
|
||||||
await ctx.LineWidthAsync(2);
|
|
||||||
await ctx.BeginPathAsync();
|
|
||||||
await ctx.ArcAsync(MouseX, MouseY, 6, 0, Math.PI * 2);
|
|
||||||
await ctx.StrokeAsync();
|
|
||||||
|
|
||||||
await ctx.SaveAsync();
|
|
||||||
|
|
||||||
await ctx.TranslateAsync(labelX + labelPadding + textWidth / 2, labelY + textHeight / 2);
|
|
||||||
await ctx.ScaleAsync(1, -1);
|
|
||||||
|
|
||||||
await ctx.FillStyleAsync("white");
|
|
||||||
await ctx.FontAsync("bold 12px Arial");
|
|
||||||
await ctx.TextAlignAsync(TextAlign.Center);
|
|
||||||
await ctx.TextBaseLineAsync(TextBaseLine.Bottom);
|
|
||||||
await ctx.FillTextAsync(coordinateText, 0, 0);
|
|
||||||
|
|
||||||
await ctx.RestoreAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task DrawRulers(Context2D ctx)
|
|
||||||
{
|
|
||||||
double visibleWorldLeft = CanvasToWorldX(0);
|
|
||||||
double visibleWorldRight = CanvasToWorldX(CanvasWidth);
|
|
||||||
double visibleWorldTop = CanvasToWorldY(0);
|
|
||||||
double visibleWorldBottom = CanvasToWorldY(CanvasHeight);
|
|
||||||
|
|
||||||
double scaleInterval = GetRulerScaleInterval();
|
|
||||||
|
|
||||||
await DrawXRuler(ctx, RulerHeight, visibleWorldLeft, visibleWorldRight, scaleInterval);
|
|
||||||
await DrawYRuler(ctx, RulerHeight, visibleWorldTop, visibleWorldBottom, scaleInterval);
|
|
||||||
}
|
|
||||||
|
|
||||||
private double GetRulerScaleInterval()
|
|
||||||
{
|
|
||||||
double pixelsPerMeter = BASE_PIXELS_PER_METER * ZoomScale;
|
|
||||||
|
|
||||||
if (pixelsPerMeter >= 400) return 0.1;
|
|
||||||
else if (pixelsPerMeter >= 200) return 0.2;
|
|
||||||
else if (pixelsPerMeter >= 100) return 0.5;
|
|
||||||
else if (pixelsPerMeter >= 50) return 1.0;
|
|
||||||
else if (pixelsPerMeter >= 25) return 2.0;
|
|
||||||
else if (pixelsPerMeter >= 12) return 5.0;
|
|
||||||
else if (pixelsPerMeter >= 6) return 10.0;
|
|
||||||
else return 20.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task DrawXRuler(Context2D ctx, double rulerHeight, double visibleWorldLeft, double visibleWorldRight, double scaleInterval)
|
|
||||||
{
|
|
||||||
await ctx.FillStyleAsync("rgba(240, 240, 240, 0.9)");
|
|
||||||
await ctx.FillRectAsync(0, 0, CanvasWidth, rulerHeight);
|
|
||||||
|
|
||||||
await ctx.StrokeStyleAsync("rgba(100, 100, 100, 0.8)");
|
|
||||||
await ctx.LineWidthAsync(1);
|
|
||||||
await ctx.BeginPathAsync();
|
|
||||||
await ctx.MoveToAsync(0, rulerHeight);
|
|
||||||
await ctx.LineToAsync(CanvasWidth, rulerHeight);
|
|
||||||
await ctx.StrokeAsync();
|
|
||||||
|
|
||||||
double startWorld = Math.Floor(visibleWorldLeft / scaleInterval) * scaleInterval;
|
|
||||||
double endWorld = Math.Ceiling(visibleWorldRight / scaleInterval) * scaleInterval;
|
|
||||||
|
|
||||||
startWorld -= scaleInterval;
|
|
||||||
endWorld += scaleInterval;
|
|
||||||
|
|
||||||
for (double worldX = startWorld; worldX <= endWorld; worldX += scaleInterval)
|
|
||||||
{
|
|
||||||
double canvasX = WorldToCanvasX(worldX);
|
|
||||||
|
|
||||||
if (canvasX < -50 || canvasX > CanvasWidth + 50) continue;
|
|
||||||
|
|
||||||
bool isMajorTick = IsNearMultiple(worldX, scaleInterval * 2) || Math.Abs(worldX) < 0.001;
|
|
||||||
double tickHeight = isMajorTick ? rulerHeight * 0.4 : rulerHeight * 0.2;
|
|
||||||
|
|
||||||
await ctx.StrokeStyleAsync("rgba(60, 60, 60, 0.8)");
|
|
||||||
await ctx.LineWidthAsync(1);
|
|
||||||
await ctx.BeginPathAsync();
|
|
||||||
await ctx.MoveToAsync(canvasX, rulerHeight);
|
|
||||||
await ctx.LineToAsync(canvasX, rulerHeight - tickHeight);
|
|
||||||
await ctx.StrokeAsync();
|
|
||||||
|
|
||||||
if (isMajorTick && canvasX >= -20 && canvasX <= CanvasWidth + 20)
|
|
||||||
{
|
|
||||||
await ctx.SaveAsync();
|
|
||||||
|
|
||||||
await ctx.TranslateAsync(canvasX, rulerHeight - tickHeight - 8);
|
|
||||||
await ctx.ScaleAsync(1, -1);
|
|
||||||
|
|
||||||
await ctx.FillStyleAsync("blue");
|
|
||||||
await ctx.FontAsync("bold 10px Arial");
|
|
||||||
await ctx.TextAlignAsync(TextAlign.Center);
|
|
||||||
|
|
||||||
string labelText = FormatRulerLabel(worldX, scaleInterval);
|
|
||||||
await ctx.FillTextAsync(labelText, 0, 0);
|
|
||||||
|
|
||||||
await ctx.RestoreAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task DrawYRuler(Context2D ctx, double rulerWidth, double visibleWorldTop, double visibleWorldBottom, double scaleInterval)
|
|
||||||
{
|
|
||||||
await ctx.FillStyleAsync("rgba(240, 240, 240, 0.9)");
|
|
||||||
await ctx.FillRectAsync(0, 0, rulerWidth, CanvasHeight);
|
|
||||||
|
|
||||||
await ctx.StrokeStyleAsync("rgba(100, 100, 100, 0.8)");
|
|
||||||
await ctx.LineWidthAsync(1);
|
|
||||||
await ctx.BeginPathAsync();
|
|
||||||
await ctx.MoveToAsync(rulerWidth, 0);
|
|
||||||
await ctx.LineToAsync(rulerWidth, CanvasHeight);
|
|
||||||
await ctx.StrokeAsync();
|
|
||||||
|
|
||||||
double startWorld = Math.Floor(visibleWorldTop / scaleInterval) * scaleInterval;
|
|
||||||
double endWorld = Math.Ceiling(visibleWorldBottom / scaleInterval) * scaleInterval;
|
|
||||||
|
|
||||||
startWorld -= scaleInterval;
|
|
||||||
endWorld += scaleInterval;
|
|
||||||
|
|
||||||
for (double worldY = startWorld; worldY <= endWorld; worldY += scaleInterval)
|
|
||||||
{
|
|
||||||
double canvasY = WorldToCanvasY(worldY);
|
|
||||||
|
|
||||||
if (canvasY < -50 || canvasY > CanvasHeight + 50) continue;
|
|
||||||
|
|
||||||
bool isMajorTick = IsNearMultiple(worldY, scaleInterval * 2) || Math.Abs(worldY) < 0.001;
|
|
||||||
double tickWidth = isMajorTick ? rulerWidth * 0.4 : rulerWidth * 0.2;
|
|
||||||
|
|
||||||
await ctx.StrokeStyleAsync("rgba(60, 60, 60, 0.8)");
|
|
||||||
await ctx.LineWidthAsync(1);
|
|
||||||
await ctx.BeginPathAsync();
|
|
||||||
await ctx.MoveToAsync(rulerWidth, canvasY);
|
|
||||||
await ctx.LineToAsync(rulerWidth - tickWidth, canvasY);
|
|
||||||
await ctx.StrokeAsync();
|
|
||||||
|
|
||||||
if (isMajorTick && canvasY >= -20 && canvasY <= CanvasHeight + 20)
|
|
||||||
{
|
|
||||||
await ctx.SaveAsync();
|
|
||||||
|
|
||||||
await ctx.TranslateAsync(rulerWidth - tickWidth - 2, canvasY);
|
|
||||||
await ctx.ScaleAsync(1, -1);
|
|
||||||
await ctx.RotateAsync(-Math.PI / 2);
|
|
||||||
|
|
||||||
await ctx.FillStyleAsync("blue");
|
|
||||||
await ctx.FontAsync("bold 10px Arial");
|
|
||||||
await ctx.TextAlignAsync(TextAlign.Center);
|
|
||||||
|
|
||||||
string labelText = FormatRulerLabel(worldY, scaleInterval);
|
|
||||||
await ctx.FillTextAsync(labelText, 0, 0);
|
|
||||||
|
|
||||||
await ctx.RestoreAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool IsNearMultiple(double value, double multiple)
|
|
||||||
{
|
|
||||||
if (multiple == 0) return false;
|
|
||||||
double remainder = Math.Abs(value % multiple);
|
|
||||||
double epsilon = multiple * 0.001;
|
|
||||||
return remainder < epsilon || remainder > multiple - epsilon;
|
|
||||||
}
|
|
||||||
|
|
||||||
private string FormatRulerLabel(double worldValue, double scaleInterval)
|
|
||||||
{
|
|
||||||
return scaleInterval < 1.0 ? $"{worldValue:F1}m" : $"{worldValue:F0}m";
|
|
||||||
}
|
|
||||||
|
|
||||||
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, 8 / ZoomScale, 0, Math.PI * 2);
|
|
||||||
await ctx.FillAsync(FillRule.NonZero);
|
|
||||||
|
|
||||||
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 - arrowHeadSize, originCanvasY);
|
|
||||||
await ctx.StrokeAsync();
|
|
||||||
|
|
||||||
await ctx.BeginPathAsync();
|
|
||||||
double xArrowTipX = originCanvasX + arrowLength;
|
|
||||||
double xArrowTipY = originCanvasY;
|
|
||||||
await ctx.MoveToAsync(xArrowTipX, xArrowTipY);
|
|
||||||
await ctx.LineToAsync(xArrowTipX - arrowHeadSize, xArrowTipY - arrowHeadSize / 2);
|
|
||||||
await ctx.LineToAsync(xArrowTipX - arrowHeadSize, xArrowTipY + 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 - arrowHeadSize);
|
|
||||||
await ctx.StrokeAsync();
|
|
||||||
|
|
||||||
await ctx.BeginPathAsync();
|
|
||||||
double yArrowTipX = originCanvasX;
|
|
||||||
double yArrowTipY = originCanvasY + arrowLength;
|
|
||||||
await ctx.MoveToAsync(yArrowTipX, yArrowTipY);
|
|
||||||
await ctx.LineToAsync(yArrowTipX - arrowHeadSize / 2, yArrowTipY - arrowHeadSize);
|
|
||||||
await ctx.LineToAsync(yArrowTipX + arrowHeadSize / 2, yArrowTipY - 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()
|
|
||||||
{
|
|
||||||
double PixelsPerMeter = BASE_PIXELS_PER_METER * ZoomScale;
|
|
||||||
if (PixelsPerMeter >= 300) return 0.2;
|
|
||||||
else if (PixelsPerMeter >= 150) return 0.5;
|
|
||||||
else if (PixelsPerMeter >= 75) return 1.0;
|
|
||||||
else if (PixelsPerMeter >= 40) return 2.0;
|
|
||||||
else if (PixelsPerMeter >= 20) return 5.0;
|
|
||||||
else if (PixelsPerMeter >= 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)
|
private async Task HandleMouseMove(MouseEventArgs e)
|
||||||
|
|
@ -751,21 +266,136 @@
|
||||||
await DrawCanvas();
|
await DrawCanvas();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task ResetView()
|
private async Task HandleTouchMove(TouchEventArgs e)
|
||||||
{
|
{
|
||||||
CanvasTranslateX = CanvasWidth / 2;
|
if (e.Touches.Length == 1)
|
||||||
CanvasTranslateY = CanvasHeight / 2;
|
{
|
||||||
ZoomScale = 1.0;
|
await HandleSingleTouchMove(e.Touches[0]);
|
||||||
|
}
|
||||||
|
else if (e.Touches.Length == 2)
|
||||||
|
{
|
||||||
|
await HandlePinchZoom(e.Touches[0], e.Touches[1]);
|
||||||
|
}
|
||||||
|
|
||||||
StateHasChanged();
|
StateHasChanged();
|
||||||
await DrawCanvas();
|
await DrawCanvas();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task ReloadMapImage()
|
private void HandleTouchStart(TouchEventArgs e)
|
||||||
{
|
{
|
||||||
MapImageLoaded = false;
|
IsTouching = true;
|
||||||
await LoadMapImage();
|
|
||||||
await DrawCanvas();
|
if (e.Touches.Length == 1)
|
||||||
StateHasChanged();
|
{
|
||||||
|
LastTouchPoint = new TouchPoint
|
||||||
|
{
|
||||||
|
X = e.Touches[0].ClientX,
|
||||||
|
Y = e.Touches[0].ClientY
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else if (e.Touches.Length == 2)
|
||||||
|
{
|
||||||
|
LastTouchPoint = new TouchPoint
|
||||||
|
{
|
||||||
|
X = e.Touches[0].ClientX,
|
||||||
|
Y = e.Touches[0].ClientY
|
||||||
|
};
|
||||||
|
LastSecondTouchPoint = new TouchPoint
|
||||||
|
{
|
||||||
|
X = e.Touches[1].ClientX,
|
||||||
|
Y = e.Touches[1].ClientY
|
||||||
|
};
|
||||||
|
|
||||||
|
LastTouchDistance = CalculateTouchDistance(LastTouchPoint, LastSecondTouchPoint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleTouchEnd(TouchEventArgs e)
|
||||||
|
{
|
||||||
|
IsTouching = false;
|
||||||
|
LastTouchPoint = null;
|
||||||
|
LastSecondTouchPoint = null;
|
||||||
|
LastTouchDistance = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task HandleSingleTouchMove(Microsoft.AspNetCore.Components.Web.TouchPoint touch)
|
||||||
|
{
|
||||||
|
if (LastTouchPoint == null) return;
|
||||||
|
|
||||||
|
var currentPoint = new TouchPoint
|
||||||
|
{
|
||||||
|
X = touch.ClientX,
|
||||||
|
Y = touch.ClientY
|
||||||
|
};
|
||||||
|
|
||||||
|
double deltaX = currentPoint.X - LastTouchPoint.X;
|
||||||
|
double deltaY = currentPoint.Y - LastTouchPoint.Y;
|
||||||
|
|
||||||
|
CanvasTranslateX += deltaX;
|
||||||
|
CanvasTranslateY += deltaY;
|
||||||
|
|
||||||
|
LastTouchPoint = currentPoint;
|
||||||
|
|
||||||
|
var canvasRect = await JS.InvokeAsync<DomRect>("getElementBoundingRect", CanvasRef);
|
||||||
|
MouseX = currentPoint.X - canvasRect.X;
|
||||||
|
MouseY = currentPoint.Y - canvasRect.Y;
|
||||||
|
IsMouseInCanvas = true;
|
||||||
|
|
||||||
|
WorldMouseX = CanvasToWorldX(MouseX);
|
||||||
|
WorldMouseY = CanvasToWorldY(MouseY);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task HandlePinchZoom(Microsoft.AspNetCore.Components.Web.TouchPoint touch1, Microsoft.AspNetCore.Components.Web.TouchPoint touch2)
|
||||||
|
{
|
||||||
|
if (LastTouchPoint == null || LastSecondTouchPoint == null) return;
|
||||||
|
|
||||||
|
var currentTouch1 = new TouchPoint { X = touch1.ClientX, Y = touch1.ClientY };
|
||||||
|
var currentTouch2 = new TouchPoint { X = touch2.ClientX, Y = touch2.ClientY };
|
||||||
|
|
||||||
|
double currentDistance = CalculateTouchDistance(currentTouch1, currentTouch2);
|
||||||
|
|
||||||
|
if (LastTouchDistance > 0)
|
||||||
|
{
|
||||||
|
double distanceRatio = currentDistance / LastTouchDistance;
|
||||||
|
double oldZoom = ZoomScale;
|
||||||
|
|
||||||
|
ZoomScale = Math.Max(MIN_ZOOM, Math.Min(MAX_ZOOM, ZoomScale * distanceRatio));
|
||||||
|
|
||||||
|
if (Math.Abs(ZoomScale - oldZoom) > 0.001)
|
||||||
|
{
|
||||||
|
double centerX = (currentTouch1.X + currentTouch2.X) / 2;
|
||||||
|
double centerY = (currentTouch1.Y + currentTouch2.Y) / 2;
|
||||||
|
|
||||||
|
var canvasRect = await JS.InvokeAsync<DomRect>("getElementBoundingRect", CanvasRef);
|
||||||
|
double canvasCenterX = centerX - canvasRect.X;
|
||||||
|
double canvasCenterY = centerY - canvasRect.Y;
|
||||||
|
|
||||||
|
ZoomAtPoint(oldZoom, canvasCenterX, canvasCenterY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LastTouchDistance = currentDistance;
|
||||||
|
LastTouchPoint = currentTouch1;
|
||||||
|
LastSecondTouchPoint = currentTouch2;
|
||||||
|
}
|
||||||
|
|
||||||
|
private double CalculateTouchDistance(TouchPoint point1, TouchPoint point2)
|
||||||
|
{
|
||||||
|
double deltaX = point2.X - point1.X;
|
||||||
|
double deltaY = point2.Y - point1.Y;
|
||||||
|
return Math.Sqrt(deltaX * deltaX + deltaY * deltaY);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ZoomAtPoint(double oldZoom, double pointX, double pointY)
|
||||||
|
{
|
||||||
|
double pointWorldX = (pointX - CanvasTranslateX) / oldZoom / BASE_PIXELS_PER_METER - OriginX;
|
||||||
|
double pointWorldY = (pointY - CanvasTranslateY) / oldZoom / BASE_PIXELS_PER_METER - OriginY;
|
||||||
|
|
||||||
|
double newPointCanvasX = (pointWorldX + OriginX) * BASE_PIXELS_PER_METER * ZoomScale;
|
||||||
|
double newPointCanvasY = (pointWorldY + OriginY) * BASE_PIXELS_PER_METER * ZoomScale;
|
||||||
|
|
||||||
|
CanvasTranslateX = pointX - newPointCanvasX;
|
||||||
|
CanvasTranslateY = pointY - newPointCanvasY;
|
||||||
}
|
}
|
||||||
|
|
||||||
private LaserScanData GenerateLaserScanData()
|
private LaserScanData GenerateLaserScanData()
|
||||||
|
|
@ -779,7 +409,7 @@
|
||||||
|
|
||||||
// Laser scanner parameters
|
// Laser scanner parameters
|
||||||
const double maxRange = 8.0; // meters
|
const double maxRange = 8.0; // meters
|
||||||
const double minRange = 0.2; // meters (fix: was 7.0, should be minimum)
|
const double minRange = 0.5; // meters (fix: was 7.0, should be minimum)
|
||||||
const int numPoints = 270; // Number of laser points
|
const int numPoints = 270; // Number of laser points
|
||||||
const double startAngle = -Math.PI / 2 - Math.PI / 4;
|
const double startAngle = -Math.PI / 2 - Math.PI / 4;
|
||||||
const double endAngle = Math.PI / 2 + Math.PI / 4;
|
const double endAngle = Math.PI / 2 + Math.PI / 4;
|
||||||
|
|
@ -830,30 +460,4 @@
|
||||||
|
|
||||||
return scanData;
|
return scanData;
|
||||||
}
|
}
|
||||||
|
|
||||||
public class LaserScanPoint
|
|
||||||
{
|
|
||||||
public double X { get; set; }
|
|
||||||
public double Y { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class LaserScanData
|
|
||||||
{
|
|
||||||
public double RobotX { get; set; }
|
|
||||||
public double RobotY { get; set; }
|
|
||||||
public double RobotOrientation { get; set; }
|
|
||||||
public List<LaserScanPoint> Points { get; set; } = new();
|
|
||||||
}
|
|
||||||
|
|
||||||
public class DomRect
|
|
||||||
{
|
|
||||||
public double Width { get; set; }
|
|
||||||
public double Height { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class ImageDimensions
|
|
||||||
{
|
|
||||||
public double Width { get; set; }
|
|
||||||
public double Height { get; set; }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
576
RobotApp.Client/Pages/Components/Mapping/MapView.razor.cs
Normal file
576
RobotApp.Client/Pages/Components/Mapping/MapView.razor.cs
Normal file
|
|
@ -0,0 +1,576 @@
|
||||||
|
using Excubo.Blazor.Canvas;
|
||||||
|
using Excubo.Blazor.Canvas.Contexts;
|
||||||
|
using Microsoft.JSInterop;
|
||||||
|
|
||||||
|
namespace RobotApp.Client.Pages.Components.Mapping;
|
||||||
|
|
||||||
|
public partial class MapView
|
||||||
|
{
|
||||||
|
public class TouchPoint
|
||||||
|
{
|
||||||
|
public double X { get; set; }
|
||||||
|
public double Y { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class LaserScanPoint
|
||||||
|
{
|
||||||
|
public double X { get; set; }
|
||||||
|
public double Y { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class LaserScanData
|
||||||
|
{
|
||||||
|
public double RobotX { get; set; }
|
||||||
|
public double RobotY { get; set; }
|
||||||
|
public double RobotOrientation { get; set; }
|
||||||
|
public List<LaserScanPoint> Points { get; set; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public class DomRect
|
||||||
|
{
|
||||||
|
public double Width { get; set; }
|
||||||
|
public double Height { get; set; }
|
||||||
|
public double X { get; set; }
|
||||||
|
public double Y { get; set; }
|
||||||
|
public double Left => X;
|
||||||
|
public double Top => Y;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 DrawMapImage(ctx);
|
||||||
|
await DrawGrid(ctx);
|
||||||
|
await DrawAxes(ctx);
|
||||||
|
await DrawLaserScannerPoints(ctx);
|
||||||
|
await ctx.RestoreAsync();
|
||||||
|
|
||||||
|
if (IsMouseInCanvas)
|
||||||
|
{
|
||||||
|
await DrawMouseIndicator(ctx);
|
||||||
|
}
|
||||||
|
await DrawRulers(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task DrawMouseIndicator(Context2D ctx)
|
||||||
|
{
|
||||||
|
await ctx.SaveAsync();
|
||||||
|
|
||||||
|
await ctx.StrokeStyleAsync("rgba(255, 50, 50, 0.8)");
|
||||||
|
await ctx.LineWidthAsync(1);
|
||||||
|
await ctx.SetLineDashAsync([3, 3]);
|
||||||
|
|
||||||
|
await ctx.BeginPathAsync();
|
||||||
|
await ctx.MoveToAsync(MouseX, RulerHeight);
|
||||||
|
await ctx.LineToAsync(MouseX, CanvasHeight);
|
||||||
|
await ctx.StrokeAsync();
|
||||||
|
|
||||||
|
await ctx.BeginPathAsync();
|
||||||
|
await ctx.MoveToAsync(RulerHeight, MouseY);
|
||||||
|
await ctx.LineToAsync(CanvasWidth, MouseY);
|
||||||
|
await ctx.StrokeAsync();
|
||||||
|
|
||||||
|
await ctx.SetLineDashAsync([]);
|
||||||
|
|
||||||
|
const double labelPadding = 7;
|
||||||
|
const double labelMargin = 8;
|
||||||
|
|
||||||
|
string coordinateText = $"({WorldMouseX:F2}m, {WorldMouseY:F2}m)";
|
||||||
|
|
||||||
|
await ctx.FontAsync("bold 12px Arial");
|
||||||
|
var textMetrics = await ctx.MeasureTextAsync(coordinateText);
|
||||||
|
double textWidth = textMetrics.Width;
|
||||||
|
double textHeight = 16;
|
||||||
|
|
||||||
|
double labelX = MouseX + labelMargin;
|
||||||
|
double labelY = MouseY - textHeight - labelPadding * 2 - labelMargin;
|
||||||
|
|
||||||
|
if (labelX + textWidth + labelPadding * 2 > CanvasWidth)
|
||||||
|
{
|
||||||
|
labelX = MouseX - textWidth - labelPadding * 2 - labelMargin;
|
||||||
|
}
|
||||||
|
if (labelY - textHeight - labelPadding * 2 < RulerHeight)
|
||||||
|
{
|
||||||
|
labelY = MouseY + labelMargin;
|
||||||
|
}
|
||||||
|
|
||||||
|
await ctx.FillStyleAsync("rgba(0, 0, 0, 0.8)");
|
||||||
|
await ctx.FillRectAsync(labelX, labelY, textWidth + labelPadding * 2, textHeight + labelPadding * 2);
|
||||||
|
|
||||||
|
await ctx.StrokeStyleAsync("rgba(255,255,255,0.6)");
|
||||||
|
await ctx.LineWidthAsync(1);
|
||||||
|
await ctx.StrokeRectAsync(labelX, labelY, textWidth + labelPadding * 2, textHeight + labelPadding * 2);
|
||||||
|
|
||||||
|
await ctx.FillStyleAsync("rgba(255, 50, 50, 0.9)");
|
||||||
|
await ctx.BeginPathAsync();
|
||||||
|
await ctx.ArcAsync(MouseX, MouseY, 3, 0, Math.PI * 2);
|
||||||
|
await ctx.FillAsync(FillRule.NonZero);
|
||||||
|
|
||||||
|
await ctx.StrokeStyleAsync("rgba(255, 255, 255, 0.8)");
|
||||||
|
await ctx.LineWidthAsync(2);
|
||||||
|
await ctx.BeginPathAsync();
|
||||||
|
await ctx.ArcAsync(MouseX, MouseY, 6, 0, Math.PI * 2);
|
||||||
|
await ctx.StrokeAsync();
|
||||||
|
|
||||||
|
await ctx.SaveAsync();
|
||||||
|
|
||||||
|
await ctx.TranslateAsync(labelX + labelPadding + textWidth / 2, labelY + textHeight / 2);
|
||||||
|
await ctx.ScaleAsync(1, -1);
|
||||||
|
|
||||||
|
await ctx.FillStyleAsync("white");
|
||||||
|
await ctx.FontAsync("bold 12px Arial");
|
||||||
|
await ctx.TextAlignAsync(TextAlign.Center);
|
||||||
|
await ctx.TextBaseLineAsync(TextBaseLine.Bottom);
|
||||||
|
await ctx.FillTextAsync(coordinateText, 0, 0);
|
||||||
|
|
||||||
|
await ctx.RestoreAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task DrawRulers(Context2D ctx)
|
||||||
|
{
|
||||||
|
double visibleWorldLeft = CanvasToWorldX(0);
|
||||||
|
double visibleWorldRight = CanvasToWorldX(CanvasWidth);
|
||||||
|
double visibleWorldTop = CanvasToWorldY(0);
|
||||||
|
double visibleWorldBottom = CanvasToWorldY(CanvasHeight);
|
||||||
|
|
||||||
|
double scaleInterval = GetRulerScaleInterval();
|
||||||
|
|
||||||
|
await DrawXRuler(ctx, RulerHeight, visibleWorldLeft, visibleWorldRight, scaleInterval);
|
||||||
|
await DrawYRuler(ctx, RulerHeight, visibleWorldTop, visibleWorldBottom, scaleInterval);
|
||||||
|
}
|
||||||
|
|
||||||
|
private double GetRulerScaleInterval()
|
||||||
|
{
|
||||||
|
double pixelsPerMeter = BASE_PIXELS_PER_METER * ZoomScale;
|
||||||
|
|
||||||
|
if (pixelsPerMeter >= 400) return 0.1;
|
||||||
|
else if (pixelsPerMeter >= 200) return 0.2;
|
||||||
|
else if (pixelsPerMeter >= 100) return 0.5;
|
||||||
|
else if (pixelsPerMeter >= 50) return 1.0;
|
||||||
|
else if (pixelsPerMeter >= 25) return 2.0;
|
||||||
|
else if (pixelsPerMeter >= 12) return 5.0;
|
||||||
|
else if (pixelsPerMeter >= 6) return 10.0;
|
||||||
|
else return 20.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task DrawXRuler(Context2D ctx, double rulerHeight, double visibleWorldLeft, double visibleWorldRight, double scaleInterval)
|
||||||
|
{
|
||||||
|
await ctx.FillStyleAsync("rgba(240, 240, 240, 0.9)");
|
||||||
|
await ctx.FillRectAsync(0, 0, CanvasWidth, rulerHeight);
|
||||||
|
|
||||||
|
await ctx.StrokeStyleAsync("rgba(100, 100, 100, 0.8)");
|
||||||
|
await ctx.LineWidthAsync(1);
|
||||||
|
await ctx.BeginPathAsync();
|
||||||
|
await ctx.MoveToAsync(0, rulerHeight);
|
||||||
|
await ctx.LineToAsync(CanvasWidth, rulerHeight);
|
||||||
|
await ctx.StrokeAsync();
|
||||||
|
|
||||||
|
double startWorld = Math.Floor(visibleWorldLeft / scaleInterval) * scaleInterval;
|
||||||
|
double endWorld = Math.Ceiling(visibleWorldRight / scaleInterval) * scaleInterval;
|
||||||
|
|
||||||
|
startWorld -= scaleInterval;
|
||||||
|
endWorld += scaleInterval;
|
||||||
|
|
||||||
|
for (double worldX = startWorld; worldX <= endWorld; worldX += scaleInterval)
|
||||||
|
{
|
||||||
|
double canvasX = WorldToCanvasX(worldX);
|
||||||
|
|
||||||
|
if (canvasX < -50 || canvasX > CanvasWidth + 50) continue;
|
||||||
|
|
||||||
|
bool isMajorTick = IsNearMultiple(worldX, scaleInterval * 2) || Math.Abs(worldX) < 0.001;
|
||||||
|
double tickHeight = isMajorTick ? rulerHeight * 0.4 : rulerHeight * 0.2;
|
||||||
|
|
||||||
|
await ctx.StrokeStyleAsync("rgba(60, 60, 60, 0.8)");
|
||||||
|
await ctx.LineWidthAsync(1);
|
||||||
|
await ctx.BeginPathAsync();
|
||||||
|
await ctx.MoveToAsync(canvasX, rulerHeight);
|
||||||
|
await ctx.LineToAsync(canvasX, rulerHeight - tickHeight);
|
||||||
|
await ctx.StrokeAsync();
|
||||||
|
|
||||||
|
if (isMajorTick && canvasX >= -20 && canvasX <= CanvasWidth + 20)
|
||||||
|
{
|
||||||
|
await ctx.SaveAsync();
|
||||||
|
|
||||||
|
await ctx.TranslateAsync(canvasX, rulerHeight - tickHeight - 8);
|
||||||
|
await ctx.ScaleAsync(1, -1);
|
||||||
|
|
||||||
|
await ctx.FillStyleAsync("blue");
|
||||||
|
await ctx.FontAsync("bold 10px Arial");
|
||||||
|
await ctx.TextAlignAsync(TextAlign.Center);
|
||||||
|
|
||||||
|
string labelText = FormatRulerLabel(worldX, scaleInterval);
|
||||||
|
await ctx.FillTextAsync(labelText, 0, 0);
|
||||||
|
|
||||||
|
await ctx.RestoreAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task DrawYRuler(Context2D ctx, double rulerWidth, double visibleWorldTop, double visibleWorldBottom, double scaleInterval)
|
||||||
|
{
|
||||||
|
await ctx.FillStyleAsync("rgba(240, 240, 240, 0.9)");
|
||||||
|
await ctx.FillRectAsync(0, 0, rulerWidth, CanvasHeight);
|
||||||
|
|
||||||
|
await ctx.StrokeStyleAsync("rgba(100, 100, 100, 0.8)");
|
||||||
|
await ctx.LineWidthAsync(1);
|
||||||
|
await ctx.BeginPathAsync();
|
||||||
|
await ctx.MoveToAsync(rulerWidth, 0);
|
||||||
|
await ctx.LineToAsync(rulerWidth, CanvasHeight);
|
||||||
|
await ctx.StrokeAsync();
|
||||||
|
|
||||||
|
double startWorld = Math.Floor(visibleWorldTop / scaleInterval) * scaleInterval;
|
||||||
|
double endWorld = Math.Ceiling(visibleWorldBottom / scaleInterval) * scaleInterval;
|
||||||
|
|
||||||
|
startWorld -= scaleInterval;
|
||||||
|
endWorld += scaleInterval;
|
||||||
|
|
||||||
|
for (double worldY = startWorld; worldY <= endWorld; worldY += scaleInterval)
|
||||||
|
{
|
||||||
|
double canvasY = WorldToCanvasY(worldY);
|
||||||
|
|
||||||
|
if (canvasY < -50 || canvasY > CanvasHeight + 50) continue;
|
||||||
|
|
||||||
|
bool isMajorTick = IsNearMultiple(worldY, scaleInterval * 2) || Math.Abs(worldY) < 0.001;
|
||||||
|
double tickWidth = isMajorTick ? rulerWidth * 0.4 : rulerWidth * 0.2;
|
||||||
|
|
||||||
|
await ctx.StrokeStyleAsync("rgba(60, 60, 60, 0.8)");
|
||||||
|
await ctx.LineWidthAsync(1);
|
||||||
|
await ctx.BeginPathAsync();
|
||||||
|
await ctx.MoveToAsync(rulerWidth, canvasY);
|
||||||
|
await ctx.LineToAsync(rulerWidth - tickWidth, canvasY);
|
||||||
|
await ctx.StrokeAsync();
|
||||||
|
|
||||||
|
if (isMajorTick && canvasY >= -20 && canvasY <= CanvasHeight + 20)
|
||||||
|
{
|
||||||
|
await ctx.SaveAsync();
|
||||||
|
|
||||||
|
await ctx.TranslateAsync(rulerWidth - tickWidth - 2, canvasY);
|
||||||
|
await ctx.ScaleAsync(1, -1);
|
||||||
|
await ctx.RotateAsync(-Math.PI / 2);
|
||||||
|
|
||||||
|
await ctx.FillStyleAsync("blue");
|
||||||
|
await ctx.FontAsync("bold 10px Arial");
|
||||||
|
await ctx.TextAlignAsync(TextAlign.Center);
|
||||||
|
|
||||||
|
string labelText = FormatRulerLabel(worldY, scaleInterval);
|
||||||
|
await ctx.FillTextAsync(labelText, 0, 0);
|
||||||
|
|
||||||
|
await ctx.RestoreAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsNearMultiple(double value, double multiple)
|
||||||
|
{
|
||||||
|
if (multiple == 0) return false;
|
||||||
|
double remainder = Math.Abs(value % multiple);
|
||||||
|
double epsilon = multiple * 0.001;
|
||||||
|
return remainder < epsilon || remainder > multiple - epsilon;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string FormatRulerLabel(double worldValue, double scaleInterval)
|
||||||
|
{
|
||||||
|
return scaleInterval < 1.0 ? $"{worldValue:F1}m" : $"{worldValue:F0}m";
|
||||||
|
}
|
||||||
|
|
||||||
|
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, 8 / ZoomScale, 0, Math.PI * 2);
|
||||||
|
await ctx.FillAsync(FillRule.NonZero);
|
||||||
|
|
||||||
|
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 - arrowHeadSize, originCanvasY);
|
||||||
|
await ctx.StrokeAsync();
|
||||||
|
|
||||||
|
await ctx.BeginPathAsync();
|
||||||
|
double xArrowTipX = originCanvasX + arrowLength;
|
||||||
|
double xArrowTipY = originCanvasY;
|
||||||
|
await ctx.MoveToAsync(xArrowTipX, xArrowTipY);
|
||||||
|
await ctx.LineToAsync(xArrowTipX - arrowHeadSize, xArrowTipY - arrowHeadSize / 2);
|
||||||
|
await ctx.LineToAsync(xArrowTipX - arrowHeadSize, xArrowTipY + 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 - arrowHeadSize);
|
||||||
|
await ctx.StrokeAsync();
|
||||||
|
|
||||||
|
await ctx.BeginPathAsync();
|
||||||
|
double yArrowTipX = originCanvasX;
|
||||||
|
double yArrowTipY = originCanvasY + arrowLength;
|
||||||
|
await ctx.MoveToAsync(yArrowTipX, yArrowTipY);
|
||||||
|
await ctx.LineToAsync(yArrowTipX - arrowHeadSize / 2, yArrowTipY - arrowHeadSize);
|
||||||
|
await ctx.LineToAsync(yArrowTipX + arrowHeadSize / 2, yArrowTipY - 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([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([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private double GetGridSpacingMeters()
|
||||||
|
{
|
||||||
|
double PixelsPerMeter = BASE_PIXELS_PER_METER * ZoomScale;
|
||||||
|
if (PixelsPerMeter >= 300) return 0.2;
|
||||||
|
else if (PixelsPerMeter >= 150) return 0.5;
|
||||||
|
else if (PixelsPerMeter >= 75) return 1.0;
|
||||||
|
else if (PixelsPerMeter >= 40) return 2.0;
|
||||||
|
else if (PixelsPerMeter >= 20) return 5.0;
|
||||||
|
else if (PixelsPerMeter >= 10) return 10.0;
|
||||||
|
else return 20.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task DrawMapImage(Context2D ctx)
|
||||||
|
{
|
||||||
|
if (!MapImageLoaded)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await ctx.SaveAsync();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
double imageWidthCanvas = MapImageWidth * BASE_PIXELS_PER_METER;
|
||||||
|
double imageHeightCanvas = MapImageHeight * BASE_PIXELS_PER_METER;
|
||||||
|
|
||||||
|
double mapCanvasX = ImageX * BASE_PIXELS_PER_METER;
|
||||||
|
double mapCanvasY = (ImageY + MapImageHeight) * BASE_PIXELS_PER_METER;
|
||||||
|
|
||||||
|
bool success = await JS.InvokeAsync<bool>("drawCachedImageOnCanvas",
|
||||||
|
CanvasRef,
|
||||||
|
MAP_CACHE_KEY,
|
||||||
|
mapCanvasX,
|
||||||
|
mapCanvasY - imageHeightCanvas,
|
||||||
|
imageWidthCanvas,
|
||||||
|
imageHeightCanvas);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
await ctx.RestoreAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task DrawLaserScannerPoints(Context2D ctx)
|
||||||
|
{
|
||||||
|
var scanData = GenerateLaserScanData();
|
||||||
|
|
||||||
|
double robotCanvasX = scanData.RobotX * BASE_PIXELS_PER_METER;
|
||||||
|
double robotCanvasY = scanData.RobotY * BASE_PIXELS_PER_METER;
|
||||||
|
|
||||||
|
await ctx.SaveAsync();
|
||||||
|
|
||||||
|
if (scanData.Points.Count > 0)
|
||||||
|
{
|
||||||
|
await ctx.BeginPathAsync();
|
||||||
|
|
||||||
|
for (int i = 0; i < scanData.Points.Count; i++)
|
||||||
|
{
|
||||||
|
var point = scanData.Points[i];
|
||||||
|
double pointCanvasX = point.X * BASE_PIXELS_PER_METER;
|
||||||
|
double pointCanvasY = point.Y * BASE_PIXELS_PER_METER;
|
||||||
|
|
||||||
|
if (i == 0)
|
||||||
|
{
|
||||||
|
await ctx.MoveToAsync(pointCanvasX, pointCanvasY);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await ctx.LineToAsync(pointCanvasX, pointCanvasY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await ctx.StrokeStyleAsync("rgba(255, 100, 100, 0.8)");
|
||||||
|
await ctx.LineWidthAsync(2 / ZoomScale);
|
||||||
|
await ctx.StrokeAsync();
|
||||||
|
|
||||||
|
await ctx.LineToAsync(robotCanvasX, robotCanvasY);
|
||||||
|
await ctx.ClosePathAsync();
|
||||||
|
await ctx.FillStyleAsync("rgba(255, 100, 100, 0.1)");
|
||||||
|
await ctx.FillAsync(FillRule.NonZero);
|
||||||
|
}
|
||||||
|
|
||||||
|
await DrawRobotImage(ctx, robotCanvasX, robotCanvasY, scanData.RobotOrientation);
|
||||||
|
await DrawRobotOrientationArrows(ctx, robotCanvasX, robotCanvasY, scanData.RobotOrientation);
|
||||||
|
|
||||||
|
await ctx.RestoreAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task DrawRobotImage(Context2D ctx, double robotCanvasX, double robotCanvasY, double robotOrientation)
|
||||||
|
{
|
||||||
|
if (!RobotImageLoaded)
|
||||||
|
{
|
||||||
|
await ctx.FillStyleAsync("rgba(0, 255, 0, 0.8)");
|
||||||
|
await ctx.BeginPathAsync();
|
||||||
|
await ctx.ArcAsync(robotCanvasX, robotCanvasY, 8 / ZoomScale, 0, Math.PI * 2);
|
||||||
|
await ctx.FillAsync(FillRule.NonZero);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await ctx.SaveAsync();
|
||||||
|
|
||||||
|
double robotWidthPixels = RobotWidth * BASE_PIXELS_PER_METER;
|
||||||
|
double robotLengthPixels = RobotLength * BASE_PIXELS_PER_METER;
|
||||||
|
|
||||||
|
double scaledWidth = ZoomScale < 1 ? robotWidthPixels / ZoomScale : robotWidthPixels;
|
||||||
|
double scaledLength = ZoomScale < 1 ? robotLengthPixels / ZoomScale : robotLengthPixels;
|
||||||
|
|
||||||
|
await ctx.TranslateAsync(robotCanvasX, robotCanvasY);
|
||||||
|
await ctx.RotateAsync(robotOrientation);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
bool success = await JS.InvokeAsync<bool>("drawImageOnCanvas",
|
||||||
|
CanvasRef,
|
||||||
|
"images/AMR-250.png",
|
||||||
|
-scaledLength / 2,
|
||||||
|
-scaledWidth / 2,
|
||||||
|
scaledLength,
|
||||||
|
scaledWidth);
|
||||||
|
|
||||||
|
if (!success)
|
||||||
|
{
|
||||||
|
await ctx.FillStyleAsync("rgba(0, 255, 0, 0.8)");
|
||||||
|
await ctx.FillRectAsync(-scaledLength / 2, -scaledWidth / 2, scaledLength, scaledWidth);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
await ctx.FillStyleAsync("rgba(0, 255, 0, 0.8)");
|
||||||
|
await ctx.FillRectAsync(-scaledLength / 2, -scaledWidth / 2, scaledLength, scaledWidth);
|
||||||
|
}
|
||||||
|
|
||||||
|
await ctx.RestoreAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task DrawRobotOrientationArrows(Context2D ctx, double robotCanvasX, double robotCanvasY, double robotOrientation)
|
||||||
|
{
|
||||||
|
double arrowLength = 30 / ZoomScale;
|
||||||
|
double arrowHeadSize = 10 / ZoomScale;
|
||||||
|
|
||||||
|
await ctx.StrokeStyleAsync("rgba(0, 100, 255, 1.0)");
|
||||||
|
await ctx.FillStyleAsync("rgba(0, 100, 255, 1.0)");
|
||||||
|
await ctx.LineWidthAsync(3 / ZoomScale);
|
||||||
|
|
||||||
|
await ctx.BeginPathAsync();
|
||||||
|
await ctx.MoveToAsync(robotCanvasX, robotCanvasY);
|
||||||
|
double xAxisEndX = robotCanvasX + Math.Cos(robotOrientation) * (arrowLength - arrowHeadSize + 1);
|
||||||
|
double xAxisEndY = robotCanvasY + Math.Sin(robotOrientation) * (arrowLength - arrowHeadSize + 1);
|
||||||
|
await ctx.LineToAsync(xAxisEndX, xAxisEndY);
|
||||||
|
await ctx.StrokeAsync();
|
||||||
|
|
||||||
|
await ctx.BeginPathAsync();
|
||||||
|
double xArrowTipX = robotCanvasX + Math.Cos(robotOrientation) * arrowLength;
|
||||||
|
double xArrowTipY = robotCanvasY + Math.Sin(robotOrientation) * arrowLength;
|
||||||
|
await ctx.MoveToAsync(xArrowTipX, xArrowTipY);
|
||||||
|
double xArrowAngle = robotOrientation + Math.PI;
|
||||||
|
await ctx.LineToAsync(xArrowTipX + Math.Cos(xArrowAngle + Math.PI / 6) * arrowHeadSize, xArrowTipY + Math.Sin(xArrowAngle + Math.PI / 6) * arrowHeadSize);
|
||||||
|
await ctx.LineToAsync(xArrowTipX + Math.Cos(xArrowAngle - Math.PI / 6) * arrowHeadSize, xArrowTipY + Math.Sin(xArrowAngle - Math.PI / 6) * arrowHeadSize);
|
||||||
|
await ctx.ClosePathAsync();
|
||||||
|
await ctx.FillAsync(FillRule.NonZero);
|
||||||
|
|
||||||
|
await ctx.StrokeStyleAsync("rgba(255, 50, 50, 1.0)");
|
||||||
|
await ctx.FillStyleAsync("rgba(255, 50, 50, 1.0)");
|
||||||
|
await ctx.LineWidthAsync(3 / ZoomScale);
|
||||||
|
|
||||||
|
double yAxisAngle = robotOrientation + Math.PI / 2;
|
||||||
|
|
||||||
|
await ctx.BeginPathAsync();
|
||||||
|
await ctx.MoveToAsync(robotCanvasX, robotCanvasY);
|
||||||
|
double yAxisEndX = robotCanvasX + Math.Cos(yAxisAngle) * (arrowLength - arrowHeadSize + 1);
|
||||||
|
double yAxisEndY = robotCanvasY + Math.Sin(yAxisAngle) * (arrowLength - arrowHeadSize + 1);
|
||||||
|
await ctx.LineToAsync(yAxisEndX, yAxisEndY);
|
||||||
|
await ctx.StrokeAsync();
|
||||||
|
|
||||||
|
await ctx.BeginPathAsync();
|
||||||
|
double yArrowTipX = robotCanvasX + Math.Cos(yAxisAngle) * arrowLength;
|
||||||
|
double yArrowTipY = robotCanvasY + Math.Sin(yAxisAngle) * arrowLength;
|
||||||
|
await ctx.MoveToAsync(yArrowTipX, yArrowTipY);
|
||||||
|
double yArrowAngle = yAxisAngle + Math.PI;
|
||||||
|
await ctx.LineToAsync(yArrowTipX + Math.Cos(yArrowAngle + Math.PI / 6) * arrowHeadSize, yArrowTipY + Math.Sin(yArrowAngle + Math.PI / 6) * arrowHeadSize);
|
||||||
|
await ctx.LineToAsync(yArrowTipX + Math.Cos(yArrowAngle - Math.PI / 6) * arrowHeadSize, yArrowTipY + Math.Sin(yArrowAngle - Math.PI / 6) * arrowHeadSize);
|
||||||
|
await ctx.ClosePathAsync();
|
||||||
|
await ctx.FillAsync(FillRule.NonZero);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
<div class="view">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
|
@ -7,8 +7,13 @@
|
||||||
<PageTitle>Map Manager</PageTitle>
|
<PageTitle>Map Manager</PageTitle>
|
||||||
|
|
||||||
<div class="d-flex w-100 h-100 p-2 overflow-hidden flex-row">
|
<div class="d-flex w-100 h-100 p-2 overflow-hidden flex-row">
|
||||||
<div class="me-4" style="height: 100%; width: 40%">
|
<div class="me-4 d-flex flex-column" style="height: 100%; width: 40%">
|
||||||
<RobotApp.Client.Pages.Components.Mapping.MapTable />
|
<div class="mb-4" style="height: 50%; width: 100%">
|
||||||
|
<RobotApp.Client.Pages.Components.Mapping.MapTable />
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow-1" style="height: 50%; width: 100%">
|
||||||
|
<RobotApp.Client.Pages.Components.Mapping.RobotInfomation />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-grow-1 h-100" style="width: 60%">
|
<div class="flex-grow-1 h-100" style="width: 60%">
|
||||||
<RobotApp.Client.Pages.Components.Mapping.MapView />
|
<RobotApp.Client.Pages.Components.Mapping.MapView />
|
||||||
|
|
|
||||||
|
|
@ -5,12 +5,23 @@
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
window.getElementBoundingRect = (element) => {
|
||||||
|
const rect = element.getBoundingClientRect();
|
||||||
|
return {
|
||||||
|
width: rect.width,
|
||||||
|
height: rect.height,
|
||||||
|
x: rect.x,
|
||||||
|
y: rect.y,
|
||||||
|
left: rect.left,
|
||||||
|
top: rect.top
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
window.setCanvasSize = (canvas, width, height) => {
|
window.setCanvasSize = (canvas, width, height) => {
|
||||||
canvas.width = width;
|
canvas.width = width;
|
||||||
canvas.height = height;
|
canvas.height = height;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Image loading and caching functionality
|
|
||||||
window.imageCache = new Map();
|
window.imageCache = new Map();
|
||||||
|
|
||||||
window.preloadImage = (imagePath) => {
|
window.preloadImage = (imagePath) => {
|
||||||
|
|
@ -47,11 +58,6 @@ window.preloadImageFromUrl = (url, cacheKey) => {
|
||||||
img.onerror = (error) => {
|
img.onerror = (error) => {
|
||||||
reject(new Error(`Failed to load image from URL: ${url}`));
|
reject(new Error(`Failed to load image from URL: ${url}`));
|
||||||
};
|
};
|
||||||
|
|
||||||
// Don't set crossOrigin for same-origin requests
|
|
||||||
// Only set it if you're loading from a different domain
|
|
||||||
// img.crossOrigin = 'anonymous';
|
|
||||||
|
|
||||||
img.src = url;
|
img.src = url;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
|
|
||||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
# Visual Studio Version 17
|
# Visual Studio Version 17
|
||||||
VisualStudioVersion = 17.14.36511.14 d17.14
|
VisualStudioVersion = 17.14.36511.14
|
||||||
MinimumVisualStudioVersion = 10.0.40219.1
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RobotApp", "RobotApp\RobotApp.csproj", "{BF0BB137-2EF9-4E1B-944E-9BF41C5284F7}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RobotApp", "RobotApp\RobotApp.csproj", "{BF0BB137-2EF9-4E1B-944E-9BF41C5284F7}"
|
||||||
EndProject
|
EndProject
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using RobotApp.Services;
|
|
||||||
|
|
||||||
namespace RobotApp.Controllers;
|
namespace RobotApp.Controllers;
|
||||||
|
|
||||||
|
|
@ -10,7 +9,7 @@ namespace RobotApp.Controllers;
|
||||||
public class ImagesController(Services.Logger<ImagesController> Logger) : ControllerBase
|
public class ImagesController(Services.Logger<ImagesController> Logger) : ControllerBase
|
||||||
{
|
{
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[Route("map")]
|
[Route("mapping")]
|
||||||
public async Task<IActionResult> GetMapImage()
|
public async Task<IActionResult> GetMapImage()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user