577 lines
21 KiB
C#
577 lines
21 KiB
C#
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;
|
|
}
|
|
}
|