This commit is contained in:
Đăng Nguyễn 2025-12-30 17:38:43 +07:00
parent b3f765d261
commit 15a61fd986
8 changed files with 169 additions and 16 deletions

View File

@ -22,7 +22,14 @@
{
<div class="robot-position-info">
<i class="mdi mdi-map-marker"></i>
<span>X: @MonitorData.RobotPosition.X.ToString("F2")m | Y: @MonitorData.RobotPosition.Y.ToString("F2")m | θ: @((MonitorData.RobotPosition.Theta * 180 / Math.PI).ToString("F1"))°</span>
<span>Robot: X: @MonitorData.RobotPosition.X.ToString("F2")m | Y: @MonitorData.RobotPosition.Y.ToString("F2")m | θ: @((MonitorData.RobotPosition.Theta * 180 / Math.PI).ToString("F1"))°</span>
</div>
}
@if (MouseWorldX.HasValue && MouseWorldY.HasValue)
{
<div class="mouse-position-info">
<i class="mdi mdi-cursor-pointer"></i>
<span>Mouse: X: @MouseWorldX.Value.ToString("F2")m | Y: @MouseWorldY.Value.ToString("F2")m</span>
</div>
}
@* <MudChip T="string" Color="@(IsConnected? Color.Success: Color.Error)" Size="Size.Small">
@ -36,18 +43,49 @@
@onmousemove="HandleMouseMove"
@onmouseup="HandleMouseUp"
@onmouseleave="HandleMouseLeave">
@* Arrow markers for origin *@
<defs>
<marker id="arrowhead-x" markerWidth="10" markerHeight="10"
refX="9" refY="3" orient="auto" markerUnits="strokeWidth">
<path d="M0,0 L0,6 L9,3 z" fill="#FF0000" />
</marker>
<marker id="arrowhead-y" markerWidth="10" markerHeight="10"
refX="9" refY="3" orient="auto" markerUnits="strokeWidth">
<path d="M0,0 L0,6 L9,3 z" fill="#00FF00" />
</marker>
</defs>
<g transform="@GetTransform()">
@* Background Map Image *@
@if (MapImageLoaded && MapImageWidth > 0 && MapImageHeight > 0)
{
<image href="@MapImageUrl"
x="@WorldToSvgX(MapImageOriginX)"
y="@WorldToSvgY(MapImageOriginY + MapImageHeight)"
width="@MapImageWidth"
height="@MapImageHeight"
preserveAspectRatio="none"
opacity="0.8"
style="pointer-events: none;"
id="map-background-image" />
}
@* Origin Marker (2 arrows: X+ and Y+) *@
<g transform="@GetOriginMarkerTransform()">
@* X+ Arrow (pointing right) *@
<line x1="0" y1="0" x2="@GetOriginMarkerSize()" y2="0"
stroke="#FF0000" stroke-width="@GetOriginMarkerStrokeWidth()"
marker-end="url(#arrowhead-x)" />
@* Y+ Arrow (pointing up in world, down in SVG) *@
<line x1="0" y1="0" x2="0" y2="@(-GetOriginMarkerSize())"
stroke="#00FF00" stroke-width="@GetOriginMarkerStrokeWidth()"
marker-end="url(#arrowhead-y)" />
@* Origin point *@
<circle cx="0" cy="0" r="@(GetOriginMarkerSize() * 0.3)"
fill="#FFFF00" stroke="#000" stroke-width="@(GetOriginMarkerStrokeWidth() * 0.5)" />
</g>
@if (MonitorData?.HasOrder == true)
{
@* @for (int i = 0; i < MonitorData.EdgeStates.Length; i++)
{
var edge = MonitorData.EdgeStates[i];
var (startX, startY, endX, endY) = GetEdgeEndpoints(i, edge);
<path d="@GetPathFromTrajectory(edge.Trajectory, startX, startY, endX, endY)"
fill="none"
stroke="#42A5F5"
stroke-width="0.08" />
} *@
<path d="@PathView"
fill="none"
stroke="#42A5F5"
@ -124,14 +162,31 @@
private string PathView = "";
private string PathIsNot = "hidden";
// Mouse world coordinates
private double? MouseWorldX = null;
private double? MouseWorldY = null;
// Map image properties
private const double MapImageOriginX = -20.0; // OriginX in world coordinates (meters)
private const double MapImageOriginY = -20.0; // OriginY in world coordinates (meters)
private const double MapImageResolution = 0.1; // Resolution: meters per pixel
private const string MapImageUrl = "images/gara20250309.png";
private bool MapImageLoaded = false;
private double MapImageWidth = 0; // Width in world coordinates (meters)
private double MapImageHeight = 0; // Height in world coordinates (meters)
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
var containerSize = await JS.InvokeAsync<ElementSize>("getElementSize", SvgContainerRef);
var containerSize = await JS.InvokeAsync<ElementSize>("robotMonitor.getElementSize", SvgContainerRef);
SvgWidth = containerSize.Width;
SvgHeight = containerSize.Height;
// Load map image and get dimensions
await LoadMapImage();
// Center view on robot if available with initial zoom
if (MonitorData?.RobotPosition != null)
{
@ -150,6 +205,31 @@
}
}
private async Task LoadMapImage()
{
try
{
var imageDimensions = await JS.InvokeAsync<ElementSize>("robotMonitor.loadImageAndGetDimensions", MapImageUrl);
// Convert pixel dimensions to world coordinates (meters)
MapImageWidth = imageDimensions.Width * MapImageResolution;
MapImageHeight = imageDimensions.Height * MapImageResolution;
Console.WriteLine($"Map image loaded: {imageDimensions.Width}x{imageDimensions.Height} pixels, {MapImageWidth}x{MapImageHeight} meters");
if (MapImageWidth > 0 && MapImageHeight > 0)
{
MapImageLoaded = true;
await InvokeAsync(StateHasChanged); // Force re-render after image is loaded
}
}
catch (Exception ex)
{
MapImageLoaded = false;
Console.WriteLine($"Failed to load map image: {ex.Message}");
}
}
private string GetTransform()
{
return $"translate({TranslateX}, {TranslateY}) scale({ZoomScale * BASE_PIXELS_PER_METER})";
@ -217,6 +297,31 @@
return -worldY;
}
private string GetOriginMarkerTransform()
{
// Origin is at (MapImageOriginX, MapImageOriginY) in world coordinates
// In SVG: (MapImageOriginX, -MapImageOriginY)
var x = WorldToSvgX(MapImageOriginX);
var y = WorldToSvgY(MapImageOriginY);
return $"translate({x}, {y})";
}
private double GetOriginMarkerSize()
{
// Marker size in world coordinates (meters)
const double BaseMarkerSize = 0.5; // 1 meter
double scaleFactor = 1.0 / ZoomScale; // Keep visual size constant
return BaseMarkerSize * scaleFactor;
}
private double GetOriginMarkerStrokeWidth()
{
// Stroke width in world coordinates
const double BaseStrokeWidth = 0.05; // 5cm
double scaleFactor = 1.0 / ZoomScale; // Keep visual size constant
return BaseStrokeWidth * scaleFactor;
}
public void UpdatePath()
{
if (MonitorData is not null && MonitorData.EdgeStates.Length > 0)
@ -291,14 +396,26 @@
PanStartY = e.ClientY - TranslateY;
}
private void HandleMouseMove(MouseEventArgs e)
private async Task HandleMouseMove(MouseEventArgs e)
{
// Calculate world coordinates of mouse
var svgRect = await JS.InvokeAsync<ElementBoundingRect>("robotMonitor.getElementBoundingRect", SvgRef);
double mouseX = e.ClientX - svgRect.X;
double mouseY = e.ClientY - svgRect.Y;
// Convert to world coordinates
// World X = (mouseX - TranslateX) / (ZoomScale * BASE_PIXELS_PER_METER)
MouseWorldX = (mouseX - TranslateX) / (ZoomScale * BASE_PIXELS_PER_METER);
// World Y = -(mouseY - TranslateY) / (ZoomScale * BASE_PIXELS_PER_METER) (flip Y axis)
MouseWorldY = -(mouseY - TranslateY) / (ZoomScale * BASE_PIXELS_PER_METER);
if (IsPanning)
{
TranslateX = e.ClientX - PanStartX;
TranslateY = e.ClientY - PanStartY;
StateHasChanged();
}
StateHasChanged();
}
private void HandleMouseUp(MouseEventArgs e)
@ -309,6 +426,9 @@
private void HandleMouseLeave(MouseEventArgs e)
{
IsPanning = false;
MouseWorldX = null;
MouseWorldY = null;
StateHasChanged();
}
private async Task HandleWheel(WheelEventArgs e)
@ -324,7 +444,7 @@
if (Math.Abs(ZoomScale - oldZoom) < 0.001) return;
// Zoom at mouse position
var svgRect = await JS.InvokeAsync<ElementBoundingRect>("getElementBoundingRect", SvgRef);
var svgRect = await JS.InvokeAsync<ElementBoundingRect>("robotMonitor.getElementBoundingRect", SvgRef);
double mouseX = e.ClientX - svgRect.X;
double mouseY = e.ClientY - svgRect.Y;

