RobotApp/RobotApp.Client/Pages/Components/Monitor/RobotMonitorView.razor
Đăng Nguyễn bc00e9ae50 update
2025-12-20 17:50:47 +07:00

343 lines
12 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="@GetNodeRadius()"
fill="#66BB6A"
stroke="#555"
stroke-width="@GetNodeStrokeWidth()" />
}
}
@* Render Robot *@
@if (MonitorData?.RobotPosition != null)
{
<g transform="@GetRobotTransform()">
@{
var (robotX, robotY, robotWidth, robotHeight) = GetRobotSize();
}
<image href="images/AMR-250.png"
x="@robotX"
y="@robotY"
width="@robotWidth"
height="@robotHeight"
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 x, double y, double width, double height) GetRobotSize()
{
// Kích thước robot trong world coordinates (mét)
const double RobotWidthMeters = 0.606;
const double RobotLengthMeters = 1.106;
// Điều chỉnh kích thước dựa trên ZoomScale
// Tăng kích thước lên 1.5x để robot to hơn
double scaleFactor = 3 / ZoomScale; // Tăng kích thước hiển thị
double width = RobotWidthMeters * scaleFactor;
double height = RobotLengthMeters * scaleFactor;
double x = -width / 2;
double y = -height / 2;
return (x, y, width, height);
}
private double GetNodeRadius()
{
// Kích thước node cơ bản trong world coordinates - tăng lên 1.5x
const double BaseNodeRadius = 0.15;
// Điều chỉnh theo ZoomScale tương tự robot
double scaleFactor = 1.5 / ZoomScale; // Tăng kích thước hiển thị
return BaseNodeRadius * scaleFactor;
}
private double GetNodeStrokeWidth()
{
// Stroke width cơ bản - tăng lên một chút
const double BaseStrokeWidth = 0.03;
// Điều chỉnh theo ZoomScale
double scaleFactor = 1.5 / ZoomScale;
return BaseStrokeWidth * scaleFactor;
}
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.Select(e => new EdgeStateDto
{
Degree = e.Degree,
StartX = WorldToSvgX(e.StartX),
StartY = WorldToSvgY(e.StartY),
EndX = WorldToSvgX(e.EndX),
EndY = WorldToSvgY(e.EndY),
ControlPoint1X = WorldToSvgX(e.ControlPoint1X),
ControlPoint1Y = WorldToSvgY(e.ControlPoint1Y),
ControlPoint2X = WorldToSvgX(e.ControlPoint2X),
ControlPoint2Y = WorldToSvgY(e.ControlPoint2Y),
}).ToList();
var inPath = $"M {path[0].StartX} {path[0].StartY}";
for (int i = 0; i < path.Count; 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();
}
}