refactor: ModelItemTreeWalker 通用遍历工具(统一 IsHidden 剪枝 + 聚合盒剪枝 + 后序传播)
新增 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 一致
This commit is contained in:
parent
da81672782
commit
818a5e0ad7
@ -339,6 +339,7 @@
|
||||
<Compile Include="src\Utils\CoordinateConverter.cs" />
|
||||
<Compile Include="src\Utils\FloorDetector.cs" />
|
||||
<Compile Include="src\Utils\ModelItemAnalysisHelper.cs" />
|
||||
<Compile Include="src\Utils\ModelItemTreeWalker.cs" />
|
||||
<Compile Include="src\Utils\GeometryHelper.cs" />
|
||||
<Compile Include="src\Utils\GeometryCacheManager.cs" />
|
||||
<Compile Include="src\Utils\DialogHelper.cs" />
|
||||
|
||||
@ -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
|
||||
|
||||
@ -279,12 +279,24 @@ namespace NavisworksTransport.Core
|
||||
var visibleSet = new HashSet<ModelItem>(); // 收集需要保留的节点(对象+祖先)
|
||||
|
||||
// 第一遍:遍历整个模型树,收集盒内对象和可见节点
|
||||
// (统一走 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,32 +317,34 @@ namespace NavisworksTransport.Core
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 递归遍历收集盒内对象和可见节点。
|
||||
/// 返回:本子树是否存在盒内对象(后序传播用——命中路径的祖先由父级逐层加入 visibleSet)。
|
||||
/// 模型树遍历访问回调(ModelItemTreeWalker 调用):
|
||||
/// - 组节点:聚合盒剪枝(prune 时)→ Prune 或 Continue
|
||||
/// - 几何节点:盒相交 + 有体积 → Hit(加入盒内对象与可见集合),否则 Prune(几何节点不深入)
|
||||
/// </summary>
|
||||
private bool TraverseAndCollect(ModelItem item, BoundingBox3D sectionBox,
|
||||
List<ModelItem> objectsInBox, HashSet<ModelItem> visibleSet, bool prune)
|
||||
private static ModelItemWalkAction TraverseVisit(
|
||||
ModelItem item,
|
||||
BoundingBox3D sectionBox,
|
||||
List<ModelItem> objectsInBox,
|
||||
HashSet<ModelItem> 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;
|
||||
}
|
||||
|
||||
// 如果有几何体,检查是否在盒内
|
||||
if (hasGeometry)
|
||||
{
|
||||
return ModelItemWalkAction.Continue;
|
||||
}
|
||||
|
||||
// 几何体节点:检查是否在盒内
|
||||
var bbox = item.BoundingBox();
|
||||
|
||||
// 检查是否有体积(三个方向尺寸都大于0)
|
||||
@ -339,39 +353,28 @@ namespace NavisworksTransport.Core
|
||||
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 ModelItemWalkAction.Hit;
|
||||
}
|
||||
|
||||
// 找到几何体,停止向下遍历(原逻辑)
|
||||
return false;
|
||||
}
|
||||
|
||||
// 没有几何体,继续遍历子节点;后序传播:任一子命中 → 自己是祖先,加入 visibleSet
|
||||
bool anyChildHit = false;
|
||||
foreach (var child in item.Children)
|
||||
{
|
||||
if (TraverseAndCollect(child, sectionBox, objectsInBox, visibleSet, prune))
|
||||
{
|
||||
anyChildHit = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (anyChildHit)
|
||||
{
|
||||
visibleSet.Add(item);
|
||||
}
|
||||
|
||||
return anyChildHit;
|
||||
// 找到几何体但不在盒内,停止向下遍历(原逻辑)
|
||||
return ModelItemWalkAction.Prune;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 【使用原来的兄弟节点算法】计算需要隐藏的节点
|
||||
/// 获取剖面盒内的所有对象(公开方法供外部调用)
|
||||
/// 【已优化】内部使用 GetObjectsAndHiddenItems 实现一次遍历
|
||||
/// </summary>
|
||||
public List<ModelItem> GetObjectsInSectionBox(Document document, BoundingBox3D sectionBox)
|
||||
{
|
||||
var result = GetObjectsAndHiddenItems(document, sectionBox);
|
||||
return result.ObjectsInBox;
|
||||
}
|
||||
|
||||
private void CalculateHiddenItems(HashSet<ModelItem> visibleSet, ModelItemCollection itemsToHide)
|
||||
{
|
||||
// 1. 收集唯一的父节点
|
||||
@ -404,20 +407,7 @@ namespace NavisworksTransport.Core
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取剖面盒内的所有对象(公开方法供外部调用)
|
||||
/// 【已优化】内部使用 GetObjectsAndHiddenItems 实现一次遍历
|
||||
/// </summary>
|
||||
public List<ModelItem> GetObjectsInSectionBox(Document document, BoundingBox3D sectionBox)
|
||||
{
|
||||
var result = GetObjectsAndHiddenItems(document, sectionBox);
|
||||
return result.ObjectsInBox;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查两个包围盒是否相交
|
||||
/// </summary>
|
||||
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) &&
|
||||
|
||||
@ -382,17 +382,8 @@ namespace NavisworksTransport.Utils
|
||||
var totalModelItems = Application.ActiveDocument.Models.RootItemDescendantsAndSelf.Count();
|
||||
LogManager.Info($"[几何体获取] 模型总数统计: {totalModelItems} 个模型项");
|
||||
|
||||
// 使用 Stack 模拟递归避免栈溢出
|
||||
// Stack 初始容量设为 1000 以减少扩容
|
||||
var stack = new Stack<ModelItem>(1000);
|
||||
var visibleItems = new List<ModelItem>();
|
||||
|
||||
// 将所有模型的根节点压入栈
|
||||
foreach (var model in Application.ActiveDocument.Models)
|
||||
{
|
||||
stack.Push(model.RootItem);
|
||||
}
|
||||
|
||||
// 统计信息
|
||||
int hiddenSkipped = 0; // 跳过的隐藏节点(包括其子节点)
|
||||
int visibleCount = 0; // 可见节点总数
|
||||
@ -404,17 +395,11 @@ namespace NavisworksTransport.Utils
|
||||
int mixedCount = 0; // 混合类型几何体
|
||||
int noGeometryCount = 0; // 无几何体节点
|
||||
|
||||
while (stack.Count > 0)
|
||||
// 统一走 ModelItemTreeWalker:IsHidden 剪枝(父隐藏 → 整棵子树跳过)内置
|
||||
ModelItemTreeWalker.WalkModels(
|
||||
Application.ActiveDocument.Models,
|
||||
item =>
|
||||
{
|
||||
var item = stack.Pop();
|
||||
|
||||
// 关键剪枝:如果节点显式隐藏,则跳过其整个分支(它的所有子节点都不可见)
|
||||
if (item.IsHidden)
|
||||
{
|
||||
hiddenSkipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
visibleCount++;
|
||||
|
||||
// 🔥 优化:只提取复合对象(IsComposite = true)或独立实体节点(HasGeometry)
|
||||
@ -423,9 +408,11 @@ namespace NavisworksTransport.Utils
|
||||
bool isComposite = item.IsComposite;
|
||||
bool hasGeometry = item.HasGeometry;
|
||||
|
||||
if (isComposite || hasGeometry)
|
||||
if (!isComposite && !hasGeometry)
|
||||
{
|
||||
bool addedCurrentItem = false;
|
||||
// 空节点:继续遍历子节点
|
||||
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;
|
||||
|
||||
|
||||
120
src/Utils/ModelItemTreeWalker.cs
Normal file
120
src/Utils/ModelItemTreeWalker.cs
Normal file
@ -0,0 +1,120 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Autodesk.Navisworks.Api;
|
||||
|
||||
namespace NavisworksTransport.Utils
|
||||
{
|
||||
/// <summary>
|
||||
/// 模型树遍历访问结果(回调决定本节点的深入/剪枝/命中行为)。
|
||||
/// </summary>
|
||||
public enum ModelItemWalkAction
|
||||
{
|
||||
/// <summary>剪掉整棵子树(不再访问任何后代)</summary>
|
||||
Prune,
|
||||
|
||||
/// <summary>继续访问后代节点</summary>
|
||||
Continue,
|
||||
|
||||
/// <summary>命中:停止深入本节点子树,并触发父级后序传播(postHitVisit)</summary>
|
||||
Hit
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 模型树深度遍历通用工具。
|
||||
///
|
||||
/// 内置语义:
|
||||
/// - IsHidden 剪枝:节点隐藏 → 整棵子树跳过(Navisworks 层级可见性规则:
|
||||
/// 父隐藏则所有子节点都不可见,无需深入)
|
||||
/// - 回调返回 Hit 时触发父级后序传播(postHitVisit)——用于收集
|
||||
/// "命中对象的所有祖先"(共享祖先只被上传一次,避免 K×D 次枚举)
|
||||
///
|
||||
/// 用途:剖面盒导出(SectionBoxExporter)、可见几何体收集
|
||||
/// (ModelItemAnalysisHelper)等模型树遍历统一复用。
|
||||
/// </summary>
|
||||
public static class ModelItemTreeWalker
|
||||
{
|
||||
/// <summary>
|
||||
/// 深度遍历模型树。
|
||||
/// </summary>
|
||||
/// <param name="item">当前节点(通常从 model.RootItem 开始)</param>
|
||||
/// <param name="visit">访问回调(决定 Prune/Continue/Hit)</param>
|
||||
/// <param name="postHitVisit">后序传播:子树有 Hit 时对祖先调用(用于收集祖先集合)</param>
|
||||
/// <param name="onHidden">隐藏节点通知(仅统计用,隐藏节点不会触发 visit)</param>
|
||||
/// <returns>本子树是否存在 Hit</returns>
|
||||
public static bool WalkVisible(
|
||||
ModelItem item,
|
||||
Func<ModelItem, ModelItemWalkAction> visit,
|
||||
Action<ModelItem> postHitVisit = null,
|
||||
Action<ModelItem> 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 遍历多个模型的根(Application.ActiveDocument.Models 场景)。
|
||||
/// 返回任意模型根命中时根节点本身也触发 postHitVisit(根是命中对象祖先之一)。
|
||||
/// </summary>
|
||||
public static bool WalkModels(
|
||||
IEnumerable<Model> models,
|
||||
Func<ModelItem, ModelItemWalkAction> visit,
|
||||
Action<ModelItem> postHitVisit = null,
|
||||
Action<ModelItem> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user