1733 lines
71 KiB
C#
1733 lines
71 KiB
C#
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 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/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)
|
|
{
|
|
return routes?.FirstOrDefault(route =>
|
|
route != null &&
|
|
route.PathType == pathType &&
|
|
!string.IsNullOrWhiteSpace(route.Name) &&
|
|
route.Name.StartsWith(DefaultAutoTestRoutePrefix, 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 = InvokePathManagerPrivate<double>(pathManager, "CalculateOptimalGridSize", bounds);
|
|
double objectHeightInMeters = config.ObjectHeightMeters;
|
|
double objectLengthInMeters = config.ObjectLengthMeters;
|
|
double objectWidthInMeters = config.ObjectWidthMeters;
|
|
double 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()
|
|
}
|
|
};
|
|
}
|
|
|
|
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 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");
|
|
|
|
PathRoute route = string.IsNullOrWhiteSpace(routeName)
|
|
? TryFindAutoTestRoute(routes, pathType)
|
|
: routes.FirstOrDefault(r => string.Equals(r.Name, routeName, StringComparison.OrdinalIgnoreCase));
|
|
|
|
if (route == null)
|
|
{
|
|
throw new InvalidOperationException(
|
|
string.IsNullOrWhiteSpace(routeName)
|
|
? $"找不到默认自动测试路径: {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();
|
|
}
|
|
}
|
|
}
|