@inject IJSRuntime JS
@if (MonitorData?.RobotPosition != null) {
X: @MonitorData.RobotPosition.X.ToString("F2")m | Y: @MonitorData.RobotPosition.Y.ToString("F2")m | θ: @((MonitorData.RobotPosition.Theta * 180 / Math.PI).ToString("F1"))°
} @(IsConnected ? "Connected" : "Disconnected")
@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); } *@ @foreach (var node in MonitorData.NodeStates) { } } @* Render Robot *@ @if (MonitorData?.RobotPosition != null) { }
@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("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("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(); } }