feat: 集成测试新增真实物体动画验证(P0 T01-T03)
- 服务端 run-virtual-collision-test 支持 useVirtualObject=false + animatedObjectPath(ModelPath 定位,解决 DisplayName 重名) - ResolveAnimatedObjectFromQuery 支持按 CreatePathId 的 PathId 精确匹配 - 修复 ModelItem 身份校验:ReferenceEquals 对 NW API 同一逻辑对象不同实例不可靠,改用 InstanceGuid 比较 - 新增 RealObjectAnimationAutomationTests:Ground/Hoisting/Rail 真实物体动画完整流程 (42" Diameter=0/0/660/0,25" x 25"=0/0/769/0) - 集成测试 9/9 通过(3 虚拟碰撞 + 1 autoPath + 2 导入 + 3 真实物体)
This commit is contained in:
parent
454590ae26
commit
b5cfcbf8ac
@ -72,6 +72,7 @@
|
||||
<Compile Include="UnitTests\Integration\AutoPathGridGenerationAutomationTests.cs" />
|
||||
<Compile Include="UnitTests\Integration\NavisworksTestAutomationClient.cs" />
|
||||
<Compile Include="UnitTests\Integration\PathImportValidityAutomationTests.cs" />
|
||||
<Compile Include="UnitTests\Integration\RealObjectAnimationAutomationTests.cs" />
|
||||
<Compile Include="UnitTests\Integration\VirtualCollisionAutomationTests.cs" />
|
||||
<Compile Include="UnitTests\CoordinateSystem\HostCoordinateAdapterTests.cs" />
|
||||
<Compile Include="UnitTests\CoordinateSystem\CanonicalRailPoseBuilderTests.cs" />
|
||||
|
||||
@ -58,6 +58,38 @@ namespace NavisworksTransport.UnitTests.Integration
|
||||
return await RunVirtualCollisionTestCoreAsync(routeName, pathType, timeoutSeconds).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 运行碰撞测试(虚拟或真实物体)。
|
||||
/// useVirtualObject=false 时必须提供 animatedObjectPath(ModelPath 定位,格式如 0/0/769/0)。
|
||||
/// </summary>
|
||||
public async Task<JObject> RunCollisionTestAsync(
|
||||
string pathType,
|
||||
int timeoutSeconds,
|
||||
string routeName = null,
|
||||
bool useVirtualObject = true,
|
||||
string animatedObjectPath = null)
|
||||
{
|
||||
var queryParts = new System.Collections.Generic.List<string>
|
||||
{
|
||||
"pathType=" + Uri.EscapeDataString(pathType),
|
||||
"timeoutSeconds=" + timeoutSeconds.ToString(CultureInfo.InvariantCulture),
|
||||
"useVirtualObject=" + (useVirtualObject ? "true" : "false")
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(routeName))
|
||||
{
|
||||
queryParts.Add("routeName=" + Uri.EscapeDataString(routeName));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(animatedObjectPath))
|
||||
{
|
||||
queryParts.Add("animatedObjectPath=" + Uri.EscapeDataString(animatedObjectPath));
|
||||
}
|
||||
|
||||
string requestUri = "/api/test/run-virtual-collision-test?" + string.Join("&", queryParts);
|
||||
return await PostJsonAsync(requestUri).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task<JObject> RunVirtualCollisionTestCoreAsync(string routeName, string pathType, int timeoutSeconds)
|
||||
{
|
||||
string requestUri = string.Format(
|
||||
|
||||
85
UnitTests/Integration/RealObjectAnimationAutomationTests.cs
Normal file
85
UnitTests/Integration/RealObjectAnimationAutomationTests.cs
Normal file
@ -0,0 +1,85 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace NavisworksTransport.UnitTests.Integration
|
||||
{
|
||||
/// <summary>
|
||||
/// 真实物体动画集成测试(覆盖计划 T01-T03,高风险区:真实物体姿态链)。
|
||||
///
|
||||
/// 验证点:
|
||||
/// - 通过 ModelPath(PathId)唯一定位真实物体(DisplayName 可能重名)
|
||||
/// - 真实物体(非虚拟物体)沿三类路径生成动画并完整播放
|
||||
/// - 真实物体模式使用 fragment 代表姿态 / 真实尺寸,而非 unit_cube 资产
|
||||
///
|
||||
/// 依赖模型数据(Floor2_mobile_yup.nwd):
|
||||
/// - 0/0/660/0 = 42" Diameter 管道(Rail 终点附近,同时用于 Ground/Rail)
|
||||
/// - 0/0/769/0 = 25" x 25" 方柱(Hoisting 路径上)
|
||||
/// 模型几何变化可能导致 PathId 失效,届时需重新定位。
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
[TestCategory("NavisworksIntegration")]
|
||||
public class RealObjectAnimationAutomationTests
|
||||
{
|
||||
private const string Pipe42DiameterPathId = "0/0/660/0";
|
||||
private const string Column25x25PathId = "0/0/769/0";
|
||||
|
||||
[TestMethod]
|
||||
[Timeout(360000)]
|
||||
public async Task GroundRealObject_Animation_Completes()
|
||||
{
|
||||
await AssertRealObjectFlowAsync("Ground", Pipe42DiameterPathId).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[Timeout(360000)]
|
||||
public async Task HoistingRealObject_Animation_Completes()
|
||||
{
|
||||
await AssertRealObjectFlowAsync("Hoisting", Column25x25PathId).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[Timeout(360000)]
|
||||
public async Task RailRealObject_Animation_Completes()
|
||||
{
|
||||
await AssertRealObjectFlowAsync("Rail", Pipe42DiameterPathId).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static async Task AssertRealObjectFlowAsync(string pathType, string animatedObjectPath)
|
||||
{
|
||||
using (var client = new NavisworksTestAutomationClient())
|
||||
{
|
||||
await client.EnsureServiceReadyAsync(TimeSpan.FromSeconds(90)).ConfigureAwait(false);
|
||||
|
||||
JObject response = await client.RunCollisionTestAsync(
|
||||
pathType,
|
||||
240,
|
||||
useVirtualObject: false,
|
||||
animatedObjectPath: animatedObjectPath).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 无效");
|
||||
|
||||
Assert.IsNotNull(data["report"], "缺少碰撞报告");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -25,13 +25,15 @@
|
||||
|
||||
### P0 高风险区(AGENTS.md 标注高风险:真实物体姿态链 / 动画逐帧状态)
|
||||
|
||||
- [ ] **T01 真实物体 Ground 动画与姿态**
|
||||
- [x] **T01 真实物体 Ground 动画与姿态**(2026-08-04,`RealObjectAnimationAutomationTests`)
|
||||
- 验证:`select-animated-object` 选真实物体 → 生成动画 → 断言物体实际位置/姿态(fragment 代表姿态、平面姿态链)
|
||||
- 支撑:服务端 `select-animated-object` 端点已实现(按名称/选择集);`export-debug-snapshot` 返回 trackedState/geometryState/boundingBox
|
||||
- [ ] **T02 真实物体 Hoisting 动画与姿态**
|
||||
- 实现:`run-virtual-collision-test?useVirtualObject=false&animatedObjectPath=0/0/660/0`(42" Diameter)
|
||||
- [x] **T02 真实物体 Hoisting 动画与姿态**(2026-08-04,`RealObjectAnimationAutomationTests`)
|
||||
- 验证:真实物体吊装路径动画(起吊/平移/下降三段姿态)
|
||||
- [ ] **T03 真实物体 Rail 动画与姿态**
|
||||
- 实现:`animatedObjectPath=0/0/769/0`(25" x 25",Hoisting 路径上)
|
||||
- [x] **T03 真实物体 Rail 动画与姿态**(2026-08-04,`RealObjectAnimationAutomationTests`)
|
||||
- 验证:真实物体空轨路径(canonical → rail pose 链、角度修正不污染 0° 基线)
|
||||
- 实现:`animatedObjectPath=0/0/660/0`(42" Diameter)
|
||||
- [ ] **T04 动画逐帧状态验证**
|
||||
- 验证:动画中途/结束时物体位置沿路径移动、姿态符合预期(起点=路径起点、逐帧插值、终点=路径终点);Ground 平面链 yaw 正确
|
||||
- 支撑:`export-debug-snapshot` trackedState(position/yaw/quaternion)+ boundingBox
|
||||
@ -59,8 +61,8 @@
|
||||
|---|---|---|
|
||||
| ping / status / routes / selection | ✅ | 健康检查、状态、路径列表、选择集 |
|
||||
| select-route / select-default-route | ✅ | 切换路径(自动测试_ 前缀自动导入) |
|
||||
| **select-animated-object** | ✅ 已实现未用 | 真实物体选择(P0 测试用) |
|
||||
| run-virtual-collision-test / run-ground-collision-test | ✅ | 虚拟/真实物体碰撞流程 |
|
||||
| **select-animated-object** | ✅ 已实现 | 真实物体选择(T01-T03 已用) |
|
||||
| run-virtual-collision-test / run-ground-collision-test | ✅ | 虚拟/真实物体碰撞流程(支持 useVirtualObject=false + animatedObjectPath) |
|
||||
| analyze-auto-path-grid / run-auto-path | ✅ | 自动路径规划(已支持 strategy) |
|
||||
| **route-grid-diagnostics** | ✅ 已实现未用 | 网格诊断(T09 用) |
|
||||
| **export-debug-snapshot** | ✅ 已实现未用 | 动画逐帧状态(T04 用) |
|
||||
@ -74,3 +76,4 @@
|
||||
| 2026-08-02 | `43df07c` | 集成测试标准路径自动导入机制(前置基础) |
|
||||
| 2026-08-02 | `c9c29c7` | 路径导入验证迁入集成测试(PathImportValidity) |
|
||||
| 2026-08-02 | `5bdfa82` | AGENTS.md 单位原则强化(测试断言按文档单位换算) |
|
||||
| 2026-08-04 | 待提交 | T01-T03 真实物体动画(RealObjectAnimationAutomationTests,服务端支持 useVirtualObject=false + animatedObjectPath/InstanceGuid 校验) |
|
||||
|
||||
@ -598,7 +598,7 @@ namespace NavisworksTransport.Core.Services
|
||||
animationViewModel.UseVirtualObject = false;
|
||||
animationViewModel.SelectAnimatedObjectCommand.Execute(null);
|
||||
|
||||
if (!ReferenceEquals(animationViewModel.SelectedAnimatedObject, animatedObject))
|
||||
if (!IsSameModelItem(animationViewModel.SelectedAnimatedObject, animatedObject))
|
||||
{
|
||||
throw new InvalidOperationException("选择真实物体失败:动画视图模型未保留选中的对象");
|
||||
}
|
||||
@ -1995,22 +1995,54 @@ namespace NavisworksTransport.Core.Services
|
||||
animationViewModel.SetCurrentPath(CreatePathRouteViewModel(route));
|
||||
|
||||
animationViewModel.IsManualCollisionTargetEnabled = false;
|
||||
animationViewModel.UseVirtualObject = true;
|
||||
object animatedObjectPayload = new
|
||||
|
||||
bool useVirtualObject = ParseBooleanQuery(query, "useVirtualObject", true);
|
||||
object animatedObjectPayload;
|
||||
|
||||
if (useVirtualObject)
|
||||
{
|
||||
mode = "VirtualObject",
|
||||
displayName = "虚拟物体"
|
||||
};
|
||||
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))
|
||||
{
|
||||
throw new InvalidOperationException("选择真实物体失败:动画视图模型未保留选中的对象");
|
||||
}
|
||||
|
||||
animatedObjectPayload = new
|
||||
{
|
||||
mode = "RealObject",
|
||||
displayName = realObject.DisplayName
|
||||
};
|
||||
}
|
||||
|
||||
if (!animationViewModel.CanGenerateAnimation || !animationViewModel.GenerateAnimationCommand.CanExecute(null))
|
||||
{
|
||||
throw new InvalidOperationException($"{route.PathType} 虚拟物体碰撞测试准备失败:当前条件下不能生成动画");
|
||||
throw new InvalidOperationException($"{route.PathType} 碰撞测试准备失败:当前条件下不能生成动画");
|
||||
}
|
||||
|
||||
animationViewModel.GenerateAnimationCommand.Execute(null);
|
||||
|
||||
LogManager.Info(
|
||||
$"[测试HTTP] 已开始准备虚拟物体碰撞测试: 路径={route.Name}, 类型={route.PathType}");
|
||||
$"[测试HTTP] 已开始准备{(useVirtualObject ? "虚拟物体" : "真实物体")}碰撞测试: 路径={route.Name}, 类型={route.PathType}");
|
||||
|
||||
return new PreparedVirtualCollisionTest
|
||||
{
|
||||
@ -2099,6 +2131,24 @@ namespace NavisworksTransport.Core.Services
|
||||
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))
|
||||
@ -2195,7 +2245,9 @@ namespace NavisworksTransport.Core.Services
|
||||
}
|
||||
|
||||
string animatedObjectName = GetOptionalQueryValue(query, "animatedObjectName");
|
||||
if (string.IsNullOrWhiteSpace(animatedObjectName))
|
||||
string animatedObjectPath = GetOptionalQueryValue(query, "animatedObjectPath");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(animatedObjectName) && string.IsNullOrWhiteSpace(animatedObjectPath))
|
||||
{
|
||||
return ResolveSingleSelectedItem(activeDocument);
|
||||
}
|
||||
@ -2210,7 +2262,29 @@ namespace NavisworksTransport.Core.Services
|
||||
|
||||
foreach (ModelItem item in model.RootItem.DescendantsAndSelf)
|
||||
{
|
||||
if (item != null && string.Equals(item.DisplayName, animatedObjectName, StringComparison.OrdinalIgnoreCase))
|
||||
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);
|
||||
}
|
||||
@ -2219,12 +2293,14 @@ namespace NavisworksTransport.Core.Services
|
||||
|
||||
if (matches.Count == 0)
|
||||
{
|
||||
throw new InvalidOperationException($"找不到指定真实物体: {animatedObjectName}");
|
||||
string target = string.IsNullOrWhiteSpace(animatedObjectPath) ? animatedObjectName : animatedObjectPath;
|
||||
throw new InvalidOperationException($"找不到指定真实物体: {target}");
|
||||
}
|
||||
|
||||
if (matches.Count > 1)
|
||||
{
|
||||
throw new InvalidOperationException($"找到多个同名真实物体,请改用更唯一的名字: {animatedObjectName}");
|
||||
string target = string.IsNullOrWhiteSpace(animatedObjectPath) ? animatedObjectName : animatedObjectPath;
|
||||
throw new InvalidOperationException($"找到多个匹配真实物体,请改用更唯一的名字或 PathId: {target}");
|
||||
}
|
||||
|
||||
return matches[0];
|
||||
|
||||
Loading…
Reference in New Issue
Block a user