2228 lines
92 KiB
C#
2228 lines
92 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 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();
|
||
}
|
||
}
|
||
}
|