# 对碰撞检测的性能进行优化

## 问题描述

1. 当前性能瓶颈：

- 动画生成建立时间长，对于某大型模型的日志：
[2025-12-08 10:02:31.838] [INFO] [动画生成] 开始构建碰撞检测缓存
[2025-12-08 10:05:28.571] [INFO] 几何对象列表缓存构建完成，耗时: 176730ms
[2025-12-08 10:05:28.571] [INFO]   - 缓存对象总数: 374425 个
[2025-12-08 10:05:28.750] [INFO] 通道对象缓存构建完成，耗时: 174ms
[2025-12-08 10:05:28.751] [INFO]   - 可通行物流根对象: 2 个
[2025-12-08 10:05:28.751] [INFO]   - 缓存总对象数: 5 个
[2025-12-08 10:05:28.751] [INFO] [动画生成] 碰撞检测缓存构建完成，耗时: 176911.7ms

[2025-12-08 10:05:28.774] [INFO] === 构建全局空间索引 ===
[2025-12-08 10:05:28.776] [INFO] [空间索引] 物体宽度: 1.00米 → 格子大小: 3.28模型单位
[2025-12-08 10:05:28.781] [INFO] [空间索引] 开始构建全局空间索引
[2025-12-08 10:05:28.782] [INFO] [空间索引] 格子大小: 3.28 模型单位
[2025-12-08 10:05:28.807] [INFO] [空间索引] 从缓存获取 374422 个非通道几何对象（已过滤通道）
[2025-12-08 10:05:31.538] [INFO] [空间索引] 构建完成
[2025-12-08 10:05:31.538] [INFO]   - 成功索引: 374422 个对象（通道已在缓存阶段过滤）
[2025-12-08 10:05:31.538] [INFO]   - 失败: 0 个对象
[2025-12-08 10:05:31.538] [INFO]   - 格子数量: 58986 个
[2025-12-08 10:05:31.540] [INFO]   - 耗时: 2756 ms

[2025-12-08 10:05:31.782] [INFO] [动画生成] 动画生成完成，总耗时: 179942.9ms

- 如果碰撞检测点过多，检测时间长。用户实际模型中，有2000多个检测点，耗时近300秒。

## 解决方案

- 优化碰撞检测算法

分组去重：代码现在会先将所有预计算的碰撞事件按 (移动物体, 被撞物体) 进行分组。
验证即止：对于每一对物体，按干涉深度排序。
逐帧验证：将物体移动到该帧的位置，进行精确 Clash Detective 检测。
智能跳过：一旦确认了该对物体发生了真实碰撞，就记录结果并立即停止对该对物体的后续检测。
统计信息：日志中现在会显示"确认碰撞 X 组，跳过 Y 个冗余检测点"，方便评估优化效果。

---

## 缓存构建性能优化 (2025-12-08)

### 问题分析

初始性能测试显示缓存构建耗时过长：

- **几何对象缓存**: 177秒 (374,425个对象)
- **空间索引构建**: 111秒
- **总耗时**: ~288秒

### 优化过程

#### 第一阶段：使用 Search API 替代遍历

**问题根因**: `RootItemDescendantsAndSelf.Where(item => item.HasGeometry)` 遍历整个模型树效率低下

**解决方案**: 使用 Navisworks Search API

```csharp
var search = new Search();
search.SearchConditions.Add(
    SearchCondition.HasCategoryByName(PropertyCategoryNames.Geometry));
search.Selection.SelectAll();
search.Locations = SearchLocations.DescendantsAndSelf;
var allItems = search.FindAll(Application.ActiveDocument, false);
```

**结果**: 177秒 → 113秒 (36%提升)

#### 第二阶段：消除 ToList() 转换瓶颈

**问题根因**: `allItems.ToList()` 强制枚举并分配内存给374k个对象

**解决方案**:

1. 将缓存类型从 `List<ModelItem>` 改为 `ModelItemCollection`
2. 直接存储 Search API 返回的 `ModelItemCollection`
3. 更新 getter 方法返回 `ModelItemCollection` 而非创建 List 副本

