数据库schema兼容+剖面盒导出修复+代码重构
- 数据库: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补充编译部署调用说明
This commit is contained in:
parent
928edb8328
commit
47af2443cd
16
AGENTS.md
16
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 助手必须把命令分成两类:
|
||||
|
||||
@ -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%');" ^
|
||||
|
||||
86
doc/bugfixes/数据库兼容性_剖面盒导出修复报告.md
Normal file
86
doc/bugfixes/数据库兼容性_剖面盒导出修复报告.md
Normal file
@ -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` | 补充编译/部署调用方式说明 |
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -80,7 +88,7 @@ namespace NavisworksTransport
|
||||
/// </summary>
|
||||
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");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 安全创建索引:先检查列是否存在,若旧数据库缺列则跳过并记录警告
|
||||
/// </summary>
|
||||
private void SafeCreateIndex(string indexSql, string tableName, params string[] columnNames)
|
||||
{
|
||||
try
|
||||
{
|
||||
var existingColumns = new HashSet<string>(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}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按需添加列:检查表是否已有指定列,没有则执行 ALTER TABLE
|
||||
/// </summary>
|
||||
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}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@ -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,后续操作自行判断。
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -17,98 +17,96 @@ namespace NavisworksTransport.Core
|
||||
public class SectionBoxExporter
|
||||
{
|
||||
/// <summary>
|
||||
/// 导出剖面盒信息
|
||||
/// 导出结果
|
||||
/// </summary>
|
||||
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; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从当前视点导出剖面盒到 NWD(完整业务流程:验证→遍历→导出)
|
||||
/// </summary>
|
||||
/// <param name="document">Navisworks文档</param>
|
||||
/// <returns>导出的文件路径,失败返回null</returns>
|
||||
public string ExportToJson(Document document)
|
||||
/// <param name="filePath">目标文件路径</param>
|
||||
/// <returns>导出结果</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 显示保存文件对话框
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1934,7 +1934,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 导出剖面盒内的对象
|
||||
/// 导出剖面盒内的对象到 NWD
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 【优化】导出剖面盒到NWD,使用预计算的遍历结果
|
||||
/// 避免重复遍历模型树
|
||||
/// </summary>
|
||||
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 = "";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试ExportToNwd API - 专门的导出API
|
||||
/// </summary>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user