新增排除对象管理功能,支持预计算分析添加到列表,支持用户手动添加和清除排除对象,支持数据库存储

This commit is contained in:
tian 2026-02-09 22:48:23 +08:00
parent 322523bc77
commit e221d42812
17 changed files with 2087 additions and 218 deletions

View File

@ -294,6 +294,64 @@ private void OnStatusChanged(string status)
}
```
### WPF UI开发注意事项
#### 必须检查XAML资源引用有效性
**问题**使用未在XAML中定义的Converter/Style资源会导致窗口无法显示
```xml
<!-- ❌ 错误使用了未定义的Converter -->
Visibility="{Binding HasItems, Converter={StaticResource InverseBoolToVisibilityConverter}}"
<!-- ✅ 正确使用已定义的Converter -->
Visibility="{Binding HasItems, Converter={StaticResource BoolToVisibilityConverter}, ConverterParameter=Inverse}"
```
**项目中已定义的资源AnimationControlView.xaml**
| 资源名 | 类型 | 说明 |
|--------|------|------|
| `BoolToVisibilityConverter` | BoolToVisibilityConverter | 布尔转可见性支持Inverse参数 |
**检查步骤**
1. 添加XAML代码时检查所有`{StaticResource xxx}`引用
2. 确认资源在文件顶部`<Window.Resources>`或父级资源字典中已定义
3. 不确定时,在项目中搜索该资源名确认存在
4. 避免复制其他项目/文件的代码直接使用(资源可能不同)
**常见错误模式**
- `InverseBoolToVisibilityConverter` → 改用 `BoolToVisibilityConverter` + `ConverterParameter=Inverse`
- `VisibilityConverter` → 改用 `BoolToVisibilityConverter`
- 自定义Style名称拼写错误 → 检查Resources中的定义
#### UI色彩规范 - Material Design
**项目整体使用 Google Material Design 色系**,确保视觉一致性。
**常用颜色定义**(来自 `PathPointRenderPlugin.cs`
| 用途 | 颜色名称 | RGB值 | 十六进制 |
|------|---------|-------|---------|
| 起点 | Material Green | (76, 175, 80) | #4CAF50 |
| 通行空间 | Material Light Green | (129, 199, 132) | #81C784 |
| 排除对象 | Material Light Green | (129, 199, 132) | #81C784 |
**其他高亮颜色**(来自 `ModelHighlightHelper.cs`
| 类别 | 颜色 | RGB值 |
|------|------|-------|
| 预计算碰撞 | Material Purple | (156, 39, 176) |
| 手工指定对象 | 橙色 | (255, 170, 0) |
| 动画车辆 | Amber/Yellow | (255, 193, 7) |
| ClashDetective结果 | 红色 | Color.Red |
| 通道预览 | 绿色 | Color.Green |
**使用规范**
1. 新增UI元素时优先使用上述Material色系
2. 如需新颜色,参考 [Material Design Color Palette](https://material.io/resources/color/)
3. 使用 `Color.FromByteRGB(r, g, b)` 定义颜色Navisworks API
4. 保持透明度一致:通行空间类用 0.8-0.9,碰撞类用不透明
## 配置系统
配置文件使用 TOML 格式,默认配置位于 `default_config.toml`

View File

@ -1,4 +1,12 @@
@echo off
:: 如果 Navisworks 正在运行,关闭它以释放 DLL 锁定
taskkill /F /IM Roamer.exe 2>nul
if %errorlevel% == 0 (
echo Navisworks process terminated.
timeout /t 1 /nobreak >nul
)
set "TARGET_DIR=C:\ProgramData\Autodesk\Navisworks Manage 2026\plugins\NavisworksTransportPlugin"
if not exist "%TARGET_DIR%" mkdir "%TARGET_DIR%"

View File

@ -2,25 +2,28 @@
## 功能点
### [2026/2/8]
1. [.] (功能)增加预计算结果分析和排除建议
2. [ ] (优化)考虑在碰撞报告中,给每一个碰撞元素自动建立截图
### [2026/2/6]
1. [x] (功能)在碰撞报告中支持多张截图
2. [x] (功能)增加提取元素的包围盒信息,并一键拷贝到坐标编辑窗口
3. [x] (功能)在状态栏增加路径可视化快捷按钮
4. [x] (优化)利用通行空间过滤空间几何体,提高非手工指定模式的性能
### [2026/2/3]
1. [x] (优化)预计算高亮正确,结果高亮错误,高亮了很多不相干的同名物体
2. [ ] BUG预计算一个目标物体161帧碰撞机制有问题
2. [x] BUG预计算一个目标物体161帧碰撞机制有问题
3. [x] BUG吊装路径终点前的一段拐弯通行空间方向不对
4. [x] BUG批处理时杀死程序重新打开有执行中的任务但删除选中没激活再运行批处理收到停止信号结束
5. [x] BUG碰撞检测历史列表不自动加载不自动刷新
6. [x] (优化)将通行空间透明度变成系统参数,可以修改
7. [x] 优化ClashDetective检测中每执行100次打印一下日志
8. [ ] (研究)如何利用剖面,过滤被隐藏的内容
9. [ ] (研究)根据路径高度范围,过滤几何体
10. [x] BUG批处理指定检测物体预计算时没有忽略空间缓存建立
9. [x] BUG批处理指定检测物体预计算时没有忽略空间缓存建立
### [2026/1/28]

View File

@ -0,0 +1,320 @@
# Human-in-the-Loop 碰撞优化功能 - 实现完成报告
## 当前状态2026-02-09
### 已完成功能
#### 第一阶段(基础分析)✅
1. **碰撞热点自动分析** - 预计算完成后自动分析碰撞结果
2. **自动高亮显示** - 自动高亮所有预计算碰撞结果(紫色 #9C27B0
3. **分析对话框** - 显示详细的碰撞统计和建议排除选项
4. **光标修复** - 对话框显示时光标恢复正常
#### 第二阶段(排除列表集成)✅
1. **排除列表数据结构** - `PathAnimationManager` 中使用 `HashSet<ModelItem>`
2. **管理方法** - `SetExcludedObjectsAndClearCache`、`AddExcludedObjects`、`ClearExcludedObjects`
3. **预计算过滤** - 在 `PrecomputeAnimationFrames()` 中应用排除列表过滤
4. **对话框集成** - 分析结果自动合并到排除列表
5. **UI区域** - 新增"检测排除对象"区域,支持手工添加和显示排除列表
#### 第三阶段UI优化
1. **碰撞分析对话框增强**
- 序号列显示
- 行点击自动高亮对应物体(紫色预计算风格)
- 候选碰撞数字黑色粗体显示
- 预计检测时间显示xxx秒/xx.x分钟
- 智能自动选中(碰撞>100次 或 占比>50%
- 单一"继续生成动画"按钮(自动判断是否有排除对象)
2. **排除列表UI完善**
- 添加"清除高亮"按钮
- 删除列表项时自动清除高亮
- 使用 `ModelItemEquals` 正确比较对象(修复 InstanceGuid 问题)
#### 第四阶段(数据库持久化)✅
1. **数据库表结构**
- `ExcludedObjects` - 存储排除对象PathId、DisplayName、ModelIndex等
- `CollisionReportExcludedObjects` - 碰撞报告与排除对象关联表
- `ClashDetectiveExcludedObjects` - ClashDetective结果与排除对象关联表
2. **数据持久化**
- 排除对象随碰撞报告一起保存到数据库
- 支持从数据库加载排除对象通过PathId查找
- 碰撞历史和报告中显示当时使用的排除对象列表
3. **核心方法**
- `SaveExcludedObjectsToDatabase()` - 保存排除对象到数据库
- `LoadExcludedObjectsFromDatabase()` - 从数据库加载排除对象
- `LinkExcludedObjectsToCollisionReport()` - 关联排除对象到碰撞报告
---
## 新增/修改文件
| 文件 | 说明 |
|------|------|
| `src/UI/WPF/Views/CollisionAnalysisDialog.xaml` | 分析对话框UI已统一项目风格 |
| `src/UI/WPF/Views/CollisionAnalysisDialog.xaml.cs` | 分析对话框逻辑(序号、高亮、智能选中) |
| `src/UI/WPF/ViewModels/AnimationControlViewModel.cs` | 添加分析逻辑、排除列表管理、数据库保存 |
| `src/Core/Animation/PathAnimationManager.cs` | 排除列表字段、预计算过滤、数据库加载/保存 |
| `src/UI/WPF/Views/AnimationControlView.xaml` | "检测排除对象"UI区域 |
| `src/Utils/ModelItemAnalysisHelper.cs` | 添加 `ModelItemEquals` 正确比较方法 |
| `src/Core/PathDatabase.cs` | 添加排除列表相关的数据库表和方法 |
---
## 技术实现细节
### 排除列表存储与比较
```csharp
// 使用 HashSet<ModelItem> 存储排除对象
private HashSet<ModelItem> _excludedObjects = new HashSet<ModelItem>();
// 正确的 ModelItem 比较(使用底层原生对象比较)
public static bool ModelItemEquals(ModelItem item1, ModelItem item2)
{
if (item1 == null && item2 == null) return true;
if (item1 == null || item2 == null) return false;
return item1.Equals(item2); // 使用 Navisworks API 的 Equals
}
```
**重要修复**:之前使用 `InstanceGuid` 比较会导致不同对象被误判为相同,现在统一使用 `ModelItemEquals`
### 预计算过滤点
```csharp
// 在 PrecomputeAnimationFrames 的循环中
var nearbyObjects = spatialIndexManager.FindInAABB(searchBounds, excludeObject: _animatedObject);
// 应用排除列表过滤
nearbyObjects = nearbyObjects.Where(obj => !_excludedObjects.Contains(obj));
```
### 智能自动选中逻辑
```csharp
// 碰撞分析对话框中的智能选中条件
IsExcluded = h.CollisionCount > 100 || h.Percentage > 50
```
### 行点击高亮
```csharp
// 使用预计算碰撞结果高亮类别(紫色 #9C27B0
private const string PrecomputedHighlightCategory = ModelHighlightHelper.PrecomputeCollisionResultsCategory;
// 点击行时高亮对应物体
ModelHighlightHelper.HighlightItems(PrecomputedHighlightCategory, items);
```
### UI 绑定
```csharp
// ViewModel 属性
public ThreadSafeObservableCollection<ExcludedObjectViewModel> ExcludedObjects { get; }
public string ExcludedObjectSummary { get; set; }
public bool HasExcludedObjects { get; }
// 命令
public ICommand AddExcludedObjectsFromSelectionCommand { get; }
public ICommand ClearExcludedObjectsCommand { get; }
public ICommand RemoveExcludedObjectCommand { get; }
public ICommand HighlightAllExcludedObjectsCommand { get; }
public ICommand ClearExcludedHighlightCommand { get; }
```
### 数据库持久化
#### 数据库表结构
```sql
-- 排除对象表
CREATE TABLE ExcludedObjects (
Id INTEGER PRIMARY KEY AUTOINCREMENT,
RouteId TEXT, -- 关联路径ID可为空表示全局排除
ModelIndex INTEGER NOT NULL,
PathId TEXT NOT NULL, -- 格式: "模型索引:路径索引数组"
DisplayName TEXT,
ObjectName TEXT,
ExcludedTime DATETIME DEFAULT CURRENT_TIMESTAMP,
Reason TEXT,
IsGlobal INTEGER DEFAULT 0
);
-- 碰撞报告与排除对象关联表
CREATE TABLE CollisionReportExcludedObjects (
Id INTEGER PRIMARY KEY AUTOINCREMENT,
ReportId INTEGER NOT NULL,
ExcludedObjectId INTEGER NOT NULL
);
```
#### 保存排除对象到数据库
```csharp
// 在生成碰撞报告时保存排除对象
public void SaveExcludedObjectsToDatabase(PathDatabase database, string routeId)
{
var records = new List<ExcludedObjectRecord>();
foreach (var obj in _excludedObjects)
{
var record = new ExcludedObjectRecord
{
RouteId = routeId,
ModelIndex = GetModelIndex(obj),
PathId = GetModelItemPathId(obj), // 格式: "0:1,2,3"
DisplayName = obj.DisplayName,
ObjectName = GetModelItemObjectName(obj),
ExcludedTime = DateTime.Now,
Reason = "用户排除"
};
records.Add(record);
}
database.SaveExcludedObjects(records);
}
```
#### 从数据库加载排除对象
```csharp
// 根据PathId查找ModelItem
private ModelItem FindModelItemByPathId(Document doc, string pathId)
{
// PathId格式: "模型索引:路径索引数组"
// 例如: "0:1,2,3" 表示第0个模型的第1->2->3个节点
var parts = pathId.Split(':');
int modelIndex = int.Parse(parts[0]);
var pathIndices = parts[1].Split(',').Select(int.Parse).ToArray();
var model = doc.Models[modelIndex];
ModelItem current = model.RootItem;
foreach (var index in pathIndices)
{
var childrenList = current.Children.ToList();
current = childrenList[index];
}
return current;
}
```
---
## 工作流程
### 1. 动画生成流程
```
用户点击"生成动画"
预计算碰撞检测
分析碰撞热点(>5次或>10%
显示分析对话框(自动选中高频物体)
用户调整勾选/点击行查看高亮
点击"继续生成动画"
[如果有排除对象] 添加到排除列表 → 清除缓存 → 重新生成
[如果无排除对象] 直接继续
```
### 2. 排除列表管理流程
```
手工添加:选择物体 → 点击"从选择添加" → 添加到列表
分析添加:分析对话框勾选 → 点击"继续生成动画" → 合并到列表
删除单个:点击列表项的删除 → 清除高亮 → 同步到Manager
清除所有:点击"清除所有" → 清空列表 → 清除高亮
高亮显示:点击"全部高亮"/"清除高亮"
```
### 3. 数据库持久化流程
```
生成碰撞报告
保存排除对象到数据库ExcludedObjects表
关联排除对象到碰撞报告CollisionReportExcludedObjects表
查看碰撞历史时
加载碰撞报告关联的排除对象列表
显示"本次检测排除了X个对象"
```
---
## 使用指南
### 1. 通过分析对话框排除
1. 生成动画时自动触发预计算分析
2. 分析对话框显示高频碰撞物体(已自动选中>100次或>50%的)
3. 点击表格行可在3D视图中高亮对应物体紫色
4. 调整勾选状态后点击"继续生成动画"
5. 勾选的对象会被加入排除列表并重新生成
### 2. 手工管理排除列表
1. 在动画控制面板的"检测排除对象"区域
2. 在模型中选择要排除的物体
3. 点击"从选择添加"
4. 已排除物体在碰撞检测中会被忽略
5. 可随时"全部高亮"查看或"清除所有"清空
---
## 预期效果
### 减少 ClashDetective 测试数量
- 典型场景379 次碰撞 -> 排除地面后可能减少到 50 次以下
- 按每次测试 133ms 计算50s -> 6.6s,节省 86% 时间
### 提高结果质量
- 减少地面/楼板假阳性碰撞
- 让用户专注于真实障碍物
- 更准确的碰撞报告
---
## 后续可能的改进方向
### 智能建议增强
- [ ] 基于包围盒大小识别大型物体(>1000立方米
- [ ] 基于位置识别:位于路径下方的物体可能是地面
- [ ] 学习用户历史排除选择,优先建议
### 碰撞类型分类
- [ ] 区分"疑似假阳性"(地面/楼板接触)
- [ ] 区分"真实碰撞"(墙体/障碍物)
### 性能优化
- [ ] 增量预计算(仅重新计算受影响的帧)
- [ ] 分析结果持久化到数据库
---
## 注意事项
1. **排除列表生命周期**:当前为运行时管理,切换路径后保留,关闭文档后失效
2. **ModelItem比较**:必须使用 `ModelItemEquals` 方法,不能用 `InstanceGuid`
3. **缓存机制**:排除列表变更会触发 `SetExcludedObjectsAndClearCache` 清除动画缓存
4. **高亮颜色**
- 排除对象:绿色 (#4CAF50)
- 预计算碰撞:紫色 (#9C27B0)
- 手动目标:橙色 (#FFAA00)

View File

@ -73,9 +73,6 @@ namespace NavisworksTransport.Commands
// 新增:被撞物体去重统计
public HashSet<string> UniqueCollidedObjects { get; set; } = new HashSet<string>();
// 新增:截图路径
public string ScreenshotPath { get; set; }
}
/// <summary>
@ -129,46 +126,6 @@ namespace NavisworksTransport.Commands
// 截图列表 - 支持多张截图
public List<CollisionReportScreenshot> Screenshots { get; set; } = new List<CollisionReportScreenshot>();
// 兼容旧代码:单张截图属性(现在指向第一张截图)
[Obsolete("请使用 Screenshots 列表")]
public string ScreenshotPath
{
get => Screenshots?.FirstOrDefault()?.FilePath;
set
{
if (!string.IsNullOrEmpty(value))
{
if (Screenshots == null) Screenshots = new List<CollisionReportScreenshot>();
if (Screenshots.Count == 0)
{
Screenshots.Add(new CollisionReportScreenshot { FilePath = value });
}
else
{
Screenshots[0].FilePath = value;
}
}
}
}
[Obsolete("请使用 Screenshots 列表")]
public string ScreenshotFormat
{
get => Screenshots?.FirstOrDefault()?.Format;
set { if (Screenshots?.FirstOrDefault() != null) Screenshots[0].Format = value; }
}
[Obsolete("请使用 Screenshots 列表")]
public int ScreenshotWidth
{
get => Screenshots?.FirstOrDefault()?.Width ?? 0;
set { if (Screenshots?.FirstOrDefault() != null) Screenshots[0].Width = value; }
}
[Obsolete("请使用 Screenshots 列表")]
public int ScreenshotHeight
{
get => Screenshots?.FirstOrDefault()?.Height ?? 0;
set { if (Screenshots?.FirstOrDefault() != null) Screenshots[0].Height = value; }
}
}
/// <summary>
@ -348,26 +305,6 @@ namespace NavisworksTransport.Commands
}).ToList();
LogManager.Info($"从数据库加载了 {result.Screenshots.Count} 张截图 (ResultId={testRecord.Id})");
}
else
{
// 回退:检查旧表中的单截图
var existingScreenshotPath = testRecord.ScreenshotPath;
if (!string.IsNullOrEmpty(existingScreenshotPath) && System.IO.File.Exists(existingScreenshotPath))
{
result.Screenshots = new List<CollisionReportScreenshot>
{
new CollisionReportScreenshot
{
FilePath = existingScreenshotPath,
Format = "JPG",
Width = 1920,
Height = 1080,
CaptureTime = testRecord.TestTime
}
};
LogManager.Info($"从旧表加载单截图: {existingScreenshotPath}");
}
}
}
}
catch (Exception ex)
@ -404,23 +341,26 @@ namespace NavisworksTransport.Commands
};
LogManager.Info($"自动生成默认截图: {screenshotPath}");
// 保存截图路径到数据库(旧表兼容
if (pathDatabase != null)
// 保存截图到数据库(新表支持多截图
if (pathDatabase != null && result.ResultId > 0)
{
try
{
var updateSql = "UPDATE ClashDetectiveResults SET ScreenshotPath = @screenshotPath WHERE TestName = @testName";
using (var cmd = new System.Data.SQLite.SQLiteCommand(updateSql, pathDatabase._connection))
{
cmd.Parameters.AddWithValue("@screenshotPath", screenshotPath);
cmd.Parameters.AddWithValue("@testName", targetTestName);
cmd.ExecuteNonQuery();
}
LogManager.Info($"已保存截图路径到数据库: {screenshotPath}");
// 保存到新表 CollisionReportScreenshots
int screenshotId = pathDatabase.SaveCollisionReportScreenshot(
result.ResultId,
screenshotPath,
"JPG",
1920,
1080,
0,
"默认场景截图"
);
LogManager.Info($"默认截图已保存到数据库: ScreenshotId={screenshotId}, ResultId={result.ResultId}, Path={screenshotPath}");
}
catch (Exception dbEx)
{
LogManager.Error($"保存截图路径到数据库失败: {dbEx.Message}");
LogManager.Error($"保存默认截图到数据库失败: {dbEx.Message}");
}
}
}
@ -539,17 +479,6 @@ namespace NavisworksTransport.Commands
// 直接使用缓存中的CollisionResult对象已经过复合对象处理和去重
allCollisions.AddRange(testCollisionsRaw);
// 从数据库获取测试信息(包括截图路径)
var pathDatabase = PathPlanningManager.Instance?.GetPathDatabase();
if (pathDatabase != null)
{
var testInfo = pathDatabase.GetClashDetectiveResultByTestName(targetTestName);
if (testInfo != null && !string.IsNullOrEmpty(testInfo.ScreenshotPath))
{
result.ScreenshotPath = testInfo.ScreenshotPath;
}
}
// 统计碰撞总数
result.ClashDetectiveCollisionCount = testCollisionsRaw.Count;
LogManager.Debug($"测试 '{targetTestName}' 包含 {testCollisionsRaw.Count} 个碰撞");

View File

@ -111,6 +111,11 @@ namespace NavisworksTransport.Core.Animation
private int _currentFrameIndex = 0; // 当前帧索引
private List<CollisionResult> _allCollisionResults; // 所有碰撞结果(不去重)
// === Human-in-the-Loop 排除列表 ===
// 注意:排除列表是实时管理的,不与动画缓存绑定。用户选择排除哪些物体后,
// 这些物体会一直生效直到被清除或手动移除,不受动画缓存影响。
private HashSet<ModelItem> _excludedObjects = new HashSet<ModelItem>(); // 用户排除的物体列表
private bool _lastHighlightState = false; // 上一帧的高亮状态
private HashSet<ModelItem> _lastCollisionObjects = new HashSet<ModelItem>(); // 上一帧碰撞对象的集合
@ -1019,7 +1024,12 @@ namespace NavisworksTransport.Core.Animation
IEnumerable<ModelItem> nearbyObjects;
if (manualOverrideActive)
{
// 🔥 Human-in-the-Loop: 手工模式下也要应用排除列表
nearbyObjects = manualTargetsSnapshot;
if (_excludedObjects.Count > 0)
{
nearbyObjects = nearbyObjects.Where(obj => !_excludedObjects.Contains(obj));
}
}
else
{
@ -1041,6 +1051,12 @@ namespace NavisworksTransport.Core.Animation
searchBounds,
excludeObject: _animatedObject
);
// 🔥 Human-in-the-Loop: 应用用户排除列表过滤
if (_excludedObjects.Count > 0)
{
nearbyObjects = nearbyObjects.Where(obj => !_excludedObjects.Contains(obj));
}
}
foreach (var collider in nearbyObjects)
@ -1103,6 +1119,12 @@ namespace NavisworksTransport.Core.Animation
LogManager.Info($"包含碰撞的帧: {framesWithCollision}");
LogManager.Info($"总碰撞次数: {totalCollisions}");
LogManager.Info($"记录的碰撞结果总数: {_allCollisionResults.Count} 个");
// 🔥 Human-in-the-Loop: 记录排除统计
if (_excludedObjects.Count > 0)
{
LogManager.Info($"[排除列表] 本次预计算排除了 {_excludedObjects.Count} 个物体");
}
// 🔥 清除移动物体集合
ClashDetectiveIntegration.ClearAnimatedObject();
@ -3336,6 +3358,387 @@ namespace NavisworksTransport.Core.Animation
#endregion
#region Human-in-the-Loop
/// <summary>
/// 添加排除物体
/// </summary>
/// <param name="obj">要排除的物体</param>
public void AddExcludedObject(ModelItem obj)
{
if (obj != null)
{
_excludedObjects.Add(obj);
LogManager.Debug($"[排除列表] 添加排除物体: {ModelItemAnalysisHelper.GetSafeDisplayName(obj)}, 当前共 {_excludedObjects.Count} 个");
}
}
/// <summary>
/// 批量添加排除物体
/// </summary>
/// <param name="objects">要排除的物体列表</param>
public void AddExcludedObjects(IEnumerable<ModelItem> objects)
{
if (objects != null)
{
foreach (var obj in objects)
{
if (obj != null) _excludedObjects.Add(obj);
}
LogManager.Info($"[排除列表] 批量添加 {objects.Count()} 个排除物体, 当前共 {_excludedObjects.Count} 个");
}
}
/// <summary>
/// 移除排除物体
/// </summary>
/// <param name="obj">要移除的物体</param>
public void RemoveExcludedObject(ModelItem obj)
{
if (obj != null && _excludedObjects.Remove(obj))
{
LogManager.Debug($"[排除列表] 移除排除物体: {ModelItemAnalysisHelper.GetSafeDisplayName(obj)}, 当前共 {_excludedObjects.Count} 个");
}
}
/// <summary>
/// 清除所有排除物体
/// </summary>
public void ClearExcludedObjects()
{
var count = _excludedObjects.Count;
_excludedObjects.Clear();
LogManager.Info($"[排除列表] 清除所有 {count} 个排除物体");
}
/// <summary>
/// 获取当前排除物体列表
/// </summary>
/// <returns>排除物体列表</returns>
public IReadOnlyCollection<ModelItem> GetExcludedObjects()
{
return _excludedObjects.ToList().AsReadOnly();
}
/// <summary>
/// 检查物体是否在排除列表中
/// </summary>
/// <param name="obj">要检查的物体</param>
/// <returns>是否被排除</returns>
public bool IsObjectExcluded(ModelItem obj)
{
return obj != null && _excludedObjects.Contains(obj);
}
/// <summary>
/// 获取排除物体数量
/// </summary>
public int ExcludedObjectCount => _excludedObjects.Count;
/// <summary>
/// 【已废弃】排除列表现在是实时管理的,不绑定到动画缓存
/// </summary>
[Obsolete("排除列表现在是实时管理的,此方法不再执行任何操作", false)]
public void SaveExclusionsToCache()
{
// 排除列表是实时管理的,不保存到动画缓存
LogManager.Debug($"[排除列表] 实时管理模式,当前共 {_excludedObjects.Count} 个排除物体");
}
/// <summary>
/// 【已废弃】排除列表现在是实时管理的,不绑定到动画缓存
/// </summary>
[Obsolete("排除列表现在是实时管理的此方法始终返回false", false)]
public bool LoadExclusionsFromCache()
{
// 排除列表是实时管理的,不从缓存加载
LogManager.Debug("[排除列表] 实时管理模式,不从缓存加载");
return false;
}
/// <summary>
/// 【已废弃】排除列表现在是实时管理的,不绑定到动画缓存
/// </summary>
[Obsolete("排除列表现在是实时管理的,此方法不再执行任何操作", false)]
public static void ClearAllExclusionCaches()
{
// 排除列表是实时管理的,没有缓存需要清除
LogManager.Debug("[排除列表] 实时管理模式,无缓存需要清除");
}
/// <summary>
/// 设置排除物体并清除当前动画缓存(用于重新生成)
/// </summary>
/// <param name="objects">新的排除物体列表</param>
public void SetExcludedObjectsAndClearCache(IEnumerable<ModelItem> objects)
{
// 1. 更新排除列表
_excludedObjects.Clear();
if (objects != null)
{
foreach (var obj in objects)
{
if (obj != null) _excludedObjects.Add(obj);
}
}
// 2. 清除当前动画缓存(强制重新预计算)
if (!string.IsNullOrEmpty(_currentAnimationHash))
{
_animationFrameCache.Remove(_currentAnimationHash);
_collisionResultCache.Remove(_currentAnimationHash);
LogManager.Info($"[排除列表] 已更新排除列表({_excludedObjects.Count}个)并清除当前动画缓存(排除列表实时生效)");
}
else
{
LogManager.Info($"[排除列表] 已更新排除列表({_excludedObjects.Count}个)(排除列表实时生效)");
}
}
/// <summary>
/// 从数据库加载排除对象
/// </summary>
public void LoadExcludedObjectsFromDatabase(PathDatabase database, string routeId = null)
{
try
{
if (database == null)
{
LogManager.Warning("[排除列表] 数据库为空,无法加载排除对象");
return;
}
// 从数据库获取排除对象记录
var records = database.GetExcludedObjects(routeId, includeGlobal: true);
if (records == null || records.Count == 0)
{
LogManager.Info($"[排除列表] 数据库中没有排除对象记录");
return;
}
// 将记录转换为 ModelItem
var doc = Autodesk.Navisworks.Api.Application.ActiveDocument;
if (doc == null || doc.IsClear)
{
LogManager.Warning("[排除列表] 没有活动文档,无法加载排除对象");
return;
}
var loadedObjects = new List<ModelItem>();
int notFoundCount = 0;
foreach (var record in records)
{
try
{
// 尝试通过 PathId 查找对象
var modelItem = FindModelItemByPathId(doc, record.PathId);
if (modelItem != null)
{
loadedObjects.Add(modelItem);
}
else
{
notFoundCount++;
LogManager.Debug($"[排除列表] 未找到排除对象: PathId={record.PathId}, Name={record.DisplayName}");
}
}
catch (Exception ex)
{
LogManager.Error($"[排除列表] 加载排除对象失败: {ex.Message}");
}
}
// 添加到排除列表
foreach (var obj in loadedObjects)
{
_excludedObjects.Add(obj);
}
LogManager.Info($"[排除列表] 从数据库加载 {loadedObjects.Count} 个排除对象,{notFoundCount} 个未找到");
}
catch (Exception ex)
{
LogManager.Error($"[排除列表] 从数据库加载排除对象失败: {ex.Message}", ex);
}
}
/// <summary>
/// 保存排除对象到数据库
/// </summary>
public void SaveExcludedObjectsToDatabase(PathDatabase database, string routeId = null)
{
try
{
if (database == null)
{
LogManager.Warning("[排除列表] 数据库为空,无法保存排除对象");
return;
}
// 清除该路径的旧排除对象记录
database.ClearExcludedObjects(routeId);
// 保存当前排除对象
var records = new List<ExcludedObjectRecord>();
foreach (var obj in _excludedObjects)
{
try
{
var record = new ExcludedObjectRecord
{
RouteId = routeId,
ModelIndex = GetModelIndex(obj),
PathId = GetModelItemPathId(obj),
DisplayName = obj.DisplayName,
ObjectName = GetModelItemObjectName(obj),
ExcludedTime = DateTime.Now,
Reason = "用户排除",
IsGlobal = string.IsNullOrEmpty(routeId)
};
records.Add(record);
}
catch (Exception ex)
{
LogManager.Error($"[排除列表] 转换排除对象记录失败: {ex.Message}");
}
}
// 批量保存到数据库
database.SaveExcludedObjects(records);
LogManager.Info($"[排除列表] 已保存 {records.Count} 个排除对象到数据库");
}
catch (Exception ex)
{
LogManager.Error($"[排除列表] 保存排除对象到数据库失败: {ex.Message}", ex);
}
}
/// <summary>
/// 根据 PathId 查找 ModelItem
/// </summary>
private ModelItem FindModelItemByPathId(Document doc, string pathId)
{
try
{
// 解析 PathId (格式: "模型索引:路径")
var parts = pathId.Split(':');
if (parts.Length < 2) return null;
if (!int.TryParse(parts[0], out int modelIndex)) return null;
// 获取模型
var model = doc.Models[modelIndex];
if (model == null) return null;
// 解析路径索引
var pathIndices = parts[1].Split(',')
.Select(p => int.TryParse(p, out int idx) ? idx : -1)
.Where(idx => idx >= 0)
.ToArray();
if (pathIndices.Length == 0) return null;
// 从根节点开始遍历
ModelItem current = model.RootItem;
foreach (var index in pathIndices)
{
if (current == null) return null;
// 将 Children 转换为列表以便索引访问
var childrenList = current.Children.ToList();
if (index >= childrenList.Count) return null;
current = childrenList[index];
}
return current;
}
catch (Exception ex)
{
LogManager.Error($"[排除列表] 查找 ModelItem 失败: {ex.Message}");
return null;
}
}
/// <summary>
/// 获取 ModelItem 的 PathId
/// </summary>
private string GetModelItemPathId(ModelItem item)
{
try
{
if (item == null) return string.Empty;
// 构建路径索引数组
var indices = new List<int>();
var current = item;
while (current != null && current.Parent != null)
{
var parent = current.Parent;
// 在父节点的子节点中查找当前节点的索引
var childrenList = parent.Children.ToList();
int index = childrenList.FindIndex(c => c == current);
if (index < 0) break;
indices.Insert(0, index);
current = parent;
}
// 获取模型索引
int modelIndex = GetModelIndex(item);
return $"{modelIndex}:{string.Join(",", indices)}";
}
catch (Exception ex)
{
LogManager.Error($"[排除列表] 获取 PathId 失败: {ex.Message}");
return string.Empty;
}
}
/// <summary>
/// 获取 ModelItem 所在的模型索引
/// </summary>
private int GetModelIndex(ModelItem item)
{
try
{
var doc = Autodesk.Navisworks.Api.Application.ActiveDocument;
if (doc == null) return -1;
// 找到根节点
var root = item;
while (root.Parent != null) root = root.Parent;
// 在模型列表中查找
for (int i = 0; i < doc.Models.Count; i++)
{
if (doc.Models[i].RootItem == root)
return i;
}
return -1;
}
catch
{
return -1;
}
}
/// <summary>
/// 获取 ModelItem 的对象名称
/// </summary>
private string GetModelItemObjectName(ModelItem item)
{
try
{
return item.ClassName ?? item.DisplayName ?? "Unknown";
}
catch
{
return "Unknown";
}
}
#endregion
#endregion
}
}

View File

@ -292,11 +292,11 @@ namespace NavisworksTransport
// 1. 从数据库读取测试信息添加JOIN获取PathName
var testInfoSql = @"
SELECT cdr.Id, pr.Name AS PathName, cdr.RouteId, cdr.IsVirtualVehicle, cdr.VehicleModelIndex, cdr.VehiclePathId,
cdr.VirtualVehicleLength, cdr.VirtualVehicleWidth, cdr.VirtualVehicleHeight, cdr.ScreenshotPath
cdr.VirtualVehicleLength, cdr.VirtualVehicleWidth, cdr.VirtualVehicleHeight
FROM ClashDetectiveResults cdr
INNER JOIN PathRoutes pr ON cdr.RouteId = pr.Id
WHERE cdr.TestName = @testName
";
";
ClashDetectiveResultRecord testInfo = null;
using (var cmd = new System.Data.SQLite.SQLiteCommand(testInfoSql, pathDatabase._connection))
@ -316,8 +316,7 @@ namespace NavisworksTransport
VehiclePathId = reader["VehiclePathId"] != DBNull.Value ? reader["VehiclePathId"].ToString() : null,
VirtualVehicleLength = reader["VirtualVehicleLength"] != DBNull.Value ? Convert.ToDouble(reader["VirtualVehicleLength"]) : 0.0,
VirtualVehicleWidth = reader["VirtualVehicleWidth"] != DBNull.Value ? Convert.ToDouble(reader["VirtualVehicleWidth"]) : 0.0,
VirtualVehicleHeight = reader["VirtualVehicleHeight"] != DBNull.Value ? Convert.ToDouble(reader["VirtualVehicleHeight"]) : 0.0,
ScreenshotPath = reader["ScreenshotPath"] != DBNull.Value ? reader["ScreenshotPath"].ToString() : null
VirtualVehicleHeight = reader["VirtualVehicleHeight"] != DBNull.Value ? Convert.ToDouble(reader["VirtualVehicleHeight"]) : 0.0
};
}
}

View File

@ -190,7 +190,6 @@ namespace NavisworksTransport
VirtualVehicleLength REAL,
VirtualVehicleWidth REAL,
VirtualVehicleHeight REAL,
ScreenshotPath TEXT,
CreatedAt DATETIME DEFAULT CURRENT_TIMESTAMP
)
");
@ -209,6 +208,7 @@ namespace NavisworksTransport
");
// 8. 碰撞报告截图表(支持多张截图)
// 注意:外键关联到 ClashDetectiveResults 表(与截图保存逻辑一致)
ExecuteNonQuery(@"
CREATE TABLE IF NOT EXISTS CollisionReportScreenshots (
Id INTEGER PRIMARY KEY AUTOINCREMENT,
@ -285,6 +285,51 @@ namespace NavisworksTransport
ExecuteNonQuery("CREATE INDEX IF NOT EXISTS idx_model_ref_reference ON ModelItemReferences(ReferenceId)");
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,
RouteId TEXT,
ModelIndex INTEGER NOT NULL,
PathId TEXT NOT NULL,
DisplayName TEXT,
ObjectName TEXT,
ExcludedTime DATETIME DEFAULT CURRENT_TIMESTAMP,
Reason TEXT,
IsGlobal INTEGER DEFAULT 0,
FOREIGN KEY(RouteId) REFERENCES PathRoutes(Id) ON DELETE CASCADE
)
");
ExecuteNonQuery("CREATE INDEX IF NOT EXISTS idx_excluded_route ON ExcludedObjects(RouteId)");
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,
ReportId INTEGER NOT NULL,
ExcludedObjectId INTEGER NOT NULL,
FOREIGN KEY(ReportId) REFERENCES CollisionReports(Id) ON DELETE CASCADE,
FOREIGN KEY(ExcludedObjectId) REFERENCES ExcludedObjects(Id) ON DELETE CASCADE
)
");
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. ClashDetective结果排除对象关联表
ExecuteNonQuery(@"
CREATE TABLE IF NOT EXISTS ClashDetectiveExcludedObjects (
Id INTEGER PRIMARY KEY AUTOINCREMENT,
ResultId INTEGER NOT NULL,
ExcludedObjectId INTEGER NOT NULL,
FOREIGN KEY(ResultId) REFERENCES ClashDetectiveResults(Id) ON DELETE CASCADE,
FOREIGN KEY(ExcludedObjectId) REFERENCES ExcludedObjects(Id) ON DELETE CASCADE
)
");
ExecuteNonQuery("CREATE INDEX IF NOT EXISTS idx_clash_excluded_result ON ClashDetectiveExcludedObjects(ResultId)");
ExecuteNonQuery("CREATE INDEX IF NOT EXISTS idx_clash_excluded_object ON ClashDetectiveExcludedObjects(ExcludedObjectId)");
}
/// <summary>
@ -382,7 +427,7 @@ namespace NavisworksTransport
}
/// <summary>
/// 保存碰撞报告
/// 保存碰撞报告返回报告ID
/// </summary>
public void SaveCollisionReport(string routeId, string pathName, string animatedObjectName,
int uniqueCollidedObjectsCount, int frameRate, double duration, double detectionGap,
@ -454,20 +499,7 @@ namespace NavisworksTransport
return;
}
// 同时更新旧表的 ScreenshotPath 字段(兼容旧版本)
var updateSql = @"
UPDATE ClashDetectiveResults
SET ScreenshotPath = @screenshotPath
WHERE Id = @resultId
";
using (var cmd = new SQLiteCommand(updateSql, _connection))
{
cmd.Parameters.AddWithValue("@resultId", resultId.Value);
cmd.Parameters.AddWithValue("@screenshotPath", screenshotPath ?? (object)DBNull.Value);
cmd.ExecuteNonQuery();
}
// 添加/更新到新的截图表
// 保存到截图表
SaveCollisionReportScreenshot(resultId.Value, screenshotPath, screenshotFormat, screenshotWidth, screenshotHeight, 0);
LogManager.Info($"碰撞报告截图已更新: RouteId={routeId}, 截图={screenshotPath}");
@ -717,10 +749,10 @@ namespace NavisworksTransport
INSERT INTO ClashDetectiveResults
(TestName, RouteId, TestTime, CollisionCount, AnimationCollisionCount,
FrameRate, Duration, DetectionGap, AnimatedObjectName, IsVirtualVehicle, VehicleModelIndex, VehiclePathId,
VirtualVehicleLength, VirtualVehicleWidth, VirtualVehicleHeight, ScreenshotPath, CreatedAt)
VirtualVehicleLength, VirtualVehicleWidth, VirtualVehicleHeight, CreatedAt)
VALUES (@testName, @routeId, @testTime, @collisionCount, @animationCollisionCount,
@frameRate, @duration, @detectionGap, @animatedObjectName, @isVirtualVehicle, @vehicleModelIndex, @vehiclePathId,
@virtualVehicleLength, @virtualVehicleWidth, @virtualVehicleHeight, @screenshotPath, @createdAt)
@virtualVehicleLength, @virtualVehicleWidth, @virtualVehicleHeight, @createdAt)
";
long newId = 0;
@ -741,7 +773,6 @@ namespace NavisworksTransport
cmd.Parameters.AddWithValue("@virtualVehicleLength", record.VirtualVehicleLength);
cmd.Parameters.AddWithValue("@virtualVehicleWidth", record.VirtualVehicleWidth);
cmd.Parameters.AddWithValue("@virtualVehicleHeight", record.VirtualVehicleHeight);
cmd.Parameters.AddWithValue("@screenshotPath", record.ScreenshotPath ?? (object)DBNull.Value);
cmd.Parameters.AddWithValue("@createdAt", record.CreatedAt);
cmd.ExecuteNonQuery();
newId = _connection.LastInsertRowId;
@ -893,7 +924,7 @@ namespace NavisworksTransport
SELECT cdr.Id, cdr.TestName, cdr.RouteId, pr.Name AS PathName, cdr.TestTime,
cdr.CollisionCount, cdr.AnimationCollisionCount,
cdr.FrameRate, cdr.Duration, cdr.DetectionGap, cdr.AnimatedObjectName,
cdr.CreatedAt, cdr.ScreenshotPath
cdr.CreatedAt
FROM ClashDetectiveResults cdr
INNER JOIN PathRoutes pr ON cdr.RouteId = pr.Id
ORDER BY cdr.TestTime DESC
@ -917,8 +948,7 @@ namespace NavisworksTransport
Duration = Convert.ToDouble(reader["Duration"]),
DetectionGap = Convert.ToDouble(reader["DetectionGap"]),
AnimatedObjectName = reader["AnimatedObjectName"].ToString(),
CreatedAt = Convert.ToDateTime(reader["CreatedAt"]),
ScreenshotPath = reader["ScreenshotPath"]?.ToString()
CreatedAt = Convert.ToDateTime(reader["CreatedAt"])
});
}
}
@ -990,11 +1020,11 @@ namespace NavisworksTransport
{
var sql = @"
SELECT cdr.Id, cdr.TestName, cdr.RouteId, pr.Name AS PathName, cdr.TestTime, cdr.CollisionCount, cdr.AnimationCollisionCount,
cdr.FrameRate, cdr.Duration, cdr.DetectionGap, cdr.AnimatedObjectName, cdr.CreatedAt, cdr.ScreenshotPath
cdr.FrameRate, cdr.Duration, cdr.DetectionGap, cdr.AnimatedObjectName, cdr.CreatedAt
FROM ClashDetectiveResults cdr
INNER JOIN PathRoutes pr ON cdr.RouteId = pr.Id
WHERE cdr.TestName = @testName
";
";
using (var cmd = new SQLiteCommand(sql, _connection))
{
@ -1016,8 +1046,7 @@ namespace NavisworksTransport
Duration = Convert.ToDouble(reader["Duration"]),
DetectionGap = Convert.ToDouble(reader["DetectionGap"]),
AnimatedObjectName = reader["AnimatedObjectName"].ToString(),
CreatedAt = Convert.ToDateTime(reader["CreatedAt"]),
ScreenshotPath = reader["ScreenshotPath"].ToString()
CreatedAt = Convert.ToDateTime(reader["CreatedAt"])
};
}
}
@ -1575,6 +1604,432 @@ namespace NavisworksTransport
}
}
#region
/// <summary>
/// 保存排除对象
/// </summary>
public int SaveExcludedObject(ExcludedObjectRecord record)
{
try
{
// 检查是否已存在根据PathId和RouteId
var checkSql = @"
SELECT Id FROM ExcludedObjects
WHERE PathId = @pathId AND (RouteId = @routeId OR (RouteId IS NULL AND @routeId IS NULL))
";
using (var checkCmd = new SQLiteCommand(checkSql, _connection))
{
checkCmd.Parameters.AddWithValue("@pathId", record.PathId);
checkCmd.Parameters.AddWithValue("@routeId", string.IsNullOrEmpty(record.RouteId) ? (object)DBNull.Value : record.RouteId);
var existingId = checkCmd.ExecuteScalar();
if (existingId != null)
{
// 已存在,更新记录
var updateSql = @"
UPDATE ExcludedObjects
SET DisplayName = @displayName, ObjectName = @objectName,
Reason = @reason, ExcludedTime = @excludedTime
WHERE Id = @id
";
using (var updateCmd = new SQLiteCommand(updateSql, _connection))
{
updateCmd.Parameters.AddWithValue("@id", Convert.ToInt32(existingId));
updateCmd.Parameters.AddWithValue("@displayName", record.DisplayName ?? "");
updateCmd.Parameters.AddWithValue("@objectName", record.ObjectName ?? "");
updateCmd.Parameters.AddWithValue("@reason", record.Reason ?? "");
updateCmd.Parameters.AddWithValue("@excludedTime", record.ExcludedTime);
updateCmd.ExecuteNonQuery();
}
return Convert.ToInt32(existingId);
}
}
// 插入新记录
var sql = @"
INSERT INTO ExcludedObjects
(RouteId, ModelIndex, PathId, DisplayName, ObjectName, ExcludedTime, Reason, IsGlobal)
VALUES (@routeId, @modelIndex, @pathId, @displayName, @objectName, @excludedTime, @reason, @isGlobal)
";
using (var cmd = new SQLiteCommand(sql, _connection))
{
cmd.Parameters.AddWithValue("@routeId", string.IsNullOrEmpty(record.RouteId) ? (object)DBNull.Value : record.RouteId);
cmd.Parameters.AddWithValue("@modelIndex", record.ModelIndex);
cmd.Parameters.AddWithValue("@pathId", record.PathId);
cmd.Parameters.AddWithValue("@displayName", record.DisplayName ?? "");
cmd.Parameters.AddWithValue("@objectName", record.ObjectName ?? "");
cmd.Parameters.AddWithValue("@excludedTime", record.ExcludedTime);
cmd.Parameters.AddWithValue("@reason", record.Reason ?? "");
cmd.Parameters.AddWithValue("@isGlobal", record.IsGlobal ? 1 : 0);
cmd.ExecuteNonQuery();
}
int newId = (int)_connection.LastInsertRowId;
LogManager.Debug($"排除对象已保存: Id={newId}, PathId={record.PathId}, Name={record.DisplayName}");
return newId;
}
catch (Exception ex)
{
LogManager.Error($"保存排除对象失败: {ex.Message}", ex);
throw;
}
}
/// <summary>
/// 批量保存排除对象
/// </summary>
public void SaveExcludedObjects(List<ExcludedObjectRecord> records)
{
if (records == null || records.Count == 0) return;
try
{
using (var transaction = _connection.BeginTransaction())
{
foreach (var record in records)
{
SaveExcludedObject(record);
}
transaction.Commit();
}
LogManager.Info($"批量保存 {records.Count} 个排除对象完成");
}
catch (Exception ex)
{
LogManager.Error($"批量保存排除对象失败: {ex.Message}", ex);
throw;
}
}
/// <summary>
/// 获取路径的排除对象列表
/// </summary>
public List<ExcludedObjectRecord> GetExcludedObjects(string routeId = null, bool includeGlobal = true)
{
var results = new List<ExcludedObjectRecord>();
try
{
string sql;
if (string.IsNullOrEmpty(routeId))
{
// 获取全局排除对象
sql = "SELECT * FROM ExcludedObjects WHERE IsGlobal = 1 ORDER BY ExcludedTime DESC";
}
else if (includeGlobal)
{
// 获取指定路径的排除对象 + 全局排除对象
sql = @"
SELECT * FROM ExcludedObjects
WHERE RouteId = @routeId OR IsGlobal = 1
ORDER BY ExcludedTime DESC
";
}
else
{
// 仅获取指定路径的排除对象
sql = "SELECT * FROM ExcludedObjects WHERE RouteId = @routeId ORDER BY ExcludedTime DESC";
}
using (var cmd = new SQLiteCommand(sql, _connection))
{
if (!string.IsNullOrEmpty(routeId))
{
cmd.Parameters.AddWithValue("@routeId", routeId);
}
using (var reader = cmd.ExecuteReader())
{
while (reader.Read())
{
results.Add(new ExcludedObjectRecord
{
Id = Convert.ToInt32(reader["Id"]),
RouteId = reader["RouteId"]?.ToString(),
ModelIndex = Convert.ToInt32(reader["ModelIndex"]),
PathId = reader["PathId"].ToString(),
DisplayName = reader["DisplayName"]?.ToString(),
ObjectName = reader["ObjectName"]?.ToString(),
ExcludedTime = Convert.ToDateTime(reader["ExcludedTime"]),
Reason = reader["Reason"]?.ToString(),
IsGlobal = Convert.ToInt32(reader["IsGlobal"]) == 1
});
}
}
}
LogManager.Debug($"获取到 {results.Count} 个排除对象");
}
catch (Exception ex)
{
LogManager.Error($"获取排除对象失败: {ex.Message}", ex);
}
return results;
}
/// <summary>
/// 删除排除对象
/// </summary>
public void DeleteExcludedObject(int id)
{
try
{
var sql = "DELETE FROM ExcludedObjects WHERE Id = @id";
using (var cmd = new SQLiteCommand(sql, _connection))
{
cmd.Parameters.AddWithValue("@id", id);
int affected = cmd.ExecuteNonQuery();
LogManager.Debug($"删除排除对象: Id={id}, 影响行数={affected}");
}
}
catch (Exception ex)
{
LogManager.Error($"删除排除对象失败: {ex.Message}", ex);
throw;
}
}
/// <summary>
/// 根据PathId删除排除对象
/// </summary>
public void DeleteExcludedObjectByPathId(string pathId, string routeId = null)
{
try
{
string sql;
if (string.IsNullOrEmpty(routeId))
{
sql = "DELETE FROM ExcludedObjects WHERE PathId = @pathId";
}
else
{
sql = "DELETE FROM ExcludedObjects WHERE PathId = @pathId AND RouteId = @routeId";
}
using (var cmd = new SQLiteCommand(sql, _connection))
{
cmd.Parameters.AddWithValue("@pathId", pathId);
if (!string.IsNullOrEmpty(routeId))
{
cmd.Parameters.AddWithValue("@routeId", routeId);
}
int affected = cmd.ExecuteNonQuery();
LogManager.Debug($"删除排除对象: PathId={pathId}, RouteId={routeId}, 影响行数={affected}");
}
}
catch (Exception ex)
{
LogManager.Error($"删除排除对象失败: {ex.Message}", ex);
throw;
}
}
/// <summary>
/// 清除路径的所有排除对象
/// </summary>
public void ClearExcludedObjects(string routeId = null)
{
try
{
string sql;
if (string.IsNullOrEmpty(routeId))
{
sql = "DELETE FROM ExcludedObjects";
}
else
{
sql = "DELETE FROM ExcludedObjects WHERE RouteId = @routeId";
}
using (var cmd = new SQLiteCommand(sql, _connection))
{
if (!string.IsNullOrEmpty(routeId))
{
cmd.Parameters.AddWithValue("@routeId", routeId);
}
int affected = cmd.ExecuteNonQuery();
LogManager.Info($"清除排除对象: RouteId={routeId}, 影响行数={affected}");
}
}
catch (Exception ex)
{
LogManager.Error($"清除排除对象失败: {ex.Message}", ex);
throw;
}
}
/// <summary>
/// 关联排除对象到碰撞报告
/// </summary>
public void LinkExcludedObjectsToCollisionReport(int reportId, List<int> excludedObjectIds)
{
if (excludedObjectIds == null || excludedObjectIds.Count == 0) return;
try
{
using (var transaction = _connection.BeginTransaction())
{
var sql = @"
INSERT OR IGNORE INTO CollisionReportExcludedObjects (ReportId, ExcludedObjectId)
VALUES (@reportId, @excludedObjectId)
";
foreach (var objectId in excludedObjectIds)
{
using (var cmd = new SQLiteCommand(sql, _connection))
{
cmd.Parameters.AddWithValue("@reportId", reportId);
cmd.Parameters.AddWithValue("@excludedObjectId", objectId);
cmd.ExecuteNonQuery();
}
}
transaction.Commit();
LogManager.Debug($"关联 {excludedObjectIds.Count} 个排除对象到碰撞报告: ReportId={reportId}");
}
}
catch (Exception ex)
{
LogManager.Error($"关联排除对象到碰撞报告失败: {ex.Message}", ex);
throw;
}
}
/// <summary>
/// 获取碰撞报告的排除对象
/// </summary>
public List<ExcludedObjectRecord> GetExcludedObjectsForCollisionReport(int reportId)
{
var results = new List<ExcludedObjectRecord>();
try
{
var sql = @"
SELECT eo.* FROM ExcludedObjects eo
INNER JOIN CollisionReportExcludedObjects creo ON eo.Id = creo.ExcludedObjectId
WHERE creo.ReportId = @reportId
ORDER BY eo.ExcludedTime DESC
";
using (var cmd = new SQLiteCommand(sql, _connection))
{
cmd.Parameters.AddWithValue("@reportId", reportId);
using (var reader = cmd.ExecuteReader())
{
while (reader.Read())
{
results.Add(new ExcludedObjectRecord
{
Id = Convert.ToInt32(reader["Id"]),
RouteId = reader["RouteId"]?.ToString(),
ModelIndex = Convert.ToInt32(reader["ModelIndex"]),
PathId = reader["PathId"].ToString(),
DisplayName = reader["DisplayName"]?.ToString(),
ObjectName = reader["ObjectName"]?.ToString(),
ExcludedTime = Convert.ToDateTime(reader["ExcludedTime"]),
Reason = reader["Reason"]?.ToString(),
IsGlobal = Convert.ToInt32(reader["IsGlobal"]) == 1
});
}
}
}
}
catch (Exception ex)
{
LogManager.Error($"获取碰撞报告的排除对象失败: {ex.Message}", ex);
}
return results;
}
/// <summary>
/// 关联排除对象到ClashDetective结果
/// </summary>
public void LinkExcludedObjectsToClashDetectiveResult(int resultId, List<int> excludedObjectIds)
{
if (excludedObjectIds == null || excludedObjectIds.Count == 0) return;
try
{
using (var transaction = _connection.BeginTransaction())
{
var sql = @"
INSERT OR IGNORE INTO ClashDetectiveExcludedObjects (ResultId, ExcludedObjectId)
VALUES (@resultId, @excludedObjectId)
";
foreach (var objectId in excludedObjectIds)
{
using (var cmd = new SQLiteCommand(sql, _connection))
{
cmd.Parameters.AddWithValue("@resultId", resultId);
cmd.Parameters.AddWithValue("@excludedObjectId", objectId);
cmd.ExecuteNonQuery();
}
}
transaction.Commit();
LogManager.Debug($"关联 {excludedObjectIds.Count} 个排除对象到ClashDetective结果: ResultId={resultId}");
}
}
catch (Exception ex)
{
LogManager.Error($"关联排除对象到ClashDetective结果失败: {ex.Message}", ex);
throw;
}
}
/// <summary>
/// 获取ClashDetective结果的排除对象
/// </summary>
public List<ExcludedObjectRecord> GetExcludedObjectsForClashDetectiveResult(int resultId)
{
var results = new List<ExcludedObjectRecord>();
try
{
var sql = @"
SELECT eo.* FROM ExcludedObjects eo
INNER JOIN ClashDetectiveExcludedObjects cdeo ON eo.Id = cdeo.ExcludedObjectId
WHERE cdeo.ResultId = @resultId
ORDER BY eo.ExcludedTime DESC
";
using (var cmd = new SQLiteCommand(sql, _connection))
{
cmd.Parameters.AddWithValue("@resultId", resultId);
using (var reader = cmd.ExecuteReader())
{
while (reader.Read())
{
results.Add(new ExcludedObjectRecord
{
Id = Convert.ToInt32(reader["Id"]),
RouteId = reader["RouteId"]?.ToString(),
ModelIndex = Convert.ToInt32(reader["ModelIndex"]),
PathId = reader["PathId"].ToString(),
DisplayName = reader["DisplayName"]?.ToString(),
ObjectName = reader["ObjectName"]?.ToString(),
ExcludedTime = Convert.ToDateTime(reader["ExcludedTime"]),
Reason = reader["Reason"]?.ToString(),
IsGlobal = Convert.ToInt32(reader["IsGlobal"]) == 1
});
}
}
}
}
catch (Exception ex)
{
LogManager.Error($"获取ClashDetective结果的排除对象失败: {ex.Message}", ex);
}
return results;
}
#endregion
/// <summary>
/// 执行非查询SQL语句
/// </summary>
@ -2138,7 +2593,6 @@ namespace NavisworksTransport
public double VirtualVehicleLength { get; set; }
public double VirtualVehicleWidth { get; set; }
public double VirtualVehicleHeight { get; set; }
public string ScreenshotPath { get; set; }
public DateTime CreatedAt { get; set; }
}
@ -2184,4 +2638,24 @@ namespace NavisworksTransport
public DateTime AnalysisTime { get; set; }
public string Strategy { get; set; }
}
#region
/// <summary>
/// 排除对象记录
/// </summary>
public class ExcludedObjectRecord
{
public int Id { get; set; }
public string RouteId { get; set; }
public int ModelIndex { get; set; }
public string PathId { get; set; }
public string DisplayName { get; set; }
public string ObjectName { get; set; }
public DateTime ExcludedTime { get; set; }
public string Reason { get; set; }
public bool IsGlobal { get; set; }
}
#endregion
}

View File

@ -8,6 +8,7 @@ using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
using System.Windows.Threading;
using Autodesk.Navisworks.Api;
using Autodesk.Navisworks.Api.Clash;
using NavisworksTransport.Core;
@ -118,6 +119,48 @@ namespace NavisworksTransport.UI.WPF.ViewModels
}
}
/// <summary>
/// 排除对象ViewModel - 用于检测排除列表
/// </summary>
public class ExcludedObjectViewModel : INotifyPropertyChanged
{
private int _index;
public ExcludedObjectViewModel(ModelItem modelItem, string displayName, string modelPath, int index = 0)
{
ModelItem = modelItem ?? throw new ArgumentNullException(nameof(modelItem));
DisplayName = displayName;
ModelPath = modelPath;
InstanceGuid = modelItem.InstanceGuid;
_index = index;
}
public ModelItem ModelItem { get; }
public string DisplayName { get; }
public string ModelPath { get; }
public Guid InstanceGuid { get; }
public int Index
{
get => _index;
set
{
if (_index != value)
{
_index = value;
OnPropertyChanged();
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
/// <summary>
/// 碰撞构件ViewModel
/// </summary>
@ -293,6 +336,11 @@ namespace NavisworksTransport.UI.WPF.ViewModels
private const string ManualTargetsHighlightCategory = ModelHighlightHelper.ManualTargetsCategory;
private const string CollisionResultsHighlightCategory = ModelHighlightHelper.PrecomputeCollisionResultsCategory;
// 检测排除对象相关字段
private ObservableCollection<ExcludedObjectViewModel> _excludedObjects;
private string _excludedObjectSummary = "未指定排除对象";
private const string ExcludedObjectsHighlightCategory = "excludedObjects"; // 与ModelHighlightHelper中定义的绿色类别一致
#endregion
#region
@ -723,6 +771,17 @@ namespace NavisworksTransport.UI.WPF.ViewModels
public bool IsManualTargetModeActive => IsManualCollisionTargetEnabled && HasManualCollisionTargets;
// 检测排除对象公共属性
public ObservableCollection<ExcludedObjectViewModel> ExcludedObjects => _excludedObjects;
public string ExcludedObjectSummary
{
get => _excludedObjectSummary;
set => SetProperty(ref _excludedObjectSummary, value);
}
public bool HasExcludedObjects => _excludedObjects?.Count > 0;
#endregion
#region ClashDetective结果管理
@ -839,6 +898,13 @@ namespace NavisworksTransport.UI.WPF.ViewModels
public ICommand RemoveManualTargetCommand { get; private set; }
public ICommand HighlightManualTargetsCommand { get; private set; }
public ICommand ClearManualHighlightsCommand { get; private set; }
// 检测排除对象命令
public ICommand AddExcludedObjectsFromSelectionCommand { get; private set; }
public ICommand ClearExcludedObjectsCommand { get; private set; }
public ICommand RemoveExcludedObjectCommand { get; private set; }
public ICommand HighlightAllExcludedObjectsCommand { get; private set; }
public ICommand ClearExcludedHighlightCommand { get; private set; }
public ICommand HighlightPrecomputedCollisionResultsCommand { get; private set; }
public ICommand ClearPrecomputedCollisionHighlightsCommand { get; private set; }
public ICommand ToggleWireframeModeCommand { get; private set; }
@ -890,6 +956,11 @@ namespace NavisworksTransport.UI.WPF.ViewModels
_manualCollisionTargets.CollectionChanged += OnManualCollisionTargetsChanged;
UpdateManualCollisionTargetSummary();
// 初始化检测排除对象集合
_excludedObjects = new ObservableCollection<ExcludedObjectViewModel>();
_excludedObjects.CollectionChanged += OnExcludedObjectsChanged;
UpdateExcludedObjectSummary();
// 初始化设置
InitializeAnimationSettings();
@ -1104,6 +1175,13 @@ namespace NavisworksTransport.UI.WPF.ViewModels
RemoveManualTargetCommand = new RelayCommand<ManualCollisionTargetViewModel>(ExecuteRemoveManualTarget, target => target != null);
HighlightManualTargetsCommand = new RelayCommand(ExecuteHighlightManualTargets, () => HasManualCollisionTargets);
ClearManualHighlightsCommand = new RelayCommand(ExecuteClearManualHighlights, () => HasManualCollisionTargets);
// 检测排除对象命令
AddExcludedObjectsFromSelectionCommand = new RelayCommand(ExecuteAddExcludedObjectsFromSelection);
ClearExcludedObjectsCommand = new RelayCommand(ExecuteClearExcludedObjects, () => HasExcludedObjects);
RemoveExcludedObjectCommand = new RelayCommand<ExcludedObjectViewModel>(ExecuteRemoveExcludedObject, obj => obj != null);
HighlightAllExcludedObjectsCommand = new RelayCommand(ExecuteHighlightAllExcludedObjects, () => HasExcludedObjects);
ClearExcludedHighlightCommand = new RelayCommand(ExecuteClearExcludedHighlight, () => HasExcludedObjects);
HighlightPrecomputedCollisionResultsCommand = new RelayCommand(ExecuteHighlightPrecomputedCollisionResults, () => HasClashDetectiveResults);
ClearPrecomputedCollisionHighlightsCommand = new RelayCommand(ExecuteClearPrecomputedCollisionHighlights, () => HasClashDetectiveResults);
ToggleWireframeModeCommand = new RelayCommand(ExecuteToggleWireframeMode);
@ -1878,9 +1956,13 @@ namespace NavisworksTransport.UI.WPF.ViewModels
// 获取路由ID
string routeId = CurrentPathRoute?.Id ?? "";
// 先保存排除对象到数据库确保排除对象有记录ID
_pathAnimationManager?.SaveExcludedObjectsToDatabase(pathDatabase, routeId);
// 从报告中获取所有需要的数据
await Task.Run(() =>
{
// 保存碰撞报告
pathDatabase.SaveCollisionReport(
routeId,
reportResult.PathName,
@ -1892,12 +1974,23 @@ namespace NavisworksTransport.UI.WPF.ViewModels
reportResult.AnimationCollisions,
reportResult.TotalCollisions
);
// 关联排除对象到 ClashDetective 结果(使用 ResultId 即 ClashDetectiveResults.Id
if (reportResult.ResultId > 0 && _excludedObjects != null && _excludedObjects.Count > 0)
{
// 获取排除对象的记录ID
var excludedRecords = pathDatabase.GetExcludedObjects(routeId, includeGlobal: true);
var excludedObjectIds = excludedRecords.Select(r => r.Id).ToList();
pathDatabase.LinkExcludedObjectsToClashDetectiveResult(reportResult.ResultId, excludedObjectIds);
LogManager.Info($"[排除列表] 已关联 {excludedObjectIds.Count} 个排除对象到 ClashDetective 结果 (ResultId={reportResult.ResultId})");
}
});
LogManager.Info($"碰撞报告已保存到数据库 - 路径:{reportResult.PathName}, " +
$"碰撞构件:{reportResult.UniqueCollidedObjectsCount}, " +
$"动画碰撞:{reportResult.AnimationCollisions}, " +
$"ClashDetective:{reportResult.TotalCollisions}");
$"ClashDetective:{reportResult.TotalCollisions}, " +
$"排除对象:{_excludedObjects?.Count ?? 0}");
}
else
{
@ -2121,6 +2214,259 @@ namespace NavisworksTransport.UI.WPF.ViewModels
#endregion
#region
/// <summary>
/// 从当前选择添加排除对象
/// </summary>
private void ExecuteAddExcludedObjectsFromSelection()
{
try
{
var doc = Autodesk.Navisworks.Api.Application.ActiveDocument;
var selectedItems = doc?.CurrentSelection?.SelectedItems;
if (selectedItems == null || selectedItems.Count == 0)
{
UpdateMainStatus("请在Navisworks中选择需要排除的对象");
LogManager.Warning("[排除对象] 未选择任何对象");
return;
}
var toAdd = new List<ExcludedObjectViewModel>();
foreach (ModelItem item in selectedItems)
{
if (item == null)
continue;
if (!ModelItemAnalysisHelper.IsModelItemValid(item) || !HasGeometryRecursive(item))
continue;
if (ExcludedObjectExists(item))
continue;
var displayName = ModelItemAnalysisHelper.GetSafeDisplayName(item);
var modelPath = BuildModelPath(item);
toAdd.Add(new ExcludedObjectViewModel(item, displayName, modelPath));
}
if (toAdd.Count == 0)
{
UpdateMainStatus("选中的对象已在排除列表中或不包含几何体");
return;
}
int startIndex = _excludedObjects.Count;
foreach (var vm in toAdd)
{
vm.Index = ++startIndex;
_excludedObjects.Add(vm);
}
UpdateMainStatus($"已添加 {toAdd.Count} 个排除对象");
LogManager.Info($"[排除对象] 添加 {toAdd.Count} 个对象到排除列表");
// 同步到PathAnimationManager
SyncExcludedObjectsToAnimationManager();
}
catch (Exception ex)
{
LogManager.Error($"添加排除对象失败: {ex.Message}");
UpdateMainStatus("添加排除对象失败");
}
}
/// <summary>
/// 清除所有排除对象
/// </summary>
private void ExecuteClearExcludedObjects()
{
try
{
int count = _excludedObjects.Count;
_excludedObjects.Clear();
ModelHighlightHelper.ClearCategory(ExcludedObjectsHighlightCategory);
// 同步到PathAnimationManager
_pathAnimationManager?.ClearExcludedObjects();
UpdateMainStatus($"已清除 {count} 个排除对象");
LogManager.Info($"[排除对象] 清除所有 {count} 个排除对象");
}
catch (Exception ex)
{
LogManager.Error($"清除排除对象失败: {ex.Message}");
}
}
/// <summary>
/// 移除单个排除对象
/// </summary>
private void ExecuteRemoveExcludedObject(ExcludedObjectViewModel obj)
{
if (obj == null)
return;
if (_excludedObjects.Remove(obj))
{
// 重新编号
for (int i = 0; i < _excludedObjects.Count; i++)
{
_excludedObjects[i].Index = i + 1;
}
UpdateMainStatus($"已移除排除对象 {obj.DisplayName}");
LogManager.Info($"[排除对象] 移除 {obj.DisplayName}");
// 同步到PathAnimationManager会清除缓存
SyncExcludedObjectsToAnimationManager();
// 清除该对象的高亮显示
ModelHighlightHelper.ClearCategory(ExcludedObjectsHighlightCategory);
}
}
/// <summary>
/// 高亮显示所有排除对象(全部高亮)
/// </summary>
private void ExecuteHighlightAllExcludedObjects()
{
try
{
if (_excludedObjects.Count == 0)
return;
var itemsToHighlight = _excludedObjects.Select(e => e.ModelItem).Where(m => m != null).ToList();
ModelHighlightHelper.HighlightItems(ExcludedObjectsHighlightCategory, itemsToHighlight);
UpdateMainStatus($"已高亮全部 {itemsToHighlight.Count} 个排除对象");
LogManager.Info($"[排除对象] 高亮显示全部 {itemsToHighlight.Count} 个对象");
}
catch (Exception ex)
{
LogManager.Error($"高亮排除对象失败: {ex.Message}");
}
}
/// <summary>
/// 清除排除对象高亮
/// </summary>
private void ExecuteClearExcludedHighlight()
{
try
{
ModelHighlightHelper.ClearCategory(ExcludedObjectsHighlightCategory);
UpdateMainStatus("已清除排除对象高亮");
LogManager.Info("[排除对象] 清除高亮");
}
catch (Exception ex)
{
LogManager.Error($"清除排除对象高亮失败: {ex.Message}");
}
}
/// <summary>
/// 高亮显示单个排除对象(点选时)
/// </summary>
public void HighlightSingleExcludedObject(ExcludedObjectViewModel obj)
{
try
{
if (obj?.ModelItem == null)
return;
var items = new List<ModelItem> { obj.ModelItem };
ModelHighlightHelper.HighlightItems(ExcludedObjectsHighlightCategory, items);
LogManager.Debug($"[排除对象] 高亮显示单个对象: {obj.DisplayName}");
}
catch (Exception ex)
{
LogManager.Error($"高亮单个排除对象失败: {ex.Message}");
}
}
/// <summary>
/// 检查排除对象是否已存在
/// </summary>
private bool ExcludedObjectExists(ModelItem item)
{
if (item == null) return false;
// 使用ModelItemEquals比较底层原生对象项目中推荐的比较方式
bool exists = _excludedObjects.Any(e => ModelItemAnalysisHelper.ModelItemEquals(e.ModelItem, item));
if (exists)
{
LogManager.Debug($"[排除对象] 对象已存在: {ModelItemAnalysisHelper.GetSafeDisplayName(item)}");
}
return exists;
}
/// <summary>
/// 同步排除对象到PathAnimationManager
/// </summary>
private void SyncExcludedObjectsToAnimationManager()
{
var objectsToExclude = _excludedObjects.Select(e => e.ModelItem).Where(m => m != null).ToList();
// 使用 SetExcludedObjectsAndClearCache 方法,更新排除列表并清除缓存(确保预计算能实时反映变更)
_pathAnimationManager?.SetExcludedObjectsAndClearCache(objectsToExclude);
LogManager.Debug($"[排除对象] 已同步 {objectsToExclude.Count} 个对象到PathAnimationManager并清除动画缓存");
}
/// <summary>
/// 从对话框结果添加排除对象到UI列表
/// </summary>
private void AddExcludedObjectsToUIList(List<ModelItem> objects)
{
if (objects == null || objects.Count == 0) return;
int startIndex = _excludedObjects.Count;
foreach (var item in objects)
{
if (item == null || ExcludedObjectExists(item))
continue;
var displayName = ModelItemAnalysisHelper.GetSafeDisplayName(item);
var modelPath = BuildModelPath(item);
var vm = new ExcludedObjectViewModel(item, displayName, modelPath, ++startIndex);
_excludedObjects.Add(vm);
}
UpdateExcludedObjectSummary();
LogManager.Info($"[排除对象] 从对话框添加 {_excludedObjects.Count} 个排除对象到UI列表");
}
/// <summary>
/// 排除对象集合变更事件处理
/// </summary>
private void OnExcludedObjectsChanged(object sender, NotifyCollectionChangedEventArgs e)
{
OnPropertyChanged(nameof(HasExcludedObjects));
UpdateExcludedObjectSummary();
// 刷新命令可用状态
System.Windows.Input.CommandManager.InvalidateRequerySuggested();
}
/// <summary>
/// 更新排除对象统计信息
/// </summary>
private void UpdateExcludedObjectSummary()
{
if (_excludedObjects == null || _excludedObjects.Count == 0)
{
ExcludedObjectSummary = "未指定排除对象";
}
else
{
ExcludedObjectSummary = $"已指定 {_excludedObjects.Count} 个排除对象";
}
}
#endregion
#region
private void ExecuteHighlightPrecomputedCollisionResults()
@ -2320,18 +2666,37 @@ namespace NavisworksTransport.UI.WPF.ViewModels
var result = dialog.ShowDialog();
if (result == true && dialog.ExcludedObjects.Count > 0)
if (result == null)
{
// 用户选择了排除某些物体
LogManager.Info($"[碰撞分析] 用户选择排除 {dialog.ExcludedObjects.Count} 个物体");
// 用户取消
UpdateMainStatus("预计算碰撞分析已取消");
return;
}
if (dialog.ExcludedObjects.Count > 0)
{
// 用户选择了排除某些物体并重新生成
LogManager.Info($"[碰撞分析] 用户选择排除 {dialog.ExcludedObjects.Count} 个物体并重新生成");
// TODO: 将排除的物体添加到排除列表,并重新生成动画
// 这需要与 ClashDetectiveIntegration 集成
// 🔥 添加排除对象到UI列表合并自动去重
AddExcludedObjectsToUIList(dialog.ExcludedObjects);
UpdateMainStatus($"已排除 {dialog.ExcludedObjects.Count} 个物体,请重新生成动画");
// 🔥 从UI列表获取所有排除对象包括之前手工添加的设置到Manager并重新生成
var allExcludedObjects = _excludedObjects.Select(e => e.ModelItem).Where(m => m != null).ToList();
_pathAnimationManager.SetExcludedObjectsAndClearCache(allExcludedObjects);
// 触发重新生成(使用 Dispatcher 避免阻塞 UI
System.Windows.Application.Current.Dispatcher.BeginInvoke(new Action(() =>
{
ExecuteGenerateAnimation();
}), DispatcherPriority.Background);
UpdateMainStatus($"已排除 {allExcludedObjects.Count} 个物体,正在重新生成...");
}
else
{
// 用户选择直接继续(无排除对象)
LogManager.Info("[碰撞分析] 用户选择直接继续生成");
UpdateMainStatus($"预计算碰撞分析完成,共 {totalCollisions} 个候选碰撞");
}
}
@ -2732,21 +3097,14 @@ namespace NavisworksTransport.UI.WPF.ViewModels
return false;
}
Guid itemGuid = item.InstanceGuid;
foreach (var target in _manualCollisionTargets)
{
if (target == null)
continue;
if (itemGuid != Guid.Empty && target.InstanceGuid != Guid.Empty)
{
if (target.InstanceGuid == itemGuid)
return true;
}
else if (ReferenceEquals(target.ModelItem, item))
{
// 使用正确的 ModelItem 比较方法
if (ModelItemAnalysisHelper.ModelItemEquals(target.ModelItem, item))
return true;
}
}
return false;

View File

@ -486,21 +486,6 @@ namespace NavisworksTransport.UI.WPF.ViewModels
SelectedScreenshot = Screenshots.FirstOrDefault();
LogManager.Info($"碰撞报告加载了 {Screenshots.Count} 张截图");
}
else if (!string.IsNullOrEmpty(reportResult.ScreenshotPath))
{
// 兼容旧版本:如果只有单张截图路径,也添加到列表
var oldScreenshot = new CollisionReportScreenshot
{
FilePath = reportResult.ScreenshotPath,
Format = reportResult.ScreenshotFormat,
Width = reportResult.ScreenshotWidth,
Height = reportResult.ScreenshotHeight,
SortOrder = 0
};
Screenshots.Add(oldScreenshot);
SelectedScreenshot = oldScreenshot;
LogManager.Info("碰撞报告加载了1张旧版截图");
}
// 设置运动构件信息
MovingObjectInfo = reportResult.MovingObjectInfo;

View File

@ -156,16 +156,16 @@ NavisworksTransport 检测动画页签视图 - 采用与类别设置和分层管
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,5,0,0">
<Button Content="选择对象"
<Button Content="从选择添加"
Command="{Binding ApplyManualTargetsFromSelectionCommand}"
Style="{StaticResource SecondaryButtonStyle}"/>
<Button Content="清除"
<Button Content="清除所有"
Command="{Binding ClearManualTargetsCommand}"
Style="{StaticResource SecondaryButtonStyle}"
IsEnabled="{Binding HasManualCollisionTargets}"
Background="#FFFFE6E6"
Foreground="#FF8B0000"/>
<Button Content="高亮显示"
<Button Content="全部高亮"
Command="{Binding HighlightManualTargetsCommand}"
Style="{StaticResource SecondaryButtonStyle}"
IsEnabled="{Binding HasManualCollisionTargets}"/>
@ -183,7 +183,8 @@ NavisworksTransport 检测动画页签视图 - 采用与类别设置和分层管
<ListView ItemsSource="{Binding ManualCollisionTargets}"
Margin="0,5,0,0"
Visibility="{Binding HasManualCollisionTargets, Converter={StaticResource BoolToVisibilityConverter}}"
MinHeight="80"
MinHeight="60"
MaxHeight="200"
BorderBrush="#FFE2E8F0"
BorderThickness="1">
<ListView.View>
@ -224,6 +225,91 @@ NavisworksTransport 检测动画页签视图 - 采用与类别设置和分层管
</StackPanel>
</Border>
<!-- 区域2.6: 检测排除对象 -->
<Border BorderBrush="#FFFFE4E1" BorderThickness="1" CornerRadius="0" Margin="0,10,0,0" Padding="10" Background="#FFFFFAFA">
<StackPanel>
<Label Content="检测排除对象" Style="{StaticResource SectionHeaderStyle}" Foreground="#FF8B4513"/>
<TextBlock Text="{Binding ExcludedObjectSummary}"
Style="{StaticResource StatusTextStyle}"/>
<StackPanel Orientation="Horizontal" Margin="0,5,0,0">
<Button Content="从选择添加"
Command="{Binding AddExcludedObjectsFromSelectionCommand}"
Style="{StaticResource SecondaryButtonStyle}"/>
<Button Content="清除所有"
Command="{Binding ClearExcludedObjectsCommand}"
Style="{StaticResource SecondaryButtonStyle}"
IsEnabled="{Binding HasExcludedObjects}"
Background="#FFFFE6E6"
Foreground="#FF8B0000"/>
<Button Content="全部高亮"
Command="{Binding HighlightAllExcludedObjectsCommand}"
Style="{StaticResource SecondaryButtonStyle}"
IsEnabled="{Binding HasExcludedObjects}"/>
<Button Content="清除高亮"
Command="{Binding ClearExcludedHighlightCommand}"
Style="{StaticResource SecondaryButtonStyle}"
IsEnabled="{Binding HasExcludedObjects}"/>
</StackPanel>
<StackPanel Margin="0,5,0,0">
<Label Content="已排除对象列表"
Style="{StaticResource SectionHeaderStyle}"/>
<ListView x:Name="ExcludedObjectsListView"
ItemsSource="{Binding ExcludedObjects}"
Margin="0,5,0,0"
Visibility="{Binding HasExcludedObjects, Converter={StaticResource BoolToVisibilityConverter}}"
MinHeight="60"
MaxHeight="200"
BorderBrush="#FFE2E8F0"
BorderThickness="1"
SelectionChanged="ExcludedObjectsListView_SelectionChanged">
<ListView.View>
<GridView>
<GridViewColumn Header="序号" Width="45">
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding Index}"
HorizontalAlignment="Center"/>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Header="排除对象" Width="275">
<GridViewColumn.CellTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding DisplayName}" FontWeight="SemiBold"/>
<TextBlock Text="{Binding ModelPath}"
Style="{StaticResource StatusTextStyle}"/>
</StackPanel>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Header="操作" Width="80">
<GridViewColumn.CellTemplate>
<DataTemplate>
<Button Content="移除"
Command="{Binding DataContext.RemoveExcludedObjectCommand, RelativeSource={RelativeSource AncestorType=ListView}}"
CommandParameter="{Binding}"
Style="{StaticResource SecondaryButtonStyle}"/>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
</GridView>
</ListView.View>
</ListView>
<TextBlock Text="暂无排除对象"
Style="{StaticResource StatusTextStyle}"
Visibility="{Binding HasExcludedObjects, Converter={StaticResource BoolToVisibilityConverter}, ConverterParameter=Inverse}"
HorizontalAlignment="Center"
Margin="0,10"/>
</StackPanel>
</StackPanel>
</Border>
<!-- 区域3: 生成动画 -->
<Border BorderBrush="#FFD4E7FF" BorderThickness="1" CornerRadius="0" Margin="0,10,0,0" Padding="10">
<StackPanel>

View File

@ -131,5 +131,28 @@ namespace NavisworksTransport.UI.WPF.Views
LogManager.Error($"[AnimationControlView] 处理碰撞构件选择变化失败: {ex.Message}", ex);
}
}
/// <summary>
/// 排除对象列表选择变化事件处理 - 点选时高亮单个对象
/// </summary>
private void ExcludedObjectsListView_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
try
{
if (ViewModel == null) return;
// 获取选中的排除对象
var listView = sender as ListView;
if (listView?.SelectedItem is ExcludedObjectViewModel selectedObject)
{
// 高亮单个对象
ViewModel.HighlightSingleExcludedObject(selectedObject);
}
}
catch (Exception ex)
{
LogManager.Error($"[AnimationControlView] 处理排除对象选择变化失败: {ex.Message}", ex);
}
}
}
}

View File

@ -5,11 +5,46 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
Title="预计算碰撞分析 - 排除建议"
Height="500" Width="700"
Height="550" Width="750"
WindowStartupLocation="CenterOwner"
ResizeMode="CanResize">
ResizeMode="CanResize"
Background="White">
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/NavisworksTransportPlugin;component/src/UI/WPF/Resources/NavisworksStyles.xaml"/>
</ResourceDictionary.MergedDictionaries>
<!-- 提示框样式 -->
<Style x:Key="InfoBorderStyle" TargetType="Border">
<Setter Property="Background" Value="{StaticResource NavisworksBackgroundBrush}"/>
<Setter Property="BorderBrush" Value="{StaticResource NavisworksLightBrush}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="CornerRadius" Value="3"/>
<Setter Property="Padding" Value="12"/>
</Style>
<!-- 详情区域样式 -->
<Style x:Key="DetailsBorderStyle" TargetType="Border">
<Setter Property="Background" Value="#FFF8F9FA"/>
<Setter Property="BorderBrush" Value="#FFDEE2E6"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="CornerRadius" Value="3"/>
<Setter Property="Padding" Value="12"/>
</Style>
<!-- 数字粗体样式 -->
<Style x:Key="NumberBoldStyle" TargetType="TextBlock">
<Setter Property="FontWeight" Value="Bold"/>
<Setter Property="Foreground" Value="Black"/>
</Style>
</ResourceDictionary>
</Window.Resources>
<Grid Margin="15">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
@ -18,63 +53,139 @@
</Grid.RowDefinitions>
<!-- 标题和统计 -->
<StackPanel Grid.Row="0" Margin="0,0,0,10">
<StackPanel Grid.Row="0" Margin="0,0,0,12">
<TextBlock Text="预计算碰撞结果分析"
FontSize="18" FontWeight="Bold" Margin="0,0,0,5"/>
FontSize="16"
FontWeight="SemiBold"
Foreground="{StaticResource NavisworksPrimaryBrush}"
Margin="0,0,0,8"/>
<TextBlock x:Name="StatsTextBlock"
Text="正在分析..."
Foreground="Gray"/>
FontSize="11"
Foreground="{StaticResource NavisworksTextBrush}"/>
</StackPanel>
<!-- 说明 -->
<Border Grid.Row="1" Background="#FFFEF3CD" BorderBrush="#FFE0C97F"
BorderThickness="1" CornerRadius="3" Padding="10" Margin="0,0,0,10">
<TextBlock TextWrapping="Wrap">
<Run FontWeight="Bold">提示:</Run>
<Border Grid.Row="1" Style="{StaticResource InfoBorderStyle}" Margin="0,0,0,12">
<TextBlock TextWrapping="Wrap" FontSize="11">
<Run FontWeight="SemiBold" Foreground="{StaticResource NavisworksPrimaryBrush}">提示:</Run>
<Run>以下物体在预计算中产生了大量碰撞检测点。这些通常是地面、楼板、墙体等大面积物体。</Run>
<LineBreak/>
<Run>勾选"排除"可以将这些物体从碰撞检测中移除,显著减少检测时间。请确认这些物体确实不会与运动物体发生真实碰撞。</Run>
</TextBlock>
</Border>
<!-- 候选碰撞统计 -->
<Border Grid.Row="2" Background="#FFF5F5F5" BorderBrush="#FFE0E0E0"
BorderThickness="1" CornerRadius="3" Padding="12" Margin="0,0,0,12">
<StackPanel>
<TextBlock x:Name="DetectionStatsTextBlock" FontSize="12">
<Run>候选碰撞检测:</Run>
<Run x:Name="CollisionCountRun" FontWeight="Bold" Foreground="Black"/>
<Run> 次,</Run>
<Run x:Name="HotspotCountRun" FontWeight="Bold" Foreground="Black"/>
<Run> 个高频碰撞物体</Run>
</TextBlock>
<TextBlock x:Name="EstimatedTimeTextBlock"
FontSize="11"
Foreground="{StaticResource NavisworksSecondaryBrush}"
Margin="0,4,0,0"/>
</StackPanel>
</Border>
<!-- 热点列表 -->
<DataGrid x:Name="HotspotsDataGrid" Grid.Row="2"
AutoGenerateColumns="False"
CanUserAddRows="False"
SelectionMode="Single"
GridLinesVisibility="Horizontal">
<DataGrid.Columns>
<DataGridCheckBoxColumn Header="排除" Width="50"
Binding="{Binding IsExcluded, Mode=TwoWay}"/>
<DataGridTextColumn Header="物体名称" Width="*"
Binding="{Binding ObjectName}" IsReadOnly="True"/>
<DataGridTextColumn Header="碰撞次数" Width="80"
Binding="{Binding CollisionCount}" IsReadOnly="True"/>
<DataGridTextColumn Header="占比" Width="60"
Binding="{Binding Percentage, StringFormat={}{0:F1}%}" IsReadOnly="True"/>
<DataGridTextColumn Header="原因分析" Width="200"
Binding="{Binding Reason}" IsReadOnly="True"/>
</DataGrid.Columns>
</DataGrid>
<Border Grid.Row="3" BorderBrush="{StaticResource NavisworksLightBrush}"
BorderThickness="1" CornerRadius="3" Margin="0,0,0,12">
<DataGrid x:Name="HotspotsDataGrid"
AutoGenerateColumns="False"
CanUserAddRows="False"
SelectionMode="Single"
GridLinesVisibility="Horizontal"
BorderThickness="0"
HeadersVisibility="Column"
RowBackground="White"
AlternatingRowBackground="{StaticResource NavisworksBackgroundBrush}"
SelectionChanged="HotspotsDataGrid_SelectionChanged">
<DataGrid.Columns>
<DataGridTextColumn Header="序号" Width="45"
Binding="{Binding Index}" IsReadOnly="True">
<DataGridTextColumn.ElementStyle>
<Style TargetType="TextBlock">
<Setter Property="HorizontalAlignment" Value="Center"/>
<Setter Property="FontSize" Value="11"/>
</Style>
</DataGridTextColumn.ElementStyle>
</DataGridTextColumn>
<DataGridCheckBoxColumn Header="排除" Width="50"
Binding="{Binding IsExcluded, Mode=TwoWay}"/>
<DataGridTextColumn Header="物体名称" Width="*"
Binding="{Binding ObjectName}" IsReadOnly="True">
<DataGridTextColumn.ElementStyle>
<Style TargetType="TextBlock">
<Setter Property="FontSize" Value="11"/>
</Style>
</DataGridTextColumn.ElementStyle>
</DataGridTextColumn>
<DataGridTextColumn Header="碰撞次数" Width="80"
Binding="{Binding CollisionCount}" IsReadOnly="True">
<DataGridTextColumn.ElementStyle>
<Style TargetType="TextBlock">
<Setter Property="HorizontalAlignment" Value="Center"/>
<Setter Property="FontSize" Value="11"/>
<Setter Property="FontWeight" Value="Bold"/>
<Setter Property="Foreground" Value="Black"/>
</Style>
</DataGridTextColumn.ElementStyle>
</DataGridTextColumn>
<DataGridTextColumn Header="占比" Width="60"
Binding="{Binding Percentage, StringFormat={}{0:F1}%}" IsReadOnly="True">
<DataGridTextColumn.ElementStyle>
<Style TargetType="TextBlock">
<Setter Property="HorizontalAlignment" Value="Center"/>
<Setter Property="FontSize" Value="11"/>
</Style>
</DataGridTextColumn.ElementStyle>
</DataGridTextColumn>
<DataGridTextColumn Header="原因分析" Width="200"
Binding="{Binding Reason}" IsReadOnly="True">
<DataGridTextColumn.ElementStyle>
<Style TargetType="TextBlock">
<Setter Property="FontSize" Value="10"/>
<Setter Property="Foreground" Value="{StaticResource NavisworksTextBrush}"/>
</Style>
</DataGridTextColumn.ElementStyle>
</DataGridTextColumn>
</DataGrid.Columns>
</DataGrid>
</Border>
<!-- 选中物体详情 -->
<Border Grid.Row="3" Background="#FFF8F9FA" BorderBrush="#FFDEE2E6"
BorderThickness="1" CornerRadius="3" Padding="10" Margin="0,10">
<Border Grid.Row="4" Style="{StaticResource DetailsBorderStyle}" Margin="0,0,0,12">
<StackPanel>
<TextBlock Text="选中物体详情" FontWeight="Bold" Margin="0,0,0,5"/>
<TextBlock x:Name="SelectedObjectDetails" Text="请选择一个物体查看详情"
Foreground="Gray" TextWrapping="Wrap"/>
<TextBlock Text="选中物体详情"
FontWeight="SemiBold"
FontSize="12"
Foreground="{StaticResource NavisworksPrimaryBrush}"
Margin="0,0,0,6"/>
<TextBlock x:Name="SelectedObjectDetails"
Text="请选择一个物体查看详情"
Foreground="{StaticResource NavisworksTextBrush}"
FontSize="11"
TextWrapping="Wrap"/>
</StackPanel>
</Border>
<!-- 按钮 -->
<StackPanel Grid.Row="4" Orientation="Horizontal" HorizontalAlignment="Right">
<Button Content="排除选中并重新生成" Width="140" Margin="0,0,10,0"
Click="ExcludeAndRegenerateButton_Click"
Background="#FFDC3545" Foreground="White"/>
<Button Content="忽略并继续" Width="100" Margin="0,0,10,0"
Click="ContinueButton_Click"/>
<Button Content="取消" Width="80" Click="CancelButton_Click"/>
<StackPanel Grid.Row="5" Orientation="Horizontal" HorizontalAlignment="Right">
<Button Content="继续生成动画"
Width="120"
Click="ActionButton_Click"
Style="{StaticResource ActionButtonStyle}"/>
<Button Content="取消"
Width="80"
Click="CancelButton_Click"
Style="{StaticResource SecondaryButtonStyle}"
Margin="10,0,0,0"/>
</StackPanel>
</Grid>
</Window>
</Window>

View File

@ -3,38 +3,77 @@ using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using Autodesk.Navisworks.Api;
using NavisworksTransport.UI.WPF.ViewModels;
using NavisworksTransport.Utils;
namespace NavisworksTransport.UI.WPF.Views
{
/// <summary>
/// 预计算碰撞分析对话框 - 提供排除建议
/// </summary>
public partial class CollisionAnalysisDialog : Window
{
private List<HotspotViewModel> _viewModels;
public List<ModelItem> ExcludedObjects { get; private set; }
// 使用项目中定义的预计算碰撞结果高亮类别(紫色 #9C27B0
private const string PrecomputedHighlightCategory = ModelHighlightHelper.PrecomputeCollisionResultsCategory;
public CollisionAnalysisDialog(List<CollisionHotspotInfo> hotspots, int totalCollisions)
{
InitializeComponent();
ExcludedObjects = new List<ModelItem>();
StatsTextBlock.Text = $"共 {totalCollisions} 个候选碰撞,发现 {hotspots.Count} 个高频碰撞物体";
// 更新统计信息
UpdateStatsText(hotspots.Count, totalCollisions);
// 转换为视图模型
// 转换为视图模型(带序号)
int index = 1;
var viewModels = hotspots.Select(h => new HotspotViewModel
{
Index = index++,
Object = h.Object,
ObjectName = h.ObjectName,
CollisionCount = h.CollisionCount,
Percentage = h.Percentage,
Reason = h.Reason,
IsExcluded = h.RecommendedAction == "建议排除"
// 智能选中:碰撞次数>100 或 占比>50% 的自动勾选
IsExcluded = h.CollisionCount > 100 || h.Percentage > 50
}).ToList();
HotspotsDataGrid.ItemsSource = viewModels;
HotspotsDataGrid.SelectionChanged += HotspotsDataGrid_SelectionChanged;
_viewModels = viewModels;
// 记录自动选中结果
int autoSelectedCount = viewModels.Count(vm => vm.IsExcluded);
LogManager.Info($"[碰撞分析] 自动选中 {autoSelectedCount}/{viewModels.Count} 个高频碰撞物体(>100次或>50%");
}
/// <summary>
/// 更新统计文本
/// </summary>
private void UpdateStatsText(int hotspotCount, int totalCollisions)
{
// 设置粗体数字
CollisionCountRun.Text = totalCollisions.ToString();
HotspotCountRun.Text = hotspotCount.ToString();
// 计算预计检测时间假设每个碰撞检测约0.5秒)
double estimatedSeconds = totalCollisions * 0.5;
double estimatedMinutes = estimatedSeconds / 60.0;
if (estimatedMinutes >= 1)
{
EstimatedTimeTextBlock.Text = $"预计检测时间:{estimatedSeconds:F0}秒({estimatedMinutes:F1}分钟)";
}
else
{
EstimatedTimeTextBlock.Text = $"预计检测时间:{estimatedSeconds:F0}秒";
}
}
private void HotspotsDataGrid_SelectionChanged(object sender, SelectionChangedEventArgs e)
@ -45,40 +84,94 @@ namespace NavisworksTransport.UI.WPF.Views
$"碰撞次数: {vm.CollisionCount} ({vm.Percentage:F1}%)\n" +
$"分析: {vm.Reason}\n\n" +
$"建议: {(vm.IsExcluded ? "" : "")}";
// 高亮选中的物体(使用预计算高亮风格 - 紫色)
HighlightSelectedObject(vm.Object);
}
}
private void ExcludeAndRegenerateButton_Click(object sender, RoutedEventArgs e)
/// <summary>
/// 高亮选中的物体
/// </summary>
private void HighlightSelectedObject(ModelItem item)
{
try
{
if (item == null) return;
var items = new List<ModelItem> { item };
// 使用预计算碰撞结果高亮类别(紫色 #9C27B0已在ModelHighlightHelper中定义
ModelHighlightHelper.HighlightItems(PrecomputedHighlightCategory, items);
LogManager.Debug($"[碰撞分析] 高亮显示物体: {item.DisplayName}");
}
catch (Exception ex)
{
LogManager.Error($"[碰撞分析] 高亮物体失败: {ex.Message}");
}
}
private void ActionButton_Click(object sender, RoutedEventArgs e)
{
// 收集选中的排除对象
ExcludedObjects = _viewModels
.Where(vm => vm.IsExcluded)
.Select(vm => vm.Object)
.ToList();
DialogResult = true;
Close();
}
private void ContinueButton_Click(object sender, RoutedEventArgs e)
{
DialogResult = false;
// 清除预计算高亮
ModelHighlightHelper.ClearCategory(PrecomputedHighlightCategory);
// 根据是否有排除对象设置对话框结果
if (ExcludedObjects.Count > 0)
{
// 有排除对象,需要重新生成
DialogResult = true;
LogManager.Info($"[碰撞分析] 用户选择排除 {ExcludedObjects.Count} 个物体并重新生成");
}
else
{
// 没有排除对象,直接继续
DialogResult = false;
LogManager.Info("[碰撞分析] 用户选择直接继续生成(无排除对象)");
}
Close();
}
private void CancelButton_Click(object sender, RoutedEventArgs e)
{
// 清除预计算高亮
ModelHighlightHelper.ClearCategory(PrecomputedHighlightCategory);
DialogResult = null;
Close();
}
private class HotspotViewModel
/// <summary>
/// 视图模型
/// </summary>
private class HotspotViewModel : System.ComponentModel.INotifyPropertyChanged
{
public int Index { get; set; }
public ModelItem Object { get; set; }
public string ObjectName { get; set; }
public int CollisionCount { get; set; }
public double Percentage { get; set; }
public string Reason { get; set; }
public bool IsExcluded { get; set; }
private bool _isExcluded;
public bool IsExcluded
{
get => _isExcluded;
set
{
_isExcluded = value;
PropertyChanged?.Invoke(this, new System.ComponentModel.PropertyChangedEventArgs(nameof(IsExcluded)));
}
}
public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;
}
}
}
}

View File

@ -101,7 +101,7 @@ namespace NavisworksTransport.Utils
// 碰撞场景截图(支持多张)
var validScreenshots = report.Screenshots?.Where(s => !string.IsNullOrEmpty(s.FilePath) && File.Exists(s.FilePath)).ToList();
if (validScreenshots?.Count > 0 || (!string.IsNullOrEmpty(report.ScreenshotPath) && File.Exists(report.ScreenshotPath)))
if (validScreenshots?.Count > 0)
{
html.AppendLine("<h2>碰撞场景截图</h2>");
@ -169,19 +169,18 @@ namespace NavisworksTransport.Utils
html.AppendLine("});");
html.AppendLine("</script>");
}
else
else if (validScreenshots?.Count == 1)
{
// 单张截图样式(兼容旧版本)
string screenshotPath = validScreenshots?.FirstOrDefault()?.FilePath ?? report.ScreenshotPath;
var screenshot = validScreenshots?.FirstOrDefault();
// 单张截图样式
var screenshot = validScreenshots.First();
html.AppendLine("<div class='screenshot-section'>");
string relativePath = PathHelper.GetRelativePath(htmlFilePath, screenshotPath);
string relativePath = PathHelper.GetRelativePath(htmlFilePath, screenshot.FilePath);
html.AppendLine("<div class='screenshot-container'>");
html.AppendLine($"<img src=\"{relativePath}\" alt=\"碰撞场景截图\" class=\"screenshot-image\"/>");
html.AppendLine("<div class='screenshot-info'>");
html.AppendLine($"<p>分辨率: {screenshot?.Width ?? report.ScreenshotWidth} x {screenshot?.Height ?? report.ScreenshotHeight}</p>");
html.AppendLine($"<p>格式: {screenshot?.Format ?? report.ScreenshotFormat}</p>");
html.AppendLine($"<p>分辨率: {screenshot.Width} x {screenshot.Height}</p>");
html.AppendLine($"<p>格式: {screenshot.Format}</p>");
html.AppendLine("</div>");
html.AppendLine("</div>");
html.AppendLine("</div>");

View File

@ -46,7 +46,8 @@ namespace NavisworksTransport.Utils
{ PrecomputeCollisionResultsCategory, Color.FromByteRGB(156, 39, 176) }, // Material Purple #9C27B0预计算碰撞与红色/橙色明显区分)
{ ChannelPreviewCategory, Color.Green },
{ ClashDetectiveResultsCategory, Color.Red },
{ AnimatedObjectCategory, Color.FromByteRGB(255, 193, 7) } // Amber/Yellow动画车辆-琥珀黄,更醒目且不与碰撞检测红色冲突)
{ AnimatedObjectCategory, Color.FromByteRGB(255, 193, 7) }, // Amber/Yellow动画车辆-琥珀黄,更醒目且不与碰撞检测红色冲突)
{ "excludedObjects", Color.FromByteRGB(76, 175, 80) } // 排除对象-Material Green (#4CAF50)
};
/// <summary>

View File

@ -92,6 +92,25 @@ namespace NavisworksTransport.Utils
}
}
/// <summary>
/// 比较两个ModelItem是否是同一个对象使用底层原生对象比较
/// 这是项目中推荐的ModelItem比较方式比使用InstanceGuid或引用比较更可靠
/// </summary>
/// <param name="item1">第一个ModelItem</param>
/// <param name="item2">第二个ModelItem</param>
/// <returns>如果是同一个对象返回true</returns>
public static bool ModelItemEquals(ModelItem item1, ModelItem item2)
{
if (item1 == null && item2 == null)
return true;
if (item1 == null || item2 == null)
return false;
// 使用ModelItem.Equals比较底层原生对象
// 这是Navisworks API提供的正确比较方式
return item1.Equals(item2);
}
/// <summary>
/// 智能查找有意义的父级容器
/// 基于节点类型进行判断:纯几何体或空节点向上查找,有意义的节点直接返回