**关键代码变更**:

```csharp
// 之前
private static List<ModelItem> _allGeometryItemsCache = null;
_allGeometryItemsCache = allItems.ToList();  // 耗时100+秒

// 之后
private static ModelItemCollection _allGeometryItemsCache = null;
_allGeometryItemsCache = search.FindAll(Application.ActiveDocument, false);  // <1秒
```

**结果**: 113秒 → 0.9秒 (**129倍提升!**)

#### 第三阶段：优化空间索引构建

**问题根因**: `GetNonChannelGeometryItemsCache()` 内部调用 `.Where().ToList()` 预过滤通道对象

**解决方案**:

1. 直接迭代 `ModelItemCollection`
2. 在循环中使用 `HashSet.Contains()` 过滤通道对象
3. 避免创建中间 List

**关键代码变更**:

```csharp
// 之前
var nonChannelItems = ClashIntegration.GetNonChannelGeometryItemsCache(); // ToList()
foreach (var item in nonChannelItems) { ... }

// 之后
var allGeometryItems = ClashIntegration.GetAllGeometryItemsCache();
var channelObjectsCache = ClashIntegration.GetChannelObjectsCache();
foreach (var item in allGeometryItems) {
    if (channelObjectsCache != null && channelObjectsCache.Contains(item)) {
        skippedChannelCount++;
        continue;
    }
    // 索引对象...
}
```

**结果**: 111秒 → 99秒 (11%提升)

#### 第四阶段：缓存空间索引

**问题根因**: 空间索引与移动物体无关，但每次生成动画都会重建

**解决方案**:

1. 在 `PathAnimationManager.cs` 中检查空间索引是否已初始化
2. 只在第一次时构建，后续复用缓存的空间索引

**关键代码变更**:

```csharp
// 之前
spatialIndexManager.BuildGlobalIndex(cellSizeInModelUnits);

// 之后
if (!spatialIndexManager.IsInitialized)
{
    LogManager.Info("[空间索引] 空间索引未初始化，开始构建...");
    spatialIndexManager.BuildGlobalIndex(cellSizeInModelUnits);
}
else
{
    LogManager.Info("[空间索引] 使用已缓存的空间索引");
}
```

**结果**: 后续动画生成从 ~100秒 → **0.3秒** (333倍提升!)

#### 第五阶段：基于可见性的缓存过滤

**问题**: 缓存包含所有几何对象（374,425个），即使是在隐藏层中的对象。用户通常会隐藏不需要的楼层进行路径规划。

**解决方案**:
采用**深度优先搜索 (DFS) + 自动剪枝**。
Search API 的 `Hidden` 属性和 `IsHidden` 属性（在扁平列表上）都无法正确处理继承的可见性（父节点隐藏）。
我们改用手动实现的迭代式遍历（使用 Stack 模拟递归），从根节点开始遍历：

1. 如果遇到隐藏节点 (`IsHidden == true`)，**直接跳过该分支**（剪枝）。
2. 如果节点可见且有几何体，则加入缓存。
3. 继续遍历子节点。

**关键代码变更**:

```csharp
// 使用 Stack 模拟递归遍历 (Top-Down DFS)
var stack = new Stack<ModelItem>(1000);
foreach (var model in Application.ActiveDocument.Models)
{
    stack.Push(model.RootItem); // 从模型根节点开始
}

while (stack.Count > 0)
{
    var item = stack.Pop();
    
    // 自动剪枝：父节点隐藏 -> 跳过整个子树
    if (item.IsHidden) continue; 

    if (item.HasGeometry)
        _allGeometryItemsCache.Add(item);

    foreach (var child in item.Children)
        stack.Push(child);
}
```

**预期收益**:

- **正确性**: 完美处理层级隐藏（如隐藏楼层文件夹，下面的墙体自动排除）。
- **性能**: 相比全量遍历，隐藏分支被直接跳过。如果隐藏了90%的模型，遍历开销也减少90%。

