From 818a5e0ad7fff8429c38f63e70423ad2455da4ec Mon Sep 17 00:00:00 2001 From: tian <11429339@qq.com> Date: Thu, 6 Aug 2026 10:53:18 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20ModelItemTreeWalker=20=E9=80=9A?= =?UTF-8?q?=E7=94=A8=E9=81=8D=E5=8E=86=E5=B7=A5=E5=85=B7=EF=BC=88=E7=BB=9F?= =?UTF-8?q?=E4=B8=80=20IsHidden=20=E5=89=AA=E6=9E=9D=20+=20=E8=81=9A?= =?UTF-8?q?=E5=90=88=E7=9B=92=E5=89=AA=E6=9E=9D=20+=20=E5=90=8E=E5=BA=8F?= =?UTF-8?q?=E4=BC=A0=E6=92=AD=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 src/Utils/ModelItemTreeWalker.cs: - WalkVisible/WalkModels:深度遍历,IsHidden 剪枝内置(父隐藏 → 整棵子树跳过) - 回调返回 ModelItemWalkAction(Prune/Continue/Hit)——剪枝条件与深入策略由调用方决定 - Hit 触发父级后序传播(postHitVisit)——共享祖先只上传一次(消除 K×D 枚举) - onHidden 回调(隐藏节点统计) 迁移: - SectionBoxExporter.TraverseAndCollect → walker(聚合盒剪枝 + 后序传播语义不变) - ModelItemAnalysisHelper.GetAllVisibleGeometryItems → walker(栈式循环替换,统计保留) 修复: - run-unit-tests.bat 过滤器排除 NavisworksIntegrationF1(F1 测试此前会被单元测试集选中, 无主 NW 环境时失败) 验证:单元 244/244、集成 25/25(SectionBox 61ms)、Architecture 111ms(无回退)、对象 228 一致 --- TransportPlugin.csproj | 1 + run-unit-tests.bat | 2 +- src/Core/SectionBoxExporter.cs | 112 ++++++++++++------------- src/Utils/ModelItemAnalysisHelper.cs | 74 ++++++----------- src/Utils/ModelItemTreeWalker.cs | 120 +++++++++++++++++++++++++++ 5 files changed, 197 insertions(+), 112 deletions(-) create mode 100644 src/Utils/ModelItemTreeWalker.cs diff --git a/TransportPlugin.csproj b/TransportPlugin.csproj index 57fa132..a899d66 100644 --- a/TransportPlugin.csproj +++ b/TransportPlugin.csproj @@ -339,6 +339,7 @@ + diff --git a/run-unit-tests.bat b/run-unit-tests.bat index 10bf1d8..871ed18 100644 --- a/run-unit-tests.bat +++ b/run-unit-tests.bat @@ -48,7 +48,7 @@ if errorlevel 1 ( exit /b 1 ) -%VSTEST_PATH% "bin\x64\Release\NavisworksTransport.UnitTests.dll" /Platform:x64 /TestAdapterPath:%TEST_ADAPTER_PATH% /TestCaseFilter:"TestCategory!=NavisworksIntegration" +%VSTEST_PATH% "bin\x64\Release\NavisworksTransport.UnitTests.dll" /Platform:x64 /TestAdapterPath:%TEST_ADAPTER_PATH% /TestCaseFilter:"TestCategory!=NavisworksIntegration&TestCategory!=NavisworksIntegrationF1" if errorlevel 1 ( echo Unit tests failed! exit /b 1 diff --git a/src/Core/SectionBoxExporter.cs b/src/Core/SectionBoxExporter.cs index 5eefeb8..65866a5 100644 --- a/src/Core/SectionBoxExporter.cs +++ b/src/Core/SectionBoxExporter.cs @@ -279,12 +279,24 @@ namespace NavisworksTransport.Core var visibleSet = new HashSet(); // 收集需要保留的节点(对象+祖先) // 第一遍:遍历整个模型树,收集盒内对象和可见节点 + // (统一走 ModelItemTreeWalker:IsHidden 剪枝 + 组节点聚合盒剪枝 + 后序传播内置) foreach (var model in document.Models) { - var rootItem = model.RootItem; - if (rootItem != null) + if (model.RootItem == null) { - TraverseAndCollect(model.RootItem, sectionBox, result.ObjectsInBox, visibleSet, prune); + continue; + } + + bool rootHit = ModelItemTreeWalker.WalkVisible( + model.RootItem, + node => TraverseVisit(node, sectionBox, result.ObjectsInBox, visibleSet, prune), + postHitVisit: node => visibleSet.Add(node), + onHidden: null); + + // 根节点本身也是命中对象的祖先(原 AncestorsAndSelf 含根) + if (rootHit) + { + visibleSet.Add(model.RootItem); } } @@ -305,73 +317,64 @@ namespace NavisworksTransport.Core } /// - /// 递归遍历收集盒内对象和可见节点。 - /// 返回:本子树是否存在盒内对象(后序传播用——命中路径的祖先由父级逐层加入 visibleSet)。 + /// 模型树遍历访问回调(ModelItemTreeWalker 调用): + /// - 组节点:聚合盒剪枝(prune 时)→ Prune 或 Continue + /// - 几何节点:盒相交 + 有体积 → Hit(加入盒内对象与可见集合),否则 Prune(几何节点不深入) /// - private bool TraverseAndCollect(ModelItem item, BoundingBox3D sectionBox, - List objectsInBox, HashSet visibleSet, bool prune) + private static ModelItemWalkAction TraverseVisit( + ModelItem item, + BoundingBox3D sectionBox, + List objectsInBox, + HashSet visibleSet, + bool prune) { - if (item == null || item.IsHidden) return false; - bool hasGeometry = item.HasGeometry; // 子树剪枝:非几何节点(组节点)先查聚合包围盒(NW 内部缓存,O(1))。 // 组节点盒与剖面盒不相交 → 其整棵子树内不可能存在盒内对象 → 跳过全部后代。 // 收益:大型模型下远处楼层/区域一次调用剪掉整棵子树,省下所有后代的 // Children/IsHidden/HasGeometry API 调用。 - if (prune && !hasGeometry) + if (!hasGeometry) { - var groupBox = item.BoundingBox(); - if (!BoundingBoxesIntersect(groupBox, sectionBox)) + if (prune && !BoundingBoxesIntersect(item.BoundingBox(), sectionBox)) { - return false; + return ModelItemWalkAction.Prune; } + + return ModelItemWalkAction.Continue; } - // 如果有几何体,检查是否在盒内 - if (hasGeometry) - { - var bbox = item.BoundingBox(); - - // 检查是否有体积(三个方向尺寸都大于0) - double sizeX = bbox.Max.X - bbox.Min.X; - double sizeY = bbox.Max.Y - bbox.Min.Y; - double sizeZ = bbox.Max.Z - bbox.Min.Z; - bool hasVolume = sizeX > 0.0001 && sizeY > 0.0001 && sizeZ > 0.0001; - - // 有体积且与剖面盒相交 → 是目标对象(只加入自己,祖先由父级后序上传) - if (hasVolume && BoundingBoxesIntersect(bbox, sectionBox)) - { - objectsInBox.Add(item); - visibleSet.Add(item); - return true; - } - - // 找到几何体,停止向下遍历(原逻辑) - return false; - } + // 几何体节点:检查是否在盒内 + var bbox = item.BoundingBox(); - // 没有几何体,继续遍历子节点;后序传播:任一子命中 → 自己是祖先,加入 visibleSet - bool anyChildHit = false; - foreach (var child in item.Children) - { - if (TraverseAndCollect(child, sectionBox, objectsInBox, visibleSet, prune)) - { - anyChildHit = true; - } - } + // 检查是否有体积(三个方向尺寸都大于0) + double sizeX = bbox.Max.X - bbox.Min.X; + double sizeY = bbox.Max.Y - bbox.Min.Y; + double sizeZ = bbox.Max.Z - bbox.Min.Z; + bool hasVolume = sizeX > 0.0001 && sizeY > 0.0001 && sizeZ > 0.0001; - if (anyChildHit) + // 有体积且与剖面盒相交 → 是目标对象(只加入自己,祖先由后序传播上传) + if (hasVolume && BoundingBoxesIntersect(bbox, sectionBox)) { + objectsInBox.Add(item); visibleSet.Add(item); + return ModelItemWalkAction.Hit; } - return anyChildHit; + // 找到几何体但不在盒内,停止向下遍历(原逻辑) + return ModelItemWalkAction.Prune; } /// - /// 【使用原来的兄弟节点算法】计算需要隐藏的节点 + /// 获取剖面盒内的所有对象(公开方法供外部调用) + /// 【已优化】内部使用 GetObjectsAndHiddenItems 实现一次遍历 /// + public List GetObjectsInSectionBox(Document document, BoundingBox3D sectionBox) + { + var result = GetObjectsAndHiddenItems(document, sectionBox); + return result.ObjectsInBox; + } + private void CalculateHiddenItems(HashSet visibleSet, ModelItemCollection itemsToHide) { // 1. 收集唯一的父节点 @@ -404,20 +407,7 @@ namespace NavisworksTransport.Core } } - /// - /// 获取剖面盒内的所有对象(公开方法供外部调用) - /// 【已优化】内部使用 GetObjectsAndHiddenItems 实现一次遍历 - /// - public List GetObjectsInSectionBox(Document document, BoundingBox3D sectionBox) - { - var result = GetObjectsAndHiddenItems(document, sectionBox); - return result.ObjectsInBox; - } - - /// - /// 检查两个包围盒是否相交 - /// - private bool BoundingBoxesIntersect(BoundingBox3D a, BoundingBox3D b) + private static bool BoundingBoxesIntersect(BoundingBox3D a, BoundingBox3D b) { return (a.Min.X <= b.Max.X && a.Max.X >= b.Min.X) && (a.Min.Y <= b.Max.Y && a.Max.Y >= b.Min.Y) && diff --git a/src/Utils/ModelItemAnalysisHelper.cs b/src/Utils/ModelItemAnalysisHelper.cs index 0316da6..914e9d3 100644 --- a/src/Utils/ModelItemAnalysisHelper.cs +++ b/src/Utils/ModelItemAnalysisHelper.cs @@ -382,17 +382,8 @@ namespace NavisworksTransport.Utils var totalModelItems = Application.ActiveDocument.Models.RootItemDescendantsAndSelf.Count(); LogManager.Info($"[几何体获取] 模型总数统计: {totalModelItems} 个模型项"); - // 使用 Stack 模拟递归避免栈溢出 - // Stack 初始容量设为 1000 以减少扩容 - var stack = new Stack(1000); var visibleItems = new List(); - // 将所有模型的根节点压入栈 - foreach (var model in Application.ActiveDocument.Models) - { - stack.Push(model.RootItem); - } - // 统计信息 int hiddenSkipped = 0; // 跳过的隐藏节点(包括其子节点) int visibleCount = 0; // 可见节点总数 @@ -404,28 +395,24 @@ namespace NavisworksTransport.Utils int mixedCount = 0; // 混合类型几何体 int noGeometryCount = 0; // 无几何体节点 - while (stack.Count > 0) - { - var item = stack.Pop(); - - // 关键剪枝:如果节点显式隐藏,则跳过其整个分支(它的所有子节点都不可见) - if (item.IsHidden) + // 统一走 ModelItemTreeWalker:IsHidden 剪枝(父隐藏 → 整棵子树跳过)内置 + ModelItemTreeWalker.WalkModels( + Application.ActiveDocument.Models, + item => { - hiddenSkipped++; - continue; - } + visibleCount++; - visibleCount++; + // 🔥 优化:只提取复合对象(IsComposite = true)或独立实体节点(HasGeometry) + // 原因:复合对象(如窗户)包含多个实体节点(玻璃、框架、把手) + // 我们只需要检测复合对象,不需要检测其子节点 + bool isComposite = item.IsComposite; + bool hasGeometry = item.HasGeometry; - // 🔥 优化:只提取复合对象(IsComposite = true)或独立实体节点(HasGeometry) - // 原因:复合对象(如窗户)包含多个实体节点(玻璃、框架、把手) - // 我们只需要检测复合对象,不需要检测其子节点 - bool isComposite = item.IsComposite; - bool hasGeometry = item.HasGeometry; - - if (isComposite || hasGeometry) - { - bool addedCurrentItem = false; + if (!isComposite && !hasGeometry) + { + // 空节点:继续遍历子节点 + return ModelItemWalkAction.Continue; + } // 复合对象或独立实体节点:检查几何体类型 var geometry = item.Geometry; @@ -442,9 +429,11 @@ namespace NavisworksTransport.Utils else mixedCount++; visibleItems.Add(item); // 只保留包含三角形的几何体 - addedCurrentItem = true; + // 已添加 → 截断子树(本节点直接作为几何候选) + return ModelItemWalkAction.Prune; } - else if (primitiveTypes == PrimitiveTypes.Lines) + + if (primitiveTypes == PrimitiveTypes.Lines) { lineOnlyCount++; // 不添加:纯线条几何体过滤掉 @@ -468,7 +457,7 @@ namespace NavisworksTransport.Utils { mixedCount++; visibleItems.Add(item); // 其他混合类型保留 - addedCurrentItem = true; + return ModelItemWalkAction.Prune; } } else @@ -477,25 +466,10 @@ namespace NavisworksTransport.Utils // 不添加:无几何体数据 } - // 只有当前节点已经可直接作为几何候选时,才截断子树。 - // 如果当前节点本身没有可用三角几何,则继续向下遍历,避免把真实几何子节点整棵截断。 - if (!addedCurrentItem) - { - foreach (var child in item.Children) - { - stack.Push(child); - } - } - } - else - { - // 空节点:继续遍历子节点 - foreach (var child in item.Children) - { - stack.Push(child); - } - } - } + // 当前节点没有可用三角几何:继续向下遍历,避免把真实几何子节点整棵截断 + return ModelItemWalkAction.Continue; + }, + onHidden: item => hiddenSkipped++); var elapsed = (DateTime.Now - startTime).TotalMilliseconds; diff --git a/src/Utils/ModelItemTreeWalker.cs b/src/Utils/ModelItemTreeWalker.cs new file mode 100644 index 0000000..517da91 --- /dev/null +++ b/src/Utils/ModelItemTreeWalker.cs @@ -0,0 +1,120 @@ +using System; +using System.Collections.Generic; +using Autodesk.Navisworks.Api; + +namespace NavisworksTransport.Utils +{ + /// + /// 模型树遍历访问结果(回调决定本节点的深入/剪枝/命中行为)。 + /// + public enum ModelItemWalkAction + { + /// 剪掉整棵子树(不再访问任何后代) + Prune, + + /// 继续访问后代节点 + Continue, + + /// 命中:停止深入本节点子树,并触发父级后序传播(postHitVisit) + Hit + } + + /// + /// 模型树深度遍历通用工具。 + /// + /// 内置语义: + /// - IsHidden 剪枝:节点隐藏 → 整棵子树跳过(Navisworks 层级可见性规则: + /// 父隐藏则所有子节点都不可见,无需深入) + /// - 回调返回 Hit 时触发父级后序传播(postHitVisit)——用于收集 + /// "命中对象的所有祖先"(共享祖先只被上传一次,避免 K×D 次枚举) + /// + /// 用途:剖面盒导出(SectionBoxExporter)、可见几何体收集 + /// (ModelItemAnalysisHelper)等模型树遍历统一复用。 + /// + public static class ModelItemTreeWalker + { + /// + /// 深度遍历模型树。 + /// + /// 当前节点(通常从 model.RootItem 开始) + /// 访问回调(决定 Prune/Continue/Hit) + /// 后序传播:子树有 Hit 时对祖先调用(用于收集祖先集合) + /// 隐藏节点通知(仅统计用,隐藏节点不会触发 visit) + /// 本子树是否存在 Hit + public static bool WalkVisible( + ModelItem item, + Func visit, + Action postHitVisit = null, + Action onHidden = null) + { + if (item == null) + { + return false; + } + + // 关键剪枝:节点隐藏 → 整棵子树跳过(其所有子节点都不可见) + if (item.IsHidden) + { + onHidden?.Invoke(item); + return false; + } + + ModelItemWalkAction action = visit(item); + if (action == ModelItemWalkAction.Prune) + { + return false; + } + + if (action == ModelItemWalkAction.Hit) + { + return true; + } + + // Continue:深入子节点;任一子节点命中 → 本节点触发后序传播 + bool anyChildHit = false; + foreach (var child in item.Children) + { + if (WalkVisible(child, visit, postHitVisit, onHidden)) + { + anyChildHit = true; + } + } + + if (anyChildHit) + { + postHitVisit?.Invoke(item); + } + + return anyChildHit; + } + + /// + /// 遍历多个模型的根(Application.ActiveDocument.Models 场景)。 + /// 返回任意模型根命中时根节点本身也触发 postHitVisit(根是命中对象祖先之一)。 + /// + public static bool WalkModels( + IEnumerable models, + Func visit, + Action postHitVisit = null, + Action onHidden = null) + { + bool anyHit = false; + foreach (var model in models) + { + if (model?.RootItem == null) + { + continue; + } + + bool rootHit = WalkVisible(model.RootItem, visit, postHitVisit, onHidden); + if (rootHit) + { + postHitVisit?.Invoke(model.RootItem); + anyHit = true; + } + } + + return anyHit; + } + } +}