View File

@ -54,6 +54,18 @@
gap: 8px;
}
.mouse-position-info {
color: #fff;
font-size: 14px;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
padding: 4px 12px;
background-color: #3d3d3d;
border-radius: 4px;
display: flex;
align-items: center;
gap: 8px;
}
.svg-container {
flex: 1;
overflow: hidden;

View File

@ -6,14 +6,14 @@
<div class="d-flex gap-2">
<!-- IMPORT -->
<MudButton Variant="Variant.Outlined"
@* <MudButton Variant="Variant.Outlined"
Color="Color.Secondary"
Size="Size.Small"
StartIcon="@Icons.Material.Filled.UploadFile"
OnClick="OnImport">
Import JSON
</MudButton>
*@
<!-- CANCEL -->
<MudButton Variant="Variant.Filled"
Color="@CancelButtonColor"

View File

@ -47,3 +47,4 @@

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@ -55,6 +55,23 @@ window.robotMonitor = {
}
return `M ${startX} ${startY} L ${endX} ${endY}`;
},
// Load image and get dimensions
loadImageAndGetDimensions: function (imageUrl) {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => {
resolve({
Width: img.naturalWidth || img.width,
Height: img.naturalHeight || img.height
});
};
img.onerror = () => {
reject(new Error(`Failed to load image: ${imageUrl}`));
};
img.src = imageUrl;
});
}
};
@ -65,3 +82,4 @@ window.robotMonitor = {

View File

@ -42,6 +42,7 @@
<script src="@Assets["_content/MudBlazor/MudBlazor.min.js"]"></script>
<script src="@Assets["js/canvas.js"]"></script>
<script src="@Assets["js/app.js"]"></script>
<script src="@Assets["js/robotMonitor.js"]"></script>
</body>
</html>

View File

@ -17,3 +17,4 @@ public class RobotMonitorHub : Hub