**预期收益**:

- **缓存大小减少**: 取决于隐藏对象的数量（如隐藏50%楼层，对象数减半）
- **性能提升**:
  - 过滤缓存构建: 与可见对象数量成正比 (如 50% 可见 -> 耗时减半)
  - 空间索引构建: 与可见对象数量成正比
  - 内存占用降低

**注意**: 修改可见性后需要刷新缓存（可以通过重新打开插件或重启 Navisworks，或者实现手动刷新功能）。

### 最终性能对比（实测数据）

| 场景 | 优化前 | 优化后 | 提升 |
|------|--------|--------|------|
| **首次动画生成** | ~288秒 | **~2秒** | **140倍** |
| **后续动画生成** | ~180秒 | **0.3秒** | **600倍** 🚀 |

**说明**:

- 首次生成包含：几何缓存构建(1.3s，含过滤) + 过滤缓存构建(~0.1s) + 空间索引构建(~1.5s) + 动画预计算(~0.3s)
- 后续生成仅需：动画预计算(~0.3s)，所有缓存直接复用

### 剩余瓶颈分析

首次动画生成的2-3秒主要消耗在：

1. **几何缓存构建** (~1.3秒): 深度优先遍历与自动剪枝
   - 这取决于可见模型的复杂度，1.3秒是正常的速度
   - **优化非常成功** (之前是 100+秒)

2. **空间索引构建** (~1.5秒): 调用 `BoundingBox()`
   - 这是一次性成本，后续复用
   - 已经是最优方案

3. **动画预计算** (~0.3秒): 碰撞检测计算
   - 已经极快，无优化必要

**结论**: 性能优化目标已超额完成。首次生成从近5分钟缩短到约2秒，后续操作实时响应。

### 关键经验教训

1. **避免不必要的集合转换**: `ToList()` 在大数据集上代价极高
2. **使用原生 API 数据结构**: `ModelItemCollection` 比 `List<ModelItem>` 更高效
3. **智能缓存策略**: 区分一次性成本和重复成本
   - 过滤后的列表应该缓存（避免重复 `ToList()`）
   - 空间索引应该缓存（与移动物体无关）
4. **识别真正的瓶颈**: 使用日志和计时器定位性能热点
5. **理解缓存的适用范围**:
   - 几何缓存：全局，模型不变就不变
   - 过滤缓存：全局，通道对象不变就不变
   - 空间索引：全局，场景对象位置不变就不变
   - 动画预计算：局部，每次路径/移动物体变化都要重算

### 修改的文件

- `ClashDetectiveIntegration.cs`:
  - 修改缓存类型为 `ModelItemCollection`
  - 优化 `BuildAllGeometryItemsCache()` 使用 Search API
  - **新增：添加 Search API 可见性过滤**
  - 添加 `_nonChannelGeometryItemsCache` 缓存过滤后的列表
  - 添加 `GetChannelObjectsCache()` 方法
  - 更新 `ClearAllCaches()` 清除所有缓存
- `SpatialIndexManager.cs`:
  - 恢复使用 `GetNonChannelGeometryItemsCache()` 获取缓存的过滤列表
  - 简化索引构建逻辑
  - 改进日志输出
- `PathAnimationManager.cs`:
  - 添加空间索引初始化检查
  - 只在首次构建空间索引，后续复用缓存
  
## 碰撞检测算法性能优化 (2025-12-10)

### 问题分析

智能去重策略实现后，通过详细的子步骤调试日志发现，单个碰撞测试的执行时间仍然较长：

```log
[2025-12-10 14:02:16.650] [DEBUG] [分组测试] 智能去重-测试添加: 临时测试添加完成, 耗时: 27ms
[2025-12-10 14:02:16.651] [DEBUG] [分组测试] 智能去重-设置几何类型: 耗时: 0ms
[2025-12-10 14:02:16.887] [DEBUG] [分组测试] 智能去重-测试执行: 临时测试运行完成, 耗时: 235ms
[2025-12-10 14:02:16.888] [DEBUG] [分组测试] 智能去重-获取测试结果: 耗时: 0ms
```

