NavisworksTransport/src/Core/Services/TestAutomationHttpService.cs

1733 lines
71 KiB
C#

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