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;
+ }
+ }
+}