NavisworksTransport/src/Core/Services/TestAutomationHttpService.cs

2228 lines
92 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Autodesk.Navisworks.Api;
using NavisworksTransport.Commands;
using NavisworksTransport.Core.Animation;
using NavisworksTransport.Core.Config;
using NavisworksTransport.PathPlanning;
using NavisworksTransport.UI.WPF.ViewModels;
using NavisworksTransport.Utils;
using NavisworksTransport.Utils.CoordinateSystem;
using Newtonsoft.Json;
namespace NavisworksTransport.Core.Services
{
/// <summary>
/// 本地测试自动化 HTTP 控制面。
/// 当前暴露最小只读/导出接口,后续可在保持协议稳定的前提下追加命令端点。
/// </summary>
public sealed class TestAutomationHttpService : IDisposable
{
private const int DefaultPort = 18777;
private const string DefaultHost = "127.0.0.1";
private const string DefaultAutoTestRoutePrefix = "自动测试_";
private static readonly Lazy<TestAutomationHttpService> _instance =
new Lazy<TestAutomationHttpService>(() => new TestAutomationHttpService());
private readonly object _syncRoot = new object();
private static int _autoConfirmCollisionAnalysisDialogRequestCount;
private static int _autoChooseCreateNewDetectionRecordRequestCount;
private TcpListener _listener;
private CancellationTokenSource _cts;
private Task _acceptLoopTask;
private DateTime _startedAtUtc;
private string _lastStartError;
private TestAutomationHttpService()
{
}
public static TestAutomationHttpService Instance => _instance.Value;
public bool IsRunning
{
get
{
lock (_syncRoot)
{
return _listener != null;
}
}
}
public int Port => DefaultPort;
public string BaseUrl => $"http://{DefaultHost}:{DefaultPort}";
public static bool ShouldAutoConfirmCollisionAnalysisDialogs =>
Interlocked.CompareExchange(ref _autoConfirmCollisionAnalysisDialogRequestCount, 0, 0) > 0;
public static bool ShouldAutoChooseCreateNewDetectionRecord =>
Interlocked.CompareExchange(ref _autoChooseCreateNewDetectionRecordRequestCount, 0, 0) > 0;
public void Start()
{
lock (_syncRoot)
{
if (_listener != null)
{
return;
}
try
{
_cts = new CancellationTokenSource();
_listener = new TcpListener(IPAddress.Loopback, DefaultPort);
_listener.Start();
_startedAtUtc = DateTime.UtcNow;
_lastStartError = null;
_acceptLoopTask = Task.Run(() => AcceptLoopAsync(_cts.Token));
LogManager.Info($"[测试HTTP] 服务已启动: {BaseUrl}");
}
catch (Exception ex)
{
_lastStartError = ex.Message;
_listener = null;
_cts?.Dispose();
_cts = null;
_acceptLoopTask = null;
LogManager.Error($"[测试HTTP] 服务启动失败: {ex.Message}", ex);
}
}
}
public void Stop()
{
Task acceptLoopTask = null;
lock (_syncRoot)
{
if (_listener == null)
{
return;
}
try
{
_cts?.Cancel();
_listener.Stop();
acceptLoopTask = _acceptLoopTask;
}
catch (Exception ex)
{
LogManager.Warning($"[测试HTTP] 服务停止时出现警告: {ex.Message}");
}
finally
{
_listener = null;
_acceptLoopTask = null;
_cts?.Dispose();
_cts = null;
}
}
try
{
acceptLoopTask?.Wait(1000);
}
catch
{
// 忽略退出等待异常
}
LogManager.Info("[测试HTTP] 服务已停止");
}
private async Task AcceptLoopAsync(CancellationToken cancellationToken)
{
while (!cancellationToken.IsCancellationRequested)
{
TcpClient client = null;
try
{
client = await _listener.AcceptTcpClientAsync().ConfigureAwait(false);
_ = Task.Run(() => HandleClientAsync(client, cancellationToken), cancellationToken);
}
catch (ObjectDisposedException)
{
break;
}
catch (SocketException) when (cancellationToken.IsCancellationRequested)
{
break;
}
catch (Exception ex)
{
LogManager.Error($"[测试HTTP] 接收请求失败: {ex.Message}", ex);
client?.Dispose();
}
}
}
private async Task HandleClientAsync(TcpClient client, CancellationToken cancellationToken)
{
using (client)
using (var stream = client.GetStream())
using (var reader = new StreamReader(stream, Encoding.UTF8, false, 4096, true))
using (var writer = new StreamWriter(stream, new UTF8Encoding(false), 4096, true) { NewLine = "\r\n", AutoFlush = true })
{
try
{
string requestLine = await reader.ReadLineAsync().ConfigureAwait(false);
if (string.IsNullOrWhiteSpace(requestLine))
{
return;
}
string[] requestLineParts = requestLine.Split(' ');
if (requestLineParts.Length < 2)
{
await WriteJsonResponseAsync(writer, 400, BuildEnvelope(false, error: "Invalid request line")).ConfigureAwait(false);
return;
}
string method = requestLineParts[0].Trim().ToUpperInvariant();
string requestTarget = requestLineParts[1].Trim();
var request = ParseRequestTarget(requestTarget);
string headerLine;
while (!string.IsNullOrEmpty(headerLine = await reader.ReadLineAsync().ConfigureAwait(false)))
{
if (cancellationToken.IsCancellationRequested)
{
return;
}
}
if (string.Equals(method, "GET", StringComparison.OrdinalIgnoreCase) &&
string.Equals(request.Path, "/api/test/ping", StringComparison.OrdinalIgnoreCase))
{
var payload = new
{
ok = true,
service = "NavisworksTransport.TestAutomation",
protocol = "http",
version = 1,
baseUrl = BaseUrl,
serverTimeUtc = DateTime.UtcNow.ToString("o")
};
await WriteJsonResponseAsync(writer, 200, payload).ConfigureAwait(false);
return;
}
if (string.Equals(method, "GET", StringComparison.OrdinalIgnoreCase) &&
string.Equals(request.Path, "/api/test/status", StringComparison.OrdinalIgnoreCase))
{
object payload = InvokeOnUiThread(BuildStatusPayload);
await WriteJsonResponseAsync(writer, 200, BuildEnvelope(true, payload)).ConfigureAwait(false);
return;
}
if (string.Equals(method, "GET", StringComparison.OrdinalIgnoreCase) &&
string.Equals(request.Path, "/api/test/routes", StringComparison.OrdinalIgnoreCase))
{
object payload = InvokeOnUiThread(BuildRoutesPayload);
await WriteJsonResponseAsync(writer, 200, BuildEnvelope(true, payload)).ConfigureAwait(false);
return;
}
if (string.Equals(method, "GET", StringComparison.OrdinalIgnoreCase) &&
string.Equals(request.Path, "/api/test/route-grid-diagnostics", StringComparison.OrdinalIgnoreCase))
{
object payload = InvokeOnUiThread(() => BuildRouteGridDiagnosticsPayload(request.Query));
await WriteJsonResponseAsync(writer, 200, BuildEnvelope(true, payload)).ConfigureAwait(false);
return;
}
if (string.Equals(method, "GET", StringComparison.OrdinalIgnoreCase) &&
string.Equals(request.Path, "/api/test/selection", StringComparison.OrdinalIgnoreCase))
{
object payload = InvokeOnUiThread(BuildSelectionPayload);
await WriteJsonResponseAsync(writer, 200, BuildEnvelope(true, payload)).ConfigureAwait(false);
return;
}
if (string.Equals(method, "POST", StringComparison.OrdinalIgnoreCase) &&
string.Equals(request.Path, "/api/test/export-debug-snapshot", StringComparison.OrdinalIgnoreCase))
{
object snapshotPayload = InvokeOnUiThread(BuildDebugSnapshotPayload);
string snapshotFilePath = WriteSnapshotToDisk(snapshotPayload);
var payload = new
{
snapshotFilePath,
snapshot = snapshotPayload
};
await WriteJsonResponseAsync(writer, 200, BuildEnvelope(true, payload)).ConfigureAwait(false);
return;
}
if (string.Equals(method, "POST", StringComparison.OrdinalIgnoreCase) &&
string.Equals(request.Path, "/api/test/select-route", StringComparison.OrdinalIgnoreCase))
{
object payload = InvokeOnUiThread(() => SelectRoutePayload(request.Query));
await WriteJsonResponseAsync(writer, 200, BuildEnvelope(true, payload)).ConfigureAwait(false);
return;
}
if (string.Equals(method, "POST", StringComparison.OrdinalIgnoreCase) &&
string.Equals(request.Path, "/api/test/select-default-route", StringComparison.OrdinalIgnoreCase))
{
object payload = InvokeOnUiThread(() => SelectDefaultRoutePayload(request.Query));
await WriteJsonResponseAsync(writer, 200, BuildEnvelope(true, payload)).ConfigureAwait(false);
return;
}
if (string.Equals(method, "POST", StringComparison.OrdinalIgnoreCase) &&
string.Equals(request.Path, "/api/test/select-animated-object", StringComparison.OrdinalIgnoreCase))
{
object payload = InvokeOnUiThread(() => SelectAnimatedObjectPayload(request.Query));
await WriteJsonResponseAsync(writer, 200, BuildEnvelope(true, payload)).ConfigureAwait(false);
return;
}
if (string.Equals(method, "POST", StringComparison.OrdinalIgnoreCase) &&
string.Equals(request.Path, "/api/test/run-ground-collision-test", StringComparison.OrdinalIgnoreCase))
{
object payload = await RunVirtualCollisionTestAsync(request.Query, PathType.Ground).ConfigureAwait(false);
await WriteJsonResponseAsync(writer, 200, BuildEnvelope(true, payload)).ConfigureAwait(false);
return;
}
if (string.Equals(method, "POST", StringComparison.OrdinalIgnoreCase) &&
string.Equals(request.Path, "/api/test/run-virtual-collision-test", StringComparison.OrdinalIgnoreCase))
{
object payload = await RunVirtualCollisionTestAsync(request.Query, null).ConfigureAwait(false);
await WriteJsonResponseAsync(writer, 200, BuildEnvelope(true, payload)).ConfigureAwait(false);
return;
}
if (string.Equals(method, "POST", StringComparison.OrdinalIgnoreCase) &&
string.Equals(request.Path, "/api/test/analyze-auto-path-grid", StringComparison.OrdinalIgnoreCase))
{
object payload = InvokeOnUiThread(() => AnalyzeAutoPathGridPayload(request.Query));
await WriteJsonResponseAsync(writer, 200, BuildEnvelope(true, payload)).ConfigureAwait(false);
return;
}
if (string.Equals(method, "POST", StringComparison.OrdinalIgnoreCase) &&
string.Equals(request.Path, "/api/test/run-auto-path", StringComparison.OrdinalIgnoreCase))
{
object payload = InvokeOnUiThread(() => RunAutoPathPayload(request.Query));
await WriteJsonResponseAsync(writer, 200, BuildEnvelope(true, payload)).ConfigureAwait(false);
return;
}
if (!string.Equals(method, "GET", StringComparison.OrdinalIgnoreCase) &&
!string.Equals(method, "POST", StringComparison.OrdinalIgnoreCase))
{
await WriteJsonResponseAsync(writer, 405, BuildEnvelope(false, error: "Only GET and POST are supported in the current test API")).ConfigureAwait(false);
return;
}
await WriteJsonResponseAsync(writer, 404, BuildEnvelope(false, error: $"Unknown endpoint: {request.Path}")).ConfigureAwait(false);
}
catch (Exception ex)
{
LogManager.Error($"[测试HTTP] 处理请求失败: {ex.Message}", ex);
await WriteJsonResponseAsync(writer, 500, BuildEnvelope(false, error: ex.Message)).ConfigureAwait(false);
}
}
}
private object BuildStatusPayload()
{
Document activeDocument = Autodesk.Navisworks.Api.Application.ActiveDocument;
PathPlanningManager pathManager = PathPlanningManager.GetActivePathManager();
PathAnimationManager animationManager = PathAnimationManager.GetInstance();
PathRoute currentRoute = pathManager?.CurrentRoute;
List<PathRoute> allRoutes = pathManager?.GetAllRoutes();
return new
{
service = new
{
name = "NavisworksTransport.TestAutomation",
protocol = "http",
version = 1,
isRunning = IsRunning,
baseUrl = BaseUrl,
port = Port,
startedAtUtc = _startedAtUtc == default(DateTime) ? null : _startedAtUtc.ToString("o"),
lastStartError = _lastStartError
},
environment = new
{
processId = System.Diagnostics.Process.GetCurrentProcess().Id,
machineName = Environment.MachineName,
logFilePath = LogManager.LogFilePath
},
document = new
{
hasActiveDocument = activeDocument != null,
fileName = activeDocument?.FileName,
title = string.IsNullOrEmpty(activeDocument?.FileName) ? null : Path.GetFileName(activeDocument.FileName),
modelCount = activeDocument?.Models?.Count ?? 0
},
pathManager = new
{
isAvailable = pathManager != null,
routeCount = allRoutes?.Count ?? 0,
currentRouteId = currentRoute?.Id,
currentRouteName = currentRoute?.Name,
currentRoutePathType = currentRoute?.PathType.ToString(),
currentRoutePointCount = currentRoute?.Points?.Count ?? 0
},
animation = new
{
isAvailable = animationManager != null,
currentState = animationManager?.CurrentState.ToString(),
isAnimating = animationManager?.IsAnimating ?? false,
currentFrame = animationManager?.CurrentFrame ?? 0,
totalFrames = animationManager?.TotalFrames ?? 0,
hasTrackedRotation = animationManager?.HasTrackedRotation ?? false,
currentYawDegrees = animationManager == null ? 0.0 : animationManager.CurrentYaw * 180.0 / Math.PI
}
};
}
private object BuildDebugSnapshotPayload()
{
Document activeDocument = Autodesk.Navisworks.Api.Application.ActiveDocument;
PathPlanningManager pathManager = PathPlanningManager.GetActivePathManager();
PathAnimationManager animationManager = PathAnimationManager.GetInstance();
PathRoute currentRoute = pathManager?.CurrentRoute;
ModelItem controlledObject = ResolveControlledObject(animationManager);
var trackedState = ResolveTrackedState(animationManager, controlledObject);
var geometryState = ResolveGeometryState(controlledObject);
var boundingBoxState = ResolveBoundingBoxState(controlledObject);
return new
{
snapshotVersion = 1,
capturedAtUtc = DateTime.UtcNow.ToString("o"),
document = new
{
hasActiveDocument = activeDocument != null,
fileName = activeDocument?.FileName,
title = string.IsNullOrEmpty(activeDocument?.FileName) ? null : Path.GetFileName(activeDocument.FileName),
modelCount = activeDocument?.Models?.Count ?? 0
},
route = new
{
isAvailable = currentRoute != null,
id = currentRoute?.Id,
name = currentRoute?.Name,
pathType = currentRoute?.PathType.ToString(),
pointCount = currentRoute?.Points?.Count ?? 0
},
animation = new
{
isAvailable = animationManager != null,
currentState = animationManager?.CurrentState.ToString(),
isAnimating = animationManager?.IsAnimating ?? false,
currentFrame = animationManager?.CurrentFrame ?? 0,
totalFrames = animationManager?.TotalFrames ?? 0,
currentYawDegrees = animationManager == null ? 0.0 : animationManager.CurrentYaw * 180.0 / Math.PI,
hasTrackedRotation = animationManager?.HasTrackedRotation ?? false
},
controlledObject = new
{
exists = controlledObject != null,
displayName = controlledObject?.DisplayName,
instanceGuid = controlledObject == null ? null : controlledObject.InstanceGuid.ToString(),
isVirtualObject = controlledObject != null &&
VirtualObjectManager.Instance.IsVirtualObjectActive &&
ReferenceEquals(VirtualObjectManager.Instance.CurrentVirtualObject, controlledObject),
trackedState,
geometryState,
boundingBox = boundingBoxState
},
environment = new
{
processId = System.Diagnostics.Process.GetCurrentProcess().Id,
machineName = Environment.MachineName,
logFilePath = LogManager.LogFilePath
}
};
}
private object BuildRoutesPayload()
{
PathPlanningManager pathManager = PathPlanningManager.GetActivePathManager();
List<PathRoute> routes = pathManager?.GetAllRoutes() ?? new List<PathRoute>();
PathRoute currentRoute = pathManager?.CurrentRoute;
return new
{
isAvailable = pathManager != null,
currentRouteId = currentRoute?.Id,
currentRouteName = currentRoute?.Name,
currentRoutePathType = currentRoute?.PathType.ToString(),
totalRouteCount = routes.Count,
routes = routes.Select(route => SerializeRoute(route, currentRoute)).ToList(),
suggestedAutoTestRoutes = new
{
ground = TryFindAutoTestRoute(routes, PathType.Ground)?.Name,
hoisting = TryFindAutoTestRoute(routes, PathType.Hoisting)?.Name,
rail = TryFindAutoTestRoute(routes, PathType.Rail)?.Name
}
};
}
private object BuildSelectionPayload()
{
Document activeDocument = Autodesk.Navisworks.Api.Application.ActiveDocument;
var selectedItems = activeDocument?.CurrentSelection?.SelectedItems?.Cast<ModelItem>().Where(item => item != null).ToList()
?? new List<ModelItem>();
return new
{
hasActiveDocument = activeDocument != null,
selectionCount = selectedItems.Count,
selectedItems = selectedItems.Select(SerializeModelItem).ToList()
};
}
private object SelectRoutePayload(Dictionary<string, string> query)
{
PathPlanningManager pathManager = RequirePathManager();
string routeName = GetRequiredQueryValue(query, "name");
List<PathRoute> routes = pathManager.GetAllRoutes() ?? new List<PathRoute>();
PathRoute selectedRoute = routes.FirstOrDefault(route =>
string.Equals(route.Name, routeName, StringComparison.OrdinalIgnoreCase));
if (selectedRoute == null)
{
throw new InvalidOperationException($"找不到指定路径: {routeName}");
}
pathManager.SetCurrentRoute(selectedRoute);
LogManager.Info($"[测试HTTP] 已切换当前路径: {selectedRoute.Name} ({selectedRoute.PathType})");
return new
{
selected = SerializeRoute(selectedRoute, selectedRoute),
totalRouteCount = routes.Count
};
}
private object SelectDefaultRoutePayload(Dictionary<string, string> query)
{
PathPlanningManager pathManager = RequirePathManager();
string pathTypeValue = GetRequiredQueryValue(query, "pathType");
string prefix = GetOptionalQueryValue(query, "prefix") ?? DefaultAutoTestRoutePrefix;
if (!Enum.TryParse(pathTypeValue, true, out PathType pathType))
{
throw new InvalidOperationException($"无法解析路径类型: {pathTypeValue}");
}
List<PathRoute> routes = pathManager.GetAllRoutes() ?? new List<PathRoute>();
PathRoute selectedRoute = routes
.FirstOrDefault(route =>
route.PathType == pathType &&
!string.IsNullOrWhiteSpace(route.Name) &&
route.Name.StartsWith(prefix, StringComparison.OrdinalIgnoreCase));
if (selectedRoute == null)
{
throw new InvalidOperationException(
$"找不到默认测试路径: prefix={prefix}, pathType={pathType}");
}
pathManager.SetCurrentRoute(selectedRoute);
LogManager.Info($"[测试HTTP] 已切换默认测试路径: {selectedRoute.Name} ({selectedRoute.PathType})");
return new
{
selectionRule = new
{
prefix,
pathType = pathType.ToString()
},
selected = SerializeRoute(selectedRoute, selectedRoute)
};
}
private object SelectAnimatedObjectPayload(Dictionary<string, string> query)
{
AnimationControlViewModel animationViewModel = RequireAnimationControlViewModel();
PathPlanningManager pathManager = RequirePathManager();
animationViewModel.SetPathPlanningManager(pathManager);
ModelItem animatedObject = ResolveAnimatedObjectFromQuery(query);
SelectDocumentItem(animatedObject);
if (!animationViewModel.SelectAnimatedObjectCommand.CanExecute(null))
{
throw new InvalidOperationException("当前动画视图模型不允许执行“选择移动物体”命令");
}
animationViewModel.UseVirtualObject = false;
animationViewModel.SelectAnimatedObjectCommand.Execute(null);
if (!ReferenceEquals(animationViewModel.SelectedAnimatedObject, animatedObject))
{
throw new InvalidOperationException("选择真实物体失败:动画视图模型未保留选中的对象");
}
return new
{
selectedAnimatedObject = SerializeModelItem(animatedObject),
canGenerateAnimation = animationViewModel.CanGenerateAnimation,
isManualCollisionTargetEnabled = animationViewModel.IsManualCollisionTargetEnabled
};
}
private async Task<object> RunVirtualCollisionTestAsync(Dictionary<string, string> query, PathType? fixedPathType)
{
using (EnableAutoConfirmCollisionAnalysisDialogs())
using (EnableAutoChooseCreateNewDetectionRecord())
{
int timeoutSeconds = ParseTimeoutSeconds(query, 180);
DateTime deadlineUtc = DateTime.UtcNow.AddSeconds(timeoutSeconds);
var setupResult = InvokeOnUiThread(() => PrepareVirtualCollisionTest(query, fixedPathType));
await WaitForConditionAsync(
deadlineUtc,
() => InvokeOnUiThread(() =>
{
var animationManager = PathAnimationManager.GetInstance();
return animationManager != null &&
animationManager.TotalFrames > 0 &&
(animationManager.CurrentState == AnimationState.Ready ||
animationManager.CurrentState == AnimationState.Finished);
}),
"等待动画生成完成超时").ConfigureAwait(false);
InvokeOnUiThread(() =>
{
StartPreparedAnimation();
return true;
});
await WaitForConditionAsync(
deadlineUtc,
() => InvokeOnUiThread(() => PathAnimationManager.GetInstance()?.CurrentState == AnimationState.Finished),
"等待动画播放和碰撞检测完成超时").ConfigureAwait(false);
await WaitForConditionAsync(
deadlineUtc,
() => InvokeOnUiThread(() =>
{
var animationVm = RequireAnimationControlViewModel();
return animationVm.HasGeneratedCollisionReport;
}),
"等待碰撞报告生成超时").ConfigureAwait(false);
return InvokeOnUiThread(() =>
{
AnimationControlViewModel animationVm = RequireAnimationControlViewModel();
PathAnimationManager animationManager = PathAnimationManager.GetInstance();
CollisionReportResult report = animationVm.LastGeneratedReport;
return new
{
route = setupResult.route,
animatedObject = setupResult.animatedObject,
requestedPathType = setupResult.pathType.ToString(),
timeoutSeconds,
automation = new
{
autoConfirmedCollisionAnalysisDialog = true,
autoChooseCreateNewDetectionRecord = true
},
animation = new
{
currentState = animationManager?.CurrentState.ToString(),
totalFrames = animationManager?.TotalFrames ?? 0,
currentFrame = animationManager?.CurrentFrame ?? 0,
detectionRecordId = animationManager?.CurrentDetectionRecordId
},
report = report == null
? null
: new
{
totalCollisions = report.TotalCollisions,
uniqueCollidedObjectsCount = report.UniqueCollidedObjectsCount,
pathName = report.PathName,
movingObjectInfo = report.MovingObjectInfo,
hasScreenshots = report.Screenshots != null && report.Screenshots.Count > 0,
screenshotCount = report.Screenshots?.Count ?? 0,
resultId = report.ResultId,
routeId = report.RouteId
}
};
});
}
}
private static object BuildEnvelope(bool ok, object data = null, string error = null)
{
return new Dictionary<string, object>
{
["ok"] = ok,
["data"] = data,
["error"] = error
};
}
private static T InvokeOnUiThread<T>(Func<T> func)
{
var dispatcher = System.Windows.Application.Current?.Dispatcher;
if (dispatcher == null || dispatcher.CheckAccess())
{
return func();
}
return dispatcher.Invoke(func);
}
private static ModelItem ResolveControlledObject(PathAnimationManager animationManager)
{
if (VirtualObjectManager.Instance.IsVirtualObjectActive &&
VirtualObjectManager.Instance.CurrentVirtualObject != null)
{
return VirtualObjectManager.Instance.CurrentVirtualObject;
}
return animationManager?.AnimatedObject;
}
private static object SerializeModelItem(ModelItem item)
{
if (item == null)
{
return null;
}
return new
{
displayName = item.DisplayName,
instanceGuid = item.InstanceGuid.ToString()
};
}
private static object SerializeRoute(PathRoute route, PathRoute currentRoute)
{
return new
{
id = route?.Id,
name = route?.Name,
pathType = route?.PathType.ToString(),
pointCount = route?.Points?.Count ?? 0,
isCurrent = currentRoute != null && ReferenceEquals(route, currentRoute),
matchesAutoTestPrefix = !string.IsNullOrWhiteSpace(route?.Name) &&
route.Name.StartsWith(DefaultAutoTestRoutePrefix, StringComparison.OrdinalIgnoreCase)
};
}
private static PathRoute TryFindAutoTestRoute(
IEnumerable<PathRoute> routes,
PathType pathType,
string routePrefix = DefaultAutoTestRoutePrefix)
{
string effectivePrefix = string.IsNullOrWhiteSpace(routePrefix)
? DefaultAutoTestRoutePrefix
: routePrefix;
return routes?.FirstOrDefault(route =>
route != null &&
route.PathType == pathType &&
!string.IsNullOrWhiteSpace(route.Name) &&
route.Name.StartsWith(effectivePrefix, StringComparison.OrdinalIgnoreCase));
}
private static object AnalyzeAutoPathGridPayload(Dictionary<string, string> query)
{
PathPlanningManager pathManager = RequirePathManager();
PathRoute route = ResolveVirtualCollisionRoute(pathManager, query, null);
if (route.Points == null || route.Points.Count < 2)
{
throw new InvalidOperationException($"路径点不足,无法分析自动路径网格: {route.Name}");
}
List<PathPoint> orderedPoints = route.Points.OrderBy(point => point.Index).ToList();
PathPoint startPoint = orderedPoints.First();
PathPoint endPoint = orderedPoints.Last();
BoundingBox3D bounds = InvokePathManagerPrivate<BoundingBox3D>(pathManager, "GetModelBounds");
double gridSize = InvokePathManagerPrivate<double>(pathManager, "CalculateOptimalGridSize", bounds);
var config = ConfigManager.Instance.Current?.PathEditing
?? throw new InvalidOperationException("缺少 PathEditing 配置,无法分析自动路径网格");
double objectLengthInMeters = config.ObjectLengthMeters;
double objectWidthInMeters = config.ObjectWidthMeters;
double objectHeightInMeters = config.ObjectHeightMeters;
double safetyMarginInMeters = config.SafetyMarginMeters;
double objectRadiusInMeters = Math.Max(objectLengthInMeters, objectWidthInMeters) / 2.0;
var gridMapGenerator = new GridMapGenerator();
GridMap gridMap = gridMapGenerator.GenerateFromBIM(
bounds,
gridSize,
objectRadiusInMeters,
safetyMarginInMeters,
startPoint.Position,
endPoint.Position,
objectHeightInMeters);
return new
{
requestedPathType = route.PathType.ToString(),
route = new
{
id = route.Id,
name = route.Name,
pathType = route.PathType.ToString(),
pointCount = route.Points.Count,
startPoint = SerializePathPoint(startPoint),
endPoint = SerializePathPoint(endPoint)
},
planningParameters = new
{
gridSizeInMeters = gridSize,
objectLengthInMeters,
objectWidthInMeters,
objectHeightInMeters,
objectRadiusInMeters,
safetyMarginInMeters
},
bounds = new
{
min = SerializePoint3D(bounds.Min),
max = SerializePoint3D(bounds.Max)
},
gridStats = SerializeGridStats(gridMap),
obstacleDiagnostics = BuildObstacleDiagnostics(gridMapGenerator, gridMap, objectHeightInMeters, safetyMarginInMeters)
};
}
private static object RunAutoPathPayload(Dictionary<string, string> query)
{
PathPlanningManager pathManager = RequirePathManager();
PathRoute route = ResolveVirtualCollisionRoute(pathManager, query, null);
if (route.Points == null || route.Points.Count < 2)
{
throw new InvalidOperationException($"路径点不足,无法执行自动路径规划: {route.Name}");
}
List<PathPoint> orderedPoints = route.Points.OrderBy(point => point.Index).ToList();
PathPoint startPoint = orderedPoints.First();
PathPoint endPoint = orderedPoints.Last();
var config = ConfigManager.Instance.Current?.PathEditing
?? throw new InvalidOperationException("缺少 PathEditing 配置,无法执行自动路径规划");
BoundingBox3D bounds = InvokePathManagerPrivate<BoundingBox3D>(pathManager, "GetModelBounds");
double gridSize = ParsePositiveDoubleQuery(
query,
"gridSizeInMeters",
InvokePathManagerPrivate<double>(pathManager, "CalculateOptimalGridSize", bounds));
double objectHeightInMeters = config.ObjectHeightMeters;
double objectLengthInMeters = ParsePositiveDoubleQuery(query, "objectLengthInMeters", config.ObjectLengthMeters);
double objectWidthInMeters = ParsePositiveDoubleQuery(query, "objectWidthInMeters", config.ObjectWidthMeters);
double safetyMarginInMeters = ParseNonNegativeDoubleQuery(query, "safetyMarginInMeters", config.SafetyMarginMeters);
double objectRadiusInMeters = Math.Max(objectLengthInMeters, objectWidthInMeters) / 2.0;
PathStrategy strategy = PathStrategy.Shortest;
string strategyText = GetOptionalQueryValue(query, "strategy");
if (!string.IsNullOrWhiteSpace(strategyText) &&
Enum.TryParse(strategyText, true, out PathStrategy parsedStrategy))
{
strategy = parsedStrategy;
}
PathRoute autoRoute = pathManager
.AutoPlanPath(
startPoint,
endPoint,
objectRadiusInMeters,
safetyMarginInMeters,
gridSize,
objectHeightInMeters,
strategy)
.GetAwaiter()
.GetResult();
if (autoRoute == null)
{
throw new InvalidOperationException("AutoPlanPath 返回空结果");
}
return new
{
sourceRoute = new
{
id = route.Id,
name = route.Name,
pathType = route.PathType.ToString()
},
generatedRoute = new
{
id = autoRoute.Id,
name = autoRoute.Name,
pathType = autoRoute.PathType.ToString(),
pointCount = autoRoute.Points?.Count ?? 0,
length = autoRoute.TotalLength
},
planningParameters = new
{
gridSizeInMeters = gridSize,
objectHeightInMeters,
objectLengthInMeters,
objectWidthInMeters,
objectRadiusInMeters,
safetyMarginInMeters,
strategy = strategy.ToString()
},
segmentValidation = ValidateRouteSegmentsAgainstGridMap(autoRoute, autoRoute.AssociatedGridMap)
};
}
private static object ValidateRouteSegmentsAgainstGridMap(PathRoute route, GridMap gridMap)
{
if (route?.Points == null || route.Points.Count < 2 || gridMap == null)
{
return new
{
isAvailable = false,
segmentCount = 0,
sampleCount = 0,
invalidSampleCount = 0,
blockedSampleCount = 0,
failures = new object[0]
};
}
var orderedPoints = route.Points.OrderBy(point => point.Index).ToList();
var failures = new List<object>();
int sampleCount = 0;
int invalidSampleCount = 0;
int blockedSampleCount = 0;
int blockedCellInteriorIntersectionCount = 0;
double sampleSpacing = Math.Max(gridMap.CellSize * 0.25, 1e-6);
for (int segmentIndex = 0; segmentIndex < orderedPoints.Count - 1; segmentIndex++)
{
Point3D start = orderedPoints[segmentIndex].Position;
Point3D end = orderedPoints[segmentIndex + 1].Position;
double segmentLength = CalculateHostHorizontalDistance(start, end, gridMap.CoordinateSystemType);
int samples = Math.Max(1, (int)Math.Ceiling(segmentLength / sampleSpacing));
for (int sampleIndex = 0; sampleIndex <= samples; sampleIndex++)
{
double t = samples == 0 ? 0.0 : (double)sampleIndex / samples;
Point3D samplePoint = InterpolateHostPoint(start, end, t);
double sampleElevation = gridMap.GetWorldElevation(samplePoint);
GridPoint2D gridPosition = gridMap.WorldToGrid(samplePoint);
sampleCount++;
if (!gridMap.IsValidGridPosition(gridPosition))
{
invalidSampleCount++;
AddFailure(failures, segmentIndex, sampleIndex, samplePoint, gridPosition, "GridOutOfRange");
continue;
}
if (!gridMap.IsPassableAtElevation(gridPosition, sampleElevation, 0.1))
{
blockedSampleCount++;
AddFailure(failures, segmentIndex, sampleIndex, samplePoint, gridPosition, "NotPassableAtElevation");
}
}
blockedCellInteriorIntersectionCount += CountBlockedCellInteriorIntersections(
start,
end,
gridMap,
segmentIndex,
failures);
}
return new
{
isAvailable = true,
segmentCount = Math.Max(0, orderedPoints.Count - 1),
sampleCount,
invalidSampleCount,
blockedSampleCount,
blockedCellInteriorIntersectionCount,
failures = failures.Take(20).ToList()
};
}
private static object BuildRouteGridDiagnosticsPayload(Dictionary<string, string> query)
{
PathPlanningManager pathManager = RequirePathManager();
string routeName = GetOptionalQueryValue(query, "routeName");
List<PathRoute> routes = pathManager.GetAllRoutes() ?? new List<PathRoute>();
PathRoute route = string.IsNullOrWhiteSpace(routeName)
? pathManager.CurrentRoute
: routes.FirstOrDefault(r => string.Equals(r.Name, routeName, StringComparison.OrdinalIgnoreCase));
if (route == null)
{
throw new InvalidOperationException(
string.IsNullOrWhiteSpace(routeName)
? "当前没有选中的路径"
: $"找不到指定路径: {routeName}");
}
GridMap gridMap = route.AssociatedGridMap;
if (gridMap == null)
{
if (!pathManager.TryRestoreGridMapForRoute(route, out gridMap, out string restoreMessage))
{
throw new InvalidOperationException($"路径没有关联 GridMap且无法按保存参数重建无法诊断: {route.Name}。{restoreMessage}");
}
}
var orderedPoints = route.Points?.OrderBy(point => point.Index).ToList() ?? new List<PathPoint>();
var segmentDiagnostics = new List<object>();
var blockedIntersections = new List<object>();
int intersectedCellCount = 0;
for (int segmentIndex = 0; segmentIndex < orderedPoints.Count - 1; segmentIndex++)
{
PathPoint startPathPoint = orderedPoints[segmentIndex];
PathPoint endPathPoint = orderedPoints[segmentIndex + 1];
Point3D start = startPathPoint.Position;
Point3D end = endPathPoint.Position;
var intersectedCells = GetSegmentIntersectedCells(start, end, gridMap);
intersectedCellCount += intersectedCells.Count;
var blockedCells = intersectedCells
.Where(cell => !cell.isPassableAtSegmentElevation)
.ToList();
foreach (var blockedCell in blockedCells)
{
if (blockedIntersections.Count < 100)
{
blockedIntersections.Add(new
{
segmentIndex,
startPointIndex = startPathPoint.Index,
endPointIndex = endPathPoint.Index,
cell = blockedCell
});
}
}
segmentDiagnostics.Add(new
{
segmentIndex,
startPointIndex = startPathPoint.Index,
endPointIndex = endPathPoint.Index,
startGrid = SerializeGridPosition(gridMap.WorldToGrid(start)),
endGrid = SerializeGridPosition(gridMap.WorldToGrid(end)),
startHorizontal = SerializeHorizontalPoint(start, gridMap),
endHorizontal = SerializeHorizontalPoint(end, gridMap),
intersectedCellCount = intersectedCells.Count,
blockedCellCount = blockedCells.Count,
intersectedCells = intersectedCells.Take(200).ToList()
});
}
return new
{
route = new
{
id = route.Id,
name = route.Name,
pathType = route.PathType.ToString(),
pointCount = orderedPoints.Count,
length = route.TotalLength
},
grid = new
{
coordinateSystemType = gridMap.CoordinateSystemType.ToString(),
width = gridMap.Width,
height = gridMap.Height,
cellSize = gridMap.CellSize,
cellSizeInMeters = UnitsConverter.ConvertToMeters(gridMap.CellSize),
origin = SerializePoint3D(gridMap.Origin),
originHorizontal = SerializeHorizontalPoint(gridMap.Origin, gridMap)
},
points = orderedPoints.Select(point => SerializePathPointWithGrid(point, gridMap)).ToList(),
segmentCount = Math.Max(0, orderedPoints.Count - 1),
intersectedCellCount,
blockedIntersectionCount = blockedIntersections.Count,
blockedIntersections,
segments = segmentDiagnostics
};
}
private static double CalculateHostHorizontalDistance(Point3D start, Point3D end, CoordinateSystemType coordinateSystemType)
{
var adapter = new HostCoordinateAdapter(coordinateSystemType);
var startHorizontal = HostPlanarGridHelper.GetHorizontalCoords3(ToVector3(start), adapter);
var endHorizontal = HostPlanarGridHelper.GetHorizontalCoords3(ToVector3(end), adapter);
double dh1 = endHorizontal.h1 - startHorizontal.h1;
double dh2 = endHorizontal.h2 - startHorizontal.h2;
return Math.Sqrt(dh1 * dh1 + dh2 * dh2);
}
private static Point3D InterpolateHostPoint(Point3D start, Point3D end, double t)
{
return new Point3D(
start.X + t * (end.X - start.X),
start.Y + t * (end.Y - start.Y),
start.Z + t * (end.Z - start.Z));
}
private static System.Numerics.Vector3 ToVector3(Point3D point)
{
return new System.Numerics.Vector3((float)point.X, (float)point.Y, (float)point.Z);
}
private static int CountBlockedCellInteriorIntersections(
Point3D start,
Point3D end,
GridMap gridMap,
int segmentIndex,
List<object> failures)
{
var startHorizontal = GetHostHorizontalCoords(start, gridMap);
var endHorizontal = GetHostHorizontalCoords(end, gridMap);
var originHorizontal = GetHostHorizontalCoords(gridMap.Origin, gridMap);
int minGridX = (int)Math.Round((Math.Min(startHorizontal.h1, endHorizontal.h1) - originHorizontal.h1) / gridMap.CellSize) - 1;
int maxGridX = (int)Math.Round((Math.Max(startHorizontal.h1, endHorizontal.h1) - originHorizontal.h1) / gridMap.CellSize) + 1;
int minGridY = (int)Math.Round((Math.Min(startHorizontal.h2, endHorizontal.h2) - originHorizontal.h2) / gridMap.CellSize) - 1;
int maxGridY = (int)Math.Round((Math.Max(startHorizontal.h2, endHorizontal.h2) - originHorizontal.h2) / gridMap.CellSize) + 1;
minGridX = Math.Max(0, minGridX);
minGridY = Math.Max(0, minGridY);
maxGridX = Math.Min(gridMap.Width - 1, maxGridX);
maxGridY = Math.Min(gridMap.Height - 1, maxGridY);
int blockedIntersections = 0;
for (int x = minGridX; x <= maxGridX; x++)
{
for (int y = minGridY; y <= maxGridY; y++)
{
var gridPosition = new GridPoint2D(x, y);
if (!gridMap.TryGetSegmentGridCellInteriorIntersection(start, end, gridPosition, out double enterT, out double exitT))
{
continue;
}
double sampleT = Math.Max(0.0, Math.Min(1.0, (enterT + exitT) * 0.5));
Point3D samplePoint = InterpolateHostPoint(start, end, sampleT);
double sampleElevation = gridMap.GetWorldElevation(samplePoint);
if (!gridMap.IsPassableAtElevation(gridPosition, sampleElevation, 0.1))
{
blockedIntersections++;
AddFailure(
failures,
segmentIndex,
-1,
samplePoint,
gridPosition,
$"BlockedCellInteriorIntersection:t=[{enterT:F6},{exitT:F6}]");
}
}
}
return blockedIntersections;
}
private static List<dynamic> GetSegmentIntersectedCells(Point3D start, Point3D end, GridMap gridMap)
{
var startHorizontal = GetHostHorizontalCoords(start, gridMap);
var endHorizontal = GetHostHorizontalCoords(end, gridMap);
var originHorizontal = GetHostHorizontalCoords(gridMap.Origin, gridMap);
int minGridX = (int)Math.Round((Math.Min(startHorizontal.h1, endHorizontal.h1) - originHorizontal.h1) / gridMap.CellSize) - 1;
int maxGridX = (int)Math.Round((Math.Max(startHorizontal.h1, endHorizontal.h1) - originHorizontal.h1) / gridMap.CellSize) + 1;
int minGridY = (int)Math.Round((Math.Min(startHorizontal.h2, endHorizontal.h2) - originHorizontal.h2) / gridMap.CellSize) - 1;
int maxGridY = (int)Math.Round((Math.Max(startHorizontal.h2, endHorizontal.h2) - originHorizontal.h2) / gridMap.CellSize) + 1;
minGridX = Math.Max(0, minGridX);
minGridY = Math.Max(0, minGridY);
maxGridX = Math.Min(gridMap.Width - 1, maxGridX);
maxGridY = Math.Min(gridMap.Height - 1, maxGridY);
var result = new List<dynamic>();
for (int x = minGridX; x <= maxGridX; x++)
{
for (int y = minGridY; y <= maxGridY; y++)
{
var gridPosition = new GridPoint2D(x, y);
if (!gridMap.TryGetSegmentGridCellInteriorIntersection(start, end, gridPosition, out double enterT, out double exitT))
{
continue;
}
double sampleT = Math.Max(0.0, Math.Min(1.0, (enterT + exitT) * 0.5));
Point3D samplePoint = InterpolateHostPoint(start, end, sampleT);
double sampleElevation = gridMap.GetWorldElevation(samplePoint);
GridCell cell = gridMap.Cells[x, y];
var bounds = gridMap.GetGridCellPlanarBounds(gridPosition);
bool isPassableAtSegmentElevation = gridMap.IsPassableAtElevation(gridPosition, sampleElevation, 0.1);
result.Add(new
{
grid = SerializeGridPosition(gridPosition),
cellType = cell.CellType,
hasAnyWalkableLayer = cell.HasAnyWalkableLayer(),
isPassableAtSegmentElevation,
sampleT,
enterT,
exitT,
sampleElevation,
samplePoint = SerializePoint3D(samplePoint),
center = SerializePoint3D(gridMap.GridToWorld2D(gridPosition)),
planarBounds = new
{
bounds.minH1,
bounds.maxH1,
bounds.minH2,
bounds.maxH2
},
heightLayers = SerializeHeightLayers(cell)
});
}
}
return result
.OrderBy(cell => cell.grid.x)
.ThenBy(cell => cell.grid.y)
.ToList();
}
private static (double h1, double h2) GetHostHorizontalCoords(Point3D point, GridMap gridMap)
{
var adapter = new HostCoordinateAdapter(gridMap.CoordinateSystemType);
return HostPlanarGridHelper.GetHorizontalCoords3(ToVector3(point), adapter);
}
private static object SerializeGridPosition(GridPoint2D gridPosition)
{
return new
{
x = gridPosition.X,
y = gridPosition.Y
};
}
private static object SerializeHorizontalPoint(Point3D point, GridMap gridMap)
{
var horizontal = GetHostHorizontalCoords(point, gridMap);
return new
{
h1 = horizontal.h1,
h2 = horizontal.h2,
elevation = gridMap.GetWorldElevation(point)
};
}
private static object SerializePathPointWithGrid(PathPoint point, GridMap gridMap)
{
GridPoint2D gridPosition = gridMap.WorldToGrid(point.Position);
return new
{
id = point.Id,
name = point.Name,
type = point.Type.ToString(),
index = point.Index,
position = SerializePoint3D(point.Position),
horizontal = SerializeHorizontalPoint(point.Position, gridMap),
grid = SerializeGridPosition(gridPosition),
cell = SerializeGridCell(gridMap, gridPosition, point.Position)
};
}
private static object SerializeGridCell(GridMap gridMap, GridPoint2D gridPosition, Point3D referencePoint)
{
if (!gridMap.IsValidGridPosition(gridPosition))
{
return new
{
isValid = false
};
}
GridCell cell = gridMap.Cells[gridPosition.X, gridPosition.Y];
var bounds = gridMap.GetGridCellPlanarBounds(gridPosition);
double elevation = gridMap.GetWorldElevation(referencePoint);
return new
{
isValid = true,
cellType = cell.CellType,
hasAnyWalkableLayer = cell.HasAnyWalkableLayer(),
isPassableAtReferenceElevation = gridMap.IsPassableAtElevation(gridPosition, elevation, 0.1),
planarBounds = new
{
bounds.minH1,
bounds.maxH1,
bounds.minH2,
bounds.maxH2
},
heightLayers = SerializeHeightLayers(cell)
};
}
private static object SerializeHeightLayers(GridCell cell)
{
return cell.HeightLayers?
.Select(layer => new
{
z = layer.Z,
passableMin = layer.PassableHeight.MinZ,
passableMax = layer.PassableHeight.MaxZ,
isWalkable = layer.IsWalkable,
isBoundary = layer.IsBoundary,
type = layer.Type
})
.ToList();
}
private static void AddFailure(
List<object> failures,
int segmentIndex,
int sampleIndex,
Point3D samplePoint,
GridPoint2D gridPosition,
string reason)
{
if (failures.Count >= 20)
{
return;
}
failures.Add(new
{
segmentIndex,
sampleIndex,
reason,
point = SerializePoint3D(samplePoint),
grid = new
{
x = gridPosition.X,
y = gridPosition.Y
}
});
}
private static PathPlanningManager RequirePathManager()
{
PathPlanningManager pathManager = PathPlanningManager.GetActivePathManager();
if (pathManager == null)
{
throw new InvalidOperationException("PathPlanningManager 当前不可用");
}
return pathManager;
}
private static string GetRequiredQueryValue(Dictionary<string, string> query, string key)
{
string value = GetOptionalQueryValue(query, key);
if (string.IsNullOrWhiteSpace(value))
{
throw new InvalidOperationException($"缺少必填参数: {key}");
}
return value;
}
private static string GetOptionalQueryValue(Dictionary<string, string> query, string key)
{
if (query == null || string.IsNullOrWhiteSpace(key))
{
return null;
}
query.TryGetValue(key, out string value);
return string.IsNullOrWhiteSpace(value) ? null : value;
}
private static double ParsePositiveDoubleQuery(Dictionary<string, string> query, string key, double defaultValue)
{
double value = ParseDoubleQuery(query, key, defaultValue);
if (value <= 0)
{
throw new InvalidOperationException($"参数 {key} 必须大于 0当前值: {value}");
}
return value;
}
private static double ParseNonNegativeDoubleQuery(Dictionary<string, string> query, string key, double defaultValue)
{
double value = ParseDoubleQuery(query, key, defaultValue);
if (value < 0)
{
throw new InvalidOperationException($"参数 {key} 不能小于 0当前值: {value}");
}
return value;
}
private static double ParseDoubleQuery(Dictionary<string, string> query, string key, double defaultValue)
{
string raw = GetOptionalQueryValue(query, key);
if (string.IsNullOrWhiteSpace(raw))
{
return defaultValue;
}
if (!double.TryParse(raw, NumberStyles.Float, CultureInfo.InvariantCulture, out double value))
{
throw new InvalidOperationException($"无法解析数值参数 {key}: {raw}");
}
return value;
}
private static RequestTarget ParseRequestTarget(string requestTarget)
{
string path = requestTarget ?? "/";
var query = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
int queryStartIndex = path.IndexOf('?');
if (queryStartIndex >= 0)
{
string queryString = path.Substring(queryStartIndex + 1);
path = path.Substring(0, queryStartIndex);
foreach (string pair in queryString.Split(new[] { '&' }, StringSplitOptions.RemoveEmptyEntries))
{
string[] kv = pair.Split(new[] { '=' }, 2);
string key = Uri.UnescapeDataString(kv[0] ?? string.Empty);
string value = kv.Length > 1 ? Uri.UnescapeDataString(kv[1] ?? string.Empty) : string.Empty;
if (!string.IsNullOrWhiteSpace(key))
{
query[key] = value;
}
}
}
return new RequestTarget
{
Path = string.IsNullOrWhiteSpace(path) ? "/" : path,
Query = query
};
}
private sealed class RequestTarget
{
public string Path { get; set; }
public Dictionary<string, string> Query { get; set; }
}
private static object ResolveTrackedState(PathAnimationManager animationManager, ModelItem controlledObject)
{
if (animationManager == null || controlledObject == null || !animationManager.ControlsAnimatedObject(controlledObject))
{
return new
{
isAvailable = false,
reason = animationManager == null ? "PathAnimationManagerUnavailable" : "ControlledObjectUnavailable"
};
}
var currentState = animationManager.GetObjectCurrentPosition(controlledObject);
return new
{
isAvailable = true,
position = SerializePoint3D(currentState.Position),
yawRadians = currentState.Yaw,
yawDegrees = currentState.Yaw * 180.0 / Math.PI,
hasTrackedRotation = animationManager.HasTrackedRotation,
trackedRotation = animationManager.HasTrackedRotation
? SerializeRotation3D(animationManager.TrackedRotation)
: null
};
}
private static object ResolveGeometryState(ModelItem controlledObject)
{
if (controlledObject == null)
{
return new
{
isAvailable = false,
reason = "ControlledObjectUnavailable"
};
}
bool hasGeometryRotation = ModelItemTransformHelper.TryGetCurrentGeometryRotation(controlledObject, out Rotation3D geometryRotation);
bool hasOverrideRotation = ModelItemTransformHelper.TryGetCurrentOverrideRotation(controlledObject, out Rotation3D overrideRotation);
return new
{
isAvailable = true,
geometryRotation = hasGeometryRotation ? SerializeRotation3D(geometryRotation) : null,
overrideRotation = hasOverrideRotation ? SerializeRotation3D(overrideRotation) : null
};
}
private static object ResolveBoundingBoxState(ModelItem controlledObject)
{
if (controlledObject == null)
{
return new
{
isAvailable = false,
reason = "ControlledObjectUnavailable"
};
}
BoundingBox3D boundingBox = controlledObject.BoundingBox();
return new
{
isAvailable = true,
min = SerializePoint3D(boundingBox.Min),
max = SerializePoint3D(boundingBox.Max),
center = SerializePoint3D(boundingBox.Center)
};
}
private static object SerializePoint3D(Point3D point)
{
if (point == null)
{
return null;
}
return new
{
x = point.X,
y = point.Y,
z = point.Z
};
}
private static object SerializePathPoint(PathPoint point)
{
if (point == null)
{
return null;
}
return new
{
id = point.Id,
name = point.Name,
type = point.Type.ToString(),
index = point.Index,
position = SerializePoint3D(point.Position)
};
}
private static object SerializeGridStats(GridMap gridMap)
{
if (gridMap == null)
{
return null;
}
int walkableCellCount = 0;
int nonWalkableCellCount = 0;
int obstacleCellCount = 0;
int holeCellCount = 0;
int boundaryLayerCount = 0;
for (int x = 0; x < gridMap.Width; x++)
{
for (int y = 0; y < gridMap.Height; y++)
{
GridCell cell = gridMap.Cells[x, y];
bool isWalkable = cell.HasAnyWalkableLayer();
if (isWalkable)
{
walkableCellCount++;
}
else
{
nonWalkableCellCount++;
}
if (string.Equals(cell.CellType, "障碍物", StringComparison.Ordinal))
{
obstacleCellCount++;
}
if (string.Equals(cell.CellType, "空洞", StringComparison.Ordinal))
{
holeCellCount++;
}
if (cell.HeightLayers != null)
{
boundaryLayerCount += cell.HeightLayers.Count(layer => layer.IsBoundary);
}
}
}
int totalCellCount = gridMap.Width * gridMap.Height;
return new
{
width = gridMap.Width,
height = gridMap.Height,
totalCellCount,
walkableCellCount,
nonWalkableCellCount,
obstacleCellCount,
holeCellCount,
boundaryLayerCount,
obstacleRatio = totalCellCount == 0 ? 0.0 : (double)obstacleCellCount / totalCellCount,
nonWalkableRatio = totalCellCount == 0 ? 0.0 : (double)nonWalkableCellCount / totalCellCount
};
}
private static object BuildObstacleDiagnostics(GridMapGenerator gridMapGenerator, GridMap gridMap, double objectHeightInMeters, double safetyMarginInMeters)
{
if (gridMapGenerator == null || gridMap == null)
{
return null;
}
double metersToModelUnits = UnitsConverter.GetMetersToUnitsConversionFactor(Application.ActiveDocument.Units);
double scanHeightInModelUnits = (objectHeightInMeters + safetyMarginInMeters) * metersToModelUnits;
var traversableItems = CategoryAttributeManager.GetAllTraversableLogisticsItems().ToList();
var irrelevantItems = CategoryAttributeManager.GetLogisticsItemsByType("无关项");
var collectRelatedItemsMethod = typeof(GridMapGenerator).GetMethod(
"CollectRelatedItems",
System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
var postProcessGeometryItemsMethod = typeof(GridMapGenerator).GetMethod(
"PostProcessGeometryItems",
System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
var getObstacleElevationRangeMethod = typeof(GridMapGenerator).GetMethod(
"GetObstacleElevationRange",
System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic,
null,
new[] { typeof(Point3D), typeof(Point3D), typeof(GridMap) },
null);
var calculateBoundingBoxGridCoverageMethod = typeof(GridMapGenerator).GetMethod(
"CalculateBoundingBoxGridCoverage",
System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
var traversableRelatedItems = (HashSet<ModelItem>)collectRelatedItemsMethod.Invoke(gridMapGenerator, new object[] { traversableItems });
var irrelevantRelatedItems = (HashSet<ModelItem>)collectRelatedItemsMethod.Invoke(gridMapGenerator, new object[] { irrelevantItems });
var geometryItems = ModelItemAnalysisHelper.GetAllVisibleGeometryItems();
var itemCache = (Dictionary<ModelItem, ItemProperties>)postProcessGeometryItemsMethod.Invoke(
gridMapGenerator,
new object[] { geometryItems, traversableRelatedItems, irrelevantRelatedItems });
var validItems = itemCache
.Where(kvp => kvp.Value.HasGeometry &&
!kvp.Value.IsContainer &&
!kvp.Value.IsChannelItem &&
!kvp.Value.IsChildOfChannel &&
kvp.Value.BoundingBox != null &&
kvp.Value.IsInScanHeightRange)
.ToList();
double walkableMinElevation = double.MaxValue;
double walkableMaxElevation = double.MinValue;
int walkableGridCount = 0;
for (int x = 0; x < gridMap.Width; x++)
{
for (int y = 0; y < gridMap.Height; y++)
{
GridCell cell = gridMap.Cells[x, y];
if (!cell.HasAnyWalkableLayer() || cell.HeightLayers == null || cell.HeightLayers.Count == 0)
{
continue;
}
walkableGridCount++;
double cellElevation = cell.HeightLayers[0].Z;
walkableMinElevation = Math.Min(walkableMinElevation, cellElevation);
walkableMaxElevation = Math.Max(walkableMaxElevation, cellElevation);
}
}
if (walkableGridCount == 0)
{
walkableMinElevation = 0.0;
walkableMaxElevation = 0.0;
}
double scanMin = walkableMinElevation;
double scanMax = walkableMaxElevation + scanHeightInModelUnits;
var keptItems = new List<object>();
var filteredSampleItems = new List<object>();
int filteredByHeightCount = 0;
foreach (var kvp in validItems)
{
BoundingBox3D bbox = kvp.Value.BoundingBox;
var elevationRange = ((double min, double max))getObstacleElevationRangeMethod.Invoke(
gridMapGenerator,
new object[] { bbox.Min, bbox.Max, gridMap });
bool isInRange = !(elevationRange.max <= scanMin || elevationRange.min > scanMax);
if (!isInRange)
{
filteredByHeightCount++;
if (filteredSampleItems.Count < 10)
{
filteredSampleItems.Add(new
{
displayName = kvp.Key.DisplayName,
bboxMin = SerializePoint3D(bbox.Min),
bboxMax = SerializePoint3D(bbox.Max),
minElevation = elevationRange.min,
maxElevation = elevationRange.max
});
}
continue;
}
if (keptItems.Count < 30)
{
var coveredCells = (List<(int x, int y)>)calculateBoundingBoxGridCoverageMethod.Invoke(
gridMapGenerator,
new object[] { bbox, gridMap });
object triangleBounds = keptItems.Count < 8
? SerializeTriangleBounds(kvp.Key)
: null;
keptItems.Add(new
{
displayName = kvp.Key.DisplayName,
bboxMin = SerializePoint3D(bbox.Min),
bboxMax = SerializePoint3D(bbox.Max),
triangleBounds,
minElevation = elevationRange.min,
maxElevation = elevationRange.max,
coveredCellCount = coveredCells.Count
});
}
}
return new
{
geometryItemCount = geometryItems.Count,
traversableItemCount = traversableItems.Count,
traversableRelatedItemCount = traversableRelatedItems.Count,
irrelevantItemCount = irrelevantItems.Count,
irrelevantRelatedItemCount = irrelevantRelatedItems.Count,
postProcessedItemCount = itemCache.Count,
validItemCount = validItems.Count,
walkableGridCount,
walkableMinElevation,
walkableMaxElevation,
scanHeightInModelUnits,
scanMin,
scanMax,
filteredByHeightCount,
keptItemSample = keptItems,
filteredByHeightSample = filteredSampleItems
};
}
private static object SerializeTriangleBounds(ModelItem item)
{
if (item == null)
{
return null;
}
List<Triangle3D> triangles = GeometryHelper.ExtractTriangles(new[] { item });
if (triangles == null || triangles.Count == 0)
{
return null;
}
double minX = triangles.Min(t => Math.Min(t.Point1.X, Math.Min(t.Point2.X, t.Point3.X)));
double minY = triangles.Min(t => Math.Min(t.Point1.Y, Math.Min(t.Point2.Y, t.Point3.Y)));
double minZ = triangles.Min(t => Math.Min(t.Point1.Z, Math.Min(t.Point2.Z, t.Point3.Z)));
double maxX = triangles.Max(t => Math.Max(t.Point1.X, Math.Max(t.Point2.X, t.Point3.X)));
double maxY = triangles.Max(t => Math.Max(t.Point1.Y, Math.Max(t.Point2.Y, t.Point3.Y)));
double maxZ = triangles.Max(t => Math.Max(t.Point1.Z, Math.Max(t.Point2.Z, t.Point3.Z)));
return new
{
min = SerializePoint3D(new Point3D(minX, minY, minZ)),
max = SerializePoint3D(new Point3D(maxX, maxY, maxZ))
};
}
private static T InvokePathManagerPrivate<T>(PathPlanningManager pathManager, string methodName, params object[] args)
{
var method = typeof(PathPlanningManager).GetMethod(
methodName,
System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
if (method == null)
{
throw new MissingMethodException(typeof(PathPlanningManager).FullName, methodName);
}
object result = method.Invoke(pathManager, args);
if (result == null)
{
return default(T);
}
return (T)result;
}
private static object SerializeRotation3D(Rotation3D rotation)
{
var linear = new Transform3D(rotation).Linear;
return new
{
quaternion = new
{
x = rotation.A,
y = rotation.B,
z = rotation.C,
w = rotation.D
},
axes = new
{
hostX = new
{
x = linear.Get(0, 0),
y = linear.Get(1, 0),
z = linear.Get(2, 0)
},
hostY = new
{
x = linear.Get(0, 1),
y = linear.Get(1, 1),
z = linear.Get(2, 1)
},
hostZ = new
{
x = linear.Get(0, 2),
y = linear.Get(1, 2),
z = linear.Get(2, 2)
}
}
};
}
private static string WriteSnapshotToDisk(object snapshotPayload)
{
string logDirectory = Path.GetDirectoryName(LogManager.LogFilePath);
string snapshotDirectory = Path.Combine(logDirectory ?? AppDomain.CurrentDomain.BaseDirectory, "test-automation");
Directory.CreateDirectory(snapshotDirectory);
string fileName = string.Format(
CultureInfo.InvariantCulture,
"debug-snapshot-{0:yyyyMMdd-HHmmss-fff}.json",
DateTime.Now);
string fullPath = Path.Combine(snapshotDirectory, fileName);
string json = JsonConvert.SerializeObject(snapshotPayload, Formatting.Indented);
File.WriteAllText(fullPath, json, new UTF8Encoding(false));
LogManager.Info($"[测试HTTP] 已导出调试快照: {fullPath}");
return fullPath;
}
private static AnimationControlViewModel RequireAnimationControlViewModel()
{
var animationViewModel = AnimationControlViewModel.Instance;
if (animationViewModel == null)
{
throw new InvalidOperationException("AnimationControlViewModel 当前不可用,请先打开插件动画页签");
}
return animationViewModel;
}
private static PreparedVirtualCollisionTest PrepareVirtualCollisionTest(Dictionary<string, string> query, PathType? fixedPathType)
{
PathPlanningManager pathManager = RequirePathManager();
AnimationControlViewModel animationViewModel = RequireAnimationControlViewModel();
animationViewModel.SetPathPlanningManager(pathManager);
PathRoute route = ResolveVirtualCollisionRoute(pathManager, query, fixedPathType);
pathManager.SetCurrentRoute(route);
animationViewModel.SetCurrentPath(CreatePathRouteViewModel(route));
animationViewModel.IsManualCollisionTargetEnabled = false;
animationViewModel.UseVirtualObject = true;
object animatedObjectPayload = new
{
mode = "VirtualObject",
displayName = "虚拟物体"
};
if (!animationViewModel.CanGenerateAnimation || !animationViewModel.GenerateAnimationCommand.CanExecute(null))
{
throw new InvalidOperationException($"{route.PathType} 虚拟物体碰撞测试准备失败:当前条件下不能生成动画");
}
animationViewModel.GenerateAnimationCommand.Execute(null);
LogManager.Info(
$"[测试HTTP] 已开始准备虚拟物体碰撞测试: 路径={route.Name}, 类型={route.PathType}");
return new PreparedVirtualCollisionTest
{
pathType = route.PathType,
route = SerializeRoute(route, route),
animatedObject = animatedObjectPayload
};
}
private static void StartPreparedAnimation()
{
AnimationControlViewModel animationViewModel = RequireAnimationControlViewModel();
PathAnimationManager animationManager = PathAnimationManager.GetInstance();
if (animationManager == null)
{
throw new InvalidOperationException("PathAnimationManager 当前不可用,无法开始播放");
}
if (animationManager.CurrentState == AnimationState.Paused)
{
animationManager.ResumeAnimation();
LogManager.Info("[测试HTTP] 已从暂停状态恢复自动测试动画播放");
return;
}
if (animationManager.IsAnimating)
{
LogManager.Info("[测试HTTP] 动画已经在播放中,跳过重复开始");
return;
}
if (animationManager.CurrentState != AnimationState.Ready &&
animationManager.CurrentState != AnimationState.Finished)
{
throw new InvalidOperationException($"动画尚未就绪,当前状态={animationManager.CurrentState}");
}
animationManager.ClearExclusionCache();
ModelHighlightHelper.ClearCollisionHighlights();
animationManager.StartAnimation();
LogManager.Info("[测试HTTP] 已启动自动测试动画播放");
}
private static PathRoute ResolveVirtualCollisionRoute(PathPlanningManager pathManager, Dictionary<string, string> query, PathType? fixedPathType)
{
string routeName = GetOptionalQueryValue(query, "routeName");
string pathTypeText = fixedPathType.HasValue
? fixedPathType.Value.ToString()
: GetOptionalQueryValue(query, "pathType");
List<PathRoute> routes = pathManager.GetAllRoutes() ?? new List<PathRoute>();
PathType pathType = fixedPathType ?? ParsePathTypeOrThrow(pathTypeText, "pathType");
string routePrefix = GetOptionalQueryValue(query, "routePrefix")
?? GetOptionalQueryValue(query, "prefix")
?? DefaultAutoTestRoutePrefix;
PathRoute route = string.IsNullOrWhiteSpace(routeName)
? TryFindAutoTestRoute(routes, pathType, routePrefix)
: routes.FirstOrDefault(r => string.Equals(r.Name, routeName, StringComparison.OrdinalIgnoreCase));
if (route == null)
{
throw new InvalidOperationException(
string.IsNullOrWhiteSpace(routeName)
? $"找不到默认自动测试路径: prefix={routePrefix}, pathType={pathType}"
: $"找不到指定路径: {routeName}");
}
if (route.PathType != pathType)
{
throw new InvalidOperationException($"指定路径类型不匹配: 期望={pathType}, 实际={route.PathType}, 路径={route.Name}");
}
return route;
}
private static PathType ParsePathTypeOrThrow(string value, string parameterName)
{
if (string.IsNullOrWhiteSpace(value))
{
throw new InvalidOperationException($"缺少必填参数: {parameterName}");
}
if (!Enum.TryParse(value, true, out PathType pathType))
{
throw new InvalidOperationException($"无法解析路径类型: {value}");
}
return pathType;
}
private static ModelItem ResolveAnimatedObjectFromQuery(Dictionary<string, string> query)
{
Document activeDocument = Autodesk.Navisworks.Api.Application.ActiveDocument;
if (activeDocument == null)
{
throw new InvalidOperationException("当前没有活动文档,无法选择真实物体");
}
string animatedObjectName = GetOptionalQueryValue(query, "animatedObjectName");
if (string.IsNullOrWhiteSpace(animatedObjectName))
{
return ResolveSingleSelectedItem(activeDocument);
}
var matches = new List<ModelItem>();
foreach (Model model in activeDocument.Models)
{
if (model?.RootItem == null)
{
continue;
}
foreach (ModelItem item in model.RootItem.DescendantsAndSelf)
{
if (item != null && string.Equals(item.DisplayName, animatedObjectName, StringComparison.OrdinalIgnoreCase))
{
matches.Add(item);
}
}
}
if (matches.Count == 0)
{
throw new InvalidOperationException($"找不到指定真实物体: {animatedObjectName}");
}
if (matches.Count > 1)
{
throw new InvalidOperationException($"找到多个同名真实物体,请改用更唯一的名字: {animatedObjectName}");
}
return matches[0];
}
private static ModelItem ResolveSingleSelectedItem(Document activeDocument)
{
var selectedItems = activeDocument.CurrentSelection?.SelectedItems?.Cast<ModelItem>().Where(item => item != null).ToList()
?? new List<ModelItem>();
if (selectedItems.Count != 1)
{
throw new InvalidOperationException($"当前选择集必须且只能包含 1 个真实物体,当前数量: {selectedItems.Count}");
}
return selectedItems[0];
}
private static void SelectDocumentItem(ModelItem item)
{
Document activeDocument = Autodesk.Navisworks.Api.Application.ActiveDocument;
if (activeDocument?.CurrentSelection == null)
{
throw new InvalidOperationException("当前文档选择集不可用");
}
activeDocument.CurrentSelection.Clear();
activeDocument.CurrentSelection.Add(item);
}
private static PathRouteViewModel CreatePathRouteViewModel(PathRoute route)
{
var routeViewModel = new PathRouteViewModel(isFromDatabase: true)
{
Route = route,
IsActive = true
};
foreach (var point in route.Points.OrderBy(p => p.Index))
{
routeViewModel.Points.Add(new PathPointViewModel
{
Id = point.Id,
Name = point.Name,
Type = point.Type,
Index = point.Index,
X = point.X,
Y = point.Y,
Z = point.Z
});
}
routeViewModel.SetTimeInfo(route.CreatedTime, route.LastModified);
return routeViewModel;
}
private static int ParseTimeoutSeconds(Dictionary<string, string> query, int defaultSeconds)
{
string raw = GetOptionalQueryValue(query, "timeoutSeconds");
if (string.IsNullOrWhiteSpace(raw))
{
return defaultSeconds;
}
if (!int.TryParse(raw, out int parsedSeconds) || parsedSeconds <= 0)
{
throw new InvalidOperationException($"无法解析 timeoutSeconds: {raw}");
}
return parsedSeconds;
}
private static bool ParseBooleanQuery(Dictionary<string, string> query, string key, bool defaultValue)
{
string raw = GetOptionalQueryValue(query, key);
if (string.IsNullOrWhiteSpace(raw))
{
return defaultValue;
}
if (bool.TryParse(raw, out bool parsed))
{
return parsed;
}
if (string.Equals(raw, "1", StringComparison.OrdinalIgnoreCase) ||
string.Equals(raw, "yes", StringComparison.OrdinalIgnoreCase) ||
string.Equals(raw, "y", StringComparison.OrdinalIgnoreCase))
{
return true;
}
if (string.Equals(raw, "0", StringComparison.OrdinalIgnoreCase) ||
string.Equals(raw, "no", StringComparison.OrdinalIgnoreCase) ||
string.Equals(raw, "n", StringComparison.OrdinalIgnoreCase))
{
return false;
}
throw new InvalidOperationException($"无法解析布尔参数 {key}: {raw}");
}
private static IDisposable EnableAutoConfirmCollisionAnalysisDialogs()
{
Interlocked.Increment(ref _autoConfirmCollisionAnalysisDialogRequestCount);
return new ActionOnDispose(() =>
{
if (Interlocked.Decrement(ref _autoConfirmCollisionAnalysisDialogRequestCount) < 0)
{
Interlocked.Exchange(ref _autoConfirmCollisionAnalysisDialogRequestCount, 0);
}
});
}
private static IDisposable EnableAutoChooseCreateNewDetectionRecord()
{
Interlocked.Increment(ref _autoChooseCreateNewDetectionRecordRequestCount);
return new ActionOnDispose(() =>
{
if (Interlocked.Decrement(ref _autoChooseCreateNewDetectionRecordRequestCount) < 0)
{
Interlocked.Exchange(ref _autoChooseCreateNewDetectionRecordRequestCount, 0);
}
});
}
private static async Task WaitForConditionAsync(DateTime deadlineUtc, Func<bool> condition, string timeoutMessage)
{
while (DateTime.UtcNow < deadlineUtc)
{
if (condition())
{
return;
}
await Task.Delay(300).ConfigureAwait(false);
}
throw new TimeoutException(timeoutMessage);
}
private sealed class PreparedVirtualCollisionTest
{
public PathType pathType { get; set; }
public object route { get; set; }
public object animatedObject { get; set; }
}
private sealed class ActionOnDispose : IDisposable
{
private Action _disposeAction;
public ActionOnDispose(Action disposeAction)
{
_disposeAction = disposeAction;
}
public void Dispose()
{
Action disposeAction = Interlocked.Exchange(ref _disposeAction, null);
disposeAction?.Invoke();
}
}
private static async Task WriteJsonResponseAsync(StreamWriter writer, int statusCode, object payload)
{
string statusText = ResolveStatusText(statusCode);
string json = JsonConvert.SerializeObject(payload, Formatting.Indented);
byte[] bodyBytes = Encoding.UTF8.GetBytes(json);
await writer.WriteLineAsync($"HTTP/1.1 {statusCode} {statusText}").ConfigureAwait(false);
await writer.WriteLineAsync("Content-Type: application/json; charset=utf-8").ConfigureAwait(false);
await writer.WriteLineAsync($"Content-Length: {bodyBytes.Length}").ConfigureAwait(false);
await writer.WriteLineAsync("Connection: close").ConfigureAwait(false);
await writer.WriteLineAsync().ConfigureAwait(false);
await writer.FlushAsync().ConfigureAwait(false);
Stream baseStream = writer.BaseStream;
await baseStream.WriteAsync(bodyBytes, 0, bodyBytes.Length).ConfigureAwait(false);
await baseStream.FlushAsync(cancellationToken: CancellationToken.None).ConfigureAwait(false);
}
private static string ResolveStatusText(int statusCode)
{
switch (statusCode)
{
case 200:
return "OK";
case 400:
return "Bad Request";
case 404:
return "Not Found";
case 405:
return "Method Not Allowed";
case 500:
return "Internal Server Error";
default:
return "OK";
}
}
public void Dispose()
{
Stop();
}
}
}