- 服务端新增 probe-animation-frames 端点:生成动画后 SeekToProgress 到多个进度点,返回物体跟踪位置/yaw - 新增 AnimationFrameProbeAutomationTests: - Ground:起点/终点 XZ 匹配路径点、高度恒定(平面链)、中间帧在起终点之间 - Hoisting:吊装剖面(起吊地面→悬挂升高→下降落地)+ 水平平移 - 集成测试 11/11 通过(P0 高风险区全部完成:真实物体 + 逐帧状态)
140 lines
7.0 KiB
C#
140 lines
7.0 KiB
C#
using System;
|
||
using System.Threading.Tasks;
|
||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||
using Newtonsoft.Json.Linq;
|
||
|
||
namespace NavisworksTransport.UnitTests.Integration
|
||
{
|
||
/// <summary>
|
||
/// 动画逐帧状态验证集成测试(覆盖计划 T04)。
|
||
///
|
||
/// 通过 probe-animation-frames 端点生成动画后 seek 到多个进度点,
|
||
/// 断言物体跟踪位置(业务跟踪点 = 原始包围盒中心)沿路径移动:
|
||
/// - Ground:起点/终点 XZ 匹配路径点,高度恒定(平面链不改变高度),中间帧在起终点之间
|
||
/// - Hoisting:高度剖面(起吊地面 → 悬挂升高 → 下降地面)+ 水平平移
|
||
///
|
||
/// 注:比较使用模型单位(Floor2 为英尺);物体跟踪点 XZ 与路径点一致,Y 为中心高度(非路径点高度)。
|
||
/// </summary>
|
||
[TestClass]
|
||
[TestCategory("NavisworksIntegration")]
|
||
public class AnimationFrameProbeAutomationTests
|
||
{
|
||
private const double PositionTolerance = 1e-2;
|
||
|
||
[TestMethod]
|
||
[Timeout(240000)]
|
||
public async Task GroundVirtualObject_FramesFollowPathStartToEnd()
|
||
{
|
||
using (var client = new NavisworksTestAutomationClient())
|
||
{
|
||
await client.EnsureServiceReadyAsync(TimeSpan.FromSeconds(90)).ConfigureAwait(false);
|
||
|
||
JObject response = await client.ProbeAnimationFramesAsync("Ground", 120).ConfigureAwait(false);
|
||
Assert.IsTrue(response.Value<bool>("ok"), "帧探针失败: " + (string)response["error"]);
|
||
|
||
JObject data = (JObject)response["data"];
|
||
Assert.AreEqual("自动测试_Ground", (string)data["route"]["name"], "探针路径不匹配");
|
||
Assert.IsTrue((int)data["totalFrames"] > 0, "总帧数应大于 0");
|
||
|
||
JObject pathStart = (JObject)data["pathStart"];
|
||
JObject pathEnd = (JObject)data["pathEnd"];
|
||
JArray probes = (JArray)data["probes"];
|
||
|
||
AssertProbeCount(probes, 5);
|
||
|
||
// probe 0:物体起点 = 路径起点(XZ 匹配,Y 为中心高度)
|
||
var p0 = (JObject)probes[0]["position"];
|
||
AssertNear((double)pathStart["x"], (double)p0["x"], PositionTolerance, "起点 X 不匹配路径起点");
|
||
AssertNear((double)pathStart["z"], (double)p0["z"], PositionTolerance, "起点 Z 不匹配路径起点");
|
||
|
||
// probe 1.0:物体终点 = 路径终点
|
||
var p1 = (JObject)probes[probes.Count - 1]["position"];
|
||
AssertNear((double)pathEnd["x"], (double)p1["x"], PositionTolerance, "终点 X 不匹配路径终点");
|
||
AssertNear((double)pathEnd["z"], (double)p1["z"], PositionTolerance, "终点 Z 不匹配路径终点");
|
||
|
||
// Ground 平面链:高度恒定(物体中心高度不变)
|
||
AssertNear((double)p0["y"], (double)p1["y"], PositionTolerance, "Ground 路径高度应恒定");
|
||
|
||
// 中间帧在起终点之间(水平距离大于 0 且小于全程)
|
||
var pm = (JObject)probes[probes.Count / 2]["position"];
|
||
double startToMid = HorizontalDistance(p0, pm);
|
||
double startToEnd = HorizontalDistance(p0, p1);
|
||
Assert.IsTrue(startToMid > PositionTolerance, "中间帧不应等于起点");
|
||
Assert.IsTrue(startToMid < startToEnd - PositionTolerance, "中间帧应在起终点之间");
|
||
|
||
// yaw 应为有限值
|
||
foreach (var probe in probes)
|
||
{
|
||
Assert.IsFalse(double.IsNaN((double)probe["yawDegrees"]), "yaw 不应为 NaN");
|
||
}
|
||
}
|
||
}
|
||
|
||
[TestMethod]
|
||
[Timeout(240000)]
|
||
public async Task HoistingVirtualObject_FramesFollowLiftProfile()
|
||
{
|
||
using (var client = new NavisworksTestAutomationClient())
|
||
{
|
||
await client.EnsureServiceReadyAsync(TimeSpan.FromSeconds(90)).ConfigureAwait(false);
|
||
|
||
JObject response = await client.ProbeAnimationFramesAsync("Hoisting", 120).ConfigureAwait(false);
|
||
Assert.IsTrue(response.Value<bool>("ok"), "帧探针失败: " + (string)response["error"]);
|
||
|
||
JObject data = (JObject)response["data"];
|
||
Assert.AreEqual("自动测试_Hoisting", (string)data["route"]["name"], "探针路径不匹配");
|
||
Assert.IsTrue((int)data["totalFrames"] > 0, "总帧数应大于 0");
|
||
|
||
JObject pathStart = (JObject)data["pathStart"];
|
||
JObject pathEnd = (JObject)data["pathEnd"];
|
||
JArray probes = (JArray)data["probes"];
|
||
|
||
AssertProbeCount(probes, 5);
|
||
|
||
var p0 = (JObject)probes[0]["position"];
|
||
var pm = (JObject)probes[probes.Count / 2]["position"];
|
||
var p1 = (JObject)probes[probes.Count - 1]["position"];
|
||
|
||
// 起吊点/落地点:XZ 匹配路径起点/终点(地面)
|
||
AssertNear((double)pathStart["x"], (double)p0["x"], PositionTolerance, "起吊点 X 不匹配");
|
||
AssertNear((double)pathStart["z"], (double)p0["z"], PositionTolerance, "起吊点 Z 不匹配");
|
||
AssertNear((double)pathEnd["x"], (double)p1["x"], PositionTolerance, "落地点 X 不匹配");
|
||
AssertNear((double)pathEnd["z"], (double)p1["z"], PositionTolerance, "落地点 Z 不匹配");
|
||
|
||
// 吊装剖面:中间帧高度高于起吊/落地(悬挂段),结束后回落
|
||
Assert.IsTrue((double)pm["y"] > (double)p0["y"] + PositionTolerance, "中间帧应高于起吊点(悬挂升高)");
|
||
AssertNear((double)p0["y"], (double)p1["y"], PositionTolerance, "落地后高度应回落到起吊高度");
|
||
|
||
// 水平平移:中间帧 X 在起点与终点之间
|
||
double startX = (double)p0["x"];
|
||
double endX = (double)p1["x"];
|
||
double midX = (double)pm["x"];
|
||
Assert.IsTrue(
|
||
midX > Math.Min(startX, endX) - PositionTolerance &&
|
||
midX < Math.Max(startX, endX) + PositionTolerance,
|
||
"中间帧 X 应在起终点之间(水平平移)");
|
||
}
|
||
}
|
||
|
||
private static void AssertProbeCount(JArray probes, int expected)
|
||
{
|
||
Assert.IsNotNull(probes, "缺少 probes");
|
||
Assert.AreEqual(expected, probes.Count, $"探针点数应为 {expected}(0/0.25/0.5/0.75/1.0)");
|
||
}
|
||
|
||
private static double HorizontalDistance(JObject a, JObject b)
|
||
{
|
||
double dx = (double)a["x"] - (double)b["x"];
|
||
double dz = (double)a["z"] - (double)b["z"];
|
||
return Math.Sqrt(dx * dx + dz * dz);
|
||
}
|
||
|
||
private static void AssertNear(double expected, double actual, double tolerance, string message)
|
||
{
|
||
Assert.IsTrue(
|
||
Math.Abs(expected - actual) <= tolerance,
|
||
$"{message}:期望 {expected:F3},实际 {actual:F3},容差 {tolerance}");
|
||
}
|
||
}
|
||
}
|