其中 `TestsRunTest()` 方法调用平均耗时约 200-250ms/次，占总检测时间的 90% 以上，成为新的性能瓶颈。

### 根本原因

1. **测试类型选择**：使用了 `ClashTestType.HardConservative`（保守硬碰撞）模式
   - 比普通 `Hard` 模式更复杂，会额外检测体积重叠但几何体不相交的情况
   - 需要处理四种特殊情况：面接触边缘、共面面重叠、边接触端点、共轴边重叠
   - 增加了大量额外的计算逻辑

2. **几何类型设置**：包含了 `PrimitiveTypes.Triangles | PrimitiveTypes.Lines | PrimitiveTypes.Points`
   - 检测所有类型的几何体，增加了检测的全面性但降低了性能
   - 点和线的碰撞检测在实际应用中很少产生有意义的结果

### 优化方案

1. **降低测试类型复杂度**：将 `HardConservative` 改为普通 `Hard` 模式
   - 普通 `Hard` 模式只检测实际相交的几何体
   - 可以显著提高性能，同时保持足够的检测精度

2. **减少几何类型**：仅保留 `PrimitiveTypes.Triangles`
   - 大多数实际碰撞都通过三角形检测就能发现
   - 去掉点和线的检测可以减少计算量

### 优化结果

优化后，单个碰撞测试的执行时间显著降低：

```log
[2025-12-10 14:15:39.277] [DEBUG] [分组测试] 智能去重-测试添加: 临时测试添加完成, 耗时: 18ms
[2025-12-10 14:15:39.278] [DEBUG] [分组测试] 智能去重-设置几何类型: 耗时: 0ms
[2025-12-10 14:15:39.412] [DEBUG] [分组测试] 智能去重-测试执行: 临时测试运行完成, 耗时: 133ms
[2025-12-10 14:15:39.412] [DEBUG] [分组测试] 智能去重-获取测试结果: 耗时: 0ms
```

**性能提升对比**：

- 测试执行时间：235ms → 133ms（约 43% 提升）
- 智能去重总耗时：~20,000ms → 16,709ms（约 16.5% 提升）
- 检测效率：确认碰撞 16 组，跳过 296 个冗余检测点（与优化前相同）

### 优化的关键代码变更

在 `ClashDetectiveIntegration.cs` 中进行了以下修改：

1. **修改测试类型**：

   ```csharp
   // 之前
   TestType = ClashTestType.HardConservative,
   
   // 之后
   TestType = ClashTestType.Hard,
   ```

2. **减少几何类型**：

   ```csharp
   // 之前
   copyTest.SelectionA.PrimitiveTypes = PrimitiveTypes.Triangles | PrimitiveTypes.Lines | PrimitiveTypes.Points;
   copyTest.SelectionB.PrimitiveTypes = PrimitiveTypes.Triangles | PrimitiveTypes.Lines | PrimitiveTypes.Points;
   
   // 之后
   copyTest.SelectionA.PrimitiveTypes = PrimitiveTypes.Triangles;
   copyTest.SelectionB.PrimitiveTypes = PrimitiveTypes.Triangles;
   ```

### 后续优化建议

1. **配置化优化参数**：
   - 添加容差值(Tolerance)配置，可根据项目需求调整检测精度
   - 提供碰撞测试类型选择选项，让用户在精度和性能间平衡

2. **批量检测优化**：
   - 实现批量碰撞检测API调用，减少Navisworks API调用开销
   - 合并相似检测任务，提高处理效率

3. **并行处理**：
   - 对独立的碰撞检测任务实现并行处理
   - 利用多核CPU资源加速检测过程

4. **碰撞预判**：
   - 添加简单的边界框碰撞预判
   - 对明显不碰撞的对象对提前过滤，避免不必要的精确检测

5. **结果缓存**：
   - 缓存相同对象对的碰撞检测结果
   - 对重复检测任务直接返回缓存结果
