This commit is contained in:
Đăng Nguyễn 2025-12-31 08:56:53 +07:00
parent 15a61fd986
commit 8362713dcc
3 changed files with 77 additions and 19 deletions

View File

@ -17,6 +17,13 @@
<i class="mdi mdi-fit-to-screen-outline icon-button"></i> <i class="mdi mdi-fit-to-screen-outline icon-button"></i>
</button> </button>
</MudTooltip> </MudTooltip>
<div class="auto-follow-control">
<MudTooltip Text="Auto Follow Robot" Placement="Placement.Bottom" Color="Color.Info">
<MudSwitch T="bool" Value="AutoFollowRobot" ValueChanged="OnAutoFollowRobotChanged" Color="Color.Info" Size="Size.Small">
<span style="color: white; font-size: 14px; margin-left: 8px;">Follow Robot</span>
</MudSwitch>
</MudTooltip>
</div>
<MudSpacer /> <MudSpacer />
@if (MonitorData?.RobotPosition != null) @if (MonitorData?.RobotPosition != null)
{ {
@ -58,6 +65,9 @@
@* Background Map Image *@ @* Background Map Image *@
@if (MapImageLoaded && MapImageWidth > 0 && MapImageHeight > 0) @if (MapImageLoaded && MapImageWidth > 0 && MapImageHeight > 0)
{ {
@* Image origin is at bottom-left corner (MapImageOriginX, MapImageOriginY) in world coordinates
In SVG (after Y flip), image top-left corner is at (MapImageOriginX, -MapImageOriginY - MapImageHeight)
So we render image at: x = MapImageOriginX, y = -MapImageOriginY - MapImageHeight *@
<image href="@MapImageUrl" <image href="@MapImageUrl"
x="@WorldToSvgX(MapImageOriginX)" x="@WorldToSvgX(MapImageOriginX)"
y="@WorldToSvgY(MapImageOriginY + MapImageHeight)" y="@WorldToSvgY(MapImageOriginY + MapImageHeight)"
@ -65,11 +75,11 @@
height="@MapImageHeight" height="@MapImageHeight"
preserveAspectRatio="none" preserveAspectRatio="none"
opacity="0.8" opacity="0.8"
style="pointer-events: none;" style="pointer-events: none; image-rendering: pixelated;"
id="map-background-image" /> id="map-background-image" />
} }
@* Origin Marker (2 arrows: X+ and Y+) *@ @* Origin Marker (2 arrows: X+ and Y+) at (MapImageOriginX, MapImageOriginY) *@
<g transform="@GetOriginMarkerTransform()"> <g transform="@GetOriginMarkerTransform()">
@* X+ Arrow (pointing right) *@ @* X+ Arrow (pointing right) *@
<line x1="0" y1="0" x2="@GetOriginMarkerSize()" y2="0" <line x1="0" y1="0" x2="@GetOriginMarkerSize()" y2="0"
@ -80,7 +90,7 @@
stroke="#00FF00" stroke-width="@GetOriginMarkerStrokeWidth()" stroke="#00FF00" stroke-width="@GetOriginMarkerStrokeWidth()"
marker-end="url(#arrowhead-y)" /> marker-end="url(#arrowhead-y)" />
@* Origin point *@ @* Origin point *@
<circle cx="0" cy="0" r="@(GetOriginMarkerSize() * 0.3)" <circle cx="0" cy="0" r="@(GetOriginMarkerSize() * 0.12)"
fill="#FFFF00" stroke="#000" stroke-width="@(GetOriginMarkerStrokeWidth() * 0.5)" /> fill="#FFFF00" stroke="#000" stroke-width="@(GetOriginMarkerStrokeWidth() * 0.5)" />
</g> </g>
@ -144,7 +154,7 @@
private ElementReference SvgRef; private ElementReference SvgRef;
private ElementReference SvgContainerRef; private ElementReference SvgContainerRef;
private double ZoomScale = 2.0; // Zoom vào robot hơn khi mở private double ZoomScale = 1.5; // Zoom vào robot hơn khi mở
private const double MIN_ZOOM = 0.1; private const double MIN_ZOOM = 0.1;
private const double MAX_ZOOM = 5.0; private const double MAX_ZOOM = 5.0;
private const double BASE_PIXELS_PER_METER = 50.0; private const double BASE_PIXELS_PER_METER = 50.0;
@ -166,6 +176,19 @@
private double? MouseWorldX = null; private double? MouseWorldX = null;
private double? MouseWorldY = null; private double? MouseWorldY = null;
// Auto-follow robot
private bool AutoFollowRobot = false;
private void OnAutoFollowRobotChanged(bool value)
{
AutoFollowRobot = value;
if (AutoFollowRobot && MonitorData?.RobotPosition != null)
{
UpdateViewToFollowRobot();
StateHasChanged();
}
}
// Map image properties // Map image properties
private const double MapImageOriginX = -20.0; // OriginX in world coordinates (meters) 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 MapImageOriginY = -20.0; // OriginY in world coordinates (meters)
@ -215,8 +238,6 @@
MapImageWidth = imageDimensions.Width * MapImageResolution; MapImageWidth = imageDimensions.Width * MapImageResolution;
MapImageHeight = imageDimensions.Height * 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) if (MapImageWidth > 0 && MapImageHeight > 0)
{ {
MapImageLoaded = true; MapImageLoaded = true;
@ -232,6 +253,12 @@
private string GetTransform() private string GetTransform()
{ {
// Transform applies: first translate (in screen pixels), then scale (pixels per meter)
// World coordinates are in meters
// After transform: screenX = TranslateX + worldX * (ZoomScale * BASE_PIXELS_PER_METER)
// screenY = TranslateY + worldY * (ZoomScale * BASE_PIXELS_PER_METER)
// But we need to flip Y: screenY = TranslateY - worldY * (ZoomScale * BASE_PIXELS_PER_METER)
// This is handled by WorldToSvgY which flips Y before applying transform
return $"translate({TranslateX}, {TranslateY}) scale({ZoomScale * BASE_PIXELS_PER_METER})"; return $"translate({TranslateX}, {TranslateY}) scale({ZoomScale * BASE_PIXELS_PER_METER})";
} }
@ -254,7 +281,7 @@
// Điều chỉnh kích thước dựa trên ZoomScale // Đ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 // 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 scaleFactor = 2 / ZoomScale; // Tăng kích thước hiển thị
double width = RobotWidthMeters * scaleFactor; double width = RobotWidthMeters * scaleFactor;
double height = RobotLengthMeters * scaleFactor; double height = RobotLengthMeters * scaleFactor;
@ -299,17 +326,20 @@
private string GetOriginMarkerTransform() private string GetOriginMarkerTransform()
{ {
// Origin is at (MapImageOriginX, MapImageOriginY) in world coordinates // Origin is at (MapImageOriginX, MapImageOriginY) in world coordinates (bottom-left corner of image)
// In SVG: (MapImageOriginX, -MapImageOriginY) // In SVG coordinates (after Y flip): (MapImageOriginX, -MapImageOriginY)
var x = WorldToSvgX(MapImageOriginX); // Note: Image is rendered at (MapImageOriginX, -MapImageOriginY - MapImageHeight) in SVG
var y = WorldToSvgY(MapImageOriginY); // So origin marker should be at (MapImageOriginX, -MapImageOriginY) in SVG
var x = WorldToSvgX(0);
var y = WorldToSvgY(0);
return $"translate({x}, {y})"; return $"translate({x}, {y})";
} }
private double GetOriginMarkerSize() private double GetOriginMarkerSize()
{ {
// Marker size in world coordinates (meters) // Marker size in world coordinates (meters)
const double BaseMarkerSize = 0.5; // 1 meter const double BaseMarkerSize = 1; // 1 meter
double scaleFactor = 1.0 / ZoomScale; // Keep visual size constant double scaleFactor = 1.0 / ZoomScale; // Keep visual size constant
return BaseMarkerSize * scaleFactor; return BaseMarkerSize * scaleFactor;
} }
@ -322,6 +352,25 @@
return BaseStrokeWidth * scaleFactor; return BaseStrokeWidth * scaleFactor;
} }
public void OnMonitorDataUpdated()
{
// Auto-follow robot when MonitorData changes
if (AutoFollowRobot && !IsPanning && MonitorData?.RobotPosition != null)
{
UpdateViewToFollowRobot();
}
}
private void UpdateViewToFollowRobot()
{
if (MonitorData?.RobotPosition == null) return;
// Center view on robot
TranslateX = SvgWidth / 2 - MonitorData.RobotPosition.X * BASE_PIXELS_PER_METER * ZoomScale;
TranslateY = SvgHeight / 2 + MonitorData.RobotPosition.Y * BASE_PIXELS_PER_METER * ZoomScale;
StateHasChanged();
}
public void UpdatePath() public void UpdatePath()
{ {
if (MonitorData is not null && MonitorData.EdgeStates.Length > 0) if (MonitorData is not null && MonitorData.EdgeStates.Length > 0)

View File

@ -66,18 +66,26 @@
gap: 8px; gap: 8px;
} }
.auto-follow-control {
display: flex;
align-items: center;
padding: 4px 8px;
background-color: #3d3d3d;
border-radius: 4px;
}
.svg-container { .svg-container {
flex: 1; flex: 1;
overflow: hidden; overflow: hidden;
position: relative; position: relative;
background-color: #fafafa; background-color: #808080;
} }
.svg-container svg { .svg-container svg {
width: 100%; width: 100%;
height: 100%; height: 100%;
cursor: grab; cursor: grab;
background-color: dimgray; background-color: #808080;
} }
.svg-container svg:active { .svg-container svg:active {

View File

@ -29,6 +29,7 @@
{ {
_monitorData = data; _monitorData = data;
RobotMonitorViewRef?.UpdatePath(); RobotMonitorViewRef?.UpdatePath();
RobotMonitorViewRef?.OnMonitorDataUpdated();
StateHasChanged(); StateHasChanged();
} }