NavisworksTransport/src/Core/Services/TestAutomationHttpService.cs
tian 539e4a9f2c perf: 剖面盒遍历组节点聚合盒剪枝(大型模型 ~1.8 倍提速)
SectionBoxExporter:
- TraverseAndCollect 对非几何节点(组节点)先查聚合包围盒,
  与剖面盒不相交 → 整棵子树跳过(NW BoundingBox 为文档级缓存 O(1),
  已用 Architecture.nwd 2 万节点 + 全厂模型 403MB 验证)
- GetObjectsAndHiddenItems 增加 prune 参数(默认 true,false 供性能对比)

验证(全厂设备模型 403MB,100x100x40 测试盒):
- 剪枝 72.9s vs 无剪枝 129.9s(~1.8 倍)
- 盒内对象/隐藏节点完全一致(3521/80043,正确性无变化)
- Architecture.nwd(2 万节点):273ms vs 580ms(~2.1 倍)
- 集成测试 25/25

测试基础设施:
- export-section-box 端点加 prune 参数 + traversalMs 计时
- bbox-profiling 端点加 enum=false 模式(根盒查询,避免大模型全枚举 10+ 分钟)
- BatchQueueF1ProcessAutomationTests 末尾等待后台批处理完全退出
  (修复 F1 后台任务残留导致后续 BatchQueue_AddItems isExecuting 断言失败)
- batch-queue-status 端点加 isExecuting 字段
2026-08-06 10:28:59 +08:00

3744 lines
158 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

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

