update
This commit is contained in:
parent
b3f765d261
commit
15a61fd986
|
|
@ -22,7 +22,14 @@
|
||||||
{
|
{
|
||||||
<div class="robot-position-info">
|
<div class="robot-position-info">
|
||||||
<i class="mdi mdi-map-marker"></i>
|
<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>
|
</div>
|
||||||
}
|
}
|
||||||
@* <MudChip T="string" Color="@(IsConnected? Color.Success: Color.Error)" Size="Size.Small">
|
@* <MudChip T="string" Color="@(IsConnected? Color.Success: Color.Error)" Size="Size.Small">
|
||||||
|
|
@ -36,18 +43,49 @@
|
||||||
@onmousemove="HandleMouseMove"
|
@onmousemove="HandleMouseMove"
|
||||||
@onmouseup="HandleMouseUp"
|
@onmouseup="HandleMouseUp"
|
||||||
@onmouseleave="HandleMouseLeave">
|
@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()">
|
<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)
|
@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"
|
<path d="@PathView"
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke="#42A5F5"
|
stroke="#42A5F5"
|
||||||
|
|
@ -124,14 +162,31 @@
|
||||||
private string PathView = "";
|
private string PathView = "";
|
||||||
private string PathIsNot = "hidden";
|
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)
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
{
|
{
|
||||||
if (firstRender)
|
if (firstRender)
|
||||||
{
|
{
|
||||||
var containerSize = await JS.InvokeAsync<ElementSize>("getElementSize", SvgContainerRef);
|
var containerSize = await JS.InvokeAsync<ElementSize>("robotMonitor.getElementSize", SvgContainerRef);
|
||||||
SvgWidth = containerSize.Width;
|
SvgWidth = containerSize.Width;
|
||||||
SvgHeight = containerSize.Height;
|
SvgHeight = containerSize.Height;
|
||||||
|
|
||||||
|
// Load map image and get dimensions
|
||||||
|
await LoadMapImage();
|
||||||
|
|
||||||
// Center view on robot if available with initial zoom
|
// Center view on robot if available with initial zoom
|
||||||
if (MonitorData?.RobotPosition != null)
|
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()
|
private string GetTransform()
|
||||||
{
|
{
|
||||||
return $"translate({TranslateX}, {TranslateY}) scale({ZoomScale * BASE_PIXELS_PER_METER})";
|
return $"translate({TranslateX}, {TranslateY}) scale({ZoomScale * BASE_PIXELS_PER_METER})";
|
||||||
|
|
@ -217,6 +297,31 @@
|
||||||
return -worldY;
|
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()
|
public void UpdatePath()
|
||||||
{
|
{
|
||||||
if (MonitorData is not null && MonitorData.EdgeStates.Length > 0)
|
if (MonitorData is not null && MonitorData.EdgeStates.Length > 0)
|
||||||
|
|
@ -291,14 +396,26 @@
|
||||||
PanStartY = e.ClientY - TranslateY;
|
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)
|
if (IsPanning)
|
||||||
{
|
{
|
||||||
TranslateX = e.ClientX - PanStartX;
|
TranslateX = e.ClientX - PanStartX;
|
||||||
TranslateY = e.ClientY - PanStartY;
|
TranslateY = e.ClientY - PanStartY;
|
||||||
StateHasChanged();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
StateHasChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void HandleMouseUp(MouseEventArgs e)
|
private void HandleMouseUp(MouseEventArgs e)
|
||||||
|
|
@ -309,6 +426,9 @@
|
||||||
private void HandleMouseLeave(MouseEventArgs e)
|
private void HandleMouseLeave(MouseEventArgs e)
|
||||||
{
|
{
|
||||||
IsPanning = false;
|
IsPanning = false;
|
||||||
|
MouseWorldX = null;
|
||||||
|
MouseWorldY = null;
|
||||||
|
StateHasChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task HandleWheel(WheelEventArgs e)
|
private async Task HandleWheel(WheelEventArgs e)
|
||||||
|
|
@ -324,7 +444,7 @@
|
||||||
if (Math.Abs(ZoomScale - oldZoom) < 0.001) return;
|
if (Math.Abs(ZoomScale - oldZoom) < 0.001) return;
|
||||||
|
|
||||||
// Zoom at mouse position
|
// 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 mouseX = e.ClientX - svgRect.X;
|
||||||
double mouseY = e.ClientY - svgRect.Y;
|
double mouseY = e.ClientY - svgRect.Y;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,18 @@
|
||||||
gap: 8px;
|
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 {
|
.svg-container {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
|
||||||
|
|
@ -6,14 +6,14 @@
|
||||||
<div class="d-flex gap-2">
|
<div class="d-flex gap-2">
|
||||||
|
|
||||||
<!-- IMPORT -->
|
<!-- IMPORT -->
|
||||||
<MudButton Variant="Variant.Outlined"
|
@* <MudButton Variant="Variant.Outlined"
|
||||||
Color="Color.Secondary"
|
Color="Color.Secondary"
|
||||||
Size="Size.Small"
|
Size="Size.Small"
|
||||||
StartIcon="@Icons.Material.Filled.UploadFile"
|
StartIcon="@Icons.Material.Filled.UploadFile"
|
||||||
OnClick="OnImport">
|
OnClick="OnImport">
|
||||||
Import JSON
|
Import JSON
|
||||||
</MudButton>
|
</MudButton>
|
||||||
|
*@
|
||||||
<!-- CANCEL -->
|
<!-- CANCEL -->
|
||||||
<MudButton Variant="Variant.Filled"
|
<MudButton Variant="Variant.Filled"
|
||||||
Color="@CancelButtonColor"
|
Color="@CancelButtonColor"
|
||||||
|
|
|
||||||
|
|
@ -47,3 +47,4 @@
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
BIN
RobotApp.Client/wwwroot/images/gara20250309.png
Normal file
BIN
RobotApp.Client/wwwroot/images/gara20250309.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
|
|
@ -55,6 +55,23 @@ window.robotMonitor = {
|
||||||
}
|
}
|
||||||
|
|
||||||
return `M ${startX} ${startY} L ${endX} ${endY}`;
|
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 = {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@
|
||||||
<script src="@Assets["_content/MudBlazor/MudBlazor.min.js"]"></script>
|
<script src="@Assets["_content/MudBlazor/MudBlazor.min.js"]"></script>
|
||||||
<script src="@Assets["js/canvas.js"]"></script>
|
<script src="@Assets["js/canvas.js"]"></script>
|
||||||
<script src="@Assets["js/app.js"]"></script>
|
<script src="@Assets["js/app.js"]"></script>
|
||||||
|
<script src="@Assets["js/robotMonitor.js"]"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -17,3 +17,4 @@ public class RobotMonitorHub : Hub
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user