Initial commit
This commit is contained in:
@@ -0,0 +1,205 @@
|
||||
@using RobotNet10.FleetManager.Client.Services
|
||||
|
||||
@implements IDisposable
|
||||
|
||||
@if (State.ShowPath)
|
||||
{
|
||||
<defs>
|
||||
<marker id="target" markerWidth="8" markerHeight="8" refX="4" refY="4">
|
||||
<circle r="0.8" cx="4" cy="4" fill="red" />
|
||||
<circle r="3" cx="4" cy="4" stroke="red" stroke-width="0.2" fill="transparent" stroke-dasharray="0.2 0.2" />
|
||||
<line x1="0" y1="4" x2="2" y2="4" stroke="red" stroke-width="0.2" />
|
||||
<line x1="6" y1="4" x2="8" y2="4" stroke="red" stroke-width="0.2" />
|
||||
<line x1="4" y1="0" x2="4" y2="2" stroke="red" stroke-width="0.2" />
|
||||
<line x1="4" y1="6" x2="4" y2="8" stroke="red" stroke-width="0.2" />
|
||||
</marker>
|
||||
</defs>
|
||||
|
||||
<g id="robot-paths-layer">
|
||||
@foreach (var robot in State.Robots.Values)
|
||||
{
|
||||
if (robot is null || robot.Data is null || robot.Data.Path is null) continue;
|
||||
var isSelected = State.SelectedRobotId == robot.RobotId;
|
||||
var strokeColor = isSelected ? "#0288D1" : "#0097A7";
|
||||
var opacity = isSelected ? "1" : "0.8";
|
||||
|
||||
@if (robot.Data.Path.RobotPath.Length > 0)
|
||||
{
|
||||
var data = UpdatePath(robot.Data.Path.RobotPath);
|
||||
var strokeWidth = isSelected ? "0.12" : "0.08";
|
||||
|
||||
<path class="robot-path"
|
||||
d="@data"
|
||||
fill="none"
|
||||
stroke="@strokeColor"
|
||||
stroke-width="@strokeWidth"
|
||||
opacity="@opacity"
|
||||
stroke-dasharray="@(isSelected ? "0.2,0.1" : "none")"
|
||||
marker-end="url(#target)" />
|
||||
}
|
||||
|
||||
@if (robot.Data.Path.RobotBasePath.Length > 0)
|
||||
{
|
||||
var data = UpdatePath(robot.Data.Path.RobotBasePath);
|
||||
var strokeWidth = isSelected ? "0.4" : "0.3";
|
||||
|
||||
<path class="robot-path"
|
||||
d="@data"
|
||||
fill="none"
|
||||
stroke="@strokeColor"
|
||||
stroke-width="@strokeWidth"
|
||||
opacity="@opacity"
|
||||
stroke-dasharray="@(isSelected ? "0.2,0.1" : "none")" />
|
||||
}
|
||||
}
|
||||
</g>
|
||||
}
|
||||
|
||||
<g id="robots-layer">
|
||||
@foreach (var robot in State.Robots.Values)
|
||||
{
|
||||
if (robot is null || robot.Data is null) continue;
|
||||
var svgPos = State.WorldToSvg(robot.Data.AgvPosition.X, robot.Data.AgvPosition.Y);
|
||||
var degrees = -robot.Data.AgvPosition.Theta * 180.0 / Math.PI;
|
||||
|
||||
var baseScale = 2 / State.Viewport.ZoomLevel;
|
||||
var minScale = 1;
|
||||
var maxScale = 10.0;
|
||||
baseScale = Math.Max(minScale, Math.Min(maxScale, baseScale));
|
||||
|
||||
@if (robot.Model != null && !string.IsNullOrEmpty(robot.ModelImageBase64))
|
||||
{
|
||||
var imageLength = robot.Model.Length * baseScale;
|
||||
var imageWidth = robot.Model.Width * baseScale;
|
||||
|
||||
@* Navigation point offset: position relative to bottom-left corner of image *@
|
||||
@* Scale navigation point offset with robot size *@
|
||||
var navPointX = robot.Model.NavigationPointX * baseScale;
|
||||
var navPointY = robot.Model.NavigationPointY * baseScale;
|
||||
var imageX = -navPointX;
|
||||
var imageY = navPointY - imageWidth;
|
||||
|
||||
<g transform="translate(@svgPos.X.ToString("F2"), @svgPos.Y.ToString("F2")) rotate(@degrees.ToString("F2"))">
|
||||
<image href="data:image/png;base64,@robot.ModelImageBase64"
|
||||
x="@imageX.ToString("F3")"
|
||||
y="@imageY.ToString("F3")"
|
||||
width="@imageLength.ToString("F3")"
|
||||
height="@imageWidth.ToString("F3")"
|
||||
preserveAspectRatio="xMidYMid"
|
||||
@onclick="() => HandleRobotClick(robot.RobotId)"
|
||||
style="cursor: pointer; pointer-events: all;" />
|
||||
</g>
|
||||
}
|
||||
else
|
||||
{
|
||||
@* Placeholder circle until image loads - scale with zoom *@
|
||||
var placeholderRadius = 0.5 * baseScale;
|
||||
<g transform="translate(@svgPos.X.ToString("F2"), @svgPos.Y.ToString("F2")) rotate(@degrees.ToString("F2"))">
|
||||
<circle cx="0"
|
||||
cy="0"
|
||||
r="@placeholderRadius.ToString("F3")"
|
||||
fill="var(--mud-palette-error)"
|
||||
stroke="var(--mud-palette-error-darken)"
|
||||
stroke-width="@(0.05 * baseScale).ToString(" F3")"
|
||||
@onclick="() => HandleRobotClick(robot.RobotId)"
|
||||
style="cursor: pointer;" />
|
||||
</g>
|
||||
}
|
||||
|
||||
@* Selection highlight *@
|
||||
@if (State.SelectedRobotId == robot.RobotId)
|
||||
{
|
||||
@* Calculate highlight radius based on robot size and scale *@
|
||||
var highlightRadius = robot.Model != null
|
||||
? (Math.Max(robot.Model.Length, robot.Model.Width) / 2.0 + 0.2) * baseScale
|
||||
: 0.5 * baseScale;
|
||||
|
||||
<circle class="robot-selection-highlight"
|
||||
cx="@svgPos.X.ToString("F2")"
|
||||
cy="@svgPos.Y.ToString("F2")"
|
||||
r="@highlightRadius.ToString("F2")"
|
||||
fill="none"
|
||||
stroke="#1976d2"
|
||||
stroke-width="@(0.1 * baseScale).ToString(" F3")"
|
||||
stroke-dasharray="0.15,0.1"
|
||||
opacity="0.8" />
|
||||
}
|
||||
|
||||
@* Robot name (if ShowName = true) *@
|
||||
@if (State.ShowName)
|
||||
{
|
||||
@* Get robot name from AvailableRobots *@
|
||||
var robotName = State.AvailableRobots.FirstOrDefault(r => r.RobotId == robot.RobotId)?.Name ?? robot.RobotId;
|
||||
var isSelected = State.SelectedRobotId == robot.RobotId;
|
||||
var fontSize = 0.4 / Math.Sqrt(State.Viewport.ZoomLevel / 1.5);
|
||||
var textColor = isSelected ? "#9C27B0" : "#3F51B5";
|
||||
var fontWeight = isSelected ? "bold" : "500";
|
||||
|
||||
var offset = 0.6 * baseScale;
|
||||
|
||||
@RenderSvgText(robotName, svgPos.X, svgPos.Y + offset, fontSize, textColor, fontWeight)
|
||||
}
|
||||
}
|
||||
</g>
|
||||
|
||||
@code {
|
||||
[Parameter]
|
||||
public RobotMonitorState State { get; set; } = default!;
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
await base.OnAfterRenderAsync(firstRender);
|
||||
if (!firstRender) return;
|
||||
State.OnDataChanged += StateHasChanged;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
State.OnDataChanged -= StateHasChanged;
|
||||
}
|
||||
|
||||
private void HandleRobotClick(string robotId)
|
||||
{
|
||||
State.SelectRobot(robotId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Render SVG text element
|
||||
/// </summary>
|
||||
private RenderFragment RenderSvgText(string content, double x, double y, double fontSize, string fillColor, string fontWeight) => builder =>
|
||||
{
|
||||
builder.OpenElement(0, "text");
|
||||
builder.AddAttribute(1, "letter-spacing", "-0.01em");
|
||||
builder.AddAttribute(2, "x", x.ToString("F2"));
|
||||
builder.AddAttribute(3, "y", y.ToString("F2"));
|
||||
builder.AddAttribute(4, "font-size", fontSize.ToString("F3"));
|
||||
builder.AddAttribute(5, "fill", fillColor);
|
||||
builder.AddAttribute(6, "text-anchor", "middle");
|
||||
builder.AddAttribute(7, "font-family", "Segoe UI");
|
||||
builder.AddAttribute(8, "font-weight", fontWeight);
|
||||
builder.AddAttribute(9, "pointer-events", "none");
|
||||
builder.AddContent(10, content);
|
||||
builder.CloseElement();
|
||||
};
|
||||
|
||||
|
||||
public string UpdatePath(Shared.DTOs.Robot.NavigationPathEdge[] path)
|
||||
{
|
||||
if (path.Length > 0)
|
||||
{
|
||||
var startSvg = State.WorldToSvg(path[0].StartX, path[0].StartY);
|
||||
var inPath = $"M {startSvg.X} {startSvg.Y}";
|
||||
for (int i = 0; i < path.Length; i++)
|
||||
{
|
||||
var endSvg = State.WorldToSvg(path[i].EndX, path[i].EndY);
|
||||
var cp1Svg = State.WorldToSvg(path[i].ControlPoint1X, path[i].ControlPoint1Y);
|
||||
var cp2Svg = State.WorldToSvg(path[i].ControlPoint2X, path[i].ControlPoint2Y);
|
||||
if (path[i].Degree == 1) inPath = $"{inPath} L {endSvg.X} {endSvg.Y}";
|
||||
else if (path[i].Degree == 2) inPath = $"{inPath} Q {cp1Svg.X} {cp1Svg.Y} {endSvg.X} {endSvg.Y}";
|
||||
else inPath = $"{inPath} C {cp1Svg.X} {cp1Svg.Y} , {cp2Svg.X} {cp2Svg.Y}, {endSvg.X} {endSvg.Y}";
|
||||
}
|
||||
return inPath;
|
||||
}
|
||||
else return "";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
|
||||
/* Robot selection highlight animation */
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
opacity: 0.6;
|
||||
stroke-width: 0.08px;
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 1;
|
||||
stroke-width: 0.12px;
|
||||
}
|
||||
}
|
||||
|
||||
.robot-selection-highlight {
|
||||
animation: pulse 1.5s ease-in-out infinite;
|
||||
stroke-dasharray: 0.15, 0.1;
|
||||
}
|
||||
|
||||
/* Path visualization */
|
||||
.robot-path {
|
||||
transition: stroke-opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.robot-path:hover {
|
||||
stroke-opacity: 1;
|
||||
}
|
||||
Reference in New Issue
Block a user