using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Autodesk.Navisworks.Api;
using NavisworksTransport.Commands;
using NavisworksTransport.Core.Animation;
using NavisworksTransport.Core.Config;
using NavisworksTransport.PathPlanning;
using NavisworksTransport.UI.WPF.ViewModels;
using NavisworksTransport.Utils;
using NavisworksTransport.Utils.CoordinateSystem;
using Newtonsoft.Json;
namespace NavisworksTransport.Core.Services
{
/// <summary>
/// 本地测试自动化 HTTP 控制面。
/// 当前暴露最小只读/导出接口,后续可在保持协议稳定的前提下追加命令端点。
/// </summary>
public sealed class TestAutomationHttpService : IDisposable
{
private const int DefaultPort = 18777;
private const string DefaultHost = "127.0.0.1";
private const string PortEnvironmentVariable = "TRANSPORTPLUGIN_TEST_PORT";
private const string DefaultAutoTestRoutePrefix = "自动测试_";
private const string AutoTestRoutesResourceFileName = "auto-test-routes.xml";
// 多实例支持:每个 Navisworks 实例通过环境变量 TRANSPORTPLUGIN_TEST_PORT 分配独立端口(默认 18777
private int _resolvedPort;
private bool _portResolved;
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;
// 测试执行串行锁:动画/路径/碰撞类请求共享 UI 线程与单例动画管理器,
// 客户端超时断开后服务端任务仍在运行,并发会污染 UI 状态(如物体选择串扰),必须串行。
private static readonly SemaphoreSlim _testExecutionLock = new SemaphoreSlim(1, 1);
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 => ResolvePort();
public string BaseUrl => $"http://{DefaultHost}:{Port}";
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, ResolvePort());
_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/bbox-profiling", StringComparison.OrdinalIgnoreCase))
{
object payload = InvokeOnUiThread(() => BuildBBoxProfilingPayload(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/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, "GET", StringComparison.OrdinalIgnoreCase) &&
string.Equals(request.Path, "/api/test/list-model-items", StringComparison.OrdinalIgnoreCase))
{
object payload = InvokeOnUiThread(() => BuildModelItemsPayload(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/detection-record", StringComparison.OrdinalIgnoreCase))
{
object payload = InvokeOnUiThread(() => BuildDetectionRecordPayload(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/analyze-path", StringComparison.OrdinalIgnoreCase))
{
object payload = InvokeOnUiThread(() => AnalyzePathPayload(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/batch-queue-add", StringComparison.OrdinalIgnoreCase))
{
object payload = await BatchQueueAddAsync(request.Query).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/batch-queue-process", StringComparison.OrdinalIgnoreCase))
{
object payload = BatchQueueProcessPayload(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/batch-queue-status", StringComparison.OrdinalIgnoreCase))
{
object payload = await BatchQueueStatusPayload(request.Query).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/edit-route", StringComparison.OrdinalIgnoreCase))
{
object payload = InvokeOnUiThread(() => EditRoutePayload(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/export-section-box", StringComparison.OrdinalIgnoreCase))
{
object payload = InvokeOnUiThread(() => ExportSectionBoxPayload(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/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, "POST", StringComparison.OrdinalIgnoreCase) &&
string.Equals(request.Path, "/api/test/import-route-file", StringComparison.OrdinalIgnoreCase))
{
object payload = InvokeOnUiThread(() => ImportRouteFilePayload(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/export-route-file", StringComparison.OrdinalIgnoreCase))
{
object payload = InvokeOnUiThread(() => ExportRouteFilePayload(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/probe-animation-frames", StringComparison.OrdinalIgnoreCase))
{
object payload = await ProbeAnimationFramesAsync(request.Query).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/animation-playback-control", StringComparison.OrdinalIgnoreCase))
{
object payload = await AnimationPlaybackControlAsync(request.Query).ConfigureAwait(false);
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);
}
}
}
/// <summary>
/// 【实验端点】NW 组节点/几何节点 BoundingBox 缓存行为分析。
/// 验证:组节点聚合盒是否为 O(1) 缓存读取(用于剖面盒遍历子树剪枝)。
/// </summary>
private object BuildBBoxProfilingPayload(Dictionary<string, string> query)
{
Document doc = Autodesk.Navisworks.Api.Application.ActiveDocument;
if (doc == null || doc.IsClear)
{
return new { error = "无活动文档" };
}
bool enumOnly = ParseBooleanQuery(query, "enum", true);
var sw = new System.Diagnostics.Stopwatch();
// 根盒不枚举O(1))——大模型下全量枚举需数分钟,根盒/剪枝验证不需要枚举
sw.Restart();
var rootBox = doc.Models[0].RootItem.BoundingBox();
long rootFirstMs = sw.ElapsedMilliseconds;
sw.Restart();
var rootBox2 = doc.Models[0].RootItem.BoundingBox();
long rootSecondMs = sw.ElapsedMilliseconds;
if (!enumOnly)
{
return new
{
enumSkipped = true,
rootFirstMs,
rootSecondMs,
rootBox = $"({rootBox.Min.X:F1},{rootBox.Min.Y:F1},{rootBox.Min.Z:F1})~({rootBox.Max.X:F1},{rootBox.Max.Y:F1},{rootBox.Max.Z:F1})"
};
}
// 全量枚举计时RootItemDescendantsAndSelf 本身耗时)
sw.Restart();
var allItems = doc.Models.SelectMany(m => m.RootItem?.DescendantsAndSelf ?? Enumerable.Empty<ModelItem>()).ToList();
long enumMs = sw.ElapsedMilliseconds;
int totalCount = allItems.Count;
var groupNodes = allItems.Where(i => !i.HasGeometry && i.Children != null && i.Children.Count() > 0).Take(200).ToList();
var geometryNodes = allItems.Where(i => i.HasGeometry).Take(200).ToList();
// 实验A不预先调用任何 BoundingBox直接批量测组节点盒
sw.Restart();
long groupColdTotalMs = 0;
int groupColdCount = 0;
foreach (var g in groupNodes)
{
sw.Restart();
var b = g.BoundingBox();
groupColdTotalMs += sw.ElapsedMilliseconds;
groupColdCount++;
}
// 实验A2几何节点盒
sw.Restart();
long geomColdTotalMs = 0;
int geomColdCount = 0;
foreach (var g in geometryNodes)
{
sw.Restart();
var b = g.BoundingBox();
geomColdTotalMs += sw.ElapsedMilliseconds;
geomColdCount++;
}
// 实验B根盒已在开头测过缓存直接批量测组节点盒
sw.Restart();
long groupHotTotalMs = 0;
int groupHotCount = 0;
foreach (var g in groupNodes)
{
sw.Restart();
var b = g.BoundingBox();
groupHotTotalMs += sw.ElapsedMilliseconds;
groupHotCount++;
}
// 重复调用同一组节点(缓存特征:第二次起应接近 0
ModelItem sampleGroup = groupNodes.FirstOrDefault();
long repeatFirstMs = 0;
long repeatSecondMs = 0;
if (sampleGroup != null)
{
sw.Restart();
var b1 = sampleGroup.BoundingBox();
repeatFirstMs = sw.ElapsedMilliseconds;
sw.Restart();
var b2 = sampleGroup.BoundingBox();
repeatSecondMs = sw.ElapsedMilliseconds;
}
return new
{
enumMs,
totalCount,
groupSampleCount = groupColdCount,
geometrySampleCount = geomColdCount,
groupColdAvgMs = groupColdCount > 0 ? groupColdTotalMs / (double)groupColdCount : 0,
geomColdAvgMs = geomColdCount > 0 ? geomColdTotalMs / (double)geomColdCount : 0,
groupColdTotalMs,
geomColdTotalMs,
rootFirstMs,
rootSecondMs,
groupHotAvgMs = groupHotCount > 0 ? groupHotTotalMs / (double)groupHotCount : 0,
groupHotTotalMs,
repeatFirstMs,
repeatSecondMs,
rootBox = $"({rootBox.Min.X:F1},{rootBox.Min.Y:F1},{rootBox.Min.Z:F1})~({rootBox.Max.X:F1},{rootBox.Max.Y:F1},{rootBox.Max.Z:F1})"
};
}
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,
isDatabaseReady = pathManager != null && pathManager.IsDatabaseReady,
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
}
};
}
/// <summary>
/// 枚举模型叶子物体DisplayName + ModelPath(PathId) + 祖先名链。
/// 供集成测试动态定位真实物体ModelPath 跨会话不稳定,测试不应硬编码 PathId
/// </summary>
private object BuildModelItemsPayload(Dictionary<string, string> query)
{
Document activeDocument = Autodesk.Navisworks.Api.Application.ActiveDocument;
if (activeDocument == null)
{
throw new InvalidOperationException("当前没有活动文档");
}
string nameFilter = GetOptionalQueryValue(query, "name");
int maxResults = 200;
if (int.TryParse(GetOptionalQueryValue(query, "maxResults"), out int parsedMax) && parsedMax > 0)
{
maxResults = Math.Min(parsedMax, 500);
}
var items = new List<object>();
foreach (Model model in activeDocument.Models)
{
if (model?.RootItem == null)
{
continue;
}
foreach (ModelItem item in model.RootItem.DescendantsAndSelf)
{
if (item == null)
{
continue;
}
// 仅枚举有显示名的节点(叶子几何节点 DisplayName 多为空,名字在层级节点上)
if (string.IsNullOrWhiteSpace(item.DisplayName))
{
continue;
}
if (!string.IsNullOrWhiteSpace(nameFilter) &&
item.DisplayName.IndexOf(nameFilter, StringComparison.OrdinalIgnoreCase) < 0 &&
BuildAncestorNameChain(item).IndexOf(nameFilter, StringComparison.OrdinalIgnoreCase) < 0)
{
continue;
}
string pathId = null;
try
{
var path = activeDocument.Models.CreatePathId(item);
pathId = path.PathId;
}
catch
{
// 忽略路径解析失败
}
// 仅在有名字过滤时计算包围盒中心(枚举全量时避免性能开销)
object boundingBoxCenter = null;
if (!string.IsNullOrWhiteSpace(nameFilter))
{
try
{
var bbox = item.BoundingBox();
boundingBoxCenter = new
{
x = bbox.Center.X,
y = bbox.Center.Y,
z = bbox.Center.Z
};
}
catch
{
// 忽略包围盒解析失败
}
}
items.Add(new
{
displayName = item.DisplayName,
instanceGuid = item.InstanceGuid.ToString(),
pathId,
boundingBoxCenter,
ancestorChain = BuildAncestorNameChain(item)
});
if (items.Count >= maxResults)
{
break;
}
}
}
return new
{
totalCount = items.Count,
maxResults,
nameFilter,
items
};
}
private static string BuildAncestorNameChain(ModelItem item)
{
try
{
var segments = new List<string>();
var current = item;
int guard = 0;
while (current != null && guard < 15)
{
var name = current.DisplayName;
if (!string.IsNullOrWhiteSpace(name))
{
segments.Add(name.Trim());
}
current = current.Parent;
guard++;
}
segments.Reverse();
return string.Join(" / ", segments);
}
catch
{
return null;
}
}
/// <summary>
/// 查询碰撞检测记录(集成测试验证检测记录持久化:动画参数/关联路径正确落库)。
/// </summary>
/// <summary>
/// 路径分析(效率/安全评分),集成测试验证分析链路与评分结构。
/// </summary>
/// <summary>
/// 剖面盒导出T16mode=bbox 用指定包围盒导出 NWD集成测试可控不依赖 UI 剖面盒状态);
/// mode=activeView 用当前视图剖面盒(需 UI 已激活 Box 模式)。
/// </summary>
private static object ExportSectionBoxPayload(Dictionary<string, string> query)
{
Document document = Autodesk.Navisworks.Api.Application.ActiveDocument;
if (document == null)
{
throw new InvalidOperationException("当前没有活动文档");
}
string mode = GetOptionalQueryValue(query, "mode") ?? "bbox";
string filePath = GetOptionalQueryValue(query, "path");
if (string.IsNullOrWhiteSpace(filePath))
{
filePath = Path.Combine(Path.GetTempPath(), $"sectionbox-export-{Guid.NewGuid():N}.nwd");
}
var exporter = new SectionBoxExporter();
if (string.Equals(mode, "bbox", StringComparison.OrdinalIgnoreCase))
{
double x1 = ParseRequiredDouble(query, "x1");
double y1 = ParseRequiredDouble(query, "y1");
double z1 = ParseRequiredDouble(query, "z1");
double x2 = ParseRequiredDouble(query, "x2");
double y2 = ParseRequiredDouble(query, "y2");
double z2 = ParseRequiredDouble(query, "z2");
var bounds = new BoundingBox3D(
new Point3D(Math.Min(x1, x2), Math.Min(y1, y2), Math.Min(z1, z2)),
new Point3D(Math.Max(x1, x2), Math.Max(y1, y2), Math.Max(z1, z2)));
bool prune = ParseBooleanQuery(query, "prune", true);
var sw = new System.Diagnostics.Stopwatch();
sw.Start();
var traversal = exporter.GetObjectsAndHiddenItems(document, bounds, prune);
sw.Stop();
if (traversal.ObjectsInBox.Count == 0)
{
throw new InvalidOperationException("指定包围盒内没有找到对象");
}
var traversalResultPayload = new
{
traversalMs = sw.ElapsedMilliseconds,
prune
};
string exportedPath = exporter.ExportToNwd(document, traversal, filePath);
if (string.IsNullOrEmpty(exportedPath) || !File.Exists(exportedPath))
{
throw new InvalidOperationException($"NWD 导出失败: {filePath}");
}
LogManager.Info($"[测试HTTP] 剖面盒导出完成(bbox): 对象={traversal.ObjectsInBox.Count}, 隐藏={traversal.ItemsToHide.Count}, 文件={exportedPath}");
return new
{
mode = "bbox",
filePath = exportedPath,
objectCount = traversal.ObjectsInBox.Count,
hiddenNodeCount = traversal.ItemsToHide.Count,
fileSize = new FileInfo(exportedPath).Length,
traversalMs = traversalResultPayload.traversalMs,
prune = traversalResultPayload.prune
};
}
// activeView 模式:依赖当前视图剖面盒
var result = exporter.ExportSectionBoxFromActiveView(document, filePath);
if (!result.Success)
{
throw new InvalidOperationException(result.ErrorMessage ?? "剖面盒导出失败");
}
return new
{
mode = "activeView",
filePath = result.FilePath,
objectCount = result.ObjectCount,
hiddenNodeCount = result.HiddenNodeCount,
fileSize = result.FileSize
};
}
/// <summary>
/// 路径编辑集成测试端点T11action=remove-point/update-point/orthogonalize/remove-loops。
/// 直接操作临时测试路径,验证 PathPlanningManager 编辑链路。
/// </summary>
private static object EditRoutePayload(Dictionary<string, string> query)
{
PathPlanningManager pathManager = RequirePathManager();
string action = GetRequiredQueryValue(query, "action");
string routeName = GetRequiredQueryValue(query, "routeName");
PathRoute route = pathManager
.GetAllRoutes()
?.FirstOrDefault(r => string.Equals(r.Name, routeName, StringComparison.OrdinalIgnoreCase));
if (route == null)
{
throw new InvalidOperationException($"找不到指定路径: {routeName}");
}
List<PathPoint> orderedPoints = route.Points?.OrderBy(p => p.Index).ToList() ?? new List<PathPoint>();
switch (action.ToLowerInvariant())
{
case "remove-point":
{
int pointIndex = ParseRequiredInt(query, "pointIndex");
if (pointIndex < 0 || pointIndex >= orderedPoints.Count)
{
throw new InvalidOperationException($"路径点索引越界: {pointIndex}");
}
// 真正的删点走 PathRoute.RemovePoint更新数据 + 重算 + 入库),
// RemovePathPoint 仅处理 3D 标记可视化
if (!route.RemovePoint(orderedPoints[pointIndex]))
{
throw new InvalidOperationException($"删除路径点失败: {routeName}[{pointIndex}]");
}
break;
}
case "update-point":
{
int pointIndex = ParseRequiredInt(query, "pointIndex");
double x = ParseRequiredDouble(query, "x");
double y = ParseRequiredDouble(query, "y");
double z = ParseRequiredDouble(query, "z");
if (pointIndex < 0 || pointIndex >= orderedPoints.Count)
{
throw new InvalidOperationException($"路径点索引越界: {pointIndex}");
}
var newPosition = new Point3D(x, y, z);
if (!pathManager.UpdatePathPointWithConstraints(route, pointIndex, newPosition))
{
throw new InvalidOperationException($"更新路径点失败: {routeName}[{pointIndex}]");
}
break;
}
case "orthogonalize":
{
if (!pathManager.OrthogonalizePath(route))
{
throw new InvalidOperationException($"路径正交化失败: {routeName}");
}
break;
}
case "remove-loops":
{
// 无矩形环可去(返回 false属正常情况不视为失败
pathManager.RemoveRectangularLoops(route);
break;
}
default:
throw new InvalidOperationException($"未知编辑动作: {action}(支持 remove-point/update-point/orthogonalize/remove-loops");
}
LogManager.Info($"[测试HTTP] 路径编辑完成: action={action}, route={routeName}");
List<PathPoint> updatedPoints = route.Points?.OrderBy(p => p.Index).ToList() ?? new List<PathPoint>();
return new
{
action,
routeName,
pointCount = updatedPoints.Count,
points = updatedPoints.Select(point => new
{
index = point.Index,
name = point.Name,
type = point.Type.ToString(),
position = SerializePoint3D(point.Position)
}).ToList()
};
}
private static int ParseRequiredInt(Dictionary<string, string> query, string key)
{
string raw = GetRequiredQueryValue(query, key);
if (!int.TryParse(raw, out int value))
{
throw new InvalidOperationException($"无法解析整数参数 {key}: {raw}");
}
return value;
}
private static double ParseRequiredDouble(Dictionary<string, string> query, string key)
{
string raw = GetRequiredQueryValue(query, key);
if (!double.TryParse(raw, NumberStyles.Float, CultureInfo.InvariantCulture, out double value))
{
throw new InvalidOperationException($"无法解析数值参数 {key}: {raw}");
}
return value;
}
/// <summary>
/// 批量任务队列添加(集成测试验证队列管理,不执行 ProcessQueueAsync 以免长时间批量动画)。
/// queryrouteName必填、frameRate、durationSeconds。
/// </summary>
/// <summary>
/// 触发批处理队列执行F1后台执行立即返回ProcessQueueAsync 含模态进度条,
/// 同步等待会阻塞 HTTP 响应;调用方轮询数据库 BatchQueueItems 状态跟踪)。
/// </summary>
private static object BatchQueueProcessPayload(Dictionary<string, string> query)
{
var manager = BatchQueueManager.Instance;
manager.SetPathPlanningManager(RequirePathManager());
LogManager.Info("[测试HTTP] 开始后台处理批处理队列F1");
Task.Run(async () =>
{
try
{
await manager.ProcessQueueAsync().ConfigureAwait(false);
}
catch (Exception ex)
{
LogManager.Error($"[测试HTTP] 后台批处理执行异常: {ex.Message}", ex);
}
});
return new
{
started = true,
queueCount = manager.QueueCount,
isExecuting = manager.IsExecuting
};
}
private static async Task<object> BatchQueueStatusPayload(Dictionary<string, string> query)
{
var manager = BatchQueueManager.Instance;
manager.SetPathPlanningManager(RequirePathManager());
var database = BatchQueueManager.Instance.GetDatabase();
int limit = int.TryParse(GetOptionalQueryValue(query, "limit"), out int parsedLimit) && parsedLimit > 0
? parsedLimit
: 10;
var items = await database.GetBatchQueueItemsAsync(
statusFilter: NavisworksTransport.Core.Models.BatchQueueStatus.All,
limit: limit).ConfigureAwait(false);
return new
{
isExecuting = manager.IsExecuting,
queueCount = manager.QueueCount,
items = items.Select(item => new
{
itemId = item.Id,
routeName = item.PathRouteName,
pathType = item.PathType.ToString(),
status = item.Status.ToString(),
isVirtualObject = item.IsVirtualObject,
movingObjectName = item.MovingObjectName,
collisionCount = item.CollisionCount,
detectionRecordId = item.DetectionRecordId,
errorMessage = item.ErrorMessage,
createdTime = item.CreatedTime.ToString("O"),
endTime = item.EndTime?.ToString("O")
}).ToList()
};
}
private static async Task<object> BatchQueueAddAsync(Dictionary<string, string> query)
{
PathPlanningManager pathManager = RequirePathManager();
string routeName = GetRequiredQueryValue(query, "routeName");
PathRoute route = pathManager
.GetAllRoutes()
?.FirstOrDefault(r => string.Equals(r.Name, routeName, StringComparison.OrdinalIgnoreCase));
if (route == null)
{
throw new InvalidOperationException($"找不到指定路径: {routeName}");
}
int frameRate = 15;
if (int.TryParse(GetOptionalQueryValue(query, "frameRate"), out int parsedFrameRate) && parsedFrameRate > 0)
{
frameRate = parsedFrameRate;
}
double durationSeconds = 5.0;
if (double.TryParse(GetOptionalQueryValue(query, "durationSeconds"), NumberStyles.Float, CultureInfo.InvariantCulture, out double parsedDuration) &&
parsedDuration > 0)
{
durationSeconds = parsedDuration;
}
bool isVirtualObject = ParseBooleanQuery(query, "isVirtualObject", true);
string movingObjectName = GetOptionalQueryValue(query, "movingObjectName");
var manager = BatchQueueManager.Instance;
manager.SetPathPlanningManager(pathManager);
var item = new NavisworksTransport.Core.Models.BatchQueueItem
{
RouteId = route.Id,
PathRouteName = route.Name,
PathType = route.PathType,
Status = NavisworksTransport.Core.Models.BatchQueueStatus.Pending,
CreatedTime = DateTime.Now,
FrameRate = frameRate,
DurationSeconds = durationSeconds,
IsVirtualObject = isVirtualObject,
MovingObjectName = isVirtualObject ? null : movingObjectName,
// 虚拟物体尺寸(米 → 模型单位DetectAllObjects 保持默认 trueF1 剖面盒自动检测场景)
VirtualObjectLength = isVirtualObject ? 0.4 * UnitsConverter.GetMetersToUnitsConversionFactor() : 0,
VirtualObjectWidth = isVirtualObject ? 0.4 * UnitsConverter.GetMetersToUnitsConversionFactor() : 0,
VirtualObjectHeight = isVirtualObject ? 0.4 * UnitsConverter.GetMetersToUnitsConversionFactor() : 0
};
int itemId = await manager.AddQueueItemAsync(item).ConfigureAwait(false);
// 真实物体:主实例解析名字并写入 MovingObject 引用BatchQueueItems 表无 MovingObjectName 列,
// F1 副实例检测从 ModelItemReferences 读取移动物体名)
if (!isVirtualObject)
{
if (string.IsNullOrWhiteSpace(movingObjectName))
{
throw new InvalidOperationException("真实物体批处理必须提供 movingObjectName");
}
string resolvedDisplayName = null;
string resolvedPathId = null;
int resolvedModelIndex = 0;
InvokeOnUiThread(() =>
{
Document doc = Autodesk.Navisworks.Api.Application.ActiveDocument;
foreach (Model model in doc.Models)
{
if (model?.RootItem == null)
{
continue;
}
foreach (ModelItem match in model.RootItem.DescendantsAndSelf)
{
if (match == null)
{
continue;
}
if (string.Equals(match.DisplayName, movingObjectName, StringComparison.OrdinalIgnoreCase))
{
resolvedDisplayName = match.DisplayName;
try
{
var path = doc.Models.CreatePathId(match);
resolvedModelIndex = path.ModelIndex;
resolvedPathId = path.PathId;
}
catch
{
}
return true;
}
}
}
return true;
});
if (string.IsNullOrWhiteSpace(resolvedDisplayName))
{
throw new InvalidOperationException($"找不到移动物体: {movingObjectName}");
}
await pathManager.AddModelItemReferenceAsync(
itemId, "BatchQueueItem", resolvedModelIndex, resolvedPathId,
resolvedDisplayName, movingObjectName, "MovingObject").ConfigureAwait(false);
LogManager.Info($"[测试HTTP] 已写入批处理移动物体引用: {movingObjectName} (itemId={itemId})");
}
LogManager.Info($"[测试HTTP] 已添加批量队列项: {routeName}, itemId={itemId}, queueCount={manager.QueueCount}");
return new
{
itemId,
routeName,
queueCount = manager.QueueCount,
isExecuting = manager.IsExecuting
};
}
private static object AnalyzePathPayload(Dictionary<string, string> query)
{
PathPlanningManager pathManager = RequirePathManager();
string routeName = GetRequiredQueryValue(query, "routeName");
string strategy = GetOptionalQueryValue(query, "strategy") ?? "安全优先";
PathRoute route = pathManager
.GetAllRoutes()
?.FirstOrDefault(r => string.Equals(r.Name, routeName, StringComparison.OrdinalIgnoreCase));
if (route == null)
{
throw new InvalidOperationException($"找不到指定路径: {routeName}");
}
PathAnalysisResult result = pathManager.AnalyzeRoute(route, strategy);
return new
{
routeName,
routeId = result.RouteId,
strategy = result.Strategy,
collisionCount = result.CollisionCount,
safetyScore = result.SafetyScore,
efficiencyScore = result.EfficiencyScore,
overallScore = result.OverallScore,
turnDifficultyScore = result.TurnDifficultyScore,
tortuosityScore = result.TortuosityScore,
redundancyScore = result.RedundancyScore,
hotspotCount = result.HotspotCount,
analyzedAt = result.AnalysisTime.ToString("o")
};
}
private static object BuildDetectionRecordPayload(Dictionary<string, string> query)
{
string raw = GetRequiredQueryValue(query, "id");
if (!int.TryParse(raw, out int id) || id <= 0)
{
throw new InvalidOperationException($"无法解析检测记录 ID: {raw}");
}
PathPlanningManager pathManager = RequirePathManager();
CollisionDetectionRecord record = pathManager.GetDetectionRecordById(id);
if (record == null)
{
throw new InvalidOperationException($"检测记录不存在: {id}");
}
return new
{
id = record.Id,
routeId = record.RouteId,
testName = record.TestName,
frameRate = record.FrameRate,
durationSeconds = record.DurationSeconds,
isVirtualObject = record.IsVirtualObject,
animatedObjectName = record.AnimatedObjectName,
collisionCount = record.CollisionCount,
createdAt = record.CreatedTime.ToString("o")
};
}
/// <summary>
/// 解析实例端口:优先环境变量 TRANSPORTPLUGIN_TEST_PORT默认 18777。
/// 支持多 Navisworks 实例并行E4 副实例检测 worker
/// </summary>
private int ResolvePort()
{
if (_portResolved)
{
return _resolvedPort;
}
int port = DefaultPort;
string raw = Environment.GetEnvironmentVariable(PortEnvironmentVariable);
if (!string.IsNullOrWhiteSpace(raw) && int.TryParse(raw, out int parsedPort) && parsedPort > 0 && parsedPort < 65536)
{
port = parsedPort;
}
_resolvedPort = port;
_portResolved = true;
return port;
}
/// <summary>
/// F2序列化碰撞对象详情name + 副实例 PathId + 层级链),
/// 供主 NW 按层级链(导出保留树结构,去根一致)映射回主模型。
/// </summary>
private static List<object> SerializeCollidedObjects(
List<CollisionResult> allCollisions, List<string> nameList)
{
var result = new List<object>();
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
if (allCollisions != null)
{
foreach (var collision in allCollisions)
{
if (collision?.Item2 == null)
{
continue;
}
string name = collision.Item2.DisplayName;
if (string.IsNullOrWhiteSpace(name) || !seen.Add(name))
{
continue;
}
if (result.Count >= 50)
{
break;
}
string pathId = null;
try
{
var path = Autodesk.Navisworks.Api.Application.ActiveDocument.Models.CreatePathId(collision.Item2);
pathId = path.PathId;
}
catch
{
}
result.Add(new
{
name,
pathId,
ancestorChain = BuildAncestorNameChain(collision.Item2)
});
}
}
// fallbackAllCollisions 无 Item2 引用时退回名字列表
if (result.Count == 0 && nameList != null)
{
foreach (string name in nameList.Take(50))
{
if (seen.Add(name))
{
result.Add(new { name, pathId = (string)null, ancestorChain = (string)null });
}
}
}
return result;
}
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 &&
string.Equals(prefix, DefaultAutoTestRoutePrefix, StringComparison.Ordinal))
{
// 标准自动测试路径缺失时自动导入后重试仅限自动测试_ 前缀)
EnsureAutoTestRoutesImported(pathManager);
routes = pathManager.GetAllRoutes() ?? new List<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 (!IsSameModelItem(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)
{
await _testExecutionLock.WaitAsync().ConfigureAwait(false);
try
{
return await RunVirtualCollisionTestCoreAsync(query, fixedPathType).ConfigureAwait(false);
}
finally
{
_testExecutionLock.Release();
}
}
private async Task<object> RunVirtualCollisionTestCoreAsync(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,
// F2碰撞对象详情name + 副实例 PathId + 层级链),用于跨实例报告映射回主模型
collidedObjects = SerializeCollidedObjects(report.AllCollisions, report.CollidedObjectsList)
}
};
});
}
}
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)
};
}
/// <summary>
/// 导入路径文件JSON/XML并注册到路径管理器返回导入路径的完整信息。
/// 供集成测试验证“导入 → 属性映射 → 长度计算 → 路径可用性”完整流程。
/// overwrite=true 时同名路径先删除后导入(保证测试可重复执行且不累积数据)。
/// </summary>
/// <summary>
/// 导出路径到文件XML/JSON供集成测试验证导出→导入回环一致性。
/// queryrouteName必填、formatxml/json默认 xml、path可选导出路径
/// </summary>
private static object ExportRouteFilePayload(Dictionary<string, string> query)
{
PathPlanningManager pathManager = RequirePathManager();
string routeName = GetRequiredQueryValue(query, "routeName");
string formatText = GetOptionalQueryValue(query, "format") ?? "xml";
PathRoute route = pathManager
.GetAllRoutes()
?.FirstOrDefault(r => string.Equals(r.Name, routeName, StringComparison.OrdinalIgnoreCase));
if (route == null)
{
throw new InvalidOperationException($"找不到指定路径: {routeName}");
}
bool isJson = string.Equals(formatText, "json", StringComparison.OrdinalIgnoreCase);
string extension = isJson ? ".json" : ".xml";
string exportPath = GetOptionalQueryValue(query, "path");
if (string.IsNullOrWhiteSpace(exportPath))
{
exportPath = Path.Combine(Path.GetTempPath(), $"route-export-{route.Id}-{Guid.NewGuid():N}{extension}");
}
string directory = Path.GetDirectoryName(exportPath);
if (!string.IsNullOrWhiteSpace(directory))
{
Directory.CreateDirectory(directory);
}
var dataManager = new PathDataManager();
bool exported = isJson
? dataManager.ExportToJson(new List<PathRoute> { route }, exportPath)
: dataManager.ExportToXml(new List<PathRoute> { route }, exportPath);
if (!exported)
{
throw new InvalidOperationException($"导出路径失败: {routeName} -> {exportPath}");
}
LogManager.Info($"[测试HTTP] 已导出路径: {routeName} ({formatText}) -> {exportPath}");
return new
{
routeName,
format = isJson ? "json" : "xml",
exportedFilePath = exportPath,
pointCount = route.Points?.Count ?? 0,
totalLengthInMeters = route.TotalLength
};
}
private static object ImportRouteFilePayload(Dictionary<string, string> query)
{
PathPlanningManager pathManager = RequirePathManager();
string filePath = GetRequiredQueryValue(query, "path");
string formatText = GetOptionalQueryValue(query, "format");
bool overwrite = ParseBooleanQuery(query, "overwrite", false);
if (!File.Exists(filePath))
{
throw new InvalidOperationException($"导入文件不存在: {filePath}");
}
ExportFormat format;
if (string.IsNullOrWhiteSpace(formatText))
{
string extension = Path.GetExtension(filePath).ToLowerInvariant();
format = string.Equals(extension, ".json", StringComparison.Ordinal)
? ExportFormat.Json
: ExportFormat.Xml;
}
else if (Enum.TryParse(formatText, true, out ExportFormat parsedFormat) &&
(parsedFormat == ExportFormat.Json || parsedFormat == ExportFormat.Xml))
{
format = parsedFormat;
}
else
{
throw new InvalidOperationException($"无法解析导入格式: {formatText}(仅支持 json/xml");
}
var dataManager = new PathDataManager();
List<PathRoute> importedRoutes = format == ExportFormat.Json
? dataManager.ImportFromJson(filePath)
: dataManager.ImportFromXml(filePath);
if (importedRoutes == null || importedRoutes.Count == 0)
{
throw new InvalidOperationException($"导入文件中没有有效路径: {filePath}");
}
var serializedRoutes = new List<object>();
foreach (PathRoute importedRoute in importedRoutes)
{
if (overwrite)
{
// 覆盖模式:先删除同名路径,保证导入名称精确、测试可重复执行
PathRoute existingRoute = pathManager
.GetAllRoutes()
?.FirstOrDefault(r => string.Equals(r.Name, importedRoute.Name, StringComparison.Ordinal));
if (existingRoute != null)
{
pathManager.DeleteRoute(existingRoute);
LogManager.Info($"[测试HTTP] 已删除同名路径以便覆盖导入: {existingRoute.Name}");
}
}
// 注册到路径管理器(自动入库),供后续切换/动画流程使用
pathManager.AddRoute(importedRoute);
serializedRoutes.Add(new
{
id = importedRoute.Id,
name = importedRoute.Name,
pathType = importedRoute.PathType.ToString(),
pointCount = importedRoute.Points?.Count ?? 0,
totalLengthInMeters = importedRoute.TotalLength,
railMountMode = importedRoute.RailMountMode.ToString(),
railPathDefinitionMode = importedRoute.RailPathDefinitionMode.ToString(),
railNormalOffset = importedRoute.RailNormalOffset,
railPreferredNormal = importedRoute.RailPreferredNormal == null
? null
: new
{
x = importedRoute.RailPreferredNormal.X,
y = importedRoute.RailPreferredNormal.Y,
z = importedRoute.RailPreferredNormal.Z
}
});
}
LogManager.Info($"[测试HTTP] 已导入路径文件 {Path.GetFileName(filePath)},共 {serializedRoutes.Count} 条");
Document activeDocument = Autodesk.Navisworks.Api.Application.ActiveDocument;
double metersToModelUnits = activeDocument == null
? 1.0
: UnitsConverter.GetMetersToUnitsConversionFactor(activeDocument.Units);
return new
{
filePath,
format = format.ToString(),
importedCount = serializedRoutes.Count,
metersToModelUnits,
routes = serializedRoutes
};
}
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);
ApplyTestAnimationParameters(animationViewModel, query);
PathRoute route = ResolveVirtualCollisionRoute(pathManager, query, fixedPathType);
pathManager.SetCurrentRoute(route);
animationViewModel.SetCurrentPath(CreatePathRouteViewModel(route));
// 清除上一次测试残留的碰撞报告,避免 HasGeneratedCollisionReport/LastGeneratedReport 跨测试串扰
animationViewModel.InvalidateLastGeneratedReport();
animationViewModel.IsManualCollisionTargetEnabled = false;
bool useVirtualObject = ParseBooleanQuery(query, "useVirtualObject", true);
object animatedObjectPayload;
if (useVirtualObject)
{
animationViewModel.UseVirtualObject = true;
animatedObjectPayload = new
{
mode = "VirtualObject",
displayName = "虚拟物体"
};
}
else
{
// 真实物体模式:按名称/当前选择集选择真实物体
ModelItem realObject = ResolveAnimatedObjectFromQuery(query);
SelectDocumentItem(realObject);
if (!animationViewModel.SelectAnimatedObjectCommand.CanExecute(null))
{
throw new InvalidOperationException("当前动画视图模型不允许执行“选择移动物体”命令");
}
animationViewModel.UseVirtualObject = false;
animationViewModel.SelectAnimatedObjectCommand.Execute(null);
if (!IsSameModelItem(animationViewModel.SelectedAnimatedObject, realObject))
{
string detail = animationViewModel.SelectedAnimatedObject == null
? "选择真实物体失败:物体在当前文档不可用(导出局部模型中未解析到该物体,常见于物体不在路径剖面盒内或层级链不匹配;请改用虚拟物体或确认物体在盒内)"
: "选择真实物体失败:动画视图模型未保留选中的对象";
throw new InvalidOperationException(detail);
}
animatedObjectPayload = new
{
mode = "RealObject",
displayName = realObject.DisplayName
};
}
if (!animationViewModel.CanGenerateAnimation || !animationViewModel.GenerateAnimationCommand.CanExecute(null))
{
throw new InvalidOperationException($"{route.PathType} 碰撞测试准备失败:当前条件下不能生成动画");
}
animationViewModel.GenerateAnimationCommand.Execute(null);
LogManager.Info(
$"[测试HTTP] 已开始准备{(useVirtualObject ? "" : "")}碰撞测试: 路径={route.Name}, 类型={route.PathType}");
return new PreparedVirtualCollisionTest
{
pathType = route.PathType,
route = SerializeRoute(route, route),
animatedObject = animatedObjectPayload
};
}
/// <summary>
/// 动画逐帧状态探针:生成动画后 seek 到指定进度点,返回各点物体跟踪位置/yaw。
/// 供集成测试验证物体沿路径移动(起点→中间→终点)与平面姿态链。
/// </summary>
/// <summary>
/// 动画播放控制集成测试序列prepare → start → pause验证帧停止→ resume验证帧前进→ finished。
/// 验证 PathAnimationManager 的暂停/恢复/播放完整链路。
/// </summary>
private static async Task<object> AnimationPlaybackControlAsync(Dictionary<string, string> query)
{
await _testExecutionLock.WaitAsync().ConfigureAwait(false);
try
{
return await AnimationPlaybackControlCoreAsync(query).ConfigureAwait(false);
}
finally
{
_testExecutionLock.Release();
}
}
private static async Task<object> AnimationPlaybackControlCoreAsync(Dictionary<string, string> query)
{
int timeoutSeconds = ParseTimeoutSeconds(query, 180);
DateTime deadlineUtc = DateTime.UtcNow.AddSeconds(timeoutSeconds);
PathRoute route = InvokeOnUiThread(() => PrepareProbeAnimation(query));
await WaitForConditionAsync(
deadlineUtc,
() => InvokeOnUiThread(() =>
{
var animationManager = PathAnimationManager.GetInstance();
return animationManager != null &&
animationManager.TotalFrames > 0 &&
(animationManager.CurrentState == AnimationState.Ready ||
animationManager.CurrentState == AnimationState.Finished);
}),
"等待动画生成完成超时").ConfigureAwait(false);
var stages = new List<object>();
int totalFrames = 0;
// 1. 开始播放
InvokeOnUiThread(() =>
{
StartPreparedAnimation();
return true;
});
// 2. 等待播放开始(帧前进)
await WaitForConditionAsync(
deadlineUtc,
() => InvokeOnUiThread(() => PathAnimationManager.GetInstance()?.CurrentFrame > 0),
"等待动画开始播放超时").ConfigureAwait(false);
totalFrames = InvokeOnUiThread(() => PathAnimationManager.GetInstance().TotalFrames);
int playingFrame = InvokeOnUiThread(() => PathAnimationManager.GetInstance().CurrentFrame);
stages.Add(RecordStage("playing", playingFrame));
// 等待至少前进一帧,确保暂停观测窗口有效(帧间隔 ~66ms @15fps
await WaitForConditionAsync(
deadlineUtc,
() => InvokeOnUiThread(() => PathAnimationManager.GetInstance()?.CurrentFrame > playingFrame),
"等待播放前进超时").ConfigureAwait(false);
// 3. 暂停并验证帧停止
InvokeOnUiThread(() =>
{
PathAnimationManager.GetInstance().PauseAnimation();
return true;
});
int pausedFrame = InvokeOnUiThread(() => PathAnimationManager.GetInstance().CurrentFrame);
stages.Add(RecordStage("paused", pausedFrame));
// 暂停期间等待 1.2 秒,帧不应前进
await Task.Delay(1200).ConfigureAwait(false);
int frameAfterPause = InvokeOnUiThread(() => PathAnimationManager.GetInstance().CurrentFrame);
bool pauseVerified = frameAfterPause == pausedFrame;
// 4. 恢复播放,帧继续前进
InvokeOnUiThread(() =>
{
PathAnimationManager.GetInstance().ResumeAnimation();
return true;
});
await WaitForConditionAsync(
deadlineUtc,
() => InvokeOnUiThread(() => PathAnimationManager.GetInstance()?.CurrentFrame > pausedFrame),
"等待动画恢复播放超时").ConfigureAwait(false);
stages.Add(RecordStage("resumed", InvokeOnUiThread(() => PathAnimationManager.GetInstance().CurrentFrame)));
// 5. 等待播放完成
await WaitForConditionAsync(
deadlineUtc,
() => InvokeOnUiThread(() => PathAnimationManager.GetInstance()?.CurrentState == AnimationState.Finished),
"等待动画播放完成超时").ConfigureAwait(false);
stages.Add(RecordStage("finished", InvokeOnUiThread(() => PathAnimationManager.GetInstance().CurrentFrame)));
return new
{
route = SerializeRoute(route, route),
totalFrames,
pauseVerified,
pausedFrame,
frameAfterPause,
stages
};
}
private static object RecordStage(string stage, int frame)
{
return new
{
stage,
frame,
state = PathAnimationManager.GetInstance()?.CurrentState.ToString()
};
}
private static async Task<object> ProbeAnimationFramesAsync(Dictionary<string, string> query)
{
await _testExecutionLock.WaitAsync().ConfigureAwait(false);
try
{
return await ProbeAnimationFramesCoreAsync(query).ConfigureAwait(false);
}
finally
{
_testExecutionLock.Release();
}
}
private static async Task<object> ProbeAnimationFramesCoreAsync(Dictionary<string, string> query)
{
int timeoutSeconds = ParseTimeoutSeconds(query, 180);
DateTime deadlineUtc = DateTime.UtcNow.AddSeconds(timeoutSeconds);
PathRoute route = InvokeOnUiThread(() => PrepareProbeAnimation(query));
await WaitForConditionAsync(
deadlineUtc,
() => InvokeOnUiThread(() =>
{
var animationManager = PathAnimationManager.GetInstance();
return animationManager != null &&
animationManager.TotalFrames > 0 &&
(animationManager.CurrentState == AnimationState.Ready ||
animationManager.CurrentState == AnimationState.Finished);
}),
"等待动画生成完成超时").ConfigureAwait(false);
return InvokeOnUiThread(() =>
{
var animationManager = PathAnimationManager.GetInstance();
var controlledObject = VirtualObjectManager.Instance.CurrentVirtualObject;
if (animationManager == null || controlledObject == null)
{
throw new InvalidOperationException("虚拟物体或动画管理器当前不可用,无法执行帧探针");
}
int totalFrames = animationManager.TotalFrames;
string probeText = GetOptionalQueryValue(query, "probeProgress") ?? "0,0.5,1.0";
var probes = new List<object>();
foreach (string part in probeText.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries))
{
if (!double.TryParse(part, NumberStyles.Float, CultureInfo.InvariantCulture, out double progress))
{
throw new InvalidOperationException($"无法解析探针进度: {part}");
}
progress = Math.Max(0.0, Math.Min(1.0, progress));
animationManager.SeekToProgress(progress);
var (position, yaw) = animationManager.GetObjectCurrentPosition(controlledObject);
probes.Add(new
{
progress,
frameIndex = animationManager.CurrentFrame,
position = SerializePoint3D(position),
yawRadians = yaw,
yawDegrees = yaw * 180.0 / Math.PI
});
}
List<PathPoint> orderedPoints = route.Points?.OrderBy(p => p.Index).ToList() ?? new List<PathPoint>();
return new
{
route = SerializeRoute(route, route),
totalFrames,
pathStart = SerializePoint3D(orderedPoints.FirstOrDefault()?.Position),
pathEnd = SerializePoint3D(orderedPoints.LastOrDefault()?.Position),
probes
};
});
}
/// <summary>
/// 生成虚拟物体动画(帧探针准备阶段),返回当前路径。
/// </summary>
private static PathRoute PrepareProbeAnimation(Dictionary<string, string> query)
{
PathPlanningManager pathManager = RequirePathManager();
AnimationControlViewModel animationViewModel = RequireAnimationControlViewModel();
animationViewModel.SetPathPlanningManager(pathManager);
ApplyTestAnimationParameters(animationViewModel, query);
PathRoute route = ResolveVirtualCollisionRoute(pathManager, query, null);
pathManager.SetCurrentRoute(route);
animationViewModel.SetCurrentPath(CreatePathRouteViewModel(route));
// 清除上一次测试残留的碰撞报告(帧探针场景同样避免跨测试串扰)
animationViewModel.InvalidateLastGeneratedReport();
animationViewModel.IsManualCollisionTargetEnabled = false;
animationViewModel.UseVirtualObject = true;
if (!animationViewModel.CanGenerateAnimation || !animationViewModel.GenerateAnimationCommand.CanExecute(null))
{
throw new InvalidOperationException($"{route.PathType} 帧探针测试准备失败:当前条件下不能生成动画");
}
animationViewModel.GenerateAnimationCommand.Execute(null);
LogManager.Info($"[测试HTTP] 帧探针测试动画已开始生成: 路径={route.Name}, 类型={route.PathType}");
return route;
}
/// <summary>
/// 应用测试动画参数(帧率/时长),加速集成测试:
/// query 支持 frameRateFPS与 durationSeconds缺省保持配置默认值。
/// </summary>
private static void ApplyTestAnimationParameters(AnimationControlViewModel animationViewModel, Dictionary<string, string> query)
{
if (int.TryParse(GetOptionalQueryValue(query, "frameRate"), out int frameRate) && frameRate > 0)
{
animationViewModel.SelectedFrameRate = frameRate;
}
if (double.TryParse(GetOptionalQueryValue(query, "durationSeconds"), NumberStyles.Float, CultureInfo.InvariantCulture, out double durationSeconds) &&
durationSeconds > 0)
{
animationViewModel.AnimationDuration = durationSeconds;
}
}
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)
{
// 集成测试依赖的标准路径缺失时,自动从插件资源文件导入后重试。
// 这是唯一允许的自动导入路径(仅限标准测试路径,不影响用户数据)。
EnsureAutoTestRoutesImported(pathManager);
routes = pathManager.GetAllRoutes() ?? new List<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;
}
/// <summary>
/// 判断两个 ModelItem 是否指向同一模型对象。
/// NW API 中同一逻辑对象通过不同途径Selection/Descendants获取的实例不保证同一引用
/// 因此用 InstanceGuid 比较,禁止用 ReferenceEquals。
/// </summary>
private static bool IsSameModelItem(ModelItem a, ModelItem b)
{
if (a == null || b == null)
{
return false;
}
return string.Equals(
a.InstanceGuid.ToString(),
b.InstanceGuid.ToString(),
StringComparison.OrdinalIgnoreCase);
}
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;
}
/// <summary>
/// 检查并导入集成测试标准路径自动测试_ 前缀 + 测试基准)。
/// 仅当对应名称的路径缺失时,从插件部署目录 resources\auto-test-routes.xml 导入。
/// 已存在或重名的路径不会被覆盖/重命名,确保不破坏用户数据。
/// </summary>
private static void EnsureAutoTestRoutesImported(PathPlanningManager pathManager)
{
string[] standardRouteNames = new[]
{
"自动测试_Ground",
"自动测试_Hoisting",
"自动测试_Rail",
"自动测试_Free",
"测试基准"
};
List<PathRoute> existingRoutes = pathManager.GetAllRoutes() ?? new List<PathRoute>();
List<string> missingNames = standardRouteNames
.Where(name => !existingRoutes.Any(route => string.Equals(route.Name, name, StringComparison.Ordinal)))
.ToList();
if (missingNames.Count == 0)
{
return;
}
string pluginDirectory = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location);
string resourcePath = Path.Combine(pluginDirectory ?? string.Empty, "resources", AutoTestRoutesResourceFileName);
if (!File.Exists(resourcePath))
{
throw new InvalidOperationException(
$"缺少标准测试路径资源文件: {resourcePath}。请确认插件已完整部署resources\\{AutoTestRoutesResourceFileName})。缺失路径: {string.Join(", ", missingNames)}");
}
List<PathRoute> importedRoutes = new PathDataManager().ImportFromXml(resourcePath);
if (importedRoutes == null || importedRoutes.Count == 0)
{
throw new InvalidOperationException($"标准测试路径资源文件为空: {resourcePath}");
}
int importedCount = 0;
foreach (PathRoute importedRoute in importedRoutes)
{
bool exists = existingRoutes.Any(route => string.Equals(route.Name, importedRoute.Name, StringComparison.Ordinal));
if (exists)
{
continue;
}
if (importedRoute.PathType != PathType.Ground &&
importedRoute.PathType != PathType.Hoisting &&
importedRoute.PathType != PathType.Rail &&
importedRoute.PathType != PathType.Free)
{
continue;
}
if (pathManager.AddRoute(importedRoute))
{
importedCount++;
LogManager.Info($"[测试HTTP] 已导入标准测试路径: {importedRoute.Name} ({importedRoute.PathType})");
}
}
if (importedCount == 0)
{
throw new InvalidOperationException(
$"标准测试路径导入失败0 条成功)。缺失路径: {string.Join(", ", missingNames)}");
}
LogManager.Info($"[测试HTTP] 标准测试路径检查完成: 缺失 {missingNames.Count} 条,已导入 {importedCount} 条");
}
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");
string animatedObjectPath = GetOptionalQueryValue(query, "animatedObjectPath");
if (string.IsNullOrWhiteSpace(animatedObjectName) && string.IsNullOrWhiteSpace(animatedObjectPath))
{
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)
{
continue;
}
bool nameMatches = !string.IsNullOrWhiteSpace(animatedObjectName) &&
string.Equals(item.DisplayName, animatedObjectName, StringComparison.OrdinalIgnoreCase);
bool pathMatches = false;
if (!string.IsNullOrWhiteSpace(animatedObjectPath))
{
try
{
var pathId = activeDocument.Models.CreatePathId(item);
pathMatches = string.Equals(pathId.PathId, animatedObjectPath, StringComparison.OrdinalIgnoreCase);
}
catch
{
// 忽略单点路径解析失败,继续匹配
}
}
if (nameMatches || pathMatches)
{
matches.Add(item);
}
}
}
if (matches.Count == 0)
{
string target = string.IsNullOrWhiteSpace(animatedObjectPath) ? animatedObjectName : animatedObjectPath;
throw new InvalidOperationException($"找不到指定真实物体: {target}");
}
if (matches.Count > 1)
{
string target = string.IsNullOrWhiteSpace(animatedObjectPath) ? animatedObjectName : animatedObjectPath;
throw new InvalidOperationException($"找到多个匹配真实物体,请改用更唯一的名字或 PathId: {target}");
}
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();
}
}
}