@inject IJSRuntime JS @inject HttpClient Http @using Excubo.Blazor.Canvas.Contexts
@code { private ElementReference CanvasRef; private ElementReference ViewContainerRef; private double ZoomScale = 1.0; private const double MIN_ZOOM = 0.1; private const double MAX_ZOOM = 5.0; private const double BASE_PIXELS_PER_METER = 50.0; private bool IsMouseInCanvas = false; private double MouseX; private double MouseY; private double OriginX = 0; private double OriginY = 0; private double WorldMouseX; private double WorldMouseY; private double CanvasWidth; private double CanvasHeight; private double CanvasTranslateX = 0; private double CanvasTranslateY = 0; private const double RulerHeight = 20; private const double RobotWidth = 0.606; private const double RobotLength = 1.106; private bool RobotImageLoaded = false; private bool MapImageLoaded = false; private const double ImageX = -10; private const double ImageY = -10; private const double ImageResolution = 0.05; private double MapImageWidth = 0; private double MapImageHeight = 0; private const string MAP_CACHE_KEY = "map_image"; protected override async Task OnAfterRenderAsync(bool first_render) { await base.OnAfterRenderAsync(first_render); if (!first_render) return; var parentSize = await JS.InvokeAsync("getElementSize", ViewContainerRef); CanvasWidth = parentSize.Width; CanvasHeight = parentSize.Height; await JS.InvokeVoidAsync("setCanvasSize", CanvasRef, CanvasWidth, CanvasHeight); CanvasTranslateX = CanvasWidth / 2; CanvasTranslateY = CanvasHeight / 2; await LoadRobotImage(); await LoadMapImage(); await DrawCanvas(); } private async Task LoadRobotImage() { try { await JS.InvokeVoidAsync("preloadImage", "images/AMR-250.png"); RobotImageLoaded = true; } catch { RobotImageLoaded = false; } } private async Task LoadMapImage() { try { MapImageLoaded = false; string baseUrl = Http.BaseAddress?.ToString() ?? ""; string apiUrl = $"{baseUrl}api/images/map"; await JS.InvokeVoidAsync("preloadImageFromUrl", apiUrl, MAP_CACHE_KEY); var imageDimensions = await JS.InvokeAsync("getImageDimensions", MAP_CACHE_KEY); MapImageWidth = imageDimensions.Width * ImageResolution; MapImageHeight = imageDimensions.Height * ImageResolution; if (MapImageWidth > 0 && MapImageHeight > 0) MapImageLoaded = true; } catch { MapImageLoaded = false; } } private async Task DrawCanvas() { await using var ctx = await JS.GetContext2DAsync(CanvasRef); await ctx.ClearRectAsync(0, 0, CanvasWidth, CanvasHeight); await DrawRulers(ctx); 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); } } 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("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("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) { MouseX = e.OffsetX; MouseY = e.OffsetY; IsMouseInCanvas = true; WorldMouseX = CanvasToWorldX(MouseX); WorldMouseY = CanvasToWorldY(MouseY); StateHasChanged(); if (e.Buttons == 4) { CanvasTranslateX += e.MovementX; CanvasTranslateY -= e.MovementY; } await DrawCanvas(); } private async Task HandleMouseLeave(MouseEventArgs e) { IsMouseInCanvas = false; MouseX = 0; MouseY = 0; StateHasChanged(); await DrawCanvas(); } private async Task HandleWheel(WheelEventArgs e) { if (e.Buttons == 4) return; const double zoomFactor = 0.1; double oldZoom = ZoomScale; if (e.DeltaY < 0) ZoomScale = Math.Min(MAX_ZOOM, ZoomScale * (1 + zoomFactor)); else ZoomScale = Math.Max(MIN_ZOOM, ZoomScale * (1 - zoomFactor)); if (Math.Abs(ZoomScale - oldZoom) < 0.001) return; MouseX = e.OffsetX; MouseY = e.OffsetY; double zoomPointWorldX = (MouseX - CanvasTranslateX) / oldZoom / BASE_PIXELS_PER_METER - OriginX; double zoomPointWorldY = (MouseY - CanvasTranslateY) / oldZoom / BASE_PIXELS_PER_METER - OriginY; double newZoomPointCanvasX = (zoomPointWorldX + OriginX) * BASE_PIXELS_PER_METER * ZoomScale; double newZoomPointCanvasY = (zoomPointWorldY + OriginY) * BASE_PIXELS_PER_METER * ZoomScale; CanvasTranslateX = MouseX - newZoomPointCanvasX; CanvasTranslateY = MouseY - newZoomPointCanvasY; WorldMouseX = CanvasToWorldX(MouseX); WorldMouseY = CanvasToWorldY(MouseY); StateHasChanged(); await DrawCanvas(); } private async Task ResetView() { CanvasTranslateX = CanvasWidth / 2; CanvasTranslateY = CanvasHeight / 2; ZoomScale = 1.0; StateHasChanged(); await DrawCanvas(); } private async Task ReloadMapImage() { MapImageLoaded = false; await LoadMapImage(); await DrawCanvas(); StateHasChanged(); } private LaserScanData GenerateLaserScanData() { // Robot position (in world coordinates) double robotX = 2; // Robot at origin for demo double robotY = 2; double robotOrientation = 0; // Robot facing right (0 degrees) Random random = new Random(42); // Fixed seed for consistent pattern // Laser scanner parameters const double maxRange = 8.0; // meters const double minRange = 0.2; // meters (fix: was 7.0, should be minimum) const int numPoints = 270; // Number of laser points const double startAngle = -Math.PI / 2 - Math.PI / 4; const double endAngle = Math.PI / 2 + Math.PI / 4; double angleStep = (endAngle - startAngle) / (numPoints - 1); var scanData = new LaserScanData { RobotX = robotX, RobotY = robotY, RobotOrientation = robotOrientation, }; // Generate laser points for (int i = 0; i < numPoints; i++) { double angle = startAngle + i * angleStep; // Random range with some clustering around obstacles double range; if (random.NextDouble() < 0.3) // 30% chance of obstacles { range = random.NextDouble() * 3.0 + 1.0; // 1-4 meters (obstacles) } else if (random.NextDouble() < 0.1) // 10% chance of very close objects { range = random.NextDouble() * 0.8 + 0.2; // 0.2-1.0 meters } else { range = random.NextDouble() * maxRange * 0.7 + maxRange * 0.3; // Far points } // Add some noise to make it realistic range += (random.NextDouble() - 0.5) * 0.1; range = Math.Max(minRange, Math.Min(maxRange, range)); // Calculate point position relative to robot double pointX = robotX + Math.Cos(angle + robotOrientation) * range; double pointY = robotY + Math.Sin(angle + robotOrientation) * range; scanData.Points.Add(new LaserScanPoint { X = pointX, Y = pointY }); } 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 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; } } }