NavisworksTransport/UnitTests/Integration/RealObjectAnimationAutomationTests.cs
tian 832caaffef feat: F1 剖面盒自动检测完整链路跑通(真实物体)
- SectionBoxBatchDetector:路径剖面盒(复用 SectionClipHelper.CalculatePathClipBox 产品语义)→ 导出局部 NWD → 副实例检测 → 报告回传
- 移动物体跨实例解析:导出剖面盒保留节点树结构,去掉根后主/副层级链一致,按 ancestorChain 匹配(用户验证结论)
- 副实例临时库:TRANSPORTPLUGIN_TEST_DB_PATH 环境变量显式指定,createIfMissing 时强制创建(解决检测记录无法落库→报告失败)
- 副实例进程定位:cmd start detach 后按新 Roamer PID 定位并关闭
- batch-queue-add:真实物体写入 ModelItemReferences(MovingObject),F1 从引用读移动物体名/PathId/层级链
- batch-queue-process:改后台执行立即返回(模态进度条不阻塞 HTTP),调用方轮询数据库状态
- 集成测试标准场景:自动测试_Ground 更新(用户修改)+ Ground 真实物体改 Chair Lounge Couch Double

验证:F1 Ground + Chair Lounge Couch Double,碰撞 28/28,链路完整
2026-08-05 12:11:37 +08:00

142 lines
6.4 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.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Newtonsoft.Json.Linq;
namespace NavisworksTransport.UnitTests.Integration
{
/// <summary>
/// 真实物体动画集成测试(覆盖计划 T01-T03高风险区真实物体姿态链
///
/// 验证点:
/// - 通过 list-model-items 动态解析真实物体ModelPath(PathId) 跨会话不稳定,禁止硬编码)
/// - 同名多实例时按“离路径起点最近”选择(如模型中有多个 42" Diameter 管道)
/// - 真实物体沿三类路径生成动画并完整播放fragment 代表姿态 / 真实尺寸)
///
/// 路径起点坐标来自 resources/auto-test-routes.xml模型坐标稳定不随会话变化
/// </summary>
[TestClass]
[TestCategory("NavisworksIntegration")]
public class RealObjectAnimationAutomationTests
{
private const int TestFrameRate = 15;
private const double TestDurationSeconds = 5.0;
// 路径起点(模型单位,来自 auto-test-routes.xml
private static readonly (double X, double Y, double Z) GroundStart = (-216.959, 14.833, 7.698);
private static readonly (double X, double Y, double Z) HoistingStart = (-221.750, 10.833, -14.097);
private static readonly (double X, double Y, double Z) RailStart = (-207.591, 29.482, 3.504);
[TestMethod]
[Timeout(360000)]
public async Task GroundRealObject_Animation_Completes()
{
await AssertRealObjectFlowAsync("Ground", "Chair Lounge Couch Double", GroundStart).ConfigureAwait(false);
}
[TestMethod]
[Timeout(360000)]
public async Task HoistingRealObject_Animation_Completes()
{
await AssertRealObjectFlowAsync("Hoisting", "25\" x 25\"", HoistingStart).ConfigureAwait(false);
}
[TestMethod]
[Timeout(360000)]
public async Task RailRealObject_Animation_Completes()
{
await AssertRealObjectFlowAsync("Rail", "42\" Diameter", RailStart).ConfigureAwait(false);
}
private static async Task AssertRealObjectFlowAsync(
string pathType,
string objectDisplayName,
(double X, double Y, double Z) routeStart)
{
using (var client = new NavisworksTestAutomationClient())
{
await client.EnsureServiceReadyAsync(TimeSpan.FromSeconds(90)).ConfigureAwait(false);
// 动态解析真实物体 PathId离路径起点最近的同名实例
string animatedObjectPath = await ResolveRealObjectPathIdAsync(
client, objectDisplayName, routeStart).ConfigureAwait(false);
JObject response = await client.RunCollisionTestAsync(
pathType,
240,
useVirtualObject: false,
animatedObjectPath: animatedObjectPath,
frameRate: TestFrameRate,
durationSeconds: TestDurationSeconds).ConfigureAwait(false);
Assert.IsTrue(response.Value<bool>("ok"), "真实物体碰撞测试 HTTP 接口返回失败: " + (string)response["error"]);
JObject data = (JObject)response["data"];
Assert.IsNotNull(data, "缺少测试结果 data");
JObject animatedObject = (JObject)data["animatedObject"];
Assert.IsNotNull(animatedObject, "缺少 animatedObject");
Assert.AreEqual("RealObject", (string)animatedObject["mode"], "当前测试应使用真实物体模式");
Assert.IsFalse(string.IsNullOrWhiteSpace((string)animatedObject["displayName"]), "真实物体显示名为空");
JObject route = (JObject)data["route"];
Assert.IsNotNull(route, "缺少 route");
Assert.AreEqual(pathType, (string)route["pathType"], "返回的路径类型不匹配");
JObject animation = (JObject)data["animation"];
Assert.IsNotNull(animation, "缺少 animation");
Assert.AreEqual("Finished", (string)animation["currentState"], "动画未完成");
Assert.IsTrue((int)animation["totalFrames"] > 0, "动画总帧数应大于 0");
Assert.IsTrue(animation["detectionRecordId"] != null && (int)animation["detectionRecordId"] > 0, "检测记录 ID 无效");
CollisionReportAssertions.AssertReportStructure(data, route);
}
}
/// <summary>
/// 枚举同名物体并选择离参考点(路径起点)最近的实例,返回其 PathId。
/// ModelPath 跨会话不稳定,必须动态解析而非硬编码。
/// </summary>
private static async Task<string> ResolveRealObjectPathIdAsync(
NavisworksTestAutomationClient client,
string displayName,
(double X, double Y, double Z) referencePoint)
{
JObject response = await client.ListModelItemsAsync(displayName, 100).ConfigureAwait(false);
Assert.IsTrue(response.Value<bool>("ok"), "枚举模型物体失败: " + (string)response["error"]);
JObject data = (JObject)response["data"];
JArray items = data["items"] as JArray;
Assert.IsNotNull(items, "缺少 items");
Assert.IsTrue(items.Count > 0, $"未找到物体: {displayName}");
string bestPathId = null;
double bestDistanceSq = double.MaxValue;
foreach (JToken token in items)
{
JObject item = (JObject)token;
JObject center = (JObject)item["boundingBoxCenter"];
string pathId = (string)item["pathId"];
if (center == null || string.IsNullOrWhiteSpace(pathId))
{
continue;
}
double dx = (double)center["x"] - referencePoint.X;
double dy = (double)center["y"] - referencePoint.Y;
double dz = (double)center["z"] - referencePoint.Z;
double distanceSq = dx * dx + dy * dy + dz * dz;
if (distanceSq < bestDistanceSq)
{
bestDistanceSq = distanceSq;
bestPathId = pathId;
}
}
Assert.IsNotNull(bestPathId, $"无法定位物体(无包围盒中心): {displayName}");
return bestPathId;
}
}
}