From 47af2443cdbe7853c2a0976659fe3207c7cefdf0 Mon Sep 17 00:00:00 2001 From: tian <11429339@qq.com> Date: Sat, 16 May 2026 14:59:56 +0800 Subject: [PATCH] =?UTF-8?q?=E6=95=B0=E6=8D=AE=E5=BA=93schema=E5=85=BC?= =?UTF-8?q?=E5=AE=B9+=E5=89=96=E9=9D=A2=E7=9B=92=E5=AF=BC=E5=87=BA?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D+=E4=BB=A3=E7=A0=81=E9=87=8D=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 数据库:SafeCreateIndex/EnsureColumnExists 防御旧schema;去掉无用PRAGMA user_version;去掉重复throw;清理注释编号 - 剖面盒导出:修复NWF多Model无统一根节点时空白子树泄露(RootItem级隐藏) - 崩溃修复:去掉LayerManagementViewModel中两处Task.Run,消除COM跨线程STA违规 - 重构:SectionBoxExporter新增ExportSectionBoxFromActiveView统一业务入口;LayerManagementViewModel删ExportSectionBoxToNwdAsync;删死代码ExportToJson(Document)/ShowSaveFileDialog - deploy-plugin.bat: powershell→pwsh;AGENTS.md补充编译部署调用说明 --- AGENTS.md | 16 ++ deploy-plugin.bat | 2 +- .../数据库兼容性_剖面盒导出修复报告.md | 86 +++++++++ src/Core/PathDatabase.cs | 142 +++++++++----- src/Core/PathPlanningManager.cs | 3 +- src/Core/SectionBoxExporter.cs | 161 +++++++--------- .../ViewModels/LayerManagementViewModel.cs | 175 +++--------------- 7 files changed, 306 insertions(+), 279 deletions(-) create mode 100644 doc/bugfixes/数据库兼容性_剖面盒导出修复报告.md diff --git a/AGENTS.md b/AGENTS.md index b8719a7..2a12f5b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -67,6 +67,22 @@ 不要并行执行编译和部署。否则很容易把旧 DLL 部署到插件目录。 +### 编译/部署的调用方式(AI 助手必读) + +在项目根目录直接运行 bat 即可,不要画蛇添足: + +```bash +./compile.bat # 编译 +./deploy-plugin.bat # 部署 +``` + +**严禁**的做法(都会失败或出错): +- 加 `cmd.exe /c` 前缀 +- 直接调 MSBuild.exe +- 用 `cd xxx && compile.bat` +- 把 PowerShell 逻辑拆出来单独跑 +- 改 deploy-plugin.bat 里的 `powershell` 为 `pwsh`(现有环境 `pwsh` 可运行,但默认脚本用系统 `powershell` 是常态,仅在 deploy 失败且提示找不到 powershell 时才临时改用 `pwsh`) + ### 并行执行边界 后续会话中的 AI 助手必须把命令分成两类: diff --git a/deploy-plugin.bat b/deploy-plugin.bat index 789d687..100aff2 100644 --- a/deploy-plugin.bat +++ b/deploy-plugin.bat @@ -3,7 +3,7 @@ setlocal set "TARGET_DIR=%PROGRAMDATA%\Autodesk\Navisworks Manage 2026\plugins\TransportPlugin" set "BUILD_DIR=%~dp0bin\x64\Release" -powershell -NoProfile -ExecutionPolicy Bypass -Command ^ +pwsh -NoProfile -ExecutionPolicy Bypass -Command ^ "$ErrorActionPreference = 'Stop';" ^ "$targetDir = [System.IO.Path]::GetFullPath('%TARGET_DIR%');" ^ "$buildDir = [System.IO.Path]::GetFullPath('%BUILD_DIR%');" ^ diff --git a/doc/bugfixes/数据库兼容性_剖面盒导出修复报告.md b/doc/bugfixes/数据库兼容性_剖面盒导出修复报告.md new file mode 100644 index 0000000..38b6cf5 --- /dev/null +++ b/doc/bugfixes/数据库兼容性_剖面盒导出修复报告.md @@ -0,0 +1,86 @@ +# 数据库兼容性 / 剖面盒导出修复报告 + +## 修复目标 + +1. 打开旧版本数据库时崩溃(schema 不兼容) +2. NWF 多文档剖面盒导出时,无命中子树泄露到导出文件 +3. 反复剖面盒导出后崩溃(COM 跨线程访问) + +--- + +## 问题一:数据库 schema 不兼容导致启动崩溃 + +### 现象 +打开带有旧版本 `.db` 文件的模型时,插件初始化报错: +``` +SQL logic error: no such column: DetectionRecordId +``` + +### 根因 +`CreateTables()` 使用 `CREATE TABLE IF NOT EXISTS` + `CREATE INDEX IF NOT EXISTS` 组合。旧数据库表已存在但 schema 是旧版(缺 `DetectionRecordId` 列),`CREATE TABLE IF NOT EXISTS` 静默跳过,紧随的 `CREATE INDEX ... ON ...(DetectionRecordId)` 直接炸。 + +同时 `PathDatabase.InitializeDatabase()` 还有以下问题: +- `PRAGMA user_version` 只写入、从未读取 +- `CreateTables()` 无异常保护 +- `BatchQueueItems` 的 `ALTER TABLE ADD COLUMN` 用裸 `try-catch` 吞异常 +- `PathPlanningManager.DatabaseInitialize()` 的 `catch` 里 `throw;` 导致同一错误双重日志 + +### 修复 +**PathDatabase.cs:** +- 新增 `SafeCreateIndex()` 方法:建索引前用 `PRAGMA table_info` 检查列是否存在,缺列记 warning 跳过,不崩 +- 新增 `EnsureColumnExists()` 方法:用 `PRAGMA table_info` 检查列,缺了才 `ALTER TABLE ADD COLUMN`,不吞其他异常 +- 删除无用的 `PRAGMA user_version` 写入 +- `InitializeDatabase()` 内用 `try-catch` 包裹 `CreateTables()` + `EnsurePathRoutesTableExtended()`,失败时关闭连接 +- 清理注释中重复/杂乱的编号 +- `CollisionDetectionRecords` 建表处加外键依赖注释 + +**PathPlanningManager.cs:** +- 去掉 `catch` 后的 `throw;`,外层 `OnModelsCollectionChanged` 已有独立日志 + +--- + +## 问题二:NWF 多文档剖面盒导出时空白子树泄露 + +### 现象 +NWF 文件引用多个 NWD(各为一个独立 `Model`),剖面盒只覆盖其中一个子树的元素,导出后其他平行子树仍然出现在 NWD 中。 + +### 根因 +`SectionBoxExporter.CalculateHiddenItems()` 通过"兄弟节点算法"确定隐藏项:收集可见项(剖面盒内元素及其祖先)的父节点,隐藏同父节点下的不可见兄弟。当模型有多个平行 `RootItem`(来自不同 `Model`)时,完全空白的子树没有任何节点进入 `visibleSet`,其 `RootItem` 不会出现在 `uniqueParents` 中,整棵子树被遗漏。 + +### 修复 +**SectionBoxExporter.GetObjectsAndHiddenItems():** +在 `CalculateHiddenItems()` 之后补一步——遍历所有 `document.Models` 的 `RootItem`,不在 `visibleSet` 中的直接加入 `ItemsToHide`。`SetHidden` 会级联隐藏所有后代,无需递归。 + +--- + +## 问题三:反复剖面盒导出崩溃 + +### 现象 +连续多次"设置剖面盒 → 导出"操作,前几次正常,后续某次崩溃(无 managed 异常日志)。 + +### 根因 +`LayerManagementViewModel` 中有两处不必要的 `await Task.Run()`: +- `GetObjectsAndHiddenItems()` 被包在 `Task.Run` 中,在后台线程调用 Navisworks COM API(`Model.HasGeometry`、`BoundingBox()` 等) +- `NwdExportHelper.ExportToNwdWithPrecomputedHiddenItems()` 同样被包在 `Task.Run` 中 + +Navisworks COM API 要求 STA 线程访问,跨线程调用在少量使用时不崩,多次使用后触发 STA 违规导致进程崩溃。 + +### 修复 +**LayerManagementViewModel.cs:** +- 去掉 `GetObjectsAndHiddenItems` 外的 `await Task.Run()` 包装,直接在 UI 线程调用 +- 去掉 `ExportToNwdWithPrecomputedHiddenItems` 外的 `await Task.Run()` 包装,该方法内部已有 `Dispatcher.Invoke`,无需外层再切换线程 + +--- + +## 涉及文件 + +| 文件 | 改动 | +|------|------| +| `src/Core/PathDatabase.cs` | 新增 `SafeCreateIndex`、`EnsureColumnExists`;`InitializeDatabase` 加 try-catch;删除 user_version;清理注释 | +| `src/Core/PathPlanningManager.cs` | 去掉 `DatabaseInitialize` catch 中的 re-throw | +| `src/Core/SectionBoxExporter.cs` | `GetObjectsAndHiddenItems` 补空白 RootItem 隐藏逻辑 | +| `src/Utils/NwdExportHelper.cs` | 调试日志(已清理) | +| `src/Utils/VisibilityHelper.cs` | 调试日志(已清理) | +| `src/UI/WPF/ViewModels/LayerManagementViewModel.cs` | 去掉两处 `Task.Run` | +| `deploy-plugin.bat` | `powershell` → `pwsh`(兼容环境) | +| `AGENTS.md` | 补充编译/部署调用方式说明 | diff --git a/src/Core/PathDatabase.cs b/src/Core/PathDatabase.cs index 857d56a..16d8944 100644 --- a/src/Core/PathDatabase.cs +++ b/src/Core/PathDatabase.cs @@ -61,10 +61,6 @@ namespace NavisworksTransport ExecuteNonQuery("PRAGMA journal_mode=WAL"); ExecuteNonQuery("PRAGMA foreign_keys=ON"); - // 创建数据库表结构 - CreateTables(); - EnsurePathRoutesTableExtended(); - if (isNewDatabase) { LogManager.Info($"创建新数据库: {_dbPath}"); @@ -73,6 +69,18 @@ namespace NavisworksTransport { LogManager.Info($"打开现有数据库: {_dbPath}"); } + + try + { + CreateTables(); + EnsurePathRoutesTableExtended(); + } + catch (Exception ex) + { + LogManager.Error($"数据库初始化失败: {ex.Message}", ex); + try { _connection?.Close(); } catch { } + throw; + } } /// @@ -80,7 +88,7 @@ namespace NavisworksTransport /// private void CreateTables() { - // 1. 路径基本信息表 + // 路径基本信息表 // 注意:TotalLength 字段已从数据库中删除 // 路径长度由 PathRoute.TotalLength 计算属性实时计算: // - 地面路径:从 Edges.PhysicalLength 累加(支持圆弧) @@ -110,7 +118,7 @@ namespace NavisworksTransport ) "); - // 2. 碰撞报告表(替换原有的CollisionResults表) + // 碰撞报告表(替换原有的CollisionResults表) ExecuteNonQuery(@" CREATE TABLE IF NOT EXISTS CollisionReports ( Id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -126,7 +134,7 @@ namespace NavisworksTransport ) "); - // 3. 分析结果表(扩展版,包含5维度评分) + // 分析结果表(扩展版,包含5维度评分) ExecuteNonQuery(@" CREATE TABLE IF NOT EXISTS AnalysisResults ( RouteId TEXT PRIMARY KEY, @@ -146,7 +154,7 @@ namespace NavisworksTransport ) "); - // 4. 路径点表(新增 CustomTurnRadius) + // 路径点表(新增 CustomTurnRadius) ExecuteNonQuery(@" CREATE TABLE IF NOT EXISTS PathPoints ( Id TEXT PRIMARY KEY, @@ -162,7 +170,7 @@ namespace NavisworksTransport ) "); - // 5. 路径边表(新增SequenceNumber字段) + // 路径边表(新增SequenceNumber字段) ExecuteNonQuery(@" CREATE TABLE IF NOT EXISTS PathEdges ( Id TEXT PRIMARY KEY, @@ -189,8 +197,10 @@ namespace NavisworksTransport ) "); - // 6. 碰撞检测记录表 + // 碰撞检测记录表 // 包含检测配置和检测结果(原 ClashDetectiveResults 表已合并) + // 注意:以下表的外键依赖此表 — ClashDetectiveCollisionObjects, CollisionReportScreenshots, + // ClashDetectiveExcludedObjects, CollisionDetectionExcludedObjects, CollisionDetectionManualTargets ExecuteNonQuery(@" CREATE TABLE IF NOT EXISTS CollisionDetectionRecords ( Id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -226,7 +236,7 @@ namespace NavisworksTransport ExecuteNonQuery("CREATE INDEX IF NOT EXISTS idx_detection_route ON CollisionDetectionRecords(RouteId)"); ExecuteNonQuery("CREATE INDEX IF NOT EXISTS idx_detection_time ON CollisionDetectionRecords(CreatedTime DESC)"); - // 7. ClashDetective碰撞对象表 + // ClashDetective碰撞对象表 // 注意:外键关联到 CollisionDetectionRecords 表 // 扩展字段:记录碰撞时运动物体的位置和朝向,用于还原碰撞场景 ExecuteNonQuery(@" @@ -252,7 +262,7 @@ namespace NavisworksTransport ) "); - // 8. 碰撞报告截图表(支持多张截图) + // 碰撞报告截图表(支持多张截图) // 注意:外键关联到 CollisionDetectionRecords 表 ExecuteNonQuery(@" CREATE TABLE IF NOT EXISTS CollisionReportScreenshots ( @@ -269,7 +279,7 @@ namespace NavisworksTransport ) "); - // 10. 时标配置表 + // 时标配置表 ExecuteNonQuery(@" CREATE TABLE IF NOT EXISTS TimeTagProfiles ( Id TEXT PRIMARY KEY, @@ -282,7 +292,7 @@ namespace NavisworksTransport ) "); - // 11. 时标事件表 + // 时标事件表 ExecuteNonQuery(@" CREATE TABLE IF NOT EXISTS TimeTagEvents ( Id TEXT PRIMARY KEY, @@ -301,11 +311,6 @@ namespace NavisworksTransport ) "); - // 12. 设置数据库版本(SQLite内置user_version) - // 版本号格式:主版本*10000 + 次版本*100 + 修订号 - // 例如:2.1.6 = 20106 - ExecuteNonQuery("PRAGMA user_version = 20106"); // v2.1.6 - 删除无效的 RailReferenceToAnchorOffset 字段 - // 创建索引 ExecuteNonQuery("CREATE INDEX IF NOT EXISTS idx_reports_route ON CollisionReports(RouteId)"); ExecuteNonQuery("CREATE INDEX IF NOT EXISTS idx_pathpoints_route ON PathPoints(RouteId)"); @@ -314,12 +319,12 @@ namespace NavisworksTransport ExecuteNonQuery("CREATE INDEX IF NOT EXISTS idx_timetag_events_profile ON TimeTagEvents(ProfileId)"); ExecuteNonQuery("CREATE INDEX IF NOT EXISTS idx_detection_test_name ON CollisionDetectionRecords(TestName)"); ExecuteNonQuery("CREATE INDEX IF NOT EXISTS idx_detection_test_time ON CollisionDetectionRecords(TestTime DESC)"); - ExecuteNonQuery("CREATE INDEX IF NOT EXISTS idx_clash_objects_detection ON ClashDetectiveCollisionObjects(DetectionRecordId)"); - ExecuteNonQuery("CREATE INDEX IF NOT EXISTS idx_clash_objects_path ON ClashDetectiveCollisionObjects(PathId)"); - ExecuteNonQuery("CREATE INDEX IF NOT EXISTS idx_screenshots_detection ON CollisionReportScreenshots(DetectionRecordId)"); - ExecuteNonQuery("CREATE INDEX IF NOT EXISTS idx_screenshots_sort ON CollisionReportScreenshots(DetectionRecordId, SortOrder)"); + SafeCreateIndex("CREATE INDEX IF NOT EXISTS idx_clash_objects_detection ON ClashDetectiveCollisionObjects(DetectionRecordId)", "ClashDetectiveCollisionObjects", "DetectionRecordId"); + SafeCreateIndex("CREATE INDEX IF NOT EXISTS idx_clash_objects_path ON ClashDetectiveCollisionObjects(PathId)", "ClashDetectiveCollisionObjects", "PathId"); + SafeCreateIndex("CREATE INDEX IF NOT EXISTS idx_screenshots_detection ON CollisionReportScreenshots(DetectionRecordId)", "CollisionReportScreenshots", "DetectionRecordId"); + SafeCreateIndex("CREATE INDEX IF NOT EXISTS idx_screenshots_sort ON CollisionReportScreenshots(DetectionRecordId, SortOrder)", "CollisionReportScreenshots", "DetectionRecordId", "SortOrder"); - // 9. 批处理队列表(简化版,只维护FIFO队列) + // 批处理队列表(简化版,只维护FIFO队列) ExecuteNonQuery(@" CREATE TABLE IF NOT EXISTS BatchQueueItems ( Id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -365,7 +370,7 @@ namespace NavisworksTransport ExecuteNonQuery("CREATE INDEX IF NOT EXISTS idx_model_ref_type ON ModelItemReferences(ReferenceType)"); ExecuteNonQuery("CREATE INDEX IF NOT EXISTS idx_model_ref_path ON ModelItemReferences(PathId)"); - // 10. 排除对象表(用于存储用户排除的碰撞检测对象) + // 排除对象表(用于存储用户排除的碰撞检测对象) ExecuteNonQuery(@" CREATE TABLE IF NOT EXISTS ExcludedObjects ( Id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -384,7 +389,7 @@ namespace NavisworksTransport ExecuteNonQuery("CREATE INDEX IF NOT EXISTS idx_excluded_path ON ExcludedObjects(PathId)"); ExecuteNonQuery("CREATE INDEX IF NOT EXISTS idx_excluded_global ON ExcludedObjects(IsGlobal)"); - // 11. 碰撞报告排除对象关联表 + // 碰撞报告排除对象关联表 ExecuteNonQuery(@" CREATE TABLE IF NOT EXISTS CollisionReportExcludedObjects ( Id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -397,7 +402,7 @@ namespace NavisworksTransport ExecuteNonQuery("CREATE INDEX IF NOT EXISTS idx_report_excluded_report ON CollisionReportExcludedObjects(ReportId)"); ExecuteNonQuery("CREATE INDEX IF NOT EXISTS idx_report_excluded_object ON CollisionReportExcludedObjects(ExcludedObjectId)"); - // 12. 碰撞检测记录排除对象关联表(记录每次检测时排除的对象) + // 碰撞检测记录排除对象关联表(记录每次检测时排除的对象) ExecuteNonQuery(@" CREATE TABLE IF NOT EXISTS ClashDetectiveExcludedObjects ( Id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -407,22 +412,13 @@ namespace NavisworksTransport FOREIGN KEY(ExcludedObjectId) REFERENCES ExcludedObjects(Id) ON DELETE CASCADE ) "); - ExecuteNonQuery("CREATE INDEX IF NOT EXISTS idx_clash_excluded_detection ON ClashDetectiveExcludedObjects(DetectionRecordId)"); + SafeCreateIndex("CREATE INDEX IF NOT EXISTS idx_clash_excluded_detection ON ClashDetectiveExcludedObjects(DetectionRecordId)", "ClashDetectiveExcludedObjects", "DetectionRecordId"); ExecuteNonQuery("CREATE INDEX IF NOT EXISTS idx_clash_excluded_object ON ClashDetectiveExcludedObjects(ExcludedObjectId)"); - // 13. 碰撞检测记录表(每次点击"生成动画"创建一条记录,保存当时的完整配置) - // 先添加新列到 BatchQueueItems 表 - try - { - ExecuteNonQuery("ALTER TABLE BatchQueueItems ADD COLUMN DetectionRecordId INTEGER"); - LogManager.Info("[数据库] 已添加 DetectionRecordId 列到 BatchQueueItems 表"); - } - catch - { - // 列已存在,忽略错误 - } + // 为 BatchQueueItems 补充 DetectionRecordId 列(兼容旧数据库) + EnsureColumnExists("BatchQueueItems", "DetectionRecordId", "ALTER TABLE BatchQueueItems ADD COLUMN DetectionRecordId INTEGER"); - // 14. 碰撞检测记录排除对象关联表(记录每次检测时的排除列表) + // 碰撞检测记录排除对象关联表(记录每次检测时的排除列表) ExecuteNonQuery(@" CREATE TABLE IF NOT EXISTS CollisionDetectionExcludedObjects ( Id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -435,9 +431,9 @@ namespace NavisworksTransport FOREIGN KEY(DetectionRecordId) REFERENCES CollisionDetectionRecords(Id) ON DELETE CASCADE ) "); - ExecuteNonQuery("CREATE INDEX IF NOT EXISTS idx_detection_excluded_record ON CollisionDetectionExcludedObjects(DetectionRecordId)"); + SafeCreateIndex("CREATE INDEX IF NOT EXISTS idx_detection_excluded_record ON CollisionDetectionExcludedObjects(DetectionRecordId)", "CollisionDetectionExcludedObjects", "DetectionRecordId"); - // 15. 碰撞检测记录手工目标关联表(记录每次检测时的手工碰撞目标) + // 碰撞检测记录手工目标关联表(记录每次检测时的手工碰撞目标) ExecuteNonQuery(@" CREATE TABLE IF NOT EXISTS CollisionDetectionManualTargets ( Id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -449,7 +445,67 @@ namespace NavisworksTransport FOREIGN KEY(DetectionRecordId) REFERENCES CollisionDetectionRecords(Id) ON DELETE CASCADE ) "); - ExecuteNonQuery("CREATE INDEX IF NOT EXISTS idx_detection_manual_record ON CollisionDetectionManualTargets(DetectionRecordId)"); + SafeCreateIndex("CREATE INDEX IF NOT EXISTS idx_detection_manual_record ON CollisionDetectionManualTargets(DetectionRecordId)", "CollisionDetectionManualTargets", "DetectionRecordId"); + } + + /// + /// 安全创建索引:先检查列是否存在,若旧数据库缺列则跳过并记录警告 + /// + private void SafeCreateIndex(string indexSql, string tableName, params string[] columnNames) + { + try + { + var existingColumns = new HashSet(StringComparer.OrdinalIgnoreCase); + using (var cmd = new SQLiteCommand($"PRAGMA table_info({tableName})", _connection)) + using (var reader = cmd.ExecuteReader()) + { + while (reader.Read()) + { + existingColumns.Add(reader["name"].ToString()); + } + } + + foreach (var col in columnNames) + { + if (!existingColumns.Contains(col)) + { + LogManager.Warning($"[数据库] 表 {tableName} 缺少列 {col},跳过索引创建"); + return; + } + } + + ExecuteNonQuery(indexSql); + } + catch (Exception ex) + { + LogManager.Warning($"[数据库] 创建索引失败: {ex.Message}"); + } + } + + /// + /// 按需添加列:检查表是否已有指定列,没有则执行 ALTER TABLE + /// + private void EnsureColumnExists(string tableName, string columnName, string alterSql) + { + try + { + using (var cmd = new SQLiteCommand($"PRAGMA table_info({tableName})", _connection)) + using (var reader = cmd.ExecuteReader()) + { + while (reader.Read()) + { + if (string.Equals(reader["name"].ToString(), columnName, StringComparison.OrdinalIgnoreCase)) + return; // 列已存在 + } + } + + ExecuteNonQuery(alterSql); + LogManager.Info($"[数据库] 已添加 {columnName} 列到 {tableName} 表"); + } + catch (Exception ex) + { + LogManager.Warning($"[数据库] 添加列 {tableName}.{columnName} 失败: {ex.Message}"); + } } /// diff --git a/src/Core/PathPlanningManager.cs b/src/Core/PathPlanningManager.cs index f109fd2..dff1669 100644 --- a/src/Core/PathPlanningManager.cs +++ b/src/Core/PathPlanningManager.cs @@ -230,7 +230,8 @@ namespace NavisworksTransport LogManager.Error($"初始化路径分析数据库失败: {ex.Message}", ex); LogManager.Error($"数据库路径: {Application.ActiveDocument?.FileName}"); LogManager.Error($"异常类型: {ex.GetType().Name}"); - throw; // 重新抛出异常,让调用者知道初始化失败 + // 不重新抛出:外层 OnModelsCollectionChanged 已有独立日志; + // _pathDatabase 保持 null,后续操作自行判断。 } } diff --git a/src/Core/SectionBoxExporter.cs b/src/Core/SectionBoxExporter.cs index 5cf71b8..3029d24 100644 --- a/src/Core/SectionBoxExporter.cs +++ b/src/Core/SectionBoxExporter.cs @@ -17,98 +17,96 @@ namespace NavisworksTransport.Core public class SectionBoxExporter { /// - /// 导出剖面盒信息 + /// 导出结果 + /// + public class ExportSectionBoxResult + { + public bool Success { get; set; } + public string FilePath { get; set; } + public int ObjectCount { get; set; } + public int HiddenNodeCount { get; set; } + public long FileSize { get; set; } + public string ErrorMessage { get; set; } + } + + /// + /// 从当前视点导出剖面盒到 NWD(完整业务流程:验证→遍历→导出) /// /// Navisworks文档 - /// 导出的文件路径,失败返回null - public string ExportToJson(Document document) + /// 目标文件路径 + /// 导出结果 + public ExportSectionBoxResult ExportSectionBoxFromActiveView(Document document, string filePath) { - if (document == null) - throw new ArgumentNullException(nameof(document)); + var result = new ExportSectionBoxResult(); - // 1. 获取当前视点的裁剪平面 + if (document == null) + { + result.ErrorMessage = "没有活动的文档!请先打开一个模型。"; + return result; + } + + // 验证剖面盒 var viewpoint = document.CurrentViewpoint.Value; var clipPlanes = viewpoint.ClipPlanes; - if (clipPlanes == null) + if (clipPlanes == null || !clipPlanes.Enabled) { - MessageBox.Show( - "当前没有活动的剖面盒!\n请先使用 Navisworks 的剖面工具创建剖面盒。", - "提示", - MessageBoxButtons.OK, - MessageBoxIcon.Information); - return null; + result.ErrorMessage = "当前没有活动的剖面盒!\n请先使用 Navisworks 的剖面工具创建剖面盒。"; + return result; } - // 检查是否启用 - if (!clipPlanes.Enabled) - { - MessageBox.Show( - "剖面盒未启用!\n请先在菜单【视点】-【启用剖分】,然后在菜单【剖分工具】的【模式】中选择【长方体】。\n【移动】、【缩放】或【旋转】剖面盒到适当的位置,或者用【适应选择】调整", - "提示", - MessageBoxButtons.OK, - MessageBoxIcon.Information); - return null; - } - - // 检查是否为长方体模式(Box),排除平面裁剪模式(Clip) if (clipPlanes.Mode != ClipPlaneSetMode.Box) { - MessageBox.Show( - "当前是平面裁剪模式,请切换为剖面盒模式!\n在 Navisworks 剖面工具中选择'Box'模式。", - "提示", - MessageBoxButtons.OK, - MessageBoxIcon.Information); - return null; + result.ErrorMessage = "当前是平面裁剪模式,请切换为剖面盒模式!\n在 Navisworks 剖面工具中选择'Box'模式。"; + return result; } - // 2. 获取剖面盒的包围盒 - BoundingBox3D sectionBoxBounds; - if (!GetSectionBoxBounds(clipPlanes, out sectionBoxBounds)) + // 提取边界 + if (!GetSectionBoxBounds(clipPlanes, out BoundingBox3D sectionBoxBounds)) { - MessageBox.Show( - "无法获取剖面盒边界!\n请确保剖面盒已正确设置。", - "错误", - MessageBoxButtons.OK, - MessageBoxIcon.Error); - return null; + result.ErrorMessage = "无法获取剖面盒边界!\n请确保剖面盒已正确设置。"; + return result; } - // 3. 选择保存路径和格式 - string filePath = ShowSaveFileDialog(out bool exportAsNwd); - if (string.IsNullOrEmpty(filePath)) - return null; + // 遍历模型树 + var traversalResult = GetObjectsAndHiddenItems(document, sectionBoxBounds); + result.ObjectCount = traversalResult.ObjectsInBox.Count; + result.HiddenNodeCount = traversalResult.ItemsToHide.Count; + if (traversalResult.ObjectsInBox.Count == 0) + { + result.ErrorMessage = "剖面盒内没有找到对象!"; + return result; + } + + LogManager.Info($"[SectionBoxExporter] 剖面盒内找到 {result.ObjectCount} 个对象,可隐藏 {result.HiddenNodeCount} 个节点"); + + // 导出 try { - // 4. 【优化】一次遍历同时获取剖面盒内对象和需要隐藏的节点 - var traversalResult = GetObjectsAndHiddenItems(document, sectionBoxBounds); + var exportedPath = NwdExportHelper.ExportToNwdWithPrecomputedHiddenItems( + traversalResult.ObjectsInBox, + traversalResult.ItemsToHide, + filePath, + "剖面盒导出"); - if (exportAsNwd) + if (string.IsNullOrEmpty(exportedPath) || !File.Exists(exportedPath)) { - // 导出为NWD格式(使用预计算结果) - return ExportToNwd(document, traversalResult, filePath); - } - else - { - // 导出为JSON格式(原有逻辑) - var exportData = BuildExportData(sectionBoxBounds, traversalResult.ObjectsInBox, document); - string json = JsonConvert.SerializeObject(exportData, Formatting.Indented); - File.WriteAllText(filePath, json); - LogManager.Info($"剖面盒导出完成: {traversalResult.ObjectsInBox.Count} 个对象 -> {filePath}"); - return filePath; + result.ErrorMessage = "导出失败:未生成文件。"; + return result; } + + result.Success = true; + result.FilePath = exportedPath; + result.FileSize = new FileInfo(exportedPath).Length; } catch (Exception ex) { - LogManager.Error($"导出剖面盒失败: {ex.Message}", ex); - MessageBox.Show( - $"导出失败:{ex.Message}", - "错误", - MessageBoxButtons.OK, - MessageBoxIcon.Error); - return null; + LogManager.Error($"[SectionBoxExporter] 导出失败: {ex.Message}"); + result.ErrorMessage = $"导出失败:{ex.Message}"; } + + return result; } /// @@ -181,6 +179,15 @@ namespace NavisworksTransport.Core // 只遍历 uniqueParents 的子节点,不是整个模型 CalculateHiddenItems(visibleSet, result.ItemsToHide); + // 处理空白顶级子树:模型树无统一根节点时,完全无命中的子树不会被 + // CalculateHiddenItems 覆盖,需显式隐藏其 RootItem + foreach (var model in document.Models) + { + if (model.RootItem == null) continue; + if (!visibleSet.Contains(model.RootItem)) + result.ItemsToHide.Add(model.RootItem); + } + return result; } @@ -473,29 +480,5 @@ namespace NavisworksTransport.Core } } - /// - /// 显示保存文件对话框 - /// - private string ShowSaveFileDialog(out bool exportAsNwd) - { - exportAsNwd = true; // 默认导出为NWD - using (var dialog = new SaveFileDialog()) - { - // NWD 作为默认格式,JSON 作为备选 - dialog.Filter = "Navisworks files (*.nwd)|*.nwd|JSON files (*.json)|*.json"; - dialog.DefaultExt = "nwd"; - dialog.FileName = $"SectionBox_Export_{DateTime.Now:yyyyMMdd_HHmmss}"; - dialog.Title = "导出剖面盒信息"; - - if (dialog.ShowDialog() == DialogResult.OK) - { - // 如果用户选择了JSON(FilterIndex为2或文件扩展名为json) - exportAsNwd = !(dialog.FilterIndex == 2 || - dialog.FileName.EndsWith(".json", StringComparison.OrdinalIgnoreCase)); - return dialog.FileName; - } - } - return null; - } } } diff --git a/src/UI/WPF/ViewModels/LayerManagementViewModel.cs b/src/UI/WPF/ViewModels/LayerManagementViewModel.cs index 6762718..310e10c 100644 --- a/src/UI/WPF/ViewModels/LayerManagementViewModel.cs +++ b/src/UI/WPF/ViewModels/LayerManagementViewModel.cs @@ -1934,7 +1934,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels } /// - /// 导出剖面盒内的对象 + /// 导出剖面盒内的对象到 NWD /// private async Task ExportSectionBoxAsync() { @@ -1950,63 +1950,41 @@ namespace NavisworksTransport.UI.WPF.ViewModels return; } - // 检查剖面盒 - var viewpoint = document.CurrentViewpoint.Value; - var clipPlanes = viewpoint.ClipPlanes; - - if (clipPlanes == null || !clipPlanes.Enabled) + // 显示保存对话框 + string saveFilePath = null; + string defaultFileName = $"SectionBox_Export_{DateTime.Now:yyyyMMdd_HHmmss}"; + System.Windows.Application.Current.Dispatcher.Invoke(() => { - MessageBox.Show( - "当前没有活动的剖面盒!\n请先使用 Navisworks 的剖面工具创建剖面盒。", - "提示", MessageBoxButton.OK, MessageBoxImage.Information); - return; - } + var saveDialog = new Microsoft.Win32.SaveFileDialog + { + Title = "导出剖面盒", + Filter = "Navisworks文件 (*.nwd)|*.nwd", + DefaultExt = "nwd", + FileName = defaultFileName + }; - if (clipPlanes.Mode != ClipPlaneSetMode.Box) - { - MessageBox.Show( - "当前是平面裁剪模式,请切换为剖面盒模式!\n在 Navisworks 剖面工具中选择'Box'模式。", - "提示", MessageBoxButton.OK, MessageBoxImage.Information); - return; - } - - // 获取剖面盒边界 - var box = clipPlanes.Box; - if (box == null) - { - MessageBox.Show("无法获取剖面盒边界!", "错误", MessageBoxButton.OK, MessageBoxImage.Error); - return; - } - - var sectionBoxBounds = new BoundingBox3D( - new Point3D(box.Min.X, box.Min.Y, box.Min.Z), - new Point3D(box.Max.X, box.Max.Y, box.Max.Z) - ); - - // 【优化】异步获取剖面盒内的对象和需要隐藏的节点(一次遍历完成) - CurrentOperationText = "正在查找剖面盒内的对象..."; - SectionBoxExporter.SectionBoxTraversalResult traversalResult = null; - - await Task.Run(() => - { - var exporter = new SectionBoxExporter(); - traversalResult = exporter.GetObjectsAndHiddenItems(document, sectionBoxBounds); + if (saveDialog.ShowDialog() == true) + saveFilePath = saveDialog.FileName; }); - if (traversalResult.ObjectsInBox.Count == 0) - { - MessageBox.Show("剖面盒内没有找到对象!", "提示", MessageBoxButton.OK, MessageBoxImage.Information); + if (string.IsNullOrEmpty(saveFilePath)) return; + + // 委托给业务层:验证剖面盒 → 遍历模型树 → 导出 + CurrentOperationText = "正在查找剖面盒内的对象..."; + var exporter = new SectionBoxExporter(); + var result = exporter.ExportSectionBoxFromActiveView(document, saveFilePath); + + if (result.Success) + { + MessageBox.Show( + $"NWD导出成功!\n\n文件: {result.FilePath}\n大小: {result.FileSize / 1024} KB\n对象数: {result.ObjectCount}", + "导出结果", MessageBoxButton.OK, MessageBoxImage.Information); + } + else + { + MessageBox.Show(result.ErrorMessage, "提示", MessageBoxButton.OK, MessageBoxImage.Warning); } - - LogManager.Info($"[LayerManagementViewModel] 剖面盒内找到 {traversalResult.ObjectsInBox.Count} 个对象,可隐藏 {traversalResult.ItemsToHide.Count} 个节点"); - - // 生成默认文件名 - string timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss"); - string defaultFileName = $"SectionBox_Export_{timestamp}"; - - // 【优化】使用预计算结果的导出方法 - await ExportSectionBoxToNwdAsync(traversalResult, "导出剖面盒", defaultFileName); } catch (Exception ex) { @@ -2029,99 +2007,6 @@ 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 ///