@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) { @{ var (robotX, robotY, robotWidth, robotHeight) = GetRobotSize(); } }
@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 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("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(); } }