新增排除对象管理功能,支持预计算分析添加到列表,支持用户手动添加和清除排除对象,支持数据库存储
This commit is contained in:
parent
322523bc77
commit
e221d42812
58
AGENTS.md
58
AGENTS.md
@ -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`:
|
||||
|
||||
@ -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%"
|
||||
|
||||
@ -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]
|
||||
|
||||
|
||||
@ -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)
|
||||
@ -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} 个碰撞");
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>");
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
/// 智能查找有意义的父级容器
|
||||
/// 基于节点类型进行判断:纯几何体或空节点向上查找,有意义的节点直接返回
|
||||
|
||||
Loading…
Reference in New Issue
Block a user