diff --git a/TransportPlugin.csproj b/TransportPlugin.csproj index fd5b58d..9fe4324 100644 --- a/TransportPlugin.csproj +++ b/TransportPlugin.csproj @@ -334,6 +334,7 @@ + diff --git a/src/Core/SectionBoxExporter.cs b/src/Core/SectionBoxExporter.cs index c825443..5cf71b8 100644 --- a/src/Core/SectionBoxExporter.cs +++ b/src/Core/SectionBoxExporter.cs @@ -6,6 +6,7 @@ using System.Windows.Forms; using Autodesk.Navisworks.Api; using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using NavisworksTransport.Utils; using NavisApplication = Autodesk.Navisworks.Api.Application; namespace NavisworksTransport.Core @@ -80,21 +81,21 @@ namespace NavisworksTransport.Core try { - // 4. 获取剖面盒内的对象 - var objectsInSectionBox = GetObjectsInSectionBox(document, sectionBoxBounds); + // 4. 【优化】一次遍历同时获取剖面盒内对象和需要隐藏的节点 + var traversalResult = GetObjectsAndHiddenItems(document, sectionBoxBounds); if (exportAsNwd) { - // 导出为NWD格式 - return ExportToNwd(document, objectsInSectionBox, filePath); + // 导出为NWD格式(使用预计算结果) + return ExportToNwd(document, traversalResult, filePath); } else { // 导出为JSON格式(原有逻辑) - var exportData = BuildExportData(sectionBoxBounds, objectsInSectionBox, document); + var exportData = BuildExportData(sectionBoxBounds, traversalResult.ObjectsInBox, document); string json = JsonConvert.SerializeObject(exportData, Formatting.Indented); File.WriteAllText(filePath, json); - LogManager.Info($"剖面盒导出完成: {objectsInSectionBox.Count} 个对象 -> {filePath}"); + LogManager.Info($"剖面盒导出完成: {traversalResult.ObjectsInBox.Count} 个对象 -> {filePath}"); return filePath; } } @@ -142,37 +143,56 @@ namespace NavisworksTransport.Core } /// - /// 获取剖面盒内的所有对象(公开方法供外部调用) + /// 剖面盒遍历结果:同时包含盒内对象和需要隐藏的节点 /// - public List GetObjectsInSectionBox(Document document, BoundingBox3D sectionBox) + public class SectionBoxTraversalResult { - var result = new List(); + /// + /// 剖面盒内的对象 + /// + public List ObjectsInBox { get; set; } = new List(); - // 手动遍历所有模型 + /// + /// 需要隐藏的节点(子树完全不在盒内的根节点) + /// + public ModelItemCollection ItemsToHide { get; set; } = new ModelItemCollection(); + } + + /// + /// 【优化】一次遍历同时获取剖面盒内对象和可见节点集合 + /// 然后使用原来的兄弟节点算法计算隐藏节点 + /// + public SectionBoxTraversalResult GetObjectsAndHiddenItems(Document document, BoundingBox3D sectionBox) + { + var result = new SectionBoxTraversalResult(); + var visibleSet = new HashSet(); // 收集需要保留的节点(对象+祖先) + + // 第一遍:遍历整个模型树,收集盒内对象和可见节点 foreach (var model in document.Models) { var rootItem = model.RootItem; if (rootItem != null) { - TraverseModelItem(rootItem, sectionBox, result); + TraverseAndCollect(model.RootItem, sectionBox, result.ObjectsInBox, visibleSet); } } + // 第二遍(局部):使用原来的兄弟节点算法计算隐藏节点 + // 只遍历 uniqueParents 的子节点,不是整个模型 + CalculateHiddenItems(visibleSet, result.ItemsToHide); + return result; } /// - /// 递归遍历模型项 - 剪枝策略:遇到几何体就停止向下遍历 + /// 递归遍历收集盒内对象和可见节点 /// - public void TraverseModelItem(ModelItem item, BoundingBox3D sectionBox, List result) + private void TraverseAndCollect(ModelItem item, BoundingBox3D sectionBox, + List objectsInBox, HashSet visibleSet) { - if (item == null) return; + if (item == null || item.IsHidden) return; - // 如果节点隐藏,跳过 - if (item.IsHidden) - return; - - // 如果有几何体信息,检查并添加,然后停止遍历该分支(剪枝) + // 如果有几何体,检查是否在盒内 if (item.HasGeometry) { var bbox = item.BoundingBox(); @@ -183,22 +203,74 @@ 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)) { - result.Add(item); + objectsInBox.Add(item); + + // 将该对象及其所有祖先标记为可见 + foreach (var ancestor in item.AncestorsAndSelf) + { + visibleSet.Add(ancestor); + } } - // 找到几何体,停止向下遍历(无论是否相交) + + // 找到几何体,停止向下遍历(原逻辑) return; } // 没有几何体,继续遍历子节点 foreach (var child in item.Children) { - TraverseModelItem(child, sectionBox, result); + TraverseAndCollect(child, sectionBox, objectsInBox, visibleSet); } } + /// + /// 【使用原来的兄弟节点算法】计算需要隐藏的节点 + /// + private void CalculateHiddenItems(HashSet visibleSet, ModelItemCollection itemsToHide) + { + // 1. 收集唯一的父节点 + var uniqueParents = new HashSet(); + foreach (var visibleItem in visibleSet) + { + if (visibleItem.Parent != null) + { + uniqueParents.Add(visibleItem.Parent); + } + } + + // 2. 收集这些父节点的所有子节点(兄弟节点) + var hiddenSet = new HashSet(); + foreach (var parent in uniqueParents) + { + foreach (var sibling in parent.Children) + { + hiddenSet.Add(sibling); + } + } + + // 3. 从隐藏集合中移除可见项 + hiddenSet.ExceptWith(visibleSet); + + // 4. 转换为 ModelItemCollection + foreach (var item in hiddenSet) + { + itemsToHide.Add(item); + } + } + + /// + /// 获取剖面盒内的所有对象(公开方法供外部调用) + /// 【已优化】内部使用 GetObjectsAndHiddenItems 实现一次遍历 + /// + public List GetObjectsInSectionBox(Document document, BoundingBox3D sectionBox) + { + var result = GetObjectsAndHiddenItems(document, sectionBox); + return result.ObjectsInBox; + } + /// /// 检查两个包围盒是否相交 /// @@ -338,36 +410,33 @@ namespace NavisworksTransport.Core } /// - /// 导出为NWD文件(公开方法供外部调用) + /// 导出为NWD文件(委托给 NwdExportHelper) + /// 适用于通用场景(如保存选择项) /// + /// Navisworks文档 + /// 要导出的对象列表 + /// 导出文件路径 + /// 导出文件路径,失败返回null public string ExportToNwd(Document document, List objects, string filePath) { - // 必须在主线程执行Navisworks API操作 - string result = null; - System.Windows.Application.Current.Dispatcher.Invoke(() => - { - // 准备要显示的项目 - var itemsToShow = new ModelItemCollection(); - foreach (var item in objects) - itemsToShow.Add(item); + return NwdExportHelper.ExportToNwd(objects, filePath); + } - // 在隔离模式下执行导出,自动保存和恢复可见性 - result = VisibilityHelper.ExecuteInIsolation(itemsToShow, () => - { - // 创建导出选项 - var exportOptions = new NwdExportOptions - { - ExcludeHiddenItems = true - }; - - // 执行导出 - document.ExportToNwd(filePath, exportOptions); - LogManager.Info($"[SectionBoxExporter] NWD导出完成: {objects.Count} 个对象 -> {filePath}"); - return filePath; - }); - }); - - return result; + /// + /// 【优化】导出为NWD文件,使用预计算的遍历结果 + /// 适用于剖面盒导出场景,避免重复遍历模型树 + /// + /// Navisworks文档 + /// 预计算的遍历结果(包含对象和隐藏节点) + /// 导出文件路径 + /// 导出文件路径,失败返回null + public string ExportToNwd(Document document, SectionBoxTraversalResult traversalResult, string filePath) + { + return NwdExportHelper.ExportToNwdWithPrecomputedHiddenItems( + traversalResult.ObjectsInBox, + traversalResult.ItemsToHide, + filePath, + "剖面盒导出"); } /// diff --git a/src/UI/WPF/ViewModels/LayerManagementViewModel.cs b/src/UI/WPF/ViewModels/LayerManagementViewModel.cs index 121325a..6762718 100644 --- a/src/UI/WPF/ViewModels/LayerManagementViewModel.cs +++ b/src/UI/WPF/ViewModels/LayerManagementViewModel.cs @@ -1866,12 +1866,10 @@ namespace NavisworksTransport.UI.WPF.ViewModels bool exportResult = false; string errorMessage = ""; - // 使用 SectionBoxExporter 进行导出 - var exporter = new SectionBoxExporter(); - if (exportAsJson) { - // JSON导出 + // JSON导出(使用 SectionBoxExporter) + var exporter = new SectionBoxExporter(); await Task.Run(() => { var result = exporter.ExportToJson(document, items, saveFilePath); @@ -1880,10 +1878,10 @@ namespace NavisworksTransport.UI.WPF.ViewModels } else { - // NWD导出 + // NWD导出(使用 NwdExportHelper) await Task.Run(() => { - var result = exporter.ExportToNwd(document, items, saveFilePath); + var result = NwdExportHelper.ExportToNwd(items, saveFilePath); exportResult = !string.IsNullOrEmpty(result); }); } @@ -1985,29 +1983,30 @@ namespace NavisworksTransport.UI.WPF.ViewModels new Point3D(box.Max.X, box.Max.Y, box.Max.Z) ); - // 异步获取剖面盒内的对象 + // 【优化】异步获取剖面盒内的对象和需要隐藏的节点(一次遍历完成) CurrentOperationText = "正在查找剖面盒内的对象..."; - List objectsInSectionBox = null; + SectionBoxExporter.SectionBoxTraversalResult traversalResult = null; await Task.Run(() => { - objectsInSectionBox = GetObjectsInSectionBox(document, sectionBoxBounds); + var exporter = new SectionBoxExporter(); + traversalResult = exporter.GetObjectsAndHiddenItems(document, sectionBoxBounds); }); - if (objectsInSectionBox.Count == 0) + if (traversalResult.ObjectsInBox.Count == 0) { MessageBox.Show("剖面盒内没有找到对象!", "提示", MessageBoxButton.OK, MessageBoxImage.Information); return; } - LogManager.Info($"[LayerManagementViewModel] 剖面盒内找到 {objectsInSectionBox.Count} 个对象"); + LogManager.Info($"[LayerManagementViewModel] 剖面盒内找到 {traversalResult.ObjectsInBox.Count} 个对象,可隐藏 {traversalResult.ItemsToHide.Count} 个节点"); // 生成默认文件名 string timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss"); string defaultFileName = $"SectionBox_Export_{timestamp}"; - // 调用通用导出方法 - await ExportItemsToNwdAsync(objectsInSectionBox, "导出剖面盒", defaultFileName, false); + // 【优化】使用预计算结果的导出方法 + await ExportSectionBoxToNwdAsync(traversalResult, "导出剖面盒", defaultFileName); } catch (Exception ex) { @@ -2030,6 +2029,99 @@ namespace NavisworksTransport.UI.WPF.ViewModels return exporter.GetObjectsInSectionBox(document, sectionBox); } + /// + /// 【优化】导出剖面盒到NWD,使用预计算的遍历结果 + /// 避免重复遍历模型树 + /// + private async Task ExportSectionBoxToNwdAsync( + SectionBoxExporter.SectionBoxTraversalResult traversalResult, + string dialogTitle, + string defaultFileName) + { + if (traversalResult?.ObjectsInBox == null || traversalResult.ObjectsInBox.Count == 0) + { + MessageBox.Show("没有要导出的对象", "导出提示", MessageBoxButton.OK, MessageBoxImage.Warning); + return; + } + + try + { + LogManager.Info($"[LayerManagementViewModel] 开始导出 {traversalResult.ObjectsInBox.Count} 个对象,预计算隐藏节点 {traversalResult.ItemsToHide.Count} 个"); + IsProcessing = true; + CurrentOperationText = "准备导出..."; + + var document = Autodesk.Navisworks.Api.Application.ActiveDocument; + + // 获取保存路径 + string saveFilePath = null; + System.Windows.Application.Current.Dispatcher.Invoke(() => + { + var saveDialog = new Microsoft.Win32.SaveFileDialog + { + Title = dialogTitle, + Filter = "Navisworks文件 (*.nwd)|*.nwd", + DefaultExt = "nwd", + FileName = defaultFileName + }; + + if (saveDialog.ShowDialog() == true) + { + saveFilePath = saveDialog.FileName; + } + }); + + if (string.IsNullOrEmpty(saveFilePath)) + { + return; + } + + bool exportResult = false; + string errorMessage = ""; + + // 【优化】使用预计算结果直接导出,无需再次计算隐藏节点 + await Task.Run(() => + { + try + { + var result = NwdExportHelper.ExportToNwdWithPrecomputedHiddenItems( + traversalResult.ObjectsInBox, + traversalResult.ItemsToHide, + saveFilePath, + "剖面盒导出"); + exportResult = !string.IsNullOrEmpty(result); + } + catch (Exception ex) + { + errorMessage = ex.Message; + LogManager.Error($"[LayerManagementViewModel] 导出失败: {ex.Message}"); + } + }); + + // 显示结果 + if (exportResult && File.Exists(saveFilePath)) + { + var fileInfo = new FileInfo(saveFilePath); + MessageBox.Show( + $"NWD导出成功!\n\n文件: {saveFilePath}\n大小: {fileInfo.Length / 1024} KB\n对象数: {traversalResult.ObjectsInBox.Count}", + "导出结果", MessageBoxButton.OK, MessageBoxImage.Information); + } + else + { + MessageBox.Show($"NWD导出失败: {errorMessage}", "导出错误", MessageBoxButton.OK, MessageBoxImage.Error); + } + } + catch (Exception ex) + { + LogManager.Error($"[LayerManagementViewModel] 导出过程异常: {ex.Message}", ex); + MessageBox.Show($"导出过程异常: {ex.Message}", "导出错误", MessageBoxButton.OK, MessageBoxImage.Error); + } + finally + { + IsProcessing = false; + CurrentOperationText = ""; + } + } + /// /// 测试ExportToNwd API - 专门的导出API /// diff --git a/src/Utils/NwdExportHelper.cs b/src/Utils/NwdExportHelper.cs new file mode 100644 index 0000000..b35f7b9 --- /dev/null +++ b/src/Utils/NwdExportHelper.cs @@ -0,0 +1,116 @@ +using System; +using System.Collections.Generic; +using System.Windows; +using Autodesk.Navisworks.Api; +using NavisApplication = Autodesk.Navisworks.Api.Application; + +namespace NavisworksTransport.Utils +{ + /// + /// NWD 文件导出工具类 + /// 提供通用的 NWD 导出功能,与具体业务场景解耦 + /// + public static class NwdExportHelper + { + /// + /// 导出指定对象到 NWD 文件 + /// 使用 ExecuteInIsolation 自动计算并隐藏无关节点 + /// + /// 要导出的对象列表 + /// 导出文件路径 + /// 是否排除隐藏项,默认 true + /// 导出文件路径,失败返回 null + public static string ExportToNwd(List objects, string filePath, bool excludeHiddenItems = true) + { + if (objects == null || objects.Count == 0) + throw new ArgumentException("对象列表不能为空", nameof(objects)); + if (string.IsNullOrEmpty(filePath)) + throw new ArgumentException("文件路径不能为空", nameof(filePath)); + + var document = NavisApplication.ActiveDocument; + if (document == null) + { + LogManager.Warning("[NwdExportHelper] 没有活动文档"); + return null; + } + + // 必须在主线程执行 + string result = null; + System.Windows.Application.Current.Dispatcher.Invoke(() => + { + // 准备要显示的项目 + var itemsToShow = new ModelItemCollection(); + foreach (var item in objects) + itemsToShow.Add(item); + + // 在隔离模式下执行导出 + result = VisibilityHelper.ExecuteInIsolation(itemsToShow, () => + { + var exportOptions = new NwdExportOptions + { + ExcludeHiddenItems = excludeHiddenItems + }; + + document.ExportToNwd(filePath, exportOptions); + LogManager.Info($"[NwdExportHelper] NWD导出完成: {objects.Count} 个对象 -> {filePath}"); + return filePath; + }); + }); + + return result; + } + + /// + /// 使用预计算的隐藏节点列表导出到 NWD 文件 + /// 适用于已经遍历并计算好隐藏节点的场景(如剖面盒导出) + /// + /// 要导出的对象列表 + /// 预计算的隐藏节点集合 + /// 导出文件路径 + /// 操作名称(用于日志) + /// 导出文件路径,失败返回 null + public static string ExportToNwdWithPrecomputedHiddenItems( + List objects, + ModelItemCollection itemsToHide, + string filePath, + string operationName = "NWD导出") + { + if (objects == null || objects.Count == 0) + throw new ArgumentException("对象列表不能为空", nameof(objects)); + if (string.IsNullOrEmpty(filePath)) + throw new ArgumentException("文件路径不能为空", nameof(filePath)); + + var document = NavisApplication.ActiveDocument; + if (document == null) + { + LogManager.Warning($"[NwdExportHelper] {operationName}失败:没有活动文档"); + return null; + } + + // 必须在主线程执行 + string result = null; + System.Windows.Application.Current.Dispatcher.Invoke(() => + { + // 使用预计算隐藏节点的方法 + result = VisibilityHelper.ExecuteWithPrecomputedHiddenItems( + itemsToHide, + () => + { + var exportOptions = new NwdExportOptions + { + ExcludeHiddenItems = true + }; + + document.ExportToNwd(filePath, exportOptions); + + LogManager.Info($"[NwdExportHelper] {operationName}完成: {objects.Count} 个对象,隐藏 {itemsToHide?.Count ?? 0} 个节点 -> {filePath}"); + return filePath; + }, + operationName + ); + }); + + return result; + } + } +} diff --git a/src/Utils/VisibilityHelper.cs b/src/Utils/VisibilityHelper.cs index 075e12e..441e4ae 100644 --- a/src/Utils/VisibilityHelper.cs +++ b/src/Utils/VisibilityHelper.cs @@ -244,6 +244,60 @@ namespace NavisworksTransport } } + /// + /// 【新增】应用预计算的隐藏节点列表,并保存当前状态用于后续恢复 + /// 用于优化场景:遍历和计算隐藏节点已经完成,只需直接应用 + /// + /// 预计算的隐藏节点集合 + /// 在应用隐藏后执行的操作 + /// 操作名称(用于日志) + /// 操作结果 + public static T ExecuteWithPrecomputedHiddenItems(ModelItemCollection itemsToHide, Func action, string operationName = "预计算隐藏") + { + var document = NavisApplication.ActiveDocument; + if (document == null) + { + LogManager.Warning($"[VisibilityHelper] {operationName}失败:没有活动文档"); + return default(T); + } + + // 保存当前状态 + var savedState = SaveVisibilityStateInternal(document); + + try + { + // 1. 全部显示 + document.Models.ResetAllHidden(); + + // 2. 直接应用预计算的隐藏节点 + if (itemsToHide != null && itemsToHide.Count > 0) + { + document.Models.SetHidden(itemsToHide, true); + LogManager.Debug($"[VisibilityHelper] {operationName}:应用 {itemsToHide.Count} 个预计算隐藏节点"); + } + + // 3. 执行操作 + return action.Invoke(); + } + finally + { + // 4. 恢复状态 + RestoreVisibilityStateInternal(document, savedState); + } + } + + /// + /// 【新增】应用预计算的隐藏节点列表(无返回值版本) + /// + public static void ExecuteWithPrecomputedHiddenItems(ModelItemCollection itemsToHide, Action action, string operationName = "预计算隐藏") + { + ExecuteWithPrecomputedHiddenItems(itemsToHide, () => + { + action?.Invoke(); + return null; + }, operationName); + } + /// /// 在隔离模式下执行操作,自动恢复可见性 ///