464 lines
16 KiB
Plaintext
464 lines
16 KiB
Plaintext
@inject IJSRuntime JS
|
|
@using Excubo.Blazor.Canvas.Contexts
|
|
|
|
<div class="view">
|
|
<div class="toolbar">
|
|
<MudTooltip Text="Zoom In" role="button" Placement="Placement.Bottom" Color="Color.Info">
|
|
<button type="button" class="btn btn-secondary action-button" @onclick="ZoomIn">
|
|
<i class="mdi mdi-magnify-plus-outline icon-button"></i>
|
|
</button>
|
|
</MudTooltip>
|
|
<MudTooltip Text="Zoom Out" role="button" Placement="Placement.Bottom" Color="Color.Info">
|
|
<button type="button" class="btn btn-secondary action-button" @onclick="ZoomOut">
|
|
<i class="mdi mdi-magnify-minus-outline icon-button"></i>
|
|
</button>
|
|
</MudTooltip>
|
|
<MudTooltip Text="Reset View" role="button" Placement="Placement.Bottom" Color="Color.Info">
|
|
<button type="button" class="btn btn-secondary action-button" @onclick="ResetView">
|
|
<i class="mdi mdi-fit-to-screen-outline icon-button"></i>
|
|
</button>
|
|
</MudTooltip>
|
|
<MudSpacer />
|
|
<MudTooltip Text="Start Localization" role="button" Placement="Placement.Bottom" Color="Color.Info">
|
|
<button type="button" class="btn btn-secondary action-button" disabled="@(false)">
|
|
<i class="mdi mdi-play icon-button"></i>
|
|
</button>
|
|
</MudTooltip>
|
|
<MudTooltip Text="Stop Localization" role="button" Placement="Placement.Bottom" Color="Color.Info">
|
|
<button type="button" class="btn btn-secondary action-button" disabled="@(true)">
|
|
<i class="mdi mdi-pause icon-button"></i>
|
|
</button>
|
|
</MudTooltip>
|
|
<MudTooltip Text="Start Mapping" role="button" Placement="Placement.Bottom" Color="Color.Info">
|
|
<button type="button" class="btn btn-secondary action-button" disabled="@(false)">
|
|
<i class="mdi mdi-plus icon-button"></i>
|
|
</button>
|
|
</MudTooltip>
|
|
<MudTooltip Text="Stop Mapping" role="button" Placement="Placement.Bottom" Color="Color.Info">
|
|
<button type="button" class="btn btn-secondary action-button" disabled="@(true)">
|
|
<i class="mdi mdi-stop icon-button"></i>
|
|
</button>
|
|
</MudTooltip>
|
|
</div>
|
|
<div @ref="ViewContainerRef">
|
|
<canvas @ref="CanvasRef"
|
|
@onwheel="HandleWheel"
|
|
@onmousemove="HandleMouseMove"
|
|
@onmouseleave="HandleMouseLeave"
|
|
@ontouchstart="HandleTouchStart"
|
|
@ontouchmove="HandleTouchMove"
|
|
@ontouchend="HandleTouchEnd"></canvas>
|
|
</div>
|
|
</div>
|
|
|
|
@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 = -5;
|
|
private const double ImageResolution = 0.05;
|
|
|
|
private double MapImageWidth = 0;
|
|
private double MapImageHeight = 0;
|
|
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)
|
|
{
|
|
await base.OnAfterRenderAsync(first_render);
|
|
if (!first_render) return;
|
|
|
|
var containerSize = await JS.InvokeAsync<DomRect>("getElementSize", ViewContainerRef);
|
|
|
|
CanvasWidth = containerSize.Width;
|
|
CanvasHeight = containerSize.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 apiUrl = "api/images/mapping";
|
|
await JS.InvokeVoidAsync("preloadImageFromUrl", apiUrl, MAP_CACHE_KEY);
|
|
|
|
var imageDimensions = await JS.InvokeAsync<DomRect>("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 ResetView()
|
|
{
|
|
CanvasTranslateX = CanvasWidth / 2;
|
|
CanvasTranslateY = CanvasHeight / 2;
|
|
ZoomScale = 1.0;
|
|
StateHasChanged();
|
|
await DrawCanvas();
|
|
}
|
|
|
|
private async Task ZoomIn()
|
|
{
|
|
const double zoomFactor = 0.15;
|
|
double oldZoom = ZoomScale;
|
|
|
|
ZoomScale = Math.Min(MAX_ZOOM, ZoomScale * (1 + zoomFactor));
|
|
|
|
if (Math.Abs(ZoomScale - oldZoom) < 0.001) return;
|
|
|
|
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)
|
|
{
|
|
WorldMouseX = CanvasToWorldX(MouseX);
|
|
WorldMouseY = CanvasToWorldY(MouseY);
|
|
}
|
|
|
|
StateHasChanged();
|
|
await DrawCanvas();
|
|
}
|
|
|
|
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 HandleTouchMove(TouchEventArgs e)
|
|
{
|
|
if (e.Touches.Length == 1)
|
|
{
|
|
await HandleSingleTouchMove(e.Touches[0]);
|
|
}
|
|
else if (e.Touches.Length == 2)
|
|
{
|
|
await HandlePinchZoom(e.Touches[0], e.Touches[1]);
|
|
}
|
|
|
|
StateHasChanged();
|
|
await DrawCanvas();
|
|
}
|
|
|
|
private void HandleTouchStart(TouchEventArgs e)
|
|
{
|
|
IsTouching = true;
|
|
|
|
if (e.Touches.Length == 1)
|
|
{
|
|
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()
|
|
{
|
|
// 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.5; // 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;
|
|
}
|
|
}
|