289 lines
10 KiB
Plaintext
289 lines
10 KiB
Plaintext
@inject IJSRuntime JS
|
|
|
|
<div class="robot-monitor-container">
|
|
<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 />
|
|
@if (MonitorData?.RobotPosition != null)
|
|
{
|
|
<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>
|
|
</div>
|
|
}
|
|
<MudChip T="string" Color="@(IsConnected ? Color.Success : Color.Error)" Size="Size.Small">
|
|
@(IsConnected ? "Connected" : "Disconnected")
|
|
</MudChip>
|
|
</div>
|
|
<div @ref="SvgContainerRef" class="svg-container">
|
|
<svg @ref="SvgRef"
|
|
@onwheel="HandleWheel"
|
|
@onmousedown="HandleMouseDown"
|
|
@onmousemove="HandleMouseMove"
|
|
@onmouseup="HandleMouseUp"
|
|
@onmouseleave="HandleMouseLeave">
|
|
<g transform="@GetTransform()">
|
|
@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"
|
|
stroke-width="0.08"
|
|
visibility="@PathIsNot" />
|
|
|
|
@foreach (var node in MonitorData.NodeStates)
|
|
{
|
|
<circle cx="@WorldToSvgX(node.NodePosition.X)"
|
|
cy="@WorldToSvgY(node.NodePosition.Y)"
|
|
r="0.1"
|
|
fill="#66BB6A"
|
|
stroke="#555"
|
|
stroke-width="0.02" />
|
|
}
|
|
}
|
|
|
|
@* Render Robot *@
|
|
@if (MonitorData?.RobotPosition != null)
|
|
{
|
|
<g transform="@GetRobotTransform()">
|
|
<image href="images/AMR-250.png"
|
|
x="-0.303"
|
|
y="-0.553"
|
|
width="0.606"
|
|
height="1.106"
|
|
preserveAspectRatio="xMidYMid meet" />
|
|
</g>
|
|
}
|
|
</g>
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
|
|
@code {
|
|
[Parameter] public RobotMonitorDto? MonitorData { get; set; }
|
|
[Parameter] public bool IsConnected { get; set; }
|
|
|
|
public class ElementSize
|
|
{
|
|
public double Width { get; set; }
|
|
public double Height { get; set; }
|
|
}
|
|
|
|
public class ElementBoundingRect
|
|
{
|
|
public double X { get; set; }
|
|
public double Y { get; set; }
|
|
public double Width { get; set; }
|
|
public double Height { get; set; }
|
|
}
|
|
|
|
private ElementReference SvgRef;
|
|
private ElementReference SvgContainerRef;
|
|
|
|
private double ZoomScale = 2.0; // Zoom vào robot hơn khi mở
|
|
private const double MIN_ZOOM = 0.1;
|
|
private const double MAX_ZOOM = 5.0;
|
|
private const double BASE_PIXELS_PER_METER = 50.0;
|
|
|
|
private double TranslateX = 0;
|
|
private double TranslateY = 0;
|
|
|
|
private bool IsPanning = false;
|
|
private double PanStartX = 0;
|
|
private double PanStartY = 0;
|
|
|
|
private double SvgWidth = 800;
|
|
private double SvgHeight = 600;
|
|
|
|
private string PathView = "";
|
|
private string PathIsNot = "hidden";
|
|
|
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
|
{
|
|
if (firstRender)
|
|
{
|
|
var containerSize = await JS.InvokeAsync<ElementSize>("getElementSize", SvgContainerRef);
|
|
SvgWidth = containerSize.Width;
|
|
SvgHeight = containerSize.Height;
|
|
|
|
// Center view on robot if available with initial zoom
|
|
if (MonitorData?.RobotPosition != null)
|
|
{
|
|
// Zoom vào robot hơn một chút
|
|
ZoomScale = 2.5;
|
|
TranslateX = SvgWidth / 2 - MonitorData.RobotPosition.X * BASE_PIXELS_PER_METER * ZoomScale;
|
|
TranslateY = SvgHeight / 2 + MonitorData.RobotPosition.Y * BASE_PIXELS_PER_METER * ZoomScale;
|
|
}
|
|
else
|
|
{
|
|
TranslateX = SvgWidth / 2;
|
|
TranslateY = SvgHeight / 2;
|
|
}
|
|
|
|
StateHasChanged();
|
|
}
|
|
}
|
|
|
|
private string GetTransform()
|
|
{
|
|
return $"translate({TranslateX}, {TranslateY}) scale({ZoomScale * BASE_PIXELS_PER_METER})";
|
|
}
|
|
|
|
private string GetRobotTransform()
|
|
{
|
|
if (MonitorData?.RobotPosition == null) return "";
|
|
var x = WorldToSvgX(MonitorData.RobotPosition.X);
|
|
var y = WorldToSvgY(MonitorData.RobotPosition.Y);
|
|
// Theta là radian, convert sang độ
|
|
// SVG rotate quay theo chiều kim đồng hồ, cần đảo dấu
|
|
var angleDegrees = -MonitorData.RobotPosition.Theta * 180 / Math.PI;
|
|
return $"translate({x}, {y}) rotate({angleDegrees})";
|
|
}
|
|
|
|
private double WorldToSvgX(double worldX)
|
|
{
|
|
return worldX;
|
|
}
|
|
|
|
private double WorldToSvgY(double worldY)
|
|
{
|
|
// Flip Y axis: World Y↑ → SVG Y↓
|
|
return -worldY;
|
|
}
|
|
|
|
public void UpdatePath()
|
|
{
|
|
if (MonitorData is not null && MonitorData.EdgeStates.Length > 0)
|
|
{
|
|
var path = MonitorData.EdgeStates;
|
|
var inPath = $"M {path[0].StartX} {path[0].StartY}";
|
|
for (int i = 0; i < path.Length; i++)
|
|
{
|
|
if (path[i].Degree == 1) inPath = $"{inPath} L {path[i].EndX} {path[i].EndY}";
|
|
else if (path[i].Degree == 2) inPath = $"{inPath} Q {path[i].ControlPoint1X} {path[i].ControlPoint1Y} {path[i].EndX} {path[i].EndY}";
|
|
else inPath = $"{inPath} C {path[i].ControlPoint1X} {path[i].ControlPoint1Y}, {path[i].ControlPoint2X} {path[i].ControlPoint2Y}, {path[i].EndX} {path[i].EndY}";
|
|
}
|
|
PathView = inPath;
|
|
PathIsNot = "visible";
|
|
}
|
|
else
|
|
{
|
|
PathView = "";
|
|
PathIsNot = "hidden";
|
|
}
|
|
StateHasChanged();
|
|
}
|
|
|
|
private async Task ZoomIn()
|
|
{
|
|
ZoomScale = Math.Min(MAX_ZOOM, ZoomScale * 1.15);
|
|
await Task.CompletedTask;
|
|
StateHasChanged();
|
|
}
|
|
|
|
private async Task ZoomOut()
|
|
{
|
|
ZoomScale = Math.Max(MIN_ZOOM, ZoomScale * 0.85);
|
|
await Task.CompletedTask;
|
|
StateHasChanged();
|
|
}
|
|
|
|
private async Task ResetView()
|
|
{
|
|
// Reset về zoom ban đầu (2.5x) khi có robot position
|
|
if (MonitorData?.RobotPosition != null)
|
|
{
|
|
ZoomScale = 2.5;
|
|
TranslateX = SvgWidth / 2 - MonitorData.RobotPosition.X * BASE_PIXELS_PER_METER * ZoomScale;
|
|
TranslateY = SvgHeight / 2 + MonitorData.RobotPosition.Y * BASE_PIXELS_PER_METER * ZoomScale;
|
|
}
|
|
else
|
|
{
|
|
ZoomScale = 1.0;
|
|
TranslateX = SvgWidth / 2;
|
|
TranslateY = SvgHeight / 2;
|
|
}
|
|
await Task.CompletedTask;
|
|
StateHasChanged();
|
|
}
|
|
|
|
private void HandleMouseDown(MouseEventArgs e)
|
|
{
|
|
IsPanning = true;
|
|
PanStartX = e.ClientX - TranslateX;
|
|
PanStartY = e.ClientY - TranslateY;
|
|
}
|
|
|
|
private void HandleMouseMove(MouseEventArgs e)
|
|
{
|
|
if (IsPanning)
|
|
{
|
|
TranslateX = e.ClientX - PanStartX;
|
|
TranslateY = e.ClientY - PanStartY;
|
|
StateHasChanged();
|
|
}
|
|
}
|
|
|
|
private void HandleMouseUp(MouseEventArgs e)
|
|
{
|
|
IsPanning = false;
|
|
}
|
|
|
|
private void HandleMouseLeave(MouseEventArgs e)
|
|
{
|
|
IsPanning = false;
|
|
}
|
|
|
|
private async Task HandleWheel(WheelEventArgs e)
|
|
{
|
|
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;
|
|
|
|
// Zoom at mouse position
|
|
var svgRect = await JS.InvokeAsync<ElementBoundingRect>("getElementBoundingRect", SvgRef);
|
|
double mouseX = e.ClientX - svgRect.X;
|
|
double mouseY = e.ClientY - svgRect.Y;
|
|
|
|
// Calculate world coordinates at mouse position
|
|
double worldX = (mouseX - TranslateX) / (oldZoom * BASE_PIXELS_PER_METER);
|
|
double worldY = -(mouseY - TranslateY) / (oldZoom * BASE_PIXELS_PER_METER);
|
|
|
|
// Adjust translate to keep world point under mouse
|
|
TranslateX = mouseX - worldX * ZoomScale * BASE_PIXELS_PER_METER;
|
|
TranslateY = mouseY + worldY * ZoomScale * BASE_PIXELS_PER_METER;
|
|
|
|
StateHasChanged();
|
|
}
|
|
}
|
|
|