diff --git a/NavisworksTransport.UnitTests.csproj b/NavisworksTransport.UnitTests.csproj index ab35c52..7068f09 100644 --- a/NavisworksTransport.UnitTests.csproj +++ b/NavisworksTransport.UnitTests.csproj @@ -84,6 +84,7 @@ + diff --git a/UnitTests/Integration/NavisworksTestAutomationClient.cs b/UnitTests/Integration/NavisworksTestAutomationClient.cs index fe813c2..ef1392a 100644 --- a/UnitTests/Integration/NavisworksTestAutomationClient.cs +++ b/UnitTests/Integration/NavisworksTestAutomationClient.cs @@ -293,6 +293,23 @@ namespace NavisworksTransport.UnitTests.Integration return await PostJsonAsync(requestUri, 60).ConfigureAwait(false); } + /// + /// 剖面盒导出(指定包围盒模式),返回导出文件信息。 + /// + public async Task ExportSectionBoxAsync( + string filePath, + double x1, double y1, double z1, + double x2, double y2, double z2) + { + string requestUri = string.Format( + CultureInfo.InvariantCulture, + "/api/test/export-section-box?mode=bbox&x1={0}&y1={1}&z1={2}&x2={3}&y2={4}&z2={5}&path={6}", + x1, y1, z1, x2, y2, z2, + Uri.EscapeDataString(filePath)); + + return await PostJsonAsync(requestUri, 120).ConfigureAwait(false); + } + private async Task GetJsonAsync(string requestUri, int timeoutSeconds = 30) { using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(timeoutSeconds))) diff --git a/UnitTests/Integration/SectionBoxExportAutomationTests.cs b/UnitTests/Integration/SectionBoxExportAutomationTests.cs new file mode 100644 index 0000000..3401c8c --- /dev/null +++ b/UnitTests/Integration/SectionBoxExportAutomationTests.cs @@ -0,0 +1,83 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Newtonsoft.Json.Linq; + +namespace NavisworksTransport.UnitTests.Integration +{ + /// + /// 剖面盒导出集成测试(覆盖计划 T16)。 + /// + /// 验证 SectionBoxExporter 的指定包围盒导出链路: + /// - 导出 NWD 文件成功(文件存在且非空) + /// - 包围盒内对象数 > 0、文件大小 > 0 + /// - 隐藏节点计算完成(hiddenNodeCount >= 0) + /// + /// 使用指定包围盒模式(bbox),不依赖 UI 剖面盒状态;包围盒覆盖模型 Hoisting/Rail 区域。 + /// + [TestClass] + [TestCategory("NavisworksIntegration")] + public class SectionBoxExportAutomationTests + { + // 模型单位:覆盖 Hoisting/Rail 区域的包围盒(已验证含 57 个对象) + private const double MinX = -240, MinY = 10, MinZ = -20; + private const double MaxX = -170, MaxY = 30, MaxZ = 25; + + [TestMethod] + [Timeout(240000)] + public async Task SectionBoxBBoxExport_ProducesValidNwd() + { + using (var client = new NavisworksTestAutomationClient()) + { + await client.EnsureServiceReadyAsync(TimeSpan.FromSeconds(90)).ConfigureAwait(false); + + string tempDir = Path.Combine(Path.GetTempPath(), "NavisworksTransportIntegrationTests", Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(tempDir); + try + { + string exportPath = Path.Combine(tempDir, "sectionbox-export.nwd"); + + JObject response = await client.ExportSectionBoxAsync( + exportPath, + MinX, MinY, MinZ, MaxX, MaxY, MaxZ).ConfigureAwait(false); + Assert.IsTrue(response.Value("ok"), "剖面盒导出失败: " + (string)response["error"]); + + JObject data = (JObject)response["data"]; + + // 文件已生成且非空 + Assert.IsFalse(string.IsNullOrWhiteSpace((string)data["filePath"]), "导出文件路径为空"); + Assert.IsTrue(File.Exists(exportPath), "导出文件不存在: " + exportPath); + Assert.IsTrue((long)data["fileSize"] > 0, "导出文件大小应大于 0"); + Assert.IsTrue(new FileInfo(exportPath).Length > 0, "导出文件实际为空"); + + // 包围盒内对象与隐藏节点 + Assert.IsTrue((int)data["objectCount"] > 0, "包围盒内应找到对象"); + Assert.IsTrue((int)data["hiddenNodeCount"] >= 0, "隐藏节点数不能为负"); + + Assert.AreEqual("bbox", (string)data["mode"], "导出模式应为 bbox"); + } + finally + { + SafeDelete(tempDir); + } + } + } + + private static void SafeDelete(string directory) + { + if (!Directory.Exists(directory)) + { + return; + } + + try + { + Directory.Delete(directory, true); + } + catch + { + } + } + } +} diff --git a/doc/working/2026-08-02-integration-test-coverage-plan.md b/doc/working/2026-08-02-integration-test-coverage-plan.md index f5b2231..f7ccfc5 100644 --- a/doc/working/2026-08-02-integration-test-coverage-plan.md +++ b/doc/working/2026-08-02-integration-test-coverage-plan.md @@ -54,6 +54,7 @@ - [x] **T13 动画播放控制**(2026-08-04,`AnimationPlaybackControlAutomationTests`)— prepare→start→pause 验证→resume→finished - [x] **T14 批量任务队列**(2026-08-04,`BatchQueueAutomationTests`)— 队列添加/计数递增(轻量版,不执行批量动画) - [x] **T15 路径分析**(2026-08-04,`PathAnalysisAutomationTests`)— 评分结构验证;顺带修复产品 bug(CollisionResults 表已废弃导致路径分析 no such table) +- [x] **T16 剖面盒导出**(2026-08-04,`SectionBoxExportAutomationTests`)— 指定包围盒模式导出 NWD(对象数/文件大小/隐藏节点),不依赖 UI 剖面盒状态 ## 4. 测试服务支撑能力 @@ -74,6 +75,7 @@ | animation-playback-control | ✅ 已实现 | 播放控制序列(T13) | | batch-queue-add | ✅ 已实现 | 批量队列添加(T14) | | analyze-path | ✅ 已实现 | 路径分析评分(T15) | +| export-section-box | ✅ 已实现 | 剖面盒导出 NWD(bbox/activeView 模式,T16) | | 其余待扩展 | — | 批量执行、TimeLiner 集成等后续迭代补充 | ## 5. 完成记录 @@ -88,3 +90,4 @@ | 2026-08-04 | `44214ba` | T05/T06 + 基础修复(Free 路径、报告断言、动态解析、串行锁、脚本、加速 15fps/5s) | | 2026-08-04 | 待提交 | T07-T10(障碍绕行、多策略、网格诊断、持久化)+ GetDetectionRecordById + detection-record 端点;P0+P1 完成,18/18 | | 2026-08-04 | 待提交 | T11-T15(路径编辑/导出/播放控制/批量队列/路径分析)+ 服务端 5 端点 + 产品 bug 修复(PathDatabase CollisionResults→CollisionReports、RemovePoint 链路);P0+P1+P2 全部完成,集成测试 24/24,完整集 74.6s | +| 2026-08-04 | 待提交 | T16 剖面盒导出(SectionBoxExportAutomationTests + export-section-box 端点,bbox 模式),25/25 | diff --git a/src/Core/Services/TestAutomationHttpService.cs b/src/Core/Services/TestAutomationHttpService.cs index 6bed637..1571215 100644 --- a/src/Core/Services/TestAutomationHttpService.cs +++ b/src/Core/Services/TestAutomationHttpService.cs @@ -298,6 +298,14 @@ namespace NavisworksTransport.Core.Services 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)) { @@ -697,6 +705,81 @@ namespace NavisworksTransport.Core.Services /// /// 路径分析(效率/安全评分),集成测试验证分析链路与评分结构。 /// + /// + /// 剖面盒导出(T16):mode=bbox 用指定包围盒导出 NWD(集成测试可控,不依赖 UI 剖面盒状态); + /// mode=activeView 用当前视图剖面盒(需 UI 已激活 Box 模式)。 + /// + private static object ExportSectionBoxPayload(Dictionary 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))); + + var traversal = exporter.GetObjectsAndHiddenItems(document, bounds); + if (traversal.ObjectsInBox.Count == 0) + { + throw new InvalidOperationException("指定包围盒内没有找到对象"); + } + + 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 + }; + } + + // 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 + }; + } + /// /// 路径编辑集成测试端点(T11):action=remove-point/update-point/orthogonalize/remove-loops。 /// 直接操作临时测试路径,验证 PathPlanningManager 编辑链路。