Initial commit

This commit is contained in:
2026-07-03 16:37:12 +07:00
commit 63b8c1ea8b
1931 changed files with 640587 additions and 0 deletions

View File

@@ -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 "";
}
}

View File

@@ -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;
}