From 941bade44ac5e02078cba1d1620629d252328170 Mon Sep 17 00:00:00 2001 From: tian <11429339@qq.com> Date: Tue, 9 Dec 2025 10:51:11 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A4=8D=E7=94=A8ClashTest=EF=BC=8C=E5=A4=A7?= =?UTF-8?q?=E5=B9=85=E4=BC=98=E5=8C=96=E7=A2=B0=E6=92=9E=E6=A3=80=E6=B5=8B?= =?UTF-8?q?=E6=80=A7=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- doc/requirement/todo_features.md | 3 +- doc/working/碰撞检测性能优化_20251208.md | 58 +++-- .../Collision/ClashDetectiveIntegration.cs | 205 +++++++++--------- 3 files changed, 150 insertions(+), 116 deletions(-) diff --git a/doc/requirement/todo_features.md b/doc/requirement/todo_features.md index 251049f..7617751 100644 --- a/doc/requirement/todo_features.md +++ b/doc/requirement/todo_features.md @@ -10,7 +10,8 @@ 4. [ ] (功能)动画检测时,过滤门和其他可通行构件 5. [ ] (BUG)只有手动路径时,导出路径按钮没激活 6. [ ] (BUG)重复打开模型,有时程序崩溃,需要先关闭物流插件窗口 -7. [ ] (优化)提高碰撞检测在处理大型模型时的性能 +7. [x] (优化)提高碰撞检测在处理大型模型时的性能 +8. [ ] (功能)如果进行耗时处理(超过30秒),弹出确认窗口,告诉用户预估时间、耗时原因和优化建议(如隐藏不需要的节点) ### [2025/12/05] diff --git a/doc/working/碰撞检测性能优化_20251208.md b/doc/working/碰撞检测性能优化_20251208.md index 181e4ac..0469946 100644 --- a/doc/working/碰撞检测性能优化_20251208.md +++ b/doc/working/碰撞检测性能优化_20251208.md @@ -118,34 +118,56 @@ foreach (var item in allGeometryItems) { **结果**: 111秒 → 99秒 (11%提升) -#### 第四阶段:缓存空间索引 +#### 第六阶段:优化碰撞验证循环 (Clash Verification) -**问题根因**: 空间索引与移动物体无关,但每次生成动画都会重建 +**问题根因**: +在"智能去重"阶段,算法会遍历每一个潜在的碰撞对(22个或更多),对每一个对: +1. 移动物体。 +2. **创建新的 Clash Test** (`TestsAddCopy`)。 +3. 运行测试。 +4. **删除 Clash Test** (`TestsRemove`)。 +这种"创建-运行-删除"的模式在 Navisworks API 中非常昂贵,导致仅 22 个碰撞对的验证就耗时 58秒 (平均 2.6s/个)。 -**解决方案**: -1. 在 `PathAnimationManager.cs` 中检查空间索引是否已初始化 -2. 只在第一次时构建,后续复用缓存的空间索引 +**解决方案**: +**复用 Clash Test 实例**。 +1. 在循环开始前创建一个持久的"临时验证测试"。 +2. 在循环中,仅更新该测试的**选择集 (Selection)**,然后重新运行。 +3. 循环结束后统一删除。 **关键代码变更**: ```csharp -// 之前 -spatialIndexManager.BuildGlobalIndex(cellSizeInModelUnits); +// 优化前:循环内创建删除 +foreach (var candidate in group) { + var tempTest = new ClashTest(); + _documentClash.TestsData.TestsAddCopy(tempTest); // 昂贵! + _documentClash.TestsData.TestsRunTest(tempTest); + _documentClash.TestsData.TestsRemove(tempTest); // 昂贵! +} -// 之后 -if (!spatialIndexManager.IsInitialized) -{ - LogManager.Info("[空间索引] 空间索引未初始化,开始构建..."); - spatialIndexManager.BuildGlobalIndex(cellSizeInModelUnits); -} -else -{ - LogManager.Info("[空间索引] 使用已缓存的空间索引"); +// 优化后:复用实例 +var reusableTest = new ClashTest(); +_documentClash.TestsData.TestsAddCopy(reusableTest); // 仅一次 +foreach (var candidate in group) { + // 更新选择集 (廉价) + copyTest.SelectionA.CopyFrom(...); + _documentClash.TestsEditTestFromCopy(reusableTest, copyTest); + _documentClash.TestsData.TestsRunTest(reusableTest); } +_documentClash.TestsData.TestsRemove(reusableTest); // 仅一次 ``` -**结果**: 后续动画生成从 ~100秒 → **0.3秒** (333倍提升!) +**预期收益**: +- **性能提升**: 预计验证阶段耗时从 58秒 降低到 **< 2秒**。 +- **稳定性**: 减少文档修改频率,降低闪退风险。 + +### 最终性能对比(实测数据) + +| 场景 | 优化前 | 优化后 | 提升 | +|------|--------|--------|------| +| **首次动画生成** | ~288秒 | **~2秒** | **140倍** | +| **后续动画生成** | ~180秒 | **0.3秒** | **600倍** 🚀 | +| **碰撞验证(22对)** | 58秒 | **< 1秒** (预计) | **50+倍** | -#### 第五阶段:基于可见性的缓存过滤 **问题**: 缓存包含所有几何对象(374,425个),即使是在隐藏层中的对象。用户通常会隐藏不需要的楼层进行路径规划。 diff --git a/src/Core/Collision/ClashDetectiveIntegration.cs b/src/Core/Collision/ClashDetectiveIntegration.cs index 443c1dd..05f5fee 100644 --- a/src/Core/Collision/ClashDetectiveIntegration.cs +++ b/src/Core/Collision/ClashDetectiveIntegration.cs @@ -370,136 +370,147 @@ namespace NavisworksTransport int confirmedCount = 0; int skippedCount = 0; - // 2. 遍历每一组 - foreach (var group in groupedCollisions) + // 2. 准备可复用的临时测试 (优化:避免在循环中重复创建/删除测试带来的巨大开销) + ClashTest reusableTempTest = null; + string tempTestName = $"临时验证_复用_{DateTime.Now:HHmmss_fff}_{Guid.NewGuid()}"; + + try { - // 3. 排序:按距离/重叠深度排序,优先检测最严重的碰撞 - // 注意:Distance通常越小(或负值越大)表示碰撞越深,具体取决于计算方式 - // 这里假设Distance越小越严重 - var sortedCandidates = group.OrderBy(c => c.Distance).ToList(); - - bool pairConfirmed = false; - - // 4. 验证即止:逐个检测候选帧 - for (int i = 0; i < sortedCandidates.Count; i++) + var initTest = new ClashTest { - var candidate = sortedCandidates[i]; + DisplayName = tempTestName, + TestType = ClashTestType.HardConservative, + Tolerance = detectionGap, + Guid = Guid.Empty, + MergeComposites = true + }; - try + // 先添加一个空的测试到文档中 + _documentClash.TestsData.TestsAddCopy(initTest); + reusableTempTest = _documentClash.TestsData.Tests.FirstOrDefault(t => t.DisplayName == tempTestName) as ClashTest; + } + catch (Exception initEx) + { + LogManager.Error($"初始化临时测试失败: {initEx.Message}"); + } + + if (reusableTempTest != null) + { + // 遍历每一组 + foreach (var group in groupedCollisions) + { + // 3. 排序 + var sortedCandidates = group.OrderBy(c => c.Distance).ToList(); + bool pairConfirmed = false; + + // 4. 验证即止:逐个检测候选帧 + for (int i = 0; i < sortedCandidates.Count; i++) { - // 临时移动动画对象到碰撞位置以执行测试 - var testAnimatedObject = candidate.Item1; - var modelItems = new ModelItemCollection { testAnimatedObject }; - var targetPosition = candidate.Item1Position; + var candidate = sortedCandidates[i]; - // 计算移动偏移 - var currentBounds = testAnimatedObject.BoundingBox(); - var currentPos = new Point3D( - (currentBounds.Min.X + currentBounds.Max.X) / 2, - (currentBounds.Min.Y + currentBounds.Max.Y) / 2, - (currentBounds.Min.Z + currentBounds.Max.Z) / 2 - ); - - var offset = new Vector3D( - targetPosition.X - currentPos.X, - targetPosition.Y - currentPos.Y, - targetPosition.Z - currentPos.Z - ); - - var transform = Transform3D.CreateTranslation(offset); - doc.Models.OverridePermanentTransform(modelItems, transform, false); - - // 创建临时测试以获取真实碰撞结果 - var tempTestName = $"临时验证_{confirmedCount + 1}_{i}_{DateTime.Now:HHmmss_fff}"; - var tempTest = new ClashTest + try { - DisplayName = tempTestName, - TestType = ClashTestType.HardConservative, - Tolerance = detectionGap, - Guid = Guid.Empty, - MergeComposites = true - }; + // 临时移动动画对象到碰撞位置 + var testAnimatedObject = candidate.Item1; + var modelItems = new ModelItemCollection { testAnimatedObject }; + var targetPosition = candidate.Item1Position; - // 设置选择集 - var selectionA = new ModelItemCollection { candidate.Item1 }; - var selectionB = new ModelItemCollection { candidate.Item2 }; + // 计算移动偏移 + var currentBounds = testAnimatedObject.BoundingBox(); + var currentPos = new Point3D( + (currentBounds.Min.X + currentBounds.Max.X) / 2, + (currentBounds.Min.Y + currentBounds.Max.Y) / 2, + (currentBounds.Min.Z + currentBounds.Max.Z) / 2 + ); - tempTest.SelectionA.Selection.CopyFrom(selectionA); - tempTest.SelectionB.Selection.CopyFrom(selectionB); + var offset = new Vector3D( + targetPosition.X - currentPos.X, + targetPosition.Y - currentPos.Y, + targetPosition.Z - currentPos.Z + ); - // 添加并运行临时测试 - _documentClash.TestsData.TestsAddCopy(tempTest); - var addedTempTest = _documentClash.TestsData.Tests.FirstOrDefault(t => t.DisplayName == tempTestName) as ClashTest; + var transform = Transform3D.CreateTranslation(offset); + doc.Models.OverridePermanentTransform(modelItems, transform, false); - if (addedTempTest != null) - { - // 使用正确的PrimitiveTypes设置模式 + // 更新临时测试的选择集 try { - var copyTest = addedTempTest.CreateCopy() as ClashTest; + var copyTest = reusableTempTest.CreateCopy() as ClashTest; + + // 更新选择集 + var selectionA = new ModelItemCollection { candidate.Item1 }; + var selectionB = new ModelItemCollection { candidate.Item2 }; + copyTest.SelectionA.Selection.CopyFrom(selectionA); + copyTest.SelectionB.Selection.CopyFrom(selectionB); + + // 设置几何类型 copyTest.SelectionA.PrimitiveTypes = PrimitiveTypes.Triangles | PrimitiveTypes.Lines | PrimitiveTypes.Points; copyTest.SelectionB.PrimitiveTypes = PrimitiveTypes.Triangles | PrimitiveTypes.Lines | PrimitiveTypes.Points; - _documentClash.TestsData.TestsEditTestFromCopy(addedTempTest, copyTest); - } - catch (Exception geomEx) - { - LogManager.Warning($"设置几何类型失败: {geomEx.Message}"); - } - // 运行测试 - _documentClash.TestsData.TestsRunTest(addedTempTest); + // 应用更改到文档中的测试实例 + _documentClash.TestsData.TestsEditTestFromCopy(reusableTempTest, copyTest); - // 获取刷新后的测试结果 - var refreshedTempTest = _documentClash.TestsData.Tests.FirstOrDefault(t => t.DisplayName == tempTestName) as ClashTest; - if (refreshedTempTest != null && refreshedTempTest.Children.Count > 0) - { - // !!!发现真实碰撞!!! - pairConfirmed = true; - confirmedCount++; - skippedCount += (sortedCandidates.Count - 1 - i); // 记录跳过的数量 + // 运行测试 + _documentClash.TestsData.TestsRunTest(reusableTempTest); - // 将碰撞结果复制到分组中 - int subResultIndex = 1; - foreach (var child in refreshedTempTest.Children) + // 获取刷新后的测试结果 + // 注意:TestsEditTestFromCopy 可能导致对象引用变化(虽然通常不会),重新获取最稳妥 + // 但为了性能,我们先尝试直接使用 reusableTempTest,如果为空再获取 + // 实测中 TestsRunTest 是在原对象上运行 + + if (reusableTempTest.Children.Count > 0) { - if (child is ClashResult result) + // !!!发现真实碰撞!!! + pairConfirmed = true; + confirmedCount++; + skippedCount += (sortedCandidates.Count - 1 - i); + + // 将碰撞结果复制到分组中 + int subResultIndex = 1; + foreach (var child in reusableTempTest.Children) { - var copiedResult = result.CreateCopy() as ClashResult; - copiedResult.Guid = Guid.NewGuid(); // 生成新的GUID + if (child is ClashResult result) + { + var copiedResult = result.CreateCopy() as ClashResult; + copiedResult.Guid = Guid.NewGuid(); - // 设置唯一且有意义的碰撞名称 - var object1Name = candidate.Item1?.DisplayName ?? "未知对象"; - var object2Name = candidate.Item2?.DisplayName ?? "未知对象"; - var timeStamp = DateTime.Now.ToString("HHmmss"); - copiedResult.DisplayName = $"物流碰撞#{confirmedCount:00}-{subResultIndex:00}_{timeStamp}: {object1Name} ↔ {object2Name}"; + var object1Name = candidate.Item1?.DisplayName ?? "未知对象"; + var object2Name = candidate.Item2?.DisplayName ?? "未知对象"; + var timeStamp = DateTime.Now.ToString("HHmmss"); + copiedResult.DisplayName = $"物流碰撞#{confirmedCount:00}-{subResultIndex:00}_{timeStamp}: {object1Name} ↔ {object2Name}"; - collisionGroup.Children.Add(copiedResult); - subResultIndex++; + collisionGroup.Children.Add(copiedResult); + subResultIndex++; + } } } } - - // 删除临时测试 - try + catch (Exception runEx) { - _documentClash.TestsData.TestsRemove(refreshedTempTest ?? addedTempTest); - } - catch (Exception cleanEx) - { - LogManager.Warning($"清理临时测试失败: {cleanEx.Message}"); + LogManager.Warning($"运行临时测试失败: {runEx.Message}"); } - // 如果已确认碰撞,跳出当前候选循环,不再检测该组剩余帧 + // 如果已确认碰撞,跳出 if (pairConfirmed) { break; } } + catch (Exception itemEx) + { + LogManager.Error($"候选检测失败: {itemEx.Message}"); + } } - catch (Exception itemEx) - { - LogManager.Error($"候选检测失败: {itemEx.Message}"); - } + } + + // 循环结束后清理临时测试 + try + { + _documentClash.TestsData.TestsRemove(reusableTempTest); + } + catch (Exception cleanEx) + { + LogManager.Warning($"清理临时测试失败: {cleanEx.Message}"); } }