# Navisworks API 使用方法指南

基于真实官方示例的正确API用法总结

 在线资源链接

  1. Autodesk Platform Services (APS)
    - 主要开发者门户：<https://aps.autodesk.com/developer/overview/navisworks>
    - 提供Navisworks集成工具和SDK
  2. AEC DevBlog
    - 官方开发博客：<https://adndevblog.typepad.com/aec/navisworks/>
    - 包含2026版本新功能和技术文章
  3. Autodesk Developer Network
    - 开发者网络：<https://www.autodesk.com/developer-network/app-store/navisworks>
    - 提供开发资源和支持
  4. 非官方在线API文档
    - ApiDocs.co：<https://apidocs.co/apps/navisworks/>
    - （注：目前仅覆盖2017-2018版本）

## 参考示例来源

基于以下官方示例文件的真实API用法：

- `C:\Users\Tellme\apps\NavisworksTransport\doc\navisworks_api\NET\examples\PlugIns\SearchComparisonPlugIn\SearchComparisonPlugIn.cs`
- `C:\Users\Tellme\apps\NavisworksTransport\doc\navisworks_api\NET\examples\PlugIns\Examiner\Examiner.cs`

## 1. 模型遍历和节点访问

### 1.1 正确的遍历方式

```csharp
// ✅ 正确：获取所有模型项
IEnumerable<ModelItem> allItems = 
    Application.ActiveDocument.Models.RootItemDescendantsAndSelf;

// ✅ 正确：遍历特定模型的所有项
foreach (Model model in document.Models)
{
    foreach (ModelItem item in model.RootItem.DescendantsAndSelf)
    {
        // 处理每个模型项
    }
}

// ✅ 正确：只获取顶级节点
foreach (Model model in document.Models)
{
    foreach (ModelItem topLevelItem in model.RootItem.Children)
    {
        // 处理顶级节点
    }
}
```

### 1.2 获取子节点

```csharp
// ✅ 正确：获取某个节点的所有后代
var childItems = selectedItem.DescendantsAndSelf.Where(x => x != selectedItem);

// ✅ 正确：只获取直接子节点
foreach (ModelItem child in parentItem.Children)
{
    // 处理直接子节点
}
```

### 1.3 遍历祖先节点

```csharp
// ✅ 正确：向上遍历父节点链
var current = selectedItem.Parent;
while (current != null)
{
    // 处理祖先节点
    current = current.Parent;
}
```

## 2. 搜索和查询

### 2.1 使用LINQ查询

```csharp
// ✅ 正确：使用LINQ查询模型项
IEnumerable<ModelItem> results = 
    Application.ActiveDocument.Models.RootItemDescendantsAndSelf
    .Where(x => 
        x.HasGeometry && 
        !x.IsHidden &&
        x.ClassDisplayName.ToLower().Contains("wall"));
```

### 2.2 使用Search类

```csharp
// ✅ 正确：使用Search API
Search search = new Search();

// 添加搜索条件
search.SearchConditions.Add(
    SearchCondition.HasCategoryByName(PropertyCategoryNames.Geometry));
search.SearchConditions.Add(
    SearchCondition.HasPropertyByName(PropertyCategoryNames.Item, DataPropertyNames.ItemHidden)
    .EqualValue(VariantData.FromBoolean(false)));

// 设置搜索范围
search.Selection.SelectAll();
search.Locations = SearchLocations.DescendantsAndSelf;

// 执行搜索
ModelItemCollection results = search.FindAll(document, false);
```

### 2.3 迭代遍历（性能对比）

```csharp
// ✅ 可用但性能较低：迭代方法
ModelItemCollection searchResults = new ModelItemCollection();
foreach (ModelItem modelItem in Application.ActiveDocument.Models.CreateCollectionFromRootItems().DescendantsAndSelf)
{
    if (modelItem.HasGeometry && !modelItem.IsHidden)
        searchResults.Add(modelItem);
}
```

## 3. 选择操作

### 3.1 操作当前选择

注意对选择进行操作，需要在主线程。

```csharp
// ✅ 正确：获取当前选择
var currentSelection = document.CurrentSelection.SelectedItems;

// ✅ 正确：清空选择
document.CurrentSelection.Clear();

// ✅ 正确：添加到选择
document.CurrentSelection.Add(modelItem);

// ✅ 正确：复制集合到选择
document.CurrentSelection.CopyFrom(modelItems);
```

## 4. 可见性控制

### 4.1 隐藏和显示

```csharp
// ✅ 正确：隐藏项目
ModelItemCollection itemsToHide = new ModelItemCollection();
itemsToHide.Add(modelItem);
document.Models.SetHidden(itemsToHide, true);

// ✅ 正确：显示项目
document.Models.SetHidden(itemsToHide, false);

// ✅ 正确：检查是否隐藏
if (modelItem.IsHidden)
{
    // 项目被隐藏
}
```

## 5. 文件导出

### 5.1 基本文件保存

```csharp
// ✅ 正确：保存NWD文件
document.SaveFile(filePath);

// ✅ 正确：指定版本保存
document.SaveFile(filePath, DocumentFileVersion.Current);
```

### 5.2 ExportToNwd API

```csharp
// ✅ 正确：使用ExportToNwd导出
var exportOptions = new NwdExportOptions();
exportOptions.ExcludeHiddenItems = true;  // 只导出可见项目
exportOptions.EmbedXrefs = false;
exportOptions.PreventObjectPropertyExport = false;

document.ExportToNwd(saveFilePath, exportOptions);
```

## 6. 性能最佳实践

### 6.1 避免的做法

```csharp
// ❌ 错误：使用不存在的API
// SearchCondition.HasAncestor(items) // 这个API不存在

// ❌ 错误：深度递归遍历
// void RecursiveTraversal(ModelItem item) // 大模型中可能导致堆栈溢出
```

### 6.2 推荐的做法

```csharp
// ✅ 推荐：使用内置的DescendantsAndSelf
var allDescendants = rootItem.DescendantsAndSelf;

// ✅ 推荐：使用LINQ进行高效查询
var filteredItems = allItems.Where(x => x.HasGeometry);

// ✅ 推荐：批量操作而不是逐个操作
ModelItemCollection batchItems = new ModelItemCollection();
// 添加所有需要处理的项目
document.Models.SetHidden(batchItems, true); // 一次性操作
```

## 7. 完整示例：多选节点导出

```csharp
public void ExportSelectedNodes(List<ModelItem> selectedItems, string filePath)
{
    var document = Application.ActiveDocument;
    var nodesToKeepVisible = new HashSet<ModelItem>();
    
    // 1. 收集需要保持可见的节点
    foreach (var selectedItem in selectedItems)
    {
        // 添加选中节点本身
        nodesToKeepVisible.Add(selectedItem);
        
        // 添加所有祖先节点
        var current = selectedItem.Parent;
        while (current != null)
        {
            nodesToKeepVisible.Add(current);
            current = current.Parent;
        }
        
        // 添加所有子节点（可选）
        var childItems = selectedItem.DescendantsAndSelf.Where(x => x != selectedItem);
        foreach (ModelItem child in childItems)
        {
            nodesToKeepVisible.Add(child);
        }
    }
    
    // 2. 收集顶级节点并决定隐藏哪些
    var itemsToHide = new ModelItemCollection();
    foreach (Model model in document.Models)
    {
        foreach (ModelItem topLevelItem in model.RootItem.Children)
        {
            bool shouldKeep = false;
            
            // 检查是否包含选中节点
            foreach (var selectedItem in selectedItems)
            {
                var current = selectedItem;
                while (current != null)
                {
                    if (current == topLevelItem)
                    {
                        shouldKeep = true;
                        break;
                    }
                    current = current.Parent;
                }
                if (shouldKeep) break;
            }
            
            if (!shouldKeep)
            {
                itemsToHide.Add(topLevelItem);
            }
        }
    }
    
    // 3. 执行隐藏和导出
    try
    {
        document.Models.SetHidden(itemsToHide, true);
        
        var exportOptions = new NwdExportOptions();
        exportOptions.ExcludeHiddenItems = true;
        document.ExportToNwd(filePath, exportOptions);
    }
    finally
    {
        // 4. 恢复可见性
        document.Models.SetHidden(itemsToHide, false);
    }
}
```

## 8. 线程安全 - 关键重要

### 8.1 Navisworks API 线程安全要求

**核心原则：所有 Navisworks API 调用必须在主 UI 线程（STA 线程）中执行**

```csharp
// ❌ 错误：在后台线程中调用 Navisworks API
await Task.Run(() =>
{
    var document = Application.ActiveDocument;  // 可能崩溃
    document.ExportToNwd(path, options);        // 会崩溃
});

// ✅ 正确：使用 Dispatcher.Invoke 确保主线程执行
await Task.Run(() =>
{
    System.Windows.Application.Current.Dispatcher.Invoke(() =>
    {
        var document = Application.ActiveDocument;
        document.ExportToNwd(path, options);  // 安全执行
    });
});
```

### 8.2 实际案例：分层导出修复

**问题场景**：`SimplifiedModelSplitterManager.ExportLayerToNwd` 方法通过后台线程调用时崩溃

```csharp
// ❌ 问题代码：导致崩溃
public bool ExportLayerToNwd(...)
{
    var document = NavisApplication.ActiveDocument;
    document.ExportToNwd(outputPath, exportOptions);  // 后台线程崩溃
}
```

**修复方案**：使用 Dispatcher.Invoke 包装所有 API 调用

```csharp
// ✅ 修复代码：线程安全
public bool ExportLayerToNwd(...)
{
    bool exportResult = false;
    Exception exportException = null;
    
    // 确保在主线程中执行所有 Navisworks API 调用
    System.Windows.Application.Current.Dispatcher.Invoke(() =>
    {
        try
        {
            var document = NavisApplication.ActiveDocument;
            
            // 保存可见性状态
            var originalVisibilityState = SaveCurrentVisibilityState(document);
            
            try
            {
                // 隐藏不需要的项目
                var itemsToHide = GetItemsToHide(...);
                document.Models.SetHidden(itemsToHide, true);
                
                // 创建导出选项
                var exportOptions = new NwdExportOptions
                {
                    ExcludeHiddenItems = true,
                    EmbedXrefs = false,
                    PreventObjectPropertyExport = false
                };
                
                // 在主线程中安全执行导出
                document.ExportToNwd(outputPath, exportOptions);
                exportResult = true;
            }
            finally
            {
                // 恢复可见性状态
                RestoreVisibilityState(document, originalVisibilityState);
            }
        }
        catch (Exception ex)
        {
            exportException = ex;
        }
    });
    
    if (exportException != null)
        throw exportException;
        
    return exportResult;
}
```

### 8.3 线程安全检查和诊断

```csharp
// ✅ 检查当前线程状态
var apartmentState = System.Threading.Thread.CurrentThread.GetApartmentState();
LogManager.Info($"当前线程状态: {apartmentState}");  // 应该是 STA

if (apartmentState != System.Threading.ApartmentState.STA)
{
    LogManager.Warning("警告：不在STA线程中，API调用可能失败");
}

// ✅ 验证是否在主线程中
bool isMainThread = System.Windows.Application.Current.Dispatcher.CheckAccess();
if (!isMainThread)
{
    LogManager.Warning("警告：不在主线程中，需要使用Dispatcher.Invoke");
}
```

### 8.4 常见线程安全问题和解决方案

| 问题场景 | 症状 | 解决方案 |
|---------|------|---------|
| 后台线程调用 API | 程序崩溃，无错误信息 | 使用 `Dispatcher.Invoke()` |
| Command.ExecuteAsync() | Task 中的 API 调用崩溃 | 在 Task 内部使用 Dispatcher |
| 异步方法调用 API | 间歇性崩溃 | 检查执行线程，确保主线程 |
| Timer 中调用 API | 定时器触发时崩溃 | Timer 回调使用 Dispatcher |

### 8.5 最佳实践模式

```csharp
// ✅ 推荐模式：安全的异步 Navisworks API 调用
public async Task<bool> SafeNavisworksOperationAsync()
{
    // 1. 后台准备数据
    var preparedData = await Task.Run(() =>
    {
        // 在后台线程中进行数据准备（不涉及 Navisworks API）
        return PrepareDataSafely();
    });
    
    // 2. 主线程执行 API 调用
    bool result = false;
    await System.Windows.Application.Current.Dispatcher.InvokeAsync(() =>
    {
        // 所有 Navisworks API 调用都在主线程中
        var document = Application.ActiveDocument;
        result = document.SomeNavisworksOperation(preparedData);
    });
    
    return result;
}

// ✅ 推荐模式：批量 API 操作
public void BatchNavisworksOperations(List<ModelItem> items)
{
    System.Windows.Application.Current.Dispatcher.Invoke(() =>
    {
        var document = Application.ActiveDocument;
        
        // 批量操作，避免多次线程切换
        var itemCollection = new ModelItemCollection();
        foreach (var item in items)
        {
            itemCollection.Add(item);
        }
        
        // 一次性完成所有操作
        document.Models.SetHidden(itemCollection, true);
        document.CurrentSelection.CopyFrom(itemCollection);
    });
}
```

## 9. 常用属性和方法速查

### ModelItem 常用属性

- `HasGeometry` - 是否有几何体
- `IsHidden` - 是否隐藏
- `IsRequired` - 是否必需
- `IsInsert` - 是否为插入对象
- `IsLayer` - 是否为图层
- `DisplayName` - 显示名称
- `ClassName` - 类名
- `ClassDisplayName` - 类显示名称
- `Parent` - 父节点
- `Children` - 子节点集合
- `DescendantsAndSelf` - 所有后代节点（包括自己）

### Document 常用方法

- `SaveFile(string path)` - 保存文件
- `ExportToNwd(string path, NwdExportOptions options)` - 导出NWD
- `CurrentSelection` - 当前选择
- `Models` - 模型集合

### Models 常用方法

- `SetHidden(ModelItemCollection items, bool hidden)` - 设置隐藏状态
- `SetRequired(ModelItemCollection items, bool required)` - 设置必需状态
- `RootItemDescendantsAndSelf` - 所有根项目的后代

## 10. 错误避免指南

1. **线程安全是第一要务**：所有 Navisworks API 调用必须在主 UI 线程中执行
2. **不要使用不存在的API**：如 `SearchCondition.HasAncestor`
3. **避免深度递归**：使用内置的 `DescendantsAndSelf` 代替手写递归
4. **批量操作**：使用 `ModelItemCollection` 进行批量设置，而不是逐个操作
5. **正确的命名空间**：确保引用 `using Autodesk.Navisworks.Api;`
6. **异常处理**：文件操作和API调用要适当处理异常
7. **资源清理**：隐藏操作后要恢复原始状态
8. **线程状态检查**：在关键操作前验证线程状态（STA）
9. **Dispatcher 模式**：后台线程中需要调用 API 时，始终使用 Dispatcher.Invoke

## 11. Transform 变换操作

### 11.1 官方文档结论

以下几条是本项目后续关于变换问题的硬基线，优先级高于历史经验和猜测：

- `ModelItem.Transform`
  - 官方说明：`Returns the Transform attached to this item in the original source design file`
  - 结论：它表示原始设计文件变换，不是当前显示姿态
  - 官方页：`doc/navisworks_api/NET/documentation/NetAPIHtml/html/P_Autodesk_Navisworks_Api_ModelItem_Transform.htm`
- `ModelGeometry.ActiveTransform`
  - 官方说明：`Returns the currently active transform of the geometry.`
  - 结论：它表示当前几何实际生效的变换
  - 官方页：`doc/navisworks_api/NET/documentation/NetAPIHtml/html/P_Autodesk_Navisworks_Api_ModelGeometry_ActiveTransform.htm`
- `DocumentModels.OverridePermanentTransform(...)`
  - 官方说明：`Apply an incremental transform to a selection.`
  - 结论：这是增量变换，不是把对象直接设成目标绝对姿态
  - 官方页：`doc/navisworks_api/NET/documentation/NetAPIHtml/html/M_Autodesk_Navisworks_Api_DocumentParts_DocumentModels_OverridePermanentTransform_3_131351c5.htm`
- `DocumentModels.ResetPermanentTransform(...)`
  - 官方说明：`Reset incremental transforms for all model items contained in the selection.`
  - 结论：它清掉的是永久增量层
  - 官方页：`doc/navisworks_api/NET/documentation/NetAPIHtml/html/M_Autodesk_Navisworks_Api_DocumentParts_DocumentModels_ResetPermanentTransform_1_75193b86.htm`
- `ModelGeometry`
  - 官方成员表明确提供：
    - `OriginalTransform`
    - `PermanentOverrideTransform`
    - `PermanentTransform`
    - `ActiveTransform`
  - 官方页：`doc/navisworks_api/NET/documentation/NetAPIHtml/html/AllMembers_T_Autodesk_Navisworks_Api_ModelGeometry.htm`

### 11.2 变换层级的正确理解

以后项目里统一这样理解：

- `ModelItem.Transform`
  - 原始设计文件层变换
  - 只读
  - 不反映 `OverridePermanentTransform` 后的当前状态
- `ModelItem.Geometry` / `ModelItem.FindFirstGeometry()`
  - 进入几何层的正式入口
- `ModelGeometry.OriginalTransform`
  - 几何加载时的原始变换
- `ModelGeometry.PermanentOverrideTransform`
  - 施加在几何上的永久增量覆盖层
- `ModelGeometry.PermanentTransform`
  - 原始变换和永久覆盖组合后的结果
- `ModelGeometry.ActiveTransform`
  - 当前真正生效的几何变换
  - 一般应优先用它判断当前姿态/当前位置

### 11.3 项目中的推荐读取入口

#### 11.3.1 读“当前实际姿态”

优先顺序：

1. `ModelItem.FindFirstGeometry()` 或 `ModelItem.Geometry`
2. `ModelGeometry.ActiveTransform`
3. 必要时再看 `ModelGeometry.PermanentTransform`

```csharp
ModelGeometry geometry = item.FindFirstGeometry();
if (geometry != null)
{
    Transform3D current = geometry.ActiveTransform;
}
```

#### 11.3.2 读“原始姿态”

优先顺序：

1. `ModelGeometry.OriginalTransform`
2. `ModelItem.Transform`

```csharp
ModelGeometry geometry = item.FindFirstGeometry();
Transform3D original = geometry != null ? geometry.OriginalTransform : item.Transform;
```

#### 11.3.3 读“当前实际位置”

推荐顺序：

1. 如果语义上需要几何真实中心，优先 `geometry.BoundingBox.Center`
2. 如果业务上有自己定义的 tracked point，就用业务 tracked point
3. 不要把 `ModelItem.Transform.Translation` 直接当成 override 后实际位置

### 11.4 `OverridePermanentTransform` 的正确语义

`OverridePermanentTransform(items, transform, updateModelTransform)` 的本质是：

- 对选中的对象施加增量变换
- 不是设置对象的最终世界姿态

另外官方 Remarks 里还有一条很重要：

- 如果 selection 包含文件，而且 `updateModelTransform = true`
- 则不是对 fragments 施加变换
- 而是更新 File Units and Transform

这也是项目里必须区分的两种用法：

- `updateModelTransform = false`
  - 对 item/geometry 的永久增量层操作
- `updateModelTransform = true`
  - 更新 model/file 层的 units and transform

### 11.5 `ResetPermanentTransform` 的正确语义

`ResetPermanentTransform(items)` 只做一件事：

- 清除选中对象的永久增量变换

它不会修改：

- 原始设计文件几何
- `ModelItem.Transform`
- `ModelGeometry.OriginalTransform`

所以“恢复到原始状态”要明确到底指哪一层：

- 对真实物体：
  - 通常是清掉 override，回到原始几何状态
- 对虚拟物体：
  - 如果业务尺寸是后续叠加出来的，单纯 reset 会把业务尺寸也一起清掉
  - 因此虚拟物体常常需要 reset 后再重放业务尺寸

### 11.5.1 真实物体 Rail 起点旋转的两个关键经验

这是本项目在真实物体沿 `Rail` 路径“移动到起点”与“角度调整”排查中确认的两条硬结论。

#### A. 正确理解 Navisworks 的“增量旋转”

`document.Models.OverridePermanentTransform(items, transform, false)` 施加的是**增量层**，不是“把对象直接设成最终姿态”。

对真实物体要区分 3 个量：

1. `OriginalTransform`
   - 原始设计文件里的姿态
2. `PermanentOverrideTransform`
   - 我们写进去的增量/覆盖姿态
3. `PermanentTransform / ActiveTransform`
   - Navisworks 最终真正显示出来的姿态

项目实测中，真实物体常见关系应按下面理解：

```text
最终显示姿态 = OriginalTransform × PermanentOverrideTransform
```

因此：

- 如果你手上拿到的是“最终想看到的目标姿态”
- 不能直接把它当 `PermanentOverrideTransform` 写进去
- 必须先换算出真正应该写入的 override 姿态

否则就会出现：

- 目标姿态日志看起来是对的
- `OverridePermanentTransform` 也写进去了
- 但最终 `ActiveTransform` 仍然被原始模型姿态再转一次

这类问题在真实物体上非常常见，尤其是 Revit 导入件。

#### B. 正确区分“宿主坐标系语义”和“物体自身坐标系语义”

UI 里的角度调整，用户理解的一定是**宿主坐标系**：

- `X/Y/Z` 表示宿主世界 `X/Y/Z`

但 Navisworks 对真实物体施加旋转时，底层消费的仍然是**物体自身轴语义**。

因此正确链路必须是：

```text
宿主世界轴角度输入
-> 映射到当前真实物体的本地轴
-> 再生成本地 correction quaternion
-> 最后交给 OverridePermanentTransform 链
```

不能直接把“宿主世界 X/Y/Z”角度传给一个只接受物体本地轴语义的方法。

#### C. 哪种映射是对的

真实物体这里至少有两套看起来很像、但不能混用的映射：

1. **宿主世界轴 -> raw 本地轴**
   - 用于 UI 角度调整
   - 例如：宿主世界 `X` 对应物体本地 `+Y`
2. **宿主语义轴 -> raw 本地轴**
   - 用于路径姿态解释
   - 例如：`Rail` 的 `forward/up` 选择

这两套映射在 `up` 轴上可能碰巧一致，但在水平轴上经常不同。

项目这次问题的根因之一，就是把：

- “路径姿态用的宿主语义映射”

误当成了：

- “角度调整用的宿主世界轴映射”

结果表现为：

- `Y` 调整看起来正确
- `X/Z` 调整方向却反了

#### D. 项目中的推荐实现

对真实物体：

1. 先从 fragment representative frame 读取 `rawAxisX / rawAxisY / rawAxisZ`
2. 单独解析：
   - 宿主世界 `X/Y/Z` 分别对应哪根 raw 本地轴
   - 宿主语义 `forward/up` 对应哪根 raw 本地轴
3. UI 角度调整只使用“宿主世界轴映射”
4. 路径姿态求解只使用“宿主语义映射”
5. 不要让这两套映射复用同一组字段名后再靠上下文猜

一句话记忆：

- **角度调整看宿主世界轴**
- **路径姿态看业务语义轴**
- **最终落到 Navisworks 时，始终要转回物体自身轴**

### 11.6 常见误区

#### 11.6.1 误区：`ModelItem.Transform` 代表当前姿态

错误：

```csharp
Transform3D current = item.Transform; // 这不是当前 override 后姿态
```

正确：

```csharp
ModelGeometry geometry = item.FindFirstGeometry();
Transform3D current = geometry != null ? geometry.ActiveTransform : item.Transform;
```

#### 11.6.2 误区：`OverridePermanentTransform` 是绝对落位

错误理解：

- “我把目标旋转/平移直接传进去，显示结果就应该等于它”

正确理解：

- 这是增量层
- 最终显示结果要结合 `OriginalTransform / PermanentTransform / ActiveTransform` 一起看

#### 11.6.3 误区：只看 `Transform` 不看 `Geometry`

对于很多真实物体和虚拟物体，真正决定当前显示姿态的是：

- `ModelGeometry.ActiveTransform`
- 或 fragment 层矩阵

而不是：

- `ModelItem.Transform`

### 11.7 Fragment 与 Geometry 的关系

项目里对 fragment 的定位要统一：

- fragment 适合做：
  - 真实物体参考姿态解释
  - fragment 代表姿态统计
  - COM 层几何分析
- fragment 不应优先替代 `ModelGeometry.ActiveTransform` 来读取“当前显示姿态”

当前结论：

- 读“当前实际姿态”优先用 `ModelGeometry`
- 读“真实物体参考姿态/原始语义姿态”时，fragment 仍然有用

### 11.8 COM Fragment 变换的使用规则

COM fragment 提供的是：

- `GetLocalToWorldMatrix()`
  - fragment 从本地到世界的完整矩阵

项目当前实测结论：

- fragment 矩阵解释必须和 `ModelGeometry.ActiveTransform` 对齐验证
- 不要只凭经验猜行列顺序
- 当前项目排查中已经验证过：
  - 解释 fragment 矩阵时，必须以 `.NET ModelGeometry` 的结果为标尺

### 11.9 推荐代码模式

#### 11.9.1 读取当前几何姿态

```csharp
public static bool TryGetCurrentGeometryTransform(ModelItem item, out Transform3D transform)
{
    transform = Transform3D.Identity;
    if (item == null)
    {
        return false;
    }

    ModelGeometry geometry = item.FindFirstGeometry();
    if (geometry == null)
    {
        return false;
    }

    transform = geometry.ActiveTransform;
    return true;
}
```

#### 11.9.2 清掉增量层后恢复到原始状态

```csharp
var doc = Application.ActiveDocument;
var items = new ModelItemCollection { item };
doc.Models.ResetPermanentTransform(items);
```

#### 11.9.3 对 item 施加增量变换

```csharp
var doc = Application.ActiveDocument;
var items = new ModelItemCollection { item };
doc.Models.OverridePermanentTransform(items, incrementalTransform, false);
```

#### 11.9.4 对 model 层更新 Units and Transform

```csharp
Document doc = Application.ActiveDocument;
Model model = doc.Models[0];
Transform3D oldTransform = model.Transform;
Transform3DComponents components = oldTransform.Factor();

components.Translation = new Vector3D(x, y, z);
components.Rotation = new Rotation3D(new UnitVector3D(0, 0, 1), angleInRadians);
components.Scale = new Vector3D(scaleX, scaleY, scaleZ);

Transform3D newTransform = components.Combine();
doc.Models.SetModelUnitsAndTransform(model, Units.Meters, newTransform, true);
```

#### 11.9.5 矩阵语义与行列顺序速查

这是项目后续排查姿态问题时的硬基线。

##### A. `Transform3D.Linear` / `Matrix3` 的项目语义

项目当前统一按下面这套语义理解：

- `Transform3D.Linear` 是 3x3 线性部分
- `Matrix3.Get(row, column)` 的参数顺序是：
  - 先 `row`
  - 后 `column`
- 在项目里，`Linear` 的 **列** 表示“局部轴在世界中的方向”
  - 第 1 列 = 本地 `X` 轴在世界中的方向
  - 第 2 列 = 本地 `Y` 轴在世界中的方向
  - 第 3 列 = 本地 `Z` 轴在世界中的方向

也就是说：

```csharp
Vector3D worldX = new Vector3D(
    linear.Get(0, 0),
    linear.Get(1, 0),
    linear.Get(2, 0));

Vector3D worldY = new Vector3D(
    linear.Get(0, 1),
    linear.Get(1, 1),
    linear.Get(2, 1));

Vector3D worldZ = new Vector3D(
    linear.Get(0, 2),
    linear.Get(1, 2),
    linear.Get(2, 2));
```

##### B. `Matrix3` 构造时的顺序

项目里当前按“逐行传参”构造 `Matrix3`：

```csharp
var linear = new Matrix3(
    m00, m01, m02,
    m10, m11, m12,
    m20, m21, m22);
```

其中：

- 第 1 行 = `(m00, m01, m02)`
- 第 2 行 = `(m10, m11, m12)`
- 第 3 行 = `(m20, m21, m22)`

如果你的业务语义是“列 = 局部轴”，那么组矩阵时应写成：

```csharp
linear = new Matrix3(
    worldX.X, worldY.X, worldZ.X,
    worldX.Y, worldY.Y, worldZ.Y,
    worldX.Z, worldY.Z, worldZ.Z);
```

##### C. `System.Numerics.Matrix4x4` 的项目用法

项目里把 `Matrix4x4` 也按同样的“列 = 基向量”语义使用：

```csharp
Matrix4x4 basis = new Matrix4x4(
    worldX.X, worldY.X, worldZ.X, 0f,
    worldX.Y, worldY.Y, worldZ.Y, 0f,
    worldX.Z, worldY.Z, worldZ.Z, 0f,
    0f,       0f,       0f,       1f);
```

在这套写法下：

- 第 1 列是局部 `X` 轴
- 第 2 列是局部 `Y` 轴
- 第 3 列是局部 `Z` 轴

然后再用：

```csharp
Quaternion q = Quaternion.CreateFromRotationMatrix(basis);
```

##### D. 最容易犯错的地方

1. 把“列是局部轴”误看成“行是局部轴”
2. 读取 `linear.Get(0, 1)` 时，以为拿到的是 `Y.X`，但后续又按行语义消费
3. 把 `Matrix3` 直接喂给手写 quaternion 公式时，没有先确认公式使用的是“行主序矩阵”还是“列向量基矩阵”
4. 日志里看到 `X=(...), Y=(...), Z=(...)` 时，没有先确认它打印的是“列”还是“行”

##### E. 项目中的统一建议

1. 只要是在解释物体三轴，就统一按“列 = 局部轴在世界中的方向”
2. `Matrix3 -> Quaternion` 优先使用 `Matrix4x4 + Quaternion.CreateFromRotationMatrix(...)`
3. 不要在不同文件里混用两套相反的行列语义
4. 新增日志时，明确写出“这里打印的是列向量/局部轴”，不要只写 `X/Y/Z`

### 11.10 项目级硬约束

1. 读当前姿态时，不要再默认用 `ModelItem.Transform`
2. 真实物体参考姿态和当前姿态是两回事，不要混
3. `OverridePermanentTransform` 是增量，不是绝对落位
4. `ResetPermanentTransform` 清的是增量层，不是原始几何
5. 涉及当前显示姿态时，优先看 `ModelGeometry.ActiveTransform`
6. 涉及 fragment 矩阵解释时，先与 `ModelGeometry.ActiveTransform` 对齐验证
## 12. Item属性和自定义属性访问

基于官方示例的正确属性访问方法总结。

### 12.1 NET API 属性访问方法

#### 基础属性访问（基于 Examiner.cs）

```csharp
// ✅ 通过PropertyCategories查找特定分类
var category = item.PropertyCategories.FindCategoryByDisplayName("Material");

// ✅ 访问基础属性
string displayName = item.DisplayName;
string className = item.ClassName;  
string classDisplayName = item.ClassDisplayName;
bool hasGeometry = item.HasGeometry;
bool isHidden = item.IsHidden;
bool isRequired = item.IsRequired;

// ✅ 通过LINQ查询特定属性的项目
IEnumerable<ModelItem> itemsWithMaterial = 
    Application.ActiveDocument.Models.RootItemDescendantsAndSelf
    .Where(x => 
        x.PropertyCategories.FindCategoryByDisplayName("Material") != null);
```

#### 高级属性搜索（基于 SearchComparisonPlugIn.cs）

```csharp
// ✅ 使用预定义属性分类和属性名进行搜索
Search search = new Search();

// 搜索有几何体的项目
search.SearchConditions.Add(
    SearchCondition.HasCategoryByName(PropertyCategoryNames.Geometry));

// 搜索非隐藏的项目
search.SearchConditions.Add(
    SearchCondition.HasPropertyByName(PropertyCategoryNames.Item, DataPropertyNames.ItemHidden)
    .EqualValue(VariantData.FromBoolean(false)));

// 设置搜索范围并执行
search.Selection.SelectAll();
search.Locations = SearchLocations.DescendantsAndSelf;
ModelItemCollection results = search.FindAll(document, false);
```

#### 属性分类和属性名常量

```csharp
// ✅ 使用预定义常量访问标准属性
// PropertyCategoryNames 包含：
// - PropertyCategoryNames.Item (项目属性)
// - PropertyCategoryNames.Geometry (几何属性)
// - PropertyCategoryNames.Material (材质属性)

// DataPropertyNames 包含：
// - DataPropertyNames.ItemHidden (隐藏状态)
// - DataPropertyNames.ItemRequired (必需状态)
// 等等...
```

### 12.2 COM API 属性访问方法（基于 AutoUserPropsExample.cs）

#### 获取和遍历属性

```csharp
// ✅ 获取选中对象的属性节点
InwOpSelection2 selection = m_state.CurrentSelection as InwOpSelection2;
if (selection.Paths().Count > 0)
{
    InwGUIPropertyNode2 propertyNode = 
        m_state.GetGUIPropertyNode(selection.Paths()[1], true) as InwGUIPropertyNode2;
    
    // 遍历所有属性分类
    foreach (InwGUIAttribute2 guiAttribute in propertyNode.GUIAttributes())
    {
        Console.WriteLine($"分类: {guiAttribute.ClassName}");
        Console.WriteLine($"显示名: {guiAttribute.ClassUserName}");
        Console.WriteLine($"用户自定义: {guiAttribute.UserDefined}");
        
        // 遍历分类中的所有属性
        foreach (InwOaProperty property in guiAttribute.Properties())
        {
            string propertyName = property.name;        // 内部名称
            string displayName = property.UserName;     // 显示名称
            string value = property.value;              // 属性值
            
            Console.WriteLine($"  {displayName}({propertyName}) = {value}");
        }
    }
}
```

#### 添加自定义属性

```csharp
// ✅ 创建新的自定义属性
private void AddCustomProperty(string categoryName, string internalName, string value)
{
    InwOpSelection2 selection = m_state.CurrentSelection as InwOpSelection2;
    if (selection.Paths().Count > 0)
    {
        InwGUIPropertyNode2 propertyNode = 
            m_state.GetGUIPropertyNode(selection.Paths()[1], true) as InwGUIPropertyNode2;
        
        // 创建属性容器
        InwOaPropertyVec propertyVector = 
            m_state.ObjectFactory(nwEObjectType.eObjectType_nwOaPropertyVec);
        
        // 创建单个属性
        InwOaProperty property = 
            m_state.ObjectFactory(nwEObjectType.eObjectType_nwOaProperty);
        property.name = "CustomProperty1";          // 内部名称
        property.UserName = "自定义属性1";          // 显示名称
        property.value = value;                     // 属性值
        
        // 添加到容器
        propertyVector.Properties().Add(property);
        
        // 设置到对象上
        propertyNode.SetUserDefined(0, categoryName, internalName, propertyVector);
    }
}

// ✅ 使用示例
AddCustomProperty("自定义分类", "Custom_Category", "自定义值");
```

#### 修改现有自定义属性

```csharp
// ✅ 正确的修改方法（基于实际修复经验和官方AutoUserPropsExample示例）
private void UpdateCustomProperty(string categoryName, string internalName, string newValue)
{
    InwOpSelection2 selection = m_state.CurrentSelection as InwOpSelection2;
    if (selection.Paths().Count > 0)
    {
        InwGUIPropertyNode2 propertyNode = 
            m_state.GetGUIPropertyNode(selection.Paths()[1], true) as InwGUIPropertyNode2;
        
        // 🔍 关键第一步：查找现有属性分类的正确索引
        int existingIndex = GetFloorAttributeIndex(propertyNode, categoryName);
        
        // 创建新的属性内容
        InwOaPropertyVec propertyVector = 
            m_state.ObjectFactory(nwEObjectType.eObjectType_nwOaPropertyVec);
        
        InwOaProperty property = 
            m_state.ObjectFactory(nwEObjectType.eObjectType_nwOaProperty);
        property.name = "Floor_Level";
        property.UserName = "楼层";
        property.value = newValue;
        
        propertyVector.Properties().Add(property);
        
        if (existingIndex >= 0)
        {
            // 🎯 存在属性时：使用正确的索引进行更新
            // 注意：SetUserDefined的索引是从1开始的
            int updateIndex = existingIndex + 1;
            propertyNode.SetUserDefined(updateIndex, categoryName, internalName, propertyVector);
        }
        else
        {
            // 🆕 不存在属性时：使用索引0创建新属性分类
            propertyNode.SetUserDefined(0, categoryName, internalName, propertyVector);
        }
    }
}

// ✅ 查找现有属性分类索引的正确方法
private int GetFloorAttributeIndex(InwGUIPropertyNode2 propertyNode, string categoryName)
{
    int userDefinedIndex = 0; // 用户定义属性的索引计数器
    
    foreach (InwGUIAttribute2 attribute in propertyNode.GUIAttributes())
    {
        if (attribute.UserDefined)
        {
            // ⚠️ 关键修复：只需要匹配ClassUserName，不需要匹配ClassName
            // ClassName是系统生成的（如"LcOaPropOverrideCat"），不是我们控制的
            if (attribute.ClassUserName == categoryName)
            {
                return userDefinedIndex; // 返回在用户定义属性中的索引位置
            }
            userDefinedIndex++;
        }
    }
    return -1; // 未找到返回-1
}

// ✅ 使用示例 - 正确的更新方式
// 第一次设置：创建新属性
UpdateCustomProperty("分层信息", "Floor_Category", "F1");
// 第二次设置：找到并更新现有属性
UpdateCustomProperty("分层信息", "Floor_Category", "F2");
// 第三次设置：继续更新同一个属性
UpdateCustomProperty("分层信息", "Floor_Category", "F3");
```

**关键要点**（基于实际修复经验）：

- **动态索引查找**：不能使用硬编码索引，必须动态查找现有属性的位置
- **只匹配ClassUserName**：`ClassName`是系统生成的标识符（如`LcOaPropOverrideCat`），我们无法控制
- **索引转换规则**：查找返回的是0基索引，`SetUserDefined`使用的是1基索引
- **创建vs更新**：index=0表示创建新属性，index>0表示更新指定位置的属性

#### 删除自定义属性

```csharp
// ✅ 删除指定的自定义属性分类
private void RemoveCustomProperty()
{
    InwOpSelection2 selection = m_state.CurrentSelection as InwOpSelection2;
    if (selection.Paths().Count > 0)
    {
        InwGUIPropertyNode2 propertyNode = 
            m_state.GetGUIPropertyNode(selection.Paths()[1], true) as InwGUIPropertyNode2;
        
        // 删除索引为0的用户自定义属性分类
        propertyNode.RemoveUserDefined(0);
    }
}
```

### 12.3 两种API的选择建议

| 操作类型 | 推荐API | 理由 |
|---------|---------|------|
| 搜索和过滤 | NET API | LINQ查询更灵活，性能更好 |
| 读取标准属性 | NET API | 类型安全，代码简洁 |
| 添加/修改自定义属性 | COM API | 提供完整的属性操作能力 |
| 删除自定义属性 | COM API | NET API不支持属性删除 |
| 批量属性操作 | NET API | 支持ModelItemCollection批量操作 |

### 12.4 属性操作最佳实践

```csharp
// ✅ 推荐模式：结合两种API的优势
public void ProcessItemsWithCustomProperties()
{
    // 1. 使用NET API进行搜索和过滤
    var itemsWithGeometry = Application.ActiveDocument.Models.RootItemDescendantsAndSelf
        .Where(item => item.HasGeometry && !item.IsHidden)
        .ToList();
    
    // 2. 对每个项目使用COM API添加自定义属性
    foreach (ModelItem item in itemsWithGeometry)
    {
        // 选中当前项目
        Application.ActiveDocument.CurrentSelection.Clear();
        Application.ActiveDocument.CurrentSelection.Add(item);
        
        // 使用COM API添加自定义属性
        AddCustomProperty("物流信息", "Logistics_Info", $"处理时间: {DateTime.Now}");
    }
}
```

### 12.5 常见属性操作示例

```csharp
// ✅ 检查项目是否有特定属性分类
public bool HasPropertyCategory(ModelItem item, string categoryName)
{
    return item.PropertyCategories.FindCategoryByDisplayName(categoryName) != null;
}

// ✅ 获取项目的所有属性信息（用于调试）
public string GetItemPropertyInfo(ModelItem item)
{
    var sb = new StringBuilder();
    sb.AppendLine($"项目: {item.DisplayName}");
    
    foreach (PropertyCategory category in item.PropertyCategories)
    {
        sb.AppendLine($"  分类: {category.DisplayName}");
        foreach (DataProperty property in category.Properties)
        {
            sb.AppendLine($"    {property.DisplayName}: {property.Value}");
        }
    }
    
    return sb.ToString();
}

// ✅ 基于属性值进行复杂搜索
public List<ModelItem> FindItemsByPropertyValue(string categoryName, string propertyName, string searchValue)
{
    return Application.ActiveDocument.Models.RootItemDescendantsAndSelf
        .Where(item => 
        {
            var category = item.PropertyCategories.FindCategoryByDisplayName(categoryName);
            if (category == null) return false;
            
            var property = category.Properties.FirstOrDefault(p => p.DisplayName == propertyName);
            return property != null && property.Value.ToString().Contains(searchValue);
        })
        .ToList();
}
```

### 12.6 Item属性的可编辑性分析

根据官方示例和Navisworks API设计，**大部分标准Item属性是只读的**，但有**少数属性可以编辑**。

#### 可编辑的Item属性

**1. 可见性和状态属性（通过Models API编辑）**

```csharp
// ✅ 可以修改：隐藏状态
document.Models.SetHidden(modelItemCollection, true);

// ✅ 可以修改：必需状态  
document.Models.SetRequired(modelItemCollection, true);

// ✅ 可以修改：颜色覆盖
document.Models.OverridePermanentColor(modelItemCollection, Color.Red);

// ✅ 可以修改：透明度覆盖
document.Models.OverridePermanentTransparency(modelItemCollection, 0.5);

// ✅ 可以修改：Transform变换
document.Models.OverridePermanentTransform(modelItemCollection, transform, false);
```

**2. 基于官方Examiner.cs示例的确认**

```csharp
// 官方示例中的这些操作证实了这些属性是可修改的
document.Models.SetRequired(items, (required == SearchForm.ChangeDecision.Yes));
document.Models.SetHidden(items, (hidden == SearchForm.ChangeDecision.Yes));
document.Models.OverridePermanentColor(items, overrideColor);
document.Models.OverridePermanentTransparency(items, overrideTransparencyValue);
```

#### 只读的Item属性

**1. 基础标识属性（只读）**

```csharp
// ❌ 只读：无法修改，由原始CAD文件决定
item.DisplayName        // 显示名称
item.ClassName          // 类名
item.ClassDisplayName   // 类显示名称
item.HasGeometry        // 几何体标志
item.IsInsert          // 插入对象标志
item.IsLayer           // 图层标志
```

**2. 结构关系属性（只读）**

```csharp
// ❌ 只读：结构关系由模型文件决定，无法通过API修改
item.Parent            // 父节点
item.Children          // 子节点
item.Ancestors         // 祖先节点
item.Descendants       // 后代节点
```

**3. 几何和材质属性（只读）**

```csharp
// ❌ 只读：由原始CAD文件决定
item.Geometry          // 几何信息
item.BoundingBox()     // 包围盒
item.PropertyCategories // 标准属性分类（但可通过COM API添加自定义分类）
```

#### 特殊情况：自定义属性完全可编辑

通过COM API可以**完全控制自定义属性**：

```csharp
// ✅ 完全可编辑：自定义属性
// 添加新的自定义属性
propertyNode.SetUserDefined(index, categoryName, internalName, propertyVector);

// 修改现有自定义属性（重新设置）
propertyNode.SetUserDefined(index, categoryName, internalName, newPropertyVector);

// 删除自定义属性
propertyNode.RemoveUserDefined(index);
```

#### 物流插件的实际应用策略

**1. 使用可编辑的标准属性进行状态管理**

```csharp
// 物流分类可见性管理
document.Models.SetHidden(obstacleItems, true);              // 隐藏障碍物
document.Models.SetHidden(loadingZoneItems, false);          // 显示装卸区

// 路径可视化
document.Models.OverridePermanentColor(pathItems, pathColor);        // 路径颜色标记
document.Models.OverridePermanentColor(selectedItems, highlightColor); // 选中高亮

// 动画和移动
document.Models.OverridePermanentTransform(movableItems, newTransform, false); // 物体移动动画
```

**2. 使用自定义属性存储业务数据**

```csharp
// 存储物流分类信息
AddCustomProperty("物流信息", "Logistics_Category", "装卸区");
AddCustomProperty("物流信息", "Access_Level", "高优先级");
AddCustomProperty("物流信息", "Capacity", "1000kg");

// 存储路径规划数据
AddCustomProperty("路径信息", "Path_ID", "Path_001");
AddCustomProperty("路径信息", "Path_Length", "15.5m");
AddCustomProperty("路径信息", "Travel_Time", "120s");

// 存储碰撞检测结果
AddCustomProperty("碰撞信息", "Collision_Status", "Safe");
AddCustomProperty("碰撞信息", "Last_Check", DateTime.Now.ToString());
```

**3. 只读属性用于查询和智能分析**

```csharp
// 基于只读属性进行智能空间分析
var suitableForStorage = items.Where(x => 
    x.HasGeometry && 
    x.ClassDisplayName.Contains("Room") &&
    !x.IsHidden &&
    x.BoundingBox().Max.Z > 3.0); // 高度足够的房间

// 基于结构关系进行逻辑分组
var floorItems = selectedFloor.DescendantsAndSelf
    .Where(x => x.HasGeometry);
```

**4. 综合应用示例：物流节点标记**

```csharp
public void MarkAsLogisticsNode(ModelItem item, string category, Dictionary<string, string> properties)
{
    var itemCollection = new ModelItemCollection { item };
    
    // 1. 使用标准可编辑属性设置可视化
    Color categoryColor = GetCategoryColor(category);
    document.Models.OverridePermanentColor(itemCollection, categoryColor);
    document.Models.SetRequired(itemCollection, true); // 标记为重要
    
    // 2. 使用自定义属性存储详细信息
    document.CurrentSelection.Clear();
    document.CurrentSelection.Add(item);
    
    AddCustomProperty("物流分类", "Category", category);
    foreach (var kvp in properties)
    {
        AddCustomProperty("物流属性", kvp.Key, kvp.Value);
    }
    
    // 3. 基于只读属性进行验证
    if (!item.HasGeometry)
    {
        LogManager.Warning($"警告：{item.DisplayName} 没有几何体，可能不适合作为物流节点");
    }
}
```

#### 属性编辑最佳实践

| 用途 | 推荐方法 | API类型 | 特点 |
|------|----------|---------|------|
| 状态标记 | SetHidden, SetRequired | NET API | 影响显示和选择 |
| 可视化 | OverridePermanentColor | NET API | 临时视觉效果 |
| 空间变换 | OverridePermanentTransform | NET API | 支持动画 |
| 业务数据 | SetUserDefined | COM API | 持久化存储 |
| 查询分析 | 只读属性 + LINQ | NET API | 高性能筛选 |

### 12.7 属性操作注意事项

1. **线程安全**：所有属性操作都必须在主UI线程中执行
2. **选择状态**：COM API属性操作需要先选中目标对象
3. **性能考虑**：大批量属性读取时，NET API性能更佳
4. **属性持久化**：自定义属性会保存在NWD文件中，标准属性覆盖是临时的
5. **属性索引**：COM API中的用户自定义属性使用索引管理
6. **错误处理**：属性不存在时API会返回null，需要检查
7. **属性类型限制**：只读属性无法通过API修改，只能通过可编辑API间接影响
8. **覆盖vs原始**：OverridePermanent系列方法是覆盖原始属性，可以恢复

### 12.8 COM API 自定义属性重要修复经验

基于实际生产环境中发现的问题和修复经验，这里记录COM API操作自定义属性时的关键要点。

#### 🚨 常见陷阱：重复创建属性分类

**问题现象**：对同一对象多次设置自定义属性时，会创建多个同名的属性分类，而不是更新现有属性。

**错误做法**：

```csharp
// ❌ 错误：硬编码索引会导致重复创建
propertyNode.SetUserDefined(1, "分层信息", "Floor_Category", propertyVector);
```

**正确做法**：

```csharp
// ✅ 正确：动态查找现有属性的索引
int existingIndex = GetFloorAttributeIndex(propertyNode, "分层信息");
if (existingIndex >= 0)
{
    // 更新现有属性（注意索引转换：查找用0基，设置用1基）
    propertyNode.SetUserDefined(existingIndex + 1, "分层信息", "Floor_Category", propertyVector);
}
else
{
    // 创建新属性
    propertyNode.SetUserDefined(0, "分层信息", "Floor_Category", propertyVector);
}
```

#### 🔍 ClassName vs ClassUserName 的区别

这是导致重复创建问题的根本原因：

**ClassUserName**：用户定义的显示名称，由我们控制

```csharp
// 我们定义的显示名称
attribute.ClassUserName == "分层信息"
```

**ClassName**：系统生成的内部标识符，我们无法控制

```csharp
// 系统生成的内部名称，每次可能不同
attribute.ClassName == "LcOaPropOverrideCat"  // 系统生成，不可预测
```

**修复要点**：

```csharp
// ❌ 错误：同时匹配两个条件会导致找不到现有属性
if (attribute.UserDefined && 
    attribute.ClassUserName == FLOOR_CATEGORY &&
    attribute.ClassName == FLOOR_CATEGORY_INTERNAL)

// ✅ 正确：只匹配用户显示名称
if (attribute.UserDefined && 
    attribute.ClassUserName == FLOOR_CATEGORY)
```

#### 📝 索引管理的正确方式

**索引计算规则**：

1. 遍历所有属性，只计算 `UserDefined == true` 的属性
2. 查找方法返回的是**0基索引**（第一个用户属性是0）
3. `SetUserDefined`方法使用的是**1基索引**（第一个用户属性是1）
4. `RemoveUserDefined`方法也使用**1基索引**

```csharp
// ✅ 正确的索引转换
int foundIndex = GetFloorAttributeIndex(propertyNode, categoryName);  // 返回0, 1, 2...
if (foundIndex >= 0)
{
    // SetUserDefined 需要1基索引
    int setIndex = foundIndex + 1;  // 转换为 1, 2, 3...
    propertyNode.SetUserDefined(setIndex, categoryName, internalName, propertyVector);
    
    // RemoveUserDefined 也需要1基索引
    int removeIndex = foundIndex + 1;  // 转换为 1, 2, 3...
    propertyNode.RemoveUserDefined(removeIndex);
}
```

#### 🛠️ 完整的修复模板

```csharp
public class CustomPropertyManager
{
    // ✅ 设置自定义属性的正确方式
    public bool SetCustomProperty(ModelItem item, string categoryName, 
                                 string internalName, string propertyName, 
                                 string displayName, string value)
    {
        return ExecuteWithUIThread(() =>
        {
            var state = ComApiBridge.State;
            var comPath = ComApiBridge.ToInwOaPath(item);
            var propertyNode = (ComApi.InwGUIPropertyNode2)state.GetGUIPropertyNode(comPath, false);

            // 🔍 关键：动态查找现有属性索引
            int existingIndex = FindCustomPropertyIndex(propertyNode, categoryName);
            
            // 创建属性内容
            var propertyCategory = (ComApi.InwOaPropertyVec)state.ObjectFactory(
                ComApi.nwEObjectType.eObjectType_nwOaPropertyVec, null, null);
            
            var property = (ComApi.InwOaProperty)state.ObjectFactory(
                ComApi.nwEObjectType.eObjectType_nwOaProperty, null, null);
            property.name = propertyName;
            property.UserName = displayName;
            property.value = value;
            propertyCategory.Properties().Add(property);

            if (existingIndex >= 0)
            {
                // 🎯 更新现有属性（索引+1）
                propertyNode.SetUserDefined(existingIndex + 1, categoryName, internalName, propertyCategory);
                LogManager.Info($"更新现有属性分类 '{categoryName}' (index={existingIndex + 1})");
            }
            else
            {
                // 🆕 创建新属性（索引0）
                propertyNode.SetUserDefined(0, categoryName, internalName, propertyCategory);
                LogManager.Info($"创建新属性分类 '{categoryName}' (index=0)");
            }
            
            return true;
        });
    }

    // ✅ 查找自定义属性索引的正确方式
    private int FindCustomPropertyIndex(ComApi.InwGUIPropertyNode2 propertyNode, string categoryName)
    {
        int userDefinedIndex = 0;
        
        foreach (ComApi.InwGUIAttribute2 attribute in propertyNode.GUIAttributes())
        {
            if (attribute.UserDefined)
            {
                // ⚠️ 关键：只匹配ClassUserName，不匹配ClassName
                if (attribute.ClassUserName == categoryName)
                {
                    return userDefinedIndex;
                }
                userDefinedIndex++;
            }
        }
        return -1;
    }

    // ✅ 清除自定义属性的正确方式
    public bool ClearCustomProperty(ModelItem item, string categoryName)
    {
        return ExecuteWithUIThread(() =>
        {
            var state = ComApiBridge.State;
            var comPath = ComApiBridge.ToInwOaPath(item);
            var propertyNode = (ComApi.InwGUIPropertyNode2)state.GetGUIPropertyNode(comPath, false);

            int existingIndex = FindCustomPropertyIndex(propertyNode, categoryName);
            if (existingIndex >= 0)
            {
                // 🗑️ 删除属性（索引+1）
                propertyNode.RemoveUserDefined(existingIndex + 1);
                LogManager.Info($"删除属性分类 '{categoryName}' (removed index={existingIndex + 1})");
                return true;
            }
            else
            {
                LogManager.Info($"属性分类 '{categoryName}' 不存在，无需删除");
                return false;
            }
        });
    }
}
```

#### 📋 问题诊断检查列表

当遇到属性重复创建问题时，检查以下要点：

- [ ] 是否使用了硬编码索引（如固定使用索引1）
- [ ] 是否同时匹配了`ClassUserName`和`ClassName`
- [ ] 是否正确进行了索引转换（0基→1基）
- [ ] 是否在主UI线程中执行COM API调用
- [ ] 日志中是否显示"未找到现有属性分类"但实际存在

#### 🎯 修复验证方法

**测试步骤**：

1. 选择一个对象，设置自定义属性（如"F1"）
2. 再次选择同一对象，设置不同值（如"F2"）
3. 检查属性面板，应该只有一个属性分类
4. 重复步骤2，设置第三个值（如"F3"）
5. 确认仍然只有一个属性分类，值为最新设置的值

**日志验证**：

```text
[FloorAttributeManager] 找到楼层属性分类，索引为: 0
[FloorAttributeManager] ✅ 成功更新现有楼层属性分类 (index=1)
```

#### 💡 经验总结

1. **COM API的ClassUserName是关键**：这是我们控制的显示名称，用于匹配现有属性
2. **ClassName不可靠**：系统生成的内部标识符，每次可能不同
3. **索引转换至关重要**：查找用0基，设置/删除用1基
4. **动态索引查找必不可少**：硬编码索引是重复创建问题的根源
5. **详细日志帮助调试**：记录索引查找和转换过程，便于问题定位

## 13. 缓存刷新和状态同步 ⚠️ 重要

### 13.1 缓存刷新的必要性

在某些API操作（特别是自定义属性的删除）后，Navisworks内部缓存可能与实际状态不同步，导致：

- 搜索结果不准确
- 属性面板显示异常
- 后续API调用出现意外行为

### 13.2 安全的缓存刷新方法 ✅

基于实际生产环境的测试和验证，以下是推荐的缓存刷新方法：

```csharp
// ✅ 最安全的缓存刷新方法：空集合的SetHidden操作
public static void SafeCacheRefresh()
{
    try
    {
        var document = NavisApplication.ActiveDocument;
        if (document?.Models != null)
        {
            // 使用空集合的SetHidden操作触发缓存更新
            // 这是一个几乎零开销的"伪操作"
            var emptyCollection = new ModelItemCollection();
            document.Models.SetHidden(emptyCollection, false);
            LogManager.WriteLog("✅ 已执行轻量级缓存刷新");
        }
    }
    catch (Exception ex)
    {
        LogManager.WriteLog($"⚠️ 缓存刷新失败，但不影响主要操作: {ex.Message}");
    }
}
```

### 13.3 危险的缓存刷新方法 ❌

**⚠️ 避免使用以下方法，已确认会导致程序崩溃：**

```csharp
// ❌ 危险：会导致程序崩溃
document.Models.ResetAllTemporaryMaterials();

// ❌ 危险：重载过重，可能影响性能
document.ActiveView.RequestDelayedRedraw(ViewRedrawRequests.All);

// ❌ 危险：可能干扰用户界面状态
document.Models.ResetOverriddenTransparency(allItems);
```

### 13.4 何时需要缓存刷新

**必须刷新的操作**：

- COM API删除自定义属性后
- 大批量修改模型可见性后
- 复杂的Transform操作后

**可选刷新的操作**：

- 添加自定义属性（通常自动同步）
- 简单的颜色或透明度覆盖
- 单个对象的操作

### 13.5 实践模式：操作后刷新

```csharp
// ✅ 推荐模式：操作 + 刷新 + 验证
public int RemoveLogisticsAttributes(ModelItemCollection items)
{
    int successCount = 0;
    
    try
    {
        // 1. 执行主要操作
        foreach (var item in items)
        {
            // COM API删除操作...
            successCount++;
        }
    }
    catch (Exception ex)
    {
        LogManager.WriteLog($"操作失败: {ex.Message}");
    }
    
    // 2. 如果有成功操作，执行缓存刷新
    if (successCount > 0)
    {
        SafeCacheRefresh(); // 使用安全的刷新方法
    }
    
    return successCount;
}
```

### 13.6 缓存问题的诊断

**常见症状**：

- 删除属性后，搜索仍能找到已删除的属性
- 属性面板显示的内容与API返回不一致
- 连续相同操作的结果不同

**诊断代码**：

```csharp
// ✅ 验证缓存同步状态
public bool VerifyCacheSync(ModelItem item, string categoryName)
{
    // 通过NET API检查
    bool netApiResult = HasPropertyCategory(item, categoryName);
    
    // 通过COM API检查
    bool comApiResult = HasPropertyCategoryViaCom(item, categoryName);
    
    if (netApiResult != comApiResult)
    {
        LogManager.Warning($"缓存不同步检测: NET={netApiResult}, COM={comApiResult}");
        SafeCacheRefresh();
        return false;
    }
    
    return true;
}
```

### 13.7 多线程环境下的缓存刷新

```csharp
// ✅ 线程安全的缓存刷新
public async Task SafeCacheRefreshAsync()
{
    await System.Windows.Application.Current.Dispatcher.InvokeAsync(() =>
    {
        SafeCacheRefresh(); // 确保在主线程中执行
    });
}
```

### 13.8 缓存刷新最佳实践

| 场景 | 刷新时机 | 刷新方法 | 必要性 |
|------|----------|----------|--------|
| 删除自定义属性 | 操作完成后 | 空集合SetHidden | 必须 |
| 批量隐藏对象 | 操作完成后 | 可选择不刷新 | 可选 |
| Transform动画 | 动画结束后 | 空集合SetHidden | 推荐 |
| 颜色覆盖 | 通常不需要 | 无 | 不需要 |

**关键原则**：

1. **安全第一**：只使用验证过的安全方法
2. **按需刷新**：不是所有操作都需要缓存刷新
3. **异常处理**：缓存刷新失败不应该影响主要功能
4. **线程安全**：在主UI线程中执行刷新操作

## 14. 参考官方示例

强烈建议查看以下官方示例了解更多用法：

- `SearchComparisonPlugIn.cs` - 搜索性能对比和属性搜索
- `Examiner.cs` - LINQ查询和属性过滤示例
- `AutoUserPropsExample.cs` - COM API自定义属性操作
- `BasicDockPanePlugin.cs` - 基础插件结构
- `DatabaseDockPane/Models.cs` - 数据库操作示例
- `ClashDetective` 相关示例 - 高级功能示例

### 碰撞检测模式说明

  Hard（硬碰撞）

- 检测模型几何体（多边形、线条和/或点）之间发生碰撞的情况，碰撞距离大于设定的容差值

  HardConservative（保守硬碰撞）

- 与Hard模式相同，但额外尝试检测那些多边形、线条和点虽然不相交，但它们所代表的体积可能重叠的情况
- 举例：两个相同大小的立方体，方向相同，沿其中一个面的垂直方向偏移很短的距离。在这种情况下，体积明显重
  叠，但没有三角形或边缘彼此穿过
- 所有"碰撞"都属于以下情况之一：
  - (i) 面仅仅接触另一个面的边缘
  - (ii) 共面的面在2D空间中重叠，但在3D中没有相交
  - (iii) 边仅仅接触另一个边的端点
  - (iv) 共轴的边在1D中重叠，但同样不是以3D方式检测到的
- HardConservative碰撞检测增加了检测这些复杂情况的逻辑，但可能会产生误报

  Clearance（间隙检测）

- 检测几何体之间的最小分离距离小于指定容差的情况
- 例如，可用于检测是否有足够空间放置CAD文件中未建模的保温材料

  Duplicate（重复检测）

- 检测模型中完全相同的几何体出现多次的情况
- 当合并包含相同第三方文件的两个文件时可能发生这种情况
- 超出容差范围的重复几何体被视为不同对象，不会被报告

  这些不同的检测模式为不同的工程应用场景提供了精确的碰撞分析能力。

  容差值说明

  对于Hard（硬碰撞）和HardConservative（保守硬碰撞）测试
  - 容差值是在干涉被认定为碰撞之前允许的重叠程度
  - 这允许您忽略数值近似问题和可以在现场解决的轻微碰撞

  对于Clearance（间隙检测）测试
  - 容差值是所需的最小间隙距离，低于此值的分离将被视为碰撞

  对于Duplicate（重复检测）测试
  - 容差值是相同几何体被视为重复的最大距离
  - 超出此距离的物体将被忽略，理论上它们虽然相同但是真正独立的实例

  简单来说：
  - Hard/HardConservative：容差 = 允许的最大重叠距离
  - Clearance：容差 = 要求的最小间隙距离
  - Duplicate：容差 = 识别重复的最大距离

  这种设计让用户可以根据实际工程需求调整检测的敏感度。

  设置几何类型：包含面、线和点以获得最全面的碰撞检测

```csharp
    // 创建临时测试以获取真实碰撞结果
    var tempTestName = $"临时测试_{resultCount}_{DateTime.Now:HHmmss_fff}";
    var tempTest = new ClashTest
    {
        DisplayName = tempTestName,
        TestType = ClashTestType.HardConservative,
        Tolerance = detectionGap,
        Guid = Guid.Empty,
        MergeComposites = true
    };

    collisionTest.SelectionA.PrimitiveTypes = PrimitiveTypes.Triangles | PrimitiveTypes.Lines | PrimitiveTypes.Points;
    collisionTest.SelectionB.PrimitiveTypes = PrimitiveTypes.Triangles | PrimitiveTypes.Lines | PrimitiveTypes.Points;
```

### ModelGeometry 属性

  ModelGeometry类公开了以下成员：

  属性

  | 名称                         | 说明                                     |
  |----------------------------|----------------------------------------|
  | ActiveColor                | 此几何体的当前（可见）颜色                          |
  | ActiveTransform            | 返回几何体当前活动的变换矩阵                         |
  | ActiveTransparency         | 此几何体的当前（可见）透明度                         |
  | BoundingBox                | 此几何体在世界坐标系中的包围盒                        |
  | FragmentCount              | 此几何体被分割成的片段数量                          |
  | IsDisposed                 | 获取一个值，指示对象是否已被释放且不再可用（继承自NativeHandle） |
  | IsReadOnly                 | 是否只读（重写NativeHandle的IsReadOnly属性）      |
  | IsSolid                    | 此几何体是否为实体？（包括所有形成封闭、流形外壳的三角形图元）        |
  | Item                       | 模型层次结构中对应此几何体的项目                       |
  | OriginalColor              | 此几何体的原始颜色（设计文件中指定的）                    |
  | OriginalTransform          | 返回几何体加载时的原始变换矩阵                        |
  | OriginalTransparency       | 此几何体的原始透明度（设计文件中指定的）                   |
  | PermanentColor             | 几何体的永久颜色。可能是原始颜色或用户明确覆盖的颜色             |
  | PermanentOverrideTransform | 应用于模型几何体原始变换的覆盖变换                      |
  | PermanentTransform         | 模型几何体的永久变换。由原始变换与覆盖变换组合形成的变换           |
  | PermanentTransparency      | 几何体的永久透明度。可能是原始透明度或用户明确覆盖的透明度          |
  | PrimitiveCount             | 定义此几何体的图元（三角形、线、点）数量                   |
  | PrimitiveTypes             | 用于定义此几何体的图元类型                          |

  这些属性提供了访问和查询Navisworks模型几何体各种状态信息的接口，包括颜色、变换、透明度、包围盒和几何体结构等重要属性。

```csharp
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Windows.Forms;

using System.Text;

using Autodesk.Navisworks.Api.Controls;

      static public void OutputFirstGeometry()
      {
         try
         {
            if (Autodesk.Navisworks.Api.Application.ActiveDocument != null &&
               !Autodesk.Navisworks.Api.Application.ActiveDocument.IsClear)
            {
               ModelGeometry first =
                  Autodesk.Navisworks.Api.Application.ActiveDocument.
                     Models[0].RootItem.FindFirstGeometry();

               if (first != null)
               {
                  string text = string.Empty;
                  text = string.Format("ActiveColor = {0}" +
                     "\nActiveTransparency = {1}" +
                     "\nBoundingBox  = {2}" +
                     "\nFragmentCount {3}" +
                     "\nIsSolid {4}" +
                     "\nItem {5}" +
                     "\nOriginalColor {6}" +
                     "\nOriginalTransparency {7}" +
                     "\nPermanentColor {8}" +
                     "\nPermanentTransparency {9}",
                     first.ActiveColor.ToString(),
                     first.ActiveTransparency,
                     first.BoundingBox.ToString(),
                     first.FragmentCount,
                     first.IsSolid.ToString(),
                     first.Item.ToString(),
                     first.OriginalColor.ToString(),
                     first.OriginalTransparency,
                     first.PermanentColor.ToString(),
                     first.PermanentTransparency);

                  MessageBox.Show(text);
               }
            }
         }
         catch (Exception e)
         {
            MessageBox.Show(e.Message + "\n\n" + e.ToString());
         }
      }
```

### Tool Enumeration

● Tool 枚举值

  | 成员名称                         | 描述                     |
  |------------------------------|------------------------|
  | None                         | 无活动工具                  |
  | Select                       | 选择工具                   |
  | SelectBox                    | 框选工具                   |
  | RedlineFreehand              | 红线手绘工具                 |
  | RedlineLine                  | 红线直线工具                 |
  | RedlineEllipse               | 红线椭圆工具                 |
  | RedlineCloud                 | 红线云形标注工具               |
  | RedlineLineString            | 红线连续线工具                |
  | RedlineTag                   | 红线标签工具                 |
  | RedlineText                  | 红线文本工具                 |
  | RedlineErase                 | 红线擦除工具                 |
  | RedlineArrow                 | 红线箭头工具                 |
  | MeasurePointToPoint          | 点到点测量工具                |
  | MeasurePointToMultiplePoints | 点到多点测量工具               |
  | MeasurePointLine             | 点线测量工具                 |
  | MeasureAccumulate            | 累积测量工具                 |
  | MeasureAngle                 | 角度测量工具                 |
  | MeasureArea                  | 面积测量工具                 |
  | MeasureSingle                | 单点测量工具                 |
  | BasicViewObjectWheel         | 基础视图对象导航轮              |
  | BasicTourBuildingWheel       | 基础建筑巡游导航轮              |
  | FullNavigationWheel          | 完整导航轮                  |
  | MiniViewObjectWheel          | 迷你视图对象导航轮              |
  | MiniTourBuildingWheel        | 迷你建筑巡游导航轮              |
  | MiniFullNavigationWheel      | 迷你完整导航轮                |
  | Full2DNavigationWheel        | 完整2D导航轮                |
  | CommonPan                    | 通用平移（所有Autodesk产品通用）   |
  | CommonZoom                   | 通用缩放（所有Autodesk产品通用）   |
  | CommonZoomWindow             | 通用窗口缩放（所有Autodesk产品通用） |
  | CommonOrbit                  | 通用轨道（所有Autodesk产品通用）   |
  | CommonFreeOrbit              | 通用自由轨道（所有Autodesk产品通用） |
  | CommonConstrainedOrbit       | 通用约束轨道（所有Autodesk产品通用） |
  | CommonLookAt                 | 通用看向（所有Autodesk产品通用）   |
  | CommonLookAround             | 通用环视（所有Autodesk产品通用）   |
  | CommonWalk                   | 通用漫游（所有Autodesk产品通用）   |
  | CommonCenter                 | 通用居中（所有Autodesk产品通用）   |
  | NavigateFixed                | 固定相机位置                 |
  | NavigateFreeLookAround       | 经典Navisworks自由环视（旋转）   |
  | NavigateFreeOrbit            | 经典Navisworks自由轨道（检查）   |
  | NavigateWalk                 | 经典Navisworks漫游         |
  | NavigateFly                  | 经典Navisworks飞行         |
  | NavigateConstrainedOrbit     | 经典Navisworks约束轨道（转盘）   |
  | NavigateZoom                 | 经典Navisworks缩放         |
  | NavigatePan                  | 经典Navisworks平移         |
  | NavigateConstrainedPan       | 经典Navisworks约束平移       |
  | NavigateLookAround           | 经典Navisworks环视（旋转）     |
  | NavigateOrbit                | 经典Navisworks轨道         |
  | NavigateZoomWindow           | 经典Navisworks窗口缩放（缩放框）  |
  | CustomToolPlugin             | 由ToolPlugin提供的自定义功能    |

  这个枚举列出了Navisworks中所有可用的工具类型，包括导航工具、测量工具、标注工具等。其中CustomToolPlugin是用于自定
  义工具插件的特殊值。

### Cursor枚举成员

  | 成员名称            | 描述                         |
  |-----------------|----------------------------|
  | Unhandled       | 特殊情况。如果已经处理则由GetCursor()返回 |
  | Handled         | 已处理光标                      |
  | Walk            | 行走光标                       |
  | Fly             | 飞行光标                       |
  | Orbit           | 环绕光标                       |
  | Swivel          | 旋转光标                       |
  | Examine         | 检查光标                       |
  | Pan             | 平移光标                       |
  | Zoom            | 缩放光标                       |
  | Turntable       | 转盘光标                       |
  | Focus           | 聚焦光标                       |
  | Application     | 应用程序光标                     |
  | ZoomBox         | 缩放框光标                      |
  | Measure         | 测量光标（十字线）                  |
  | HyperHand       | 超级手势光标                     |
  | PanWorld        | 世界平移光标                     |
  | Roll            | 滚动光标                       |
  | Stop            | 停止光标                       |
  | MeasureEdge     | 测量边缘光标                     |
  | MeasureVertex   | 测量顶点光标                     |
  | Redline         | 红线光标                       |
  | Erase           | 擦除光标                       |
  | Wheel           | 滚轮光标                       |
  | MarkupSelection | 标记选择光标                     |
  | MarkupSnapping  | 标记捕捉光标                     |
  | MarkupEraser    | 标记擦除光标                     |
  | MarkupQuickPick | 标记快速选择光标                   |
  | MarkupBucket    | 标记桶光标                      |
  | MarkupAutoPoly  | 标记自动多边形光标                  |

使用方式：
  
```csharp
public class PathClickToolPlugin : ToolPlugin
{
    ....

    /// <summary>
    /// 重写光标样式，使用捕捉光标提供更好的视觉反馈
    /// </summary>
    /// <param name="view">当前视图</param>
    /// <param name="modifier">键盘修饰键</param>
    /// <returns>返回捕捉光标</returns>
    public override Cursor GetCursor(View view, KeyModifiers modifier)
    {
        // 使用捕捉光标，提供更好的视觉反馈用于精确路径点选择
        return Cursor.MarkupSnapping;
    }
}
```

### 只显示选中项

// 这个示例是"隐藏未选中项"的逻辑

- Navisworks 的层级隐藏机制：
  - 父节点可见 → 子节点自动可见
  - 父节点隐藏 → 子节点自动隐藏

// 关键点：

  1. visible集合添加了 AncestorsAndSelf（祖先路径）
  2. visible集合添加了 Descendants（所有后代）
  3. 但这是为了"保护"这些节点不被隐藏，而不是为了隐藏它们

```csharp
Examples
CopyHide the unselected ModelItems
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Windows.Forms;

using System.Text;

using Autodesk.Navisworks.Api.Controls;

      static public void HideUnselected()
      {
         //Create hidden collection
         ModelItemCollection hidden = new ModelItemCollection();

         //create a store for the visible items
         ModelItemCollection visible = new ModelItemCollection();

         //Add all the items that are visible to the visible collection
         foreach (ModelItem item in Autodesk.Navisworks.Api.Application.ActiveDocument.CurrentSelection.SelectedItems)
         {
            if (item.AncestorsAndSelf != null)
               visible.AddRange(item.AncestorsAndSelf);
            if (item.Descendants != null)
               visible.AddRange(item.Descendants);
         }

         //mark as invisible all the siblings of the visible items as well as the visible items
         foreach (ModelItem toShow in visible)
         {
            if (toShow.Parent != null)
            {
               hidden.AddRange(toShow.Parent.Children);
            }
         }

         //remove the visible items from the collection
         foreach (ModelItem toShow in visible)
         {
            hidden.Remove(toShow);
         }

         //hide the remaining items
         Autodesk.Navisworks.Api.Application.ActiveDocument.Models.
            SetHidden(hidden, true);
      }
```

### 使用进度条

When instantiated a progress bar in the Navisworks style is displayed to the user inside the Navisworks main application. This can then be updated by the program or plugin with textual information as well as the percentage completed of the process.

#### 基础示例

```csharp
/// <summary>
/// Shows the basic concept of using the Progress class
/// </summary>
private static void SimpleProgressExample()
{
   //first get reference to an instance of the Progress class
   Progress progress = Autodesk.Navisworks.Api.Application.BeginProgress();

   double stage;

   for (stage = 0.0; stage < 1.0; stage += 0.1)
   {
      //Update progress bar
      progress.Update(stage);

      //Do something for a period of time
      System.Threading.Thread.Sleep(1000);
   }

   //Update progress bar to 100%
   progress.Update(1.0);

   //notify the API that the operation has finished
   Autodesk.Navisworks.Api.Application.EndProgress();
}
```

#### 14.1 Progress API 核心方法详解

**Navisworks Progress API** 提供了原生的进度条显示功能，可以在耗时操作中向用户展示进度和允许取消操作。

**核心方法**：

```csharp
// 1. BeginProgress() - 开始进度跟踪（无参数版本）
Progress progress = Application.BeginProgress();

// 2. BeginProgress(string title) - 带标题版本（推荐）
Progress progress = Application.BeginProgress("提取几何体");

// 3. progress.Update(double) - 更新进度（0.0 到 1.0）
progress.Update(0.5); // 50% 进度

// 4. progress.IsCanceled - 检查用户是否取消
if (progress.IsCanceled)
{
    Application.EndProgress();
    return null;
}

// 5. Application.EndProgress() - 结束进度跟踪
Application.EndProgress();
```

**Progress 类的重要成员**：

| 方法/属性 | 说明 |
|---------|------|
| `Update(double progress)` | 更新进度（0.0-1.0），显示百分比 |
| `IsCanceled` | 只读属性，用户是否点击取消按钮 |
| `BeginSubOperation(double, string)` | 开始子操作，分配总进度的一部分 |
| `EndSubOperation()` | 结束当前子操作 |
| `Hide()` | 隐藏进度对话框（如需打开其他窗口） |
| `Show()` | 显示之前隐藏的进度对话框 |
| `ReportError(int, string)` | 向用户报告错误 |

#### 14.2 带标题的进度条（推荐用法）

```csharp
/// <summary>
/// 带标题的进度条示例 - 提供更好的用户体验
/// </summary>
public void ProcessWithTitle()
{
    Progress progress = null;
    try
    {
        // 创建带标题的进度条
        progress = Application.BeginProgress("处理模型数据",
            "正在分析 3516 个几何片段...");

        int totalItems = 100;
        for (int i = 0; i < totalItems; i++)
        {
            // 执行处理逻辑
            ProcessItem(i);

            // 更新进度
            progress.Update((double)i / totalItems);
        }

        // 完成
        progress.Update(1.0);
    }
    finally
    {
        // 确保进度条被关闭
        if (progress != null)
        {
            Application.EndProgress();
        }
    }
}
```

#### 14.3 支持用户取消操作

```csharp
/// <summary>
/// 支持取消的进度条示例
/// </summary>
public List<Triangle3D> ExtractWithCancellation(IEnumerable<ModelItem> items)
{
    var results = new List<Triangle3D>();
    Progress progress = null;

    try
    {
        progress = Application.BeginProgress("提取三角形",
            "正在提取几何数据，可随时取消...");

        var itemList = items.ToList();
        int totalCount = itemList.Count;

        for (int i = 0; i < totalCount; i++)
        {
            // ✅ 关键：检查用户是否取消
            if (progress.IsCanceled)
            {
                LogManager.Info($"[提取] 用户取消操作，已处理 {i}/{totalCount}");
                break; // 退出循环
            }

            // 执行处理
            var triangles = ExtractTrianglesFromItem(itemList[i]);
            results.AddRange(triangles);

            // 更新进度
            progress.Update((double)(i + 1) / totalCount);
        }

        LogManager.Info($"[提取] 完成，共提取 {results.Count} 个三角形");
    }
    catch (Exception ex)
    {
        LogManager.Error($"[提取] 发生错误: {ex.Message}");
        throw;
    }
    finally
    {
        // ✅ 关键：无论成功、失败还是取消，都要关闭进度条
        if (progress != null)
        {
            Application.EndProgress();
        }
    }

    return results;
}
```

#### 14.4 子操作（SubOperation）用法

当任务有多个阶段时，可以使用子操作来细化进度显示：

```csharp
/// <summary>
/// 使用子操作的多阶段进度示例
/// </summary>
public void MultiStageProcess()
{
    Progress progress = null;

    try
    {
        progress = Application.BeginProgress("处理模型");

        // 阶段1：分析模型结构（占总进度的20%）
        progress.BeginSubOperation(0.2, "正在分析模型结构...");
        var allFragments = AnalyzeModelStructure();
        progress.Update(1.0); // 子操作完成100%
        progress.EndSubOperation();

        // 阶段2：提取几何数据（占总进度的70%）
        progress.BeginSubOperation(0.7, "正在提取几何数据...");
        for (int i = 0; i < allFragments.Count; i++)
        {
            if (progress.IsCanceled) break;

            ProcessFragment(allFragments[i]);
            progress.Update((double)i / allFragments.Count);
        }
        progress.EndSubOperation();

        // 阶段3：保存结果（占总进度的10%）
        progress.BeginSubOperation(0.1, "正在保存结果...");
        SaveResults();
        progress.Update(1.0);
        progress.EndSubOperation();

        LogManager.Info("所有阶段完成");
    }
    finally
    {
        if (progress != null)
        {
            Application.EndProgress();
        }
    }
}
```

#### 14.5 实际应用：几何体提取中使用进度条

基于实际的 `ExtractTriangles` 方法，展示如何集成 Progress API：

```csharp
/// <summary>
/// 批量提取三角形（带进度条和取消支持）
/// </summary>
/// <param name="modelItems">模型项集合</param>
/// <returns>三角形集合</returns>
public static List<Triangle3D> ExtractTriangles(IEnumerable<ModelItem> modelItems)
{
    var triangles = new List<Triangle3D>();
    ComApi.InwOpSelection comSelection = null;
    Progress progress = null;

    try
    {
        // 第1步：批量转换为 COM 选择
        var modelCollection = new ModelItemCollection();
        int itemCount = 0;
        foreach (var item in modelItems)
        {
            modelCollection.Add(item);
            itemCount++;
        }

        LogManager.Info($"[批量提取] 开始批量提取 {itemCount} 个模型项的三角形");

        var comState = ComStateManager.GetState();
        comSelection = ComApiBridge.ToInwOpSelection(modelCollection);

        // 第2步：获取所有片段
        var allFragments = GetAllFragments(comSelection);
        LogManager.Info($"[批量提取] 获取到 {allFragments.Count} 个片段");

        // 🎯 第3步：开始进度条
        progress = Application.BeginProgress("提取几何体",
            $"正在提取 {allFragments.Count} 个片段的三角形数据...");

        try
        {
            int processedFragments = 0;
            foreach (var fragmentInfo in allFragments)
            {
                // 🎯 检查用户是否取消
                if (progress.IsCanceled)
                {
                    LogManager.Info($"[批量提取] 用户取消操作，已处理 {processedFragments}/{allFragments.Count}");
                    break;
                }

                try
                {
                    var callback = new OptimizedGeometryCallback(fragmentInfo.TransformMatrix);
                    fragmentInfo.Fragment.GenerateSimplePrimitives(
                        ComApi.nwEVertexProperty.eNORMAL,
                        callback);

                    var fragmentTriangles = callback.GetTriangles();
                    triangles.AddRange(fragmentTriangles);
                    processedFragments++;

                    // 🎯 更新进度（每处理完一个片段更新一次）
                    progress.Update((double)processedFragments / allFragments.Count);
                }
                catch (Exception ex)
                {
                    LogManager.Error($"[批量提取] 处理片段失败: {ex.Message}");
                }
            }

            LogManager.Info($"[批量提取] 片段处理完成，共提取 {triangles.Count} 个三角形");
        }
        finally
        {
            // 释放所有片段COM对象
            foreach (var fragmentInfo in allFragments)
            {
                try
                {
                    if (fragmentInfo.Fragment != null)
                    {
                        Marshal.ReleaseComObject(fragmentInfo.Fragment);
                    }
                }
                catch (Exception ex)
                {
                    LogManager.Warning($"[批量提取] 释放片段COM对象失败: {ex.Message}");
                }
            }
        }
    }
    catch (Exception ex)
    {
        LogManager.Error($"[批量提取] 提取三角形失败: {ex.Message}");
    }
    finally
    {
        // 🎯 确保进度条被关闭
        if (progress != null)
        {
            Application.EndProgress();
        }

        // 释放COM对象，避免内存泄漏
        if (comSelection != null)
        {
            Marshal.ReleaseComObject(comSelection);
        }
    }

    return triangles;
}
```

#### 14.6 Progress API 最佳实践

**1. 合理的更新频率**

```csharp
// ✅ 推荐：每个片段更新一次（3516次调用，可接受）
progress.Update((double)processedFragments / allFragments.Count);

// ❌ 避免：每个三角形更新（可能数十万次，太频繁）
// progress.Update((double)triangleCount / totalTriangles); // 性能问题

// ✅ 推荐：如果更新太频繁，可以批量更新
if (processedFragments % 10 == 0) // 每10个更新一次
{
    progress.Update((double)processedFragments / allFragments.Count);
}
```

**2. 异常处理中确保关闭**

```csharp
// ✅ 正确模式：使用 finally 确保关闭
Progress progress = null;
try
{
    progress = Application.BeginProgress("处理中...");
    // ... 处理逻辑
}
catch (Exception ex)
{
    LogManager.Error($"处理失败: {ex.Message}");
    // 即使发生异常，进度条也会在 finally 中关闭
}
finally
{
    if (progress != null)
    {
        Application.EndProgress();
    }
}
```

**3. 取消操作的资源清理**

```csharp
// ✅ 正确：取消时也要清理资源
foreach (var item in items)
{
    if (progress.IsCanceled)
    {
        LogManager.Info("用户取消操作");
        CleanupPartialResults(); // 清理部分结果
        break;
    }
    // ... 处理
}
```

**4. 线程安全考虑**

```csharp
// ✅ Progress API 调用必须在主UI线程中
System.Windows.Application.Current.Dispatcher.Invoke(() =>
{
    Progress progress = null;
    try
    {
        progress = Application.BeginProgress("处理中...");
        // 所有 Navisworks API 调用都在主线程中
        ProcessInMainThread();
    }
    finally
    {
        if (progress != null)
        {
            Application.EndProgress();
        }
    }
});
```

#### 14.7 进度条 vs 日志输出对比

| 特性 | Progress API | 日志输出 |
|------|-------------|----------|
| **用户体验** | ✅ 实时可视化进度，直观 | ❌ 需要打开日志文件查看 |
| **取消操作** | ✅ 支持用户取消 | ❌ 无法中断 |
| **性能开销** | ✅ 极小（内存操作） | ❌ 磁盘I/O开销 |
| **调试信息** | ❌ 不保留历史 | ✅ 可追溯问题 |
| **适用场景** | 耗时操作的用户交互 | 问题诊断和审计 |

**推荐方案：同时使用两者**

```csharp
// 进度条：给用户看，提供交互
progress.Update((double)i / total);

// 日志：只记录关键节点（开始、结束、错误）
LogManager.Info($"[批量提取] 开始提取 {total} 个片段");
// ... 处理过程中不频繁写日志
LogManager.Info($"[批量提取] 完成，共提取 {count} 个三角形");
```

#### 14.8 完整使用模式总结

**推荐的标准模板**：

```csharp
public ReturnType MethodWithProgress(InputType input)
{
    Progress progress = null;
    try
    {
        // 1. 开始进度条（带描述性标题）
        progress = Application.BeginProgress("操作标题", "详细描述...");

        // 2. 计算总工作量
        int totalWork = CalculateTotalWork(input);
        int completedWork = 0;

        // 3. 执行主要工作循环
        foreach (var item in workItems)
        {
            // 3.1 检查取消
            if (progress.IsCanceled)
            {
                LogManager.Info("用户取消操作");
                CleanupResources();
                break;
            }

            // 3.2 执行工作
            try
            {
                ProcessItem(item);
                completedWork++;
            }
            catch (Exception ex)
            {
                LogManager.Error($"处理项目失败: {ex.Message}");
                // 决定是继续还是中断
            }

            // 3.3 更新进度
            progress.Update((double)completedWork / totalWork);
        }

        // 4. 完成
        LogManager.Info($"操作完成，处理了 {completedWork}/{totalWork} 项");
        return result;
    }
    catch (Exception ex)
    {
        LogManager.Error($"操作失败: {ex.Message}");
        throw;
    }
    finally
    {
        // 5. 确保清理
        if (progress != null)
        {
            Application.EndProgress();
        }
        CleanupResources();
    }
}
```

**常见陷阱和避免方法**：

```csharp
// ❌ 陷阱1：忘记关闭进度条
Progress progress = Application.BeginProgress("处理中...");
// ... 处理逻辑
// 忘记调用 Application.EndProgress(); // 进度条会一直显示

// ✅ 正确：使用 try-finally
Progress progress = null;
try
{
    progress = Application.BeginProgress("处理中...");
    // ... 处理逻辑
}
finally
{
    if (progress != null) Application.EndProgress();
}

// ❌ 陷阱2：进度值超出范围
progress.Update(1.5); // 错误！应该是 0.0-1.0

// ✅ 正确：确保范围
double progressValue = Math.Min(1.0, (double)completed / total);
progress.Update(progressValue);

// ❌ 陷阱3：在后台线程中调用
await Task.Run(() =>
{
    var progress = Application.BeginProgress("处理中..."); // 崩溃！
});

// ✅ 正确：在主线程中调用
await System.Windows.Application.Current.Dispatcher.InvokeAsync(() =>
{
    var progress = Application.BeginProgress("处理中...");
    // ... 主线程中的处理
});
```

**性能优化建议**：

| 场景 | 更新策略 | 原因 |
|------|---------|------|
| 少量项目（<100） | 每项更新一次 | 用户体验好，开销可忽略 |
| 中等项目（100-1000） | 每项更新一次 | 平衡体验和性能 |
| 大量项目（1000-10000） | 每10项更新一次 | 减少UI刷新开销 |
| 海量项目（>10000） | 每100项更新一次 | 显著减少UI开销 |

**官方示例参考**：

官方示例文件中的 Progress API 使用案例：

- `ClashDetective/ClashGrouper/ClashGrouperUtils.cs` - 碰撞分组中的进度条使用
- 展示了在复杂算法中如何正确使用 Progress API 和取消支持

### Navisworks的Models.CollectionChanged事件模式

  事件模式总结

  场景1：第一次打开文件（2ND_FLOOR）

- 事件#1 (6.6秒)：Count=0，状态保持无模型（文档清空准备加载）
- 事件#2 (6.7秒)：Count=1，从无模型→有模型（文件加载完成）

  场景2：重新打开新文件（4TH_FLOOR）

- 事件#3 (25.6秒)：Count=0，从有模型→无模型（旧文件被清空）
- 事件#4 (25.6秒)：Count=1，从无模型→有模型（新文件加载完成）

  场景3：关闭程序

- 事件#5 (39.8秒)：Count=0，从有模型→无模型（程序关闭前清理）

  关键发现

  1. 每次文档操作都有两个事件：
  - 先清空Models（Count=0）
  - 再加载新Models（Count>0）
  1. 状态转换清晰：
  - 无模型→无模型：初始清理
  - 无模型→有模型：文档加载完成 ✅ 这是我们需要初始化的时机
  - 有模型→无模型：文档关闭/切换 ✅ 这是我们需要清理的时机
  1. 时间间隔很短：事件成对出现，间隔仅0.1秒左右

### Navisworks Fragment和Path的API分析

  从COM API文档可以看到几个关键信息：

  1. Fragment概念（第8页）

  A Fragment is the representation of (possibly part of) a particular instance
  of a Geometry node within the scene graph. Large Geometry may be broken into
  multiple Fragments for more efficient displaying.

  关键点：

- Fragment是几何体的一个片段或部分
- 大型几何体会被分割为多个Fragment以提高显示效率
- 每个Fragment提供访问：
  - 完整的Transform3f（从本地到世界坐标空间）
  - Fragment代表的Path
  - 用于显示的几何体
  - 用于显示的外观

  1. Path概念（第8页）

  A Path is the sequence of nodes from the Partition at the root of the
  scene graph to a particular node within it that uniquely specifies
  a particular instance.

  关键点：

- Path是从根分区到特定节点的节点序列
- Path用来唯一标识特定实例
- 但这里的"实例"指的是场景图中的节点实例，不是几何体的不同部分

  1. 你的地板问题分析

  真正的问题：

  COM API明确说明了"大型几何体会被分割为多个Fragment"。你的地板有226个Fragment，这些Fragment都有相同的路径键值 [1,1,1,1,1,1,1]，这是完全正常的：

- 相同Path：因为这226个Fragment都属于同一个ModelItem（地板）
- 不同Fragment：因为地板几何体很复杂，被Navisworks自动分割为226个显示片段

  去重逻辑的错误：

  当前的去重逻辑错误地认为：
  // 错误假设：相同路径 = 重复实例
  if (pathKey相同) {
      去重(); // 错误！丢失了地板的225个片段
  }

  正确的理解：

- 多实例去重：应该针对不同ModelItem的相同几何体
- 多片段保留：应该保留同一ModelItem的所有Fragment

  1. 解决方案建议

  需要修改去重逻辑，区分：

  1. 真正的多实例：不同位置的相同几何体（如多个相同的门）
  2. 几何体分片：同一几何体的多个显示片段（如复杂地板）

  可能的改进方向：
  // 改进的去重逻辑
  var uniqueKey = new {
      PathKey = fragment.path.ArrayData,
      GeometryHash = fragment.GetGeometryHash(), // 需要调研是否有此API
      Transform = fragment.GetLocalToWorldMatrix() // 考虑变换信息
  };

  这样可以正确处理：

- ✅ 相同门窗的多实例 → 正确去重
- ✅ 地板的多片段 → 正确保留

### Setting “Units and Transform” values via Navisworks API

<https://blog.autodesk.io/setting-units-and-transform-values-via-navisworks-api/>

DocumentModels models = doc.Models;
//Get the required model from DocumentModels
Model model;
Transform3D oldTransform3d=model.Transform;
Transform3DComponents transform3dComponents = oldTransform3d.Factor();
//Get Values
Vector3D originVector3D = transform3dComponents.Translation;
Vector3D scaleVector3D = transform3dComponents.Scale;
Rotation3D rotationVector3D = transform3dComponents.Rotation;
//Set Values
transform3dComponents.Translation = new Vector3D(origin_X, origin_Y, origin_Z);  //Eg: new Vector3D(10,10,10);
  transform3dComponents.Rotation = new Rotation3D(new UnitVector3D(0, 0, 1), 0.872665);  //Here 50 degree=0.872665 radian
  transform3dComponents.Scale = new Vector3D(scale_X, scale_Y, scale_Z);  //Eg: new Vector3D(2,2,2);
  Transform3D newTransform3D = transform3dComponents.Combine();
//Change model units value
Units units = Units.Meters;
models.SetModelUnitsAndTransform(model, units , newTransform3D, true);

### 如何在运行时唯一标识一个ModelItem

使用 ModelItem.GetHashCode() 作为唯一标识符
使用 HashSet<ModelItem> 来存储和比较碰撞对象，这样可以正确使用 ModelItem.Equals() 和 GetHashCode() 方法，避免哈希冲突和跨运行时不一致的问题

## 13. 使用 DocumentModels API 持久化 ModelItem 引用

### 13.1 核心概念

**问题场景**：需要在不同会话之间保存和恢复 ModelItem 的引用（如碰撞检测结果、动画对象等）

**核心洞察**：使用 `DocumentModels.CreatePathId()` 和 `ResolvePathId()` 方法实现跨会话的 ModelItem 持久化

### 13.2 ModelItemPathId 结构

```csharp
// ✅ ModelItemPathId 包含两个关键信息
public class ModelItemPathId
{
    public int ModelIndex { get; set; }  // 模型索引（NWD文件中的模型编号）
    public string PathId { get; set; }   // 路径标识符（如 "0/682/0"）
}

// 🔍 PathId 格式说明：
// - 使用斜杠分隔的层级路径
// - 例如："0/682/0" 表示从根节点到目标节点的路径
// - 每个数字代表该层级的索引位置
```

### 13.3 保存 ModelItem 到数据库

```csharp
// ✅ 正确方法：使用 CreatePathId 获取持久化标识
public void SaveCollisionObject(ModelItem collidedObject, int resultId)
{
    var document = Application.ActiveDocument;
    
    // 获取 ModelItem 的持久化标识
    var pathId = document.Models.CreatePathId(collidedObject);
    
    // 保存到数据库
    var record = new ClashDetectiveCollisionObjectRecord
    {
        ResultId = resultId,
        ModelIndex = pathId.ModelIndex,      // 保存模型索引
        PathId = pathId.PathId,               // 保存路径标识
        DisplayName = ModelItemAnalysisHelper.GetSafeDisplayName(collidedObject),
        ObjectName = ModelItemAnalysisHelper.GetSafeDisplayName(collidedObject)
    };
    
    pathDatabase.SaveClashDetectiveCollisionObject(record);
}
```

### 13.4 从数据库恢复 ModelItem

```csharp
// ✅ 正确方法：使用 ResolvePathId 恢复 ModelItem
public ModelItem LoadCollisionObject(int? modelIndex, string pathId)
{
    try
    {
        var document = Application.ActiveDocument;
        
        // 验证必需参数
        if (!modelIndex.HasValue)
        {
            throw new InvalidOperationException("ModelIndex 为空，无法恢复 ModelItem");
        }
        
        if (string.IsNullOrEmpty(pathId))
        {
            throw new InvalidOperationException("PathId 为空，无法恢复 ModelItem");
        }
        
        // 构造 ModelItemPathId 对象
        var pathIdObj = new Autodesk.Navisworks.Api.DocumentParts.ModelItemPathId();
        pathIdObj.ModelIndex = modelIndex.Value;
        pathIdObj.PathId = pathId;
        
        // 解析路径并恢复 ModelItem
        ModelItem restoredItem = document.Models.ResolvePathId(pathIdObj);
        
        if (restoredItem == null)
        {
            throw new InvalidOperationException($"无法通过 PathId 找到 ModelItem: ModelIndex={modelIndex}, PathId={pathId}");
        }
        
        // 验证恢复的对象是否有效
        if (!ModelItemAnalysisHelper.IsModelItemValid(restoredItem))
        {
            throw new InvalidOperationException($"恢复的 ModelItem 无效: ModelIndex={modelIndex}, PathId={pathId}");
        }
        
        return restoredItem;
    }
    catch (Exception ex)
    {
        LogManager.Error($"恢复 ModelItem 失败: ModelIndex={modelIndex}, PathId={pathId}", ex);
        throw;
    }
}
```

### 13.5 完整示例：碰撞检测结果的保存和加载

```csharp
// ✅ 保存碰撞检测结果
public void SaveClashDetectiveResults(List<CollisionResult> collisions, ModelItem object)
{
    var document = Application.ActiveDocument;
    
    // 1. 保存物体对象信息
    var objectPathId = document.Models.CreatePathId(object);
    var testRecord = new ClashDetectiveResultRecord
    {
        TestName = GenerateTestName(),
        IsVirtualObject = false,
        ObjectModelIndex = objectPathId.ModelIndex,
        ObjectPathId = objectPathId.PathId,
        // ... 其他字段
    };
    
    int resultId = pathDatabase.SaveClashDetectiveResult(testRecord);
    
    // 2. 保存所有被撞对象
    var collisionObjects = new List<ClashDetectiveCollisionObjectRecord>();
    
    foreach (var collision in collisions)
    {
        if (collision.Item2 != null)  // 只保存被撞对象（Item2）
        {
            var objectPathId = document.Models.CreatePathId(collision.Item2);
            
            collisionObjects.Add(new ClashDetectiveCollisionObjectRecord
            {
                ResultId = resultId,
                ModelIndex = objectPathId.ModelIndex,
                PathId = objectPathId.PathId,
                DisplayName = ModelItemAnalysisHelper.GetSafeDisplayName(collision.Item2),
                ObjectName = ModelItemAnalysisHelper.GetSafeDisplayName(collision.Item2)
            });
        }
    }
    
    pathDatabase.SaveClashDetectiveCollisionObjects(resultId, collisionObjects);
}

// ✅ 加载碰撞检测结果
public List<CollisionResult> LoadClashDetectiveResults(string testName)
{
    var document = Application.ActiveDocument;
    
    // 1. 从数据库读取测试信息
    var testInfo = pathDatabase.GetClashDetectiveResult(testName);
    if (testInfo == null)
    {
        throw new InvalidOperationException($"未找到测试记录: {testName}");
    }
    
    // 2. 恢复物体对象
    ModelItem object;
    if (testInfo.IsVirtualObject)
    {
        // 虚拟物体：创建新的虚拟物体对象
        object = VirtualObjectManager.Instance.CreateVirtualObject(
            testInfo.VirtualObjectLength,
            testInfo.VirtualObjectWidth,
            testInfo.VirtualObjectHeight
        );
    }
    else
    {
        // 真实物体：使用 ResolvePathId 恢复
        object = LoadCollisionObject(testInfo.ObjectModelIndex, testInfo.ObjectPathId);
    }
    
    // 3. 恢复所有被撞对象并重建碰撞结果
    var collisionObjects = pathDatabase.GetClashDetectiveCollisionObjects(testInfo.Id);
    var results = new List<CollisionResult>();
    
    foreach (var obj in collisionObjects)
    {
        ModelItem collidedObject = LoadCollisionObject(obj.ModelIndex, obj.PathId);
        
        var collisionResult = new CollisionResult
        {
            ClashGuid = Guid.NewGuid(),
            DisplayName = $"历史碰撞: {obj.DisplayName}",
            Status = ClashResultStatus.Active,
            Item1 = object,
            Item2 = collidedObject,
            Center = collidedObject.BoundingBox().Center,
            Distance = 0.0,
            CreatedTime = DateTime.Now,
            OriginalItem1 = object,
            OriginalItem2 = collidedObject,
            HasContainerMapping = true
        };
        
        results.Add(collisionResult);
    }
    
    return results;
}
```

### 13.6 数据库表设计建议

```sql
-- ✅ 推荐的数据库表结构（最小化冗余）
CREATE TABLE ClashDetectiveCollisionObjects (
    Id INTEGER PRIMARY KEY AUTOINCREMENT,
    ResultId INTEGER NOT NULL,
    ModelIndex INTEGER,           -- 模型索引（可为空）
    PathId TEXT,                  -- 路径标识符（可为空）
    DisplayName TEXT,             -- 显示名称（用于UI显示）
    ObjectName TEXT,              -- 对象名称（用于日志）
    FOREIGN KEY(ResultId) REFERENCES ClashDetectiveResults(Id)
);

-- ❌ 避免冗余字段
-- ModelItemPath TEXT,  -- 与 PathId 重复，不需要
```

**关键要点**：

- **最小化存储**：只需要 `ModelIndex` 和 `PathId` 两个字段即可恢复对象
- **避免冗余**：不要存储与 PathId 相同的 ModelItemPath 字段
- **保留显示信息**：DisplayName 和 ObjectName 用于 UI 显示和日志，不影响对象恢复

### 13.7 常见问题和解决方案

#### 问题1：PathId 类型混淆

```csharp
// ❌ 错误：以为 PathId 是 int 类型
public class ClashDetectiveCollisionObjectRecord
{
    public int? PathId { get; set; }  // 错误：PathId 是 string，不是 int
}

// ✅ 正确：PathId 是 string 类型
public class ClashDetectiveCollisionObjectRecord
{
    public string PathId { get; set; }  // 正确：格式如 "0/682/0"
}
```

#### 问题2：参数验证不充分

```csharp
// ❌ 错误：没有验证参数
public ModelItem LoadObject(int? modelIndex, string pathId)
{
    var pathIdObj = new ModelItemPathId();
    pathIdObj.ModelIndex = modelIndex.Value;  // 可能为空
    pathIdObj.PathId = pathId;                // 可能为空
    return document.Models.ResolvePathId(pathIdObj);
}

// ✅ 正确：验证参数并抛出明确异常
public ModelItem LoadObject(int? modelIndex, string pathId)
{
    if (!modelIndex.HasValue)
    {
        throw new InvalidOperationException("ModelIndex 为空，无法恢复 ModelItem");
    }
    
    if (string.IsNullOrEmpty(pathId))
    {
        throw new InvalidOperationException("PathId 为空，无法恢复 ModelItem");
    }
    
    var pathIdObj = new ModelItemPathId();
    pathIdObj.ModelIndex = modelIndex.Value;
    pathIdObj.PathId = pathId;
    
    var restoredItem = document.Models.ResolvePathId(pathIdObj);
    
    if (restoredItem == null)
    {
        throw new InvalidOperationException($"无法通过 PathId 找到 ModelItem: ModelIndex={modelIndex}, PathId={pathId}");
    }
    
    return restoredItem;
}
```

#### 问题3：向后兼容代码掩盖问题

```csharp
// ❌ 错误：使用向后兼容逻辑掩盖问题
public ModelItem LoadObject(int? modelIndex, string pathId, string oldPath)
{
    ModelItem item = null;
    
    // 尝试新方法
    if (modelIndex.HasValue && !string.IsNullOrEmpty(pathId))
    {
        try
        {
            item = document.Models.ResolvePathId(new ModelItemPathId { ModelIndex = modelIndex.Value, PathId = pathId });
        }
        catch { }  // 静默失败
    }
    
    // 回退到旧方法
    if (item == null && !string.IsNullOrEmpty(oldPath))
    {
        item = FindModelItemByOldPath(oldPath);  // 旧方法
    }
    
    return item;  // 可能返回 null，掩盖了问题
}

// ✅ 正确：失败直接抛出异常，不掩盖问题
public ModelItem LoadObject(int? modelIndex, string pathId)
{
    if (!modelIndex.HasValue || string.IsNullOrEmpty(pathId))
    {
        throw new InvalidOperationException($"参数无效: ModelIndex={modelIndex}, PathId={pathId}");
    }
    
    try
    {
        var pathIdObj = new ModelItemPathId { ModelIndex = modelIndex.Value, PathId = pathId };
        var item = document.Models.ResolvePathId(pathIdObj);
        
        if (item == null)
        {
            throw new InvalidOperationException($"无法找到 ModelItem: ModelIndex={modelIndex}, PathId={pathId}");
        }
        
        return item;
    }
    catch (Exception ex)
    {
        throw new InvalidOperationException($"恢复 ModelItem 失败: ModelIndex={modelIndex}, PathId={pathId}", ex);
    }
}
```

### 13.8 最佳实践总结

| 最佳实践 | 说明 | 示例 |
|---------|------|------|
| **使用 CreatePathId** | 获取 ModelItem 的持久化标识 | `var pathId = document.Models.CreatePathId(item)` |
| **保存 ModelIndex 和 PathId** | 两个字段都是必需的 | `ModelIndex = pathId.ModelIndex, PathId = pathId.PathId` |
| **验证参数** | 加载前验证参数有效性 | `if (!modelIndex.HasValue) throw ...` |
| **失败抛出异常** | 不掩盖问题，直接抛出异常 | `if (item == null) throw ...` |
| **避免冗余字段** | 不存储与 PathId 重复的数据 | 删除 ModelItemPath 字段 |
| **保留显示信息** | DisplayName 用于 UI 显示 | `DisplayName = item.DisplayName` |
| **线程安全** | 在主线程中执行 API 调用 | 使用 `Dispatcher.Invoke` 包装 |
| **错误处理** | 提供详细的错误信息 | 包含 ModelIndex 和 PathId 在异常消息中 |

### 13.9 性能和可靠性考虑

1. **PathId 的稳定性**：
   - 只要 NWD 文件结构不变，PathId 就会保持稳定
   - 如果 NWD 文件被重新导出或结构改变，PathId 可能失效

2. **批量恢复优化**：

   ```csharp
   // ✅ 批量恢复时，先收集所有 PathId
   var pathIds = collisionObjects.Select(obj => new ModelItemPathId 
   { 
       ModelIndex = obj.ModelIndex.Value, 
       PathId = obj.PathId 
   }).ToList();
   
   // 然后一次性恢复（如果 API 支持）
   // 或者逐个恢复并收集失败的对象
   var failedItems = new List<string>();
   foreach (var pathId in pathIds)
   {
       try
       {
           var item = document.Models.ResolvePathId(pathId);
           // 处理恢复的对象
       }
       catch (Exception ex)
       {
           failedItems.Add($"{pathId.ModelIndex}:{pathId.PathId}");
       }
   }
   ```

3. **缓存机制**：

   ```csharp
   // ✅ 使用缓存减少重复查询
   private Dictionary<string, ModelItem> _modelItemCache = new Dictionary<string, ModelItem>();
   
   public ModelItem GetOrLoadModelItem(int? modelIndex, string pathId)
   {
       string cacheKey = $"{modelIndex}:{pathId}";
       
       if (_modelItemCache.TryGetValue(cacheKey, out ModelItem cachedItem))
       {
           return cachedItem;
       }
       
       var item = LoadCollisionObject(modelIndex, pathId);
       _modelItemCache[cacheKey] = item;
       return item;
   }
   ```

### 13.10 与其他持久化方法的对比

| 方法 | 优点 | 缺点 | 适用场景 |
|------|------|------|---------|
| **CreatePathId/ResolvePathId** | 官方API，稳定可靠，跨会话有效 | 需要保存两个字段 | 推荐用于所有持久化场景 |
| **ModelItem.GetHashCode()** | 简单，单个值 | 跨会话无效，哈希冲突 | 仅用于运行时去重 |

## 14. Clash Detective API 使用方法

### 14.1 Clash Detective 检测行为的关键洞察 ⚠️ 重要

基于实际测试验证的核心发现（2026-01-13）。

#### 14.1.1 输入 vs 输出的对象类型差异

**⚠️ 核心发现：Clash Detective 返回的对象与输入的对象类型不同**

```csharp
// 🔍 实际测试验证：
// 输入给 Clash Detective 的对象：
var selectionA = new ModelItemCollection { compositeItem };  // IsComposite=True
var selectionB = new ModelItemCollection { compositeItem2 }; // IsComposite=True

test.SelectionA.Selection.CopyFrom(selectionA);
test.SelectionB.Selection.CopyFrom(selectionB);

// Clash Detective 返回的对象：
ClashResult result = test.Children[0] as ClashResult;
// result.Item1.IsComposite = False  ← 注意：变成了 False
// result.Item2.IsComposite = False  ← 注意：变成了 False
// result.Item1.DisplayName = ""     ← 注意：名字是空的
// result.Item2.DisplayName = ""     ← 注意：名字是空的
```

**日志验证**：

```
[ClashDetective输入] 候选1: Item1=Chair Sitting Square (IsComposite=True), Item2=Chair-Stacking (IsComposite=True)
[ClashDetective结果] 原始碰撞: Item1= (IsComposite=False), Item2= (IsComposite=False)
[ClashDetective结果] 容器映射: Item1=Chair Sitting Square (IsComposite=True), Item2=Chair-Stacking (IsComposite=True)
```

#### 14.1.2 Clash Detective 的内部行为

**关键理解**：

1. **输入对象**：我们给 Clash Detective 的是复合对象（`IsComposite=True`）
2. **检测过程**：Clash Detective 深入到几何体级别进行碰撞检测
3. **返回对象**：Clash Detective 返回的是实际发生碰撞的几何体子节点（`IsComposite=False`）
4. **命名问题**：几何体子节点通常没有 DisplayName，所以返回的名字是空的

**为什么这样设计**：

- Clash Detective 需要精确到几何体级别进行碰撞检测
- 复合对象可能包含多个几何体子节点
- 返回具体的几何体节点可以提供更精确的碰撞位置信息

#### 14.1.3 正确的处理方法

**解决方案：使用 FindNamedParentContainer 映射回复合对象**

```csharp
// ✅ 正确方法：容器映射
foreach (var child in collisionGroup.Children)
{
    if (child is ClashResult clashResult)
    {
        // 1. Clash Detective 返回的是几何体级别的子节点
        var originalItem1 = clashResult.Item1;   // IsComposite=False, DisplayName=""
        var originalItem2 = clashResult.Item2;   // IsComposite=False, DisplayName=""
        
        // 2. 向上查找有意义的父级容器
        var compositeItem1 = ModelItemAnalysisHelper.FindNamedParentContainer(clashResult.Item1);
        var compositeItem2 = ModelItemAnalysisHelper.FindNamedParentContainer(clashResult.Item2);
        
        // 3. 使用复合对象创建碰撞结果
        var collisionResult = new CollisionResult
        {
            ClashGuid = clashResult.Guid,
            DisplayName = clashResult.DisplayName,
            Status = clashResult.Status,
            Item1 = compositeItem1,  // IsComposite=True, DisplayName="Chair Sitting Square"
            Item2 = compositeItem2,  // IsComposite=True, DisplayName="Chair-Stacking"
            Center = clashResult.Center,
            Distance = clashResult.Distance,
            CreatedTime = DateTime.Now
        };
    }
}
```

#### 14.1.4 FindNamedParentContainer 的实现

```csharp
// ✅ 向上查找有意义的父级容器
public static ModelItem FindNamedParentContainer(ModelItem geometryItem)
{
    if (geometryItem == null)
        return null;
    
    ModelItem current = geometryItem;
    
    // 向上遍历父节点链
    while (current != null)
    {
        // 如果当前节点有名字且是复合对象，返回它
        if (!string.IsNullOrEmpty(current.DisplayName) && current.IsComposite)
        {
            return current;
        }
        
        // 继续向上查找
        current = current.Parent;
    }
    
    // 如果没找到，返回原始对象
    return geometryItem;
}
```

#### 14.1.5 容器映射的必要性

**为什么必须进行容器映射**：

1. **用户可读性**：复合对象有有意义的名字（如"Chair-Stacking"），几何体子节点没有名字
2. **高亮显示**：高亮复合对象比高亮几何体子节点更直观
3. **碰撞报告**：报告需要显示有意义的对象名称
4. **数据一致性**：预计算使用复合对象，Clash Detective 结果也应该使用复合对象

**不进行容器映射的问题**：

```csharp
// ❌ 错误：直接使用 Clash Detective 返回的对象
var collisionResult = new CollisionResult
{
    Item1 = clashResult.Item1,  // DisplayName="", IsComposite=False
    Item2 = clashResult.Item2   // DisplayName="", IsComposite=False
};

// 问题：
// 1. 碰撞报告中显示"未命名对象"
// 2. 高亮显示的是几何体子节点（用户看不到）
// 3. 无法与预计算结果对应（预计算使用复合对象）
```

#### 14.1.6 性能考虑

**容器映射的性能影响**：

- 每次碰撞结果都需要向上遍历父节点链
- 对于大型模型，可能影响性能
- 建议缓存容器映射结果

**优化方案**：

```csharp
// ✅ 使用缓存优化容器映射
private Dictionary<ModelItem, ModelItem> _containerCache = new Dictionary<ModelItem, ModelItem>();

public ModelItem FindNamedParentContainerCached(ModelItem geometryItem)
{
    if (geometryItem == null)
        return null;
    
    // 检查缓存
    if (_containerCache.TryGetValue(geometryItem, out ModelItem cachedContainer))
    {
        return cachedContainer;
    }
    
    // 执行容器映射
    var container = FindNamedParentContainer(geometryItem);
    
    // 缓存结果
    _containerCache[geometryItem] = container;
    
    return container;
}
```

#### 14.1.7 与 MergeComposites 选项的关系

**MergeComposites 选项的作用**：

```csharp
var test = new ClashTest
{
    DisplayName = testName,
    TestType = ClashTestType.HardConservative,
    Tolerance = detectionGap,
    MergeComposites = true  // 这个选项
};
```

**MergeComposites = true 的效果**：

- 将同一个复合对象的多个子节点碰撞合并为一个碰撞
- 减少重复的碰撞结果
- 但仍然返回几何体级别的对象（不是复合对象）

**验证**：

```
// 即使设置了 MergeComposites=true
// Clash Detective 返回的仍然是：
// result.Item1.IsComposite = False
// result.Item2.IsComposite = False
// result.Item1.DisplayName = ""
```

**结论**：`MergeComposites` 选项只是减少碰撞数量，不会改变返回对象的类型。容器映射仍然是必需的。

#### 14.1.8 最佳实践总结

| 场景 | 方法 | 注意事项 |
|------|------|---------|
| 碰撞检测输入 | 使用复合对象 | 提高检测效率 |
| 碰撞结果提取 | 使用容器映射 | 获取有意义的对象名称 |
| 碰撞报告显示 | 使用复合对象 | 显示可读的名称 |
| 高亮显示 | 使用复合对象 | 高亮整个对象 |
| 性能优化 | 缓存容器映射 | 减少重复计算 |

**关键要点**：

1. **Clash Detective 输入和输出的对象类型不同**
2. **必须使用 FindNamedParentContainer 进行容器映射**
3. **容器映射确保碰撞结果的名称和可读性**
4. **使用缓存优化容器映射的性能**
| **索引路径（旧方法）** | 简单易懂 | 不稳定，易失效 | 不推荐使用 |
| **DisplayName** | 易于理解 | 不唯一，可能重复 | 仅用于UI显示 |

**结论**：`CreatePathId/ResolvePathId` 是 Navisworks API 提供的官方持久化方法，应该作为首选方案。

## 15. Viewpoint 视角控制 - 实现标准俯视图

基于实际测试验证的关键发现（2026-01-27）。

### 15.1 核心问题：如何实现"摆正"的俯视图

**需求场景**：在 Navisworks 中将视角调整为标准的俯视图，视图与世界坐标轴对齐，不倾斜。

**常见误区**：使用 `AlignDirection`、`AlignUp` 或正交投影等方法。

### 15.2 失败的方法及原因分析

#### 15.2.1 使用 AlignDirection 设置相机方向

```csharp
// ❌ 失败的方法：AlignDirection 不能改变相机方向
Viewpoint newViewpoint = doc.CurrentViewpoint.Value.CreateCopy();
newViewpoint.Position = cameraPosition;
newViewpoint.AlignDirection(new Vector3D(0, 0, -1));  // 指向下
newViewpoint.WorldUpVector = new UnitVector3D(0, 1, 0);

// 实际效果：视图方向没有改变，仍然是倾斜的
```

**失败原因**：`AlignDirection` 方法只是调整相机方向，但不能完全重置相机的旋转状态。它依赖于当前的相机姿态进行增量调整，无法实现彻底的"摆正"。

#### 15.2.2 使用 AlignUp 设置向上向量

```csharp
// ❌ 失败的方法：AlignUp 只调整向上向量
Viewpoint newViewpoint = doc.CurrentViewpoint.Value.CreateCopy();
newViewpoint.Position = cameraPosition;
newViewpoint.PointAt(focusCenter);
newViewpoint.AlignUp(new UnitVector3D(0, 0, 1));  // Z轴向上

// 实际效果：视图仍然是俯视，但方向没有摆正
```

**失败原因**：`AlignUp` 只是将相机的向上向量绕视图方向旋转，使其与给定的向上向量对齐。但它不改变视图方向本身，只是调整相机的"顶部朝向"。

#### 15.2.3 使用正交投影

```csharp
// ❌ 错误理解：正交投影 = 摆正视图
newViewpoint.Projection = ViewpointProjection.Orthographic;

// 实际效果：视图仍然是倾斜的，只是投影方式改变了
```

**失败原因**：正交投影（Orthographic Projection）是投影方式，不是视图方向。它只影响透视效果，不会改变相机的姿态和方向。

### 15.3 成功的方法：直接设置 Rotation

#### 15.3.1 核心发现

**关键洞察**：要实现标准的俯视图，必须直接设置 `Rotation` 属性为无旋转的单位四元数。

```csharp
// ✅ 成功的方法：直接设置 Rotation
Viewpoint newViewpoint = doc.CurrentViewpoint.Value.CreateCopy();

// 1. 设置相机位置在目标正上方
newViewpoint.Position = cameraPosition;

// 2. 🎯 关键：设置 Rotation 为无旋转（单位四元数）
// 这样可以彻底清除任何旋转，使视图与世界坐标轴对齐
newViewpoint.Rotation = new Rotation3D();

// 3. 设置向上向量：Y 轴向上（标准俯视图的向上方向）
newViewpoint.WorldUpVector = new UnitVector3D(0, 1, 0);
```

#### 15.3.2 原理说明

**Rotation3D() 是单位四元数**：

- 单位四元数表示"无旋转"状态
- 它代表相机的初始姿态与世界坐标轴完全对齐
- 相机从原点沿 X 轴正方向看，Y 轴向上，Z 轴向右

**为什么 Rotation3D() 有效**：

1. **彻底清除旋转**：不依赖任何增量调整，从零开始
2. **与世界坐标轴对齐**：相机姿态完全重置到初始状态
3. **配合 Position 设置**：相机在目标正上方 + 无旋转 = 标准俯视图

**视图方向推导**：

```
初始姿态（无旋转）：
  相机位置：原点 (0, 0, 0)
  视图方向：X 轴正方向 (1, 0, 0)
  向上向量：Y 轴正方向 (0, 1, 0)

设置 Position = (x, y, z + cameraDistance)：
  相机位置：目标正上方
  视图方向：仍然是 X 轴正方向
  向上向量：仍然是 Y 轴正方向

结果：
  相机在目标上方，沿 X 轴方向看
  这不是标准的俯视图
```

**问题**：标准的俯视图应该是从 Z 轴向下看，而不是从 X 轴看。

**修正方案**：需要结合 `AlignDirection` 或 `PointAt` 来设置视图方向。

#### 15.3.3 完整的正确实现

```csharp
// ✅ 完整的标准俯视图实现
public static void AdjustToStandardTopView(Point3D focusCenter, double cameraDistance)
{
    Document doc = Application.ActiveDocument;
    Viewpoint newViewpoint = doc.CurrentViewpoint.Value.CreateCopy();

    // 1. 设置相机位置在目标正上方
    newViewpoint.Position = new Point3D(
        focusCenter.X,
        focusCenter.Y,
        focusCenter.Z + cameraDistance
    );

    // 2. 🎯 关键步骤1：设置 Rotation 为无旋转
    newViewpoint.Rotation = new Rotation3D();

    // 3. 🎯 关键步骤2：让相机指向目标（Z 轴负方向）
    // 因为 Rotation3D() 后，相机沿 X 轴看，所以需要旋转到向下看
    newViewpoint.AlignDirection(new Vector3D(0, 0, -1));

    // 4. 设置向上向量：Y 轴向上
    newViewpoint.WorldUpVector = new UnitVector3D(0, 1, 0);

    // 5. 应用视角
    doc.CurrentViewpoint.CopyFrom(newViewpoint);
}
```

**或者使用 PointAt 方法**：

```csharp
// ✅ 使用 PointAt 的替代方案
Viewpoint newViewpoint = doc.CurrentViewpoint.Value.CreateCopy();
newViewpoint.Position = cameraPosition;
newViewpoint.Rotation = new Rotation3D();  // 先清除旋转
newViewpoint.PointAt(focusCenter);         // 然后指向目标
newViewpoint.WorldUpVector = new UnitVector3D(0, 1, 0);
```

### 15.4 6 个标准视图方向

根据 3D 坐标系的标准定义，6 个视图方向的配置如下：

| 视图 | 相机位置 | 视图方向 | 向上向量 | 说明 |
|------|---------|---------|---------|------|
| 俯视 | (x, y, z+height) | (0, 0, -1) | (0, 1, 0) | 从 Z 轴正方向向下看 |
| 仰视 | (x, y, z-height) | (0, 0, 1) | (0, 1, 0) | 从 Z 轴负方向向上看 |
| 前视 | (x, y-height, z) | (0, 1, 0) | (0, 0, 1) | 从 Y 轴负方向向前看 |
| 后视 | (x, y+height, z) | (0, -1, 0) | (0, 0, 1) | 从 Y 轴正方向向后看 |
| 右视 | (x+width, y, z) | (-1, 0, 0) | (0, 0, 1) | 从 X 轴正方向向左看 |
| 左视 | (x-width, y, z) | (1, 0, 0) | (0, 0, 1) | 从 X 轴负方向向右看 |

**实现示例**：

```csharp
// ✅ 通用的标准视图设置方法
public static void SetStandardView(Point3D focusCenter, double distance, StandardViewDirection viewDir)
{
    Document doc = Application.ActiveDocument;
    Viewpoint newViewpoint = doc.CurrentViewpoint.Value.CreateCopy();

    // 1. 根据视图方向设置相机位置
    Point3D cameraPos;
    Vector3D viewDirection;
    UnitVector3D upVector;

    switch (viewDir)
    {
        case StandardViewDirection.Top:
            cameraPos = new Point3D(focusCenter.X, focusCenter.Y, focusCenter.Z + distance);
            viewDirection = new Vector3D(0, 0, -1);
            upVector = new UnitVector3D(0, 1, 0);
            break;
        case StandardViewDirection.Front:
            cameraPos = new Point3D(focusCenter.X, focusCenter.Y - distance, focusCenter.Z);
            viewDirection = new Vector3D(0, 1, 0);
            upVector = new UnitVector3D(0, 0, 1);
            break;
        // ... 其他视图方向
        default:
            throw new ArgumentException("不支持的视图方向");
    }

    // 2. 设置相机位置和清除旋转
    newViewpoint.Position = cameraPos;
    newViewpoint.Rotation = new Rotation3D();

    // 3. 设置视图方向和向上向量
    newViewpoint.AlignDirection(viewDirection);
    newViewpoint.WorldUpVector = upVector;

    // 4. 应用视角
    doc.CurrentViewpoint.CopyFrom(newViewpoint);
}
```

### 15.5 Viewpoint 属性之间的关系

**关键理解**：Viewpoint 的多个属性共同决定相机的最终姿态。

| 属性 | 作用 | 设置顺序 | 重要性 |
|------|------|---------|-------|
| Position | 相机位置 | 第一 | 决定相机在哪里 |
| Rotation | 相机旋转 | 第二 | 决定相机朝向和倾斜 |
| WorldUpVector | 向上向量 | 第三 | 决定相机的"顶部"方向 |
| Projection | 投影方式 | 最后 | 决定透视效果 |

**正确的设置顺序**：

```csharp
// ✅ 推荐的设置顺序
1. newViewpoint.Position = cameraPosition;        // 先设置位置
2. newViewpoint.Rotation = new Rotation3D();      // 清除旋转（关键）
3. newViewpoint.AlignDirection(viewDirection);    // 设置视图方向
4. newViewpoint.WorldUpVector = upVector;         // 设置向上向量
5. newViewpoint.Projection = ViewpointProjection.Perspective; // 最后设置投影
```

### 15.6 常见 Viewpoint 问题及解决方案

| 问题 | 症状 | 原因 | 解决方案 |
|------|------|------|---------|
| 视图倾斜 | 视图不与世界坐标轴对齐 | 没有清除旋转 | 先设置 `Rotation = new Rotation3D()` |
| 方向错误 | 相机看向错误的方向 | AlignDirection 无效 | 确保先清除旋转再调用 AlignDirection |
| 向上向量错误 | 视图上下颠倒 | WorldUpVector 设置错误 | 根据视图方向选择正确的向上向量 |
| 正交投影误解 | 视图仍然倾斜 | 误以为正交投影 = 摆正 | 正交投影不改变方向，只是投影方式 |

### 15.7 Viewpoint 最佳实践

```csharp
// ✅ 推荐模式：创建标准俯视图
public static Viewpoint CreateStandardTopView(Point3D focusCenter, double distance)
{
    Document doc = Application.ActiveDocument;
    Viewpoint newViewpoint = doc.CurrentViewpoint.Value.CreateCopy();

    // 1. 设置相机位置（在目标正上方）
    newViewpoint.Position = new Point3D(
        focusCenter.X,
        focusCenter.Y,
        focusCenter.Z + distance
    );

    // 2. 🎯 清除旋转（关键步骤）
    newViewpoint.Rotation = new Rotation3D();

    // 3. 设置视图方向（向下看）
    newViewpoint.AlignDirection(new Vector3D(0, 0, -1));

    // 4. 设置向上向量（Y 轴向上）
    newViewpoint.WorldUpVector = new UnitVector3D(0, 1, 0);

    return newViewpoint;
}

// ✅ 推荐模式：保存和恢复视角
public static void SaveAndRestoreViewpointExample()
{
    Document doc = Application.ActiveDocument;

    // 保存当前视角
    Viewpoint savedViewpoint = doc.CurrentViewpoint.Value.CreateCopy();

    try
    {
        // 设置俯视图
        Point3D focusCenter = CalculatePathCenter();
        Viewpoint topView = CreateStandardTopView(focusCenter, 10.0);
        doc.CurrentViewpoint.CopyFrom(topView);

        // 执行操作（如截图、碰撞检测等）
        PerformOperationInTopView();
    }
    finally
    {
        // 恢复原始视角
        doc.CurrentViewpoint.CopyFrom(savedViewpoint);
    }
}
```

**关键要点**：

1. **Rotation3D() 是摆正视图的关键**：彻底清除旋转状态
2. **AlignDirection 需要在清除旋转后使用**：才能正确设置视图方向
3. **理解每个属性的作用**：不要混淆"视图方向"和"向上向量"
4. **正确的设置顺序很重要**：Position → Rotation → AlignDirection → WorldUpVector
5. **使用 CreateCopy() 而不是 new Viewpoint()**：保留当前视角的其他属性

## 13. 视角控制（Viewpoint）API

基于 Focus 功能实现的真实API用法总结（ViewpointHelper开发经验）。

### 13.1 核心API概念

**关键发现**：`ModelItem.Transform` **不反映** override 后的位置变化，获取实际位置应使用 `BoundingBox().Center`。

#### 视角控制的核心类

| 类/方法 | 用途 | 关键特性 |
|---------|------|---------|
| `Viewpoint` | 视角状态对象 | 可修改 Position、Rotation、WorldUpVector |
| `doc.CurrentViewpoint.Value` | 获取当前视角 | 需要用 `CreateCopy()` 创建副本修改 |
| `AlignDirection(Vector3D)` | 旋转相机指向方向 | 使用最短路径旋转 |
| `AlignUp(Vector3D)` | 旋转相机对齐向上向量 | 围绕视线方向旋转 |
| `ZoomBox(BoundingBox3D)` | 强制适应视图 | **会覆盖**相机距离设置 |
| `Rotation3D()` | 清除旋转状态 | 用于创建标准视角（俯视图等） |

### 13.2 聚焦到模型对象的两种方法

#### 方法一：距离计算法（精确控制物体占屏比例）

适用于**聚焦到单个对象**，精确控制物体在视图中的占比。

```csharp
// ✅ 正确：计算相机距离实现目标占比
// 推荐参数：viewAngleDegrees = 60.0（视场角）, targetViewRatio = 0.25（1/4占屏）
public void FocusOnModelItem(ModelItem item, double viewAngleDegrees, 
    double targetViewRatio)
{
    Document doc = Application.ActiveDocument;
    
    // 1. 获取对象包围盒（模型单位）
    var boundingBox = item.BoundingBox();
    Point3D targetCenter = boundingBox.Center;
    double targetSize = Math.Max(boundingBox.Size.X, boundingBox.Size.Y);
    
    // 2. 计算相机距离（标准透视投影公式）
    // distance = (targetSize / 2) / tan(FOV / 2) / targetViewRatio
    double fovRadians = viewAngleDegrees * Math.PI / 180.0;
    double cameraDistance = (targetSize / 2.0) / Math.Tan(fovRadians / 2.0) / targetViewRatio;
    
    // 3. 计算相机位置（使用模型标准45度斜上视角向量）
    Vector3D viewDirection = doc.FrontRightTopViewVector;
    Vector3D upVector = doc.FrontRightTopViewUpVector;
    
    Point3D cameraPosition = new Point3D(
        targetCenter.X - viewDirection.X * cameraDistance,
        targetCenter.Y - viewDirection.Y * cameraDistance,
        targetCenter.Z - viewDirection.Z * cameraDistance
    );
    
    // 4. 应用视角
    ApplyViewpoint(cameraPosition, targetCenter, upVector, useAlignDirection: true);
}

// 通用视角应用方法
private static void ApplyViewpoint(Point3D cameraPosition, Point3D targetPoint, 
    Vector3D upVector, bool useAlignDirection)
{
    Document doc = Application.ActiveDocument;
    Viewpoint newViewpoint = doc.CurrentViewpoint.Value.CreateCopy();
    
    newViewpoint.Position = cameraPosition;
    
    if (useAlignDirection)
    {
        // 使用AlignDirection/AlignUp精确控制方向
        Vector3D lookDirection = new Vector3D(
            targetPoint.X - cameraPosition.X,
            targetPoint.Y - cameraPosition.Y,
            targetPoint.Z - cameraPosition.Z);
        lookDirection.Normalize();
        
        newViewpoint.AlignDirection(lookDirection);
        newViewpoint.AlignUp(upVector);
    }
    else
    {
        // 使用Rotation直接设置（适用于标准视角）
        newViewpoint.Rotation = new Rotation3D();
        newViewpoint.WorldUpVector = new UnitVector3D(upVector.X, upVector.Y, upVector.Z);
    }
    
    doc.CurrentViewpoint.CopyFrom(newViewpoint);
}
```

**关键公式**：
```
cameraDistance = (targetSize / 2) / tan(FOV / 2) / targetViewRatio

示例：
- targetSize = 2米, FOV = 45°, ratio = 0.25 (25%占屏)
- cameraDistance = 1.0 / tan(22.5°) / 0.25 ≈ 9.66 米
```

#### 方法二：ZoomBox法（确保完整可见）

适用于**显示路径、多个对象或大型物体**，确保完全可见。

```csharp
// ✅ 正确：使用ZoomBox确保完整可见性
public void FocusOnBoundingBox(BoundingBox3D bounds, double marginRatio = 0.1)
{
    Document doc = Application.ActiveDocument;
    
    // 1. 计算扩展后的包围盒（添加边距）
    Vector3D size = bounds.Size;
    Vector3D margin = new Vector3D(
        size.X * marginRatio,
        size.Y * marginRatio,
        size.Z * marginRatio
    );
    
    BoundingBox3D expandedBox = new BoundingBox3D(
        new Point3D(bounds.Min.X - margin.X, bounds.Min.Y - margin.Y, bounds.Min.Z - margin.Z),
        new Point3D(bounds.Max.X + margin.X, bounds.Max.Y + margin.Y, bounds.Max.Z + margin.Z)
    );
    
    // 2. 设置相机位置（估算）
    Point3D targetCenter = expandedBox.Center;
    Vector3D viewDirection = doc.FrontRightTopViewVector;
    double estimatedDistance = Math.Max(size.X, size.Y) * 2.0;
    
    Point3D cameraPosition = new Point3D(
        targetCenter.X - viewDirection.X * estimatedDistance,
        targetCenter.Y - viewDirection.Y * estimatedDistance,
        targetCenter.Z - viewDirection.Z * estimatedDistance
    );
    
    // 3. 应用视角（设置Rotation和UpVector）
    ApplyViewpoint(cameraPosition, targetCenter, doc.FrontRightTopViewUpVector, 
        useAlignDirection: false);
    
    // 4. 关键：使用ZoomBox强制适应视图
    doc.ActiveView.ZoomBox(expandedBox);  // ZoomBox会覆盖相机距离
}
```

### 13.3 模型标准视角向量

**重要发现**：Navisworks 提供模型特定的标准视角向量。

```csharp
// ✅ 获取模型的标准视角方向（45度俯视）
Vector3D standardViewDirection = doc.FrontRightTopViewVector;
Vector3D standardUpVector = doc.FrontRightTopViewUpVector;

// 其他可用视角向量：
// - FrontViewVector / FrontViewUpVector        // 正视图
// - TopViewVector / TopViewUpVector            // 俯视图
// - RightViewVector / RightViewUpVector        // 右视图
// - FrontRightTopViewVector / ...              // 45度俯视（最常用）
```

**使用场景**：
- **Focus功能**：使用 `FrontRightTopViewVector` 保持与UI一致的标准视角
- **正视图截图**：使用 `FrontViewVector` 获取正交投影效果
- **俯视图分析**：使用 `TopViewVector` 进行平面分析

### 13.4 相机距离计算方法对比

| 方法 | 适用场景 | 优点 | 缺点 |
|------|---------|------|------|
| FOV公式法 | 单个对象聚焦 | 精确控制占屏比例 | 需要计算目标尺寸 |
| ZoomBox法 | 路径/多对象显示 | 确保完全可见，自动计算距离 | 覆盖相机位置设置 |
| 固定距离 | 快速预览 | 简单快速 | 对象大小不一时效果差 |

**FOV计算代码详解**：

```csharp
// 计算相机距离的完整公式
private static double CalculateCameraDistance(double targetSize, double fovDegrees, 
    double targetRatio)
{
    // 1. 转换FOV为弧度
    double fovRadians = fovDegrees * Math.PI / 180.0;
    
    // 2. 计算半角正切值
    double halfFovTan = Math.Tan(fovRadians / 2.0);
    
    // 3. 计算目标在FOV中占据一半时的距离
    //    targetSize/2 是目标半宽，halfFovTan 是半角正切
    double distanceForFullTarget = (targetSize / 2.0) / halfFovTan;
    
    // 4. 根据目标占屏比例调整距离
    //    ratio=0.5表示目标占视图50%，距离减半
    double finalDistance = distanceForFullTarget / targetRatio;
    
    return finalDistance;
}

// 使用示例：
// targetSize=3m, FOV=45°, ratio=0.25 → distance ≈ 14.5m
// 这意味着相机距离目标14.5米时，3米宽的物体将占据视图的25%
```

### 13.5 常见错误和解决方案

#### 错误1：混用距离计算和ZoomBox

```csharp
// ❌ 错误：先设置相机距离，再用ZoomBox覆盖
var viewpoint = doc.CurrentViewpoint.Value.CreateCopy();
viewpoint.Position = calculatedPosition;  // 设置位置
doc.CurrentViewpoint.CopyFrom(viewpoint);
doc.ActiveView.ZoomBox(bounds);  // ZoomBox会覆盖之前的距离设置！

// ✅ 正确：二选一
// 方案A：只用距离计算（不调用ZoomBox）
viewpoint.Position = calculatedPosition;
doc.CurrentViewpoint.CopyFrom(viewpoint);

// 方案B：用ZoomBox（距离会被自动计算）
doc.ActiveView.ZoomBox(bounds);
```

#### 错误2：忽略Rotation3D的影响

```csharp
// ❌ 错误：不清除旋转直接设置视角
var viewpoint = doc.CurrentViewpoint.Value.CreateCopy();
viewpoint.Position = newPosition;
viewpoint.AlignDirection(newDirection);  // 可能受原有旋转影响
doc.CurrentViewpoint.CopyFrom(viewpoint);

// ✅ 正确：创建新Rotation3D清除旋转状态
var viewpoint = doc.CurrentViewpoint.Value.CreateCopy();
viewpoint.Rotation = new Rotation3D();  // 清除旋转
viewpoint.AlignDirection(newDirection);  // 从标准状态开始
viewpoint.AlignUp(newUpVector);
doc.CurrentViewpoint.CopyFrom(viewpoint);
```

#### 错误3：使用错误的向上向量

```csharp
// ❌ 错误：假设世界Z轴是向上向量
Vector3D upVector = new Vector3D(0, 0, 1);

// ✅ 正确：使用模型的标准向上向量
Vector3D upVector = doc.FrontRightTopViewUpVector;

// 或者根据需要计算正确的向上向量
// 例如：俯视图中，Y轴可能是向上
Vector3D topViewUp = new Vector3D(0, 1, 0);
```

### 13.6 实用工具代码

```csharp
/// <summary>
/// 视角控制工具类 - 基于实际项目经验
/// </summary>
public static class ViewpointHelper
{
    /// <summary>
    /// 聚焦到单个模型对象（精确控制占屏比例）
    /// </summary>
    public static void FocusOnModelItem(ModelItem item, double viewAngleDegrees, 
        double targetViewRatio)
    {
        if (item == null) return;
        
        var bounds = item.BoundingBox();
        Point3D targetCenter = bounds.Center;
        double targetSize = Math.Max(bounds.Size.X, bounds.Size.Y);
        
        // 计算相机距离
        double cameraDistance = CalculateCameraDistance(
            targetSize, viewAngleDegrees, targetViewRatio);
        
        // 使用模型标准视角
        Document doc = Application.ActiveDocument;
        Vector3D viewDir = doc.FrontRightTopViewVector;
        Vector3D upVector = doc.FrontRightTopViewUpVector;
        
        Point3D cameraPos = new Point3D(
            targetCenter.X - viewDir.X * cameraDistance,
            targetCenter.Y - viewDir.Y * cameraDistance,
            targetCenter.Z - viewDir.Z * cameraDistance
        );
        
        ApplyViewpoint(cameraPos, targetCenter, upVector, useAlignDirection: true);
    }
    
    /// <summary>
    /// 聚焦到碰撞点（两个对象的中间位置）
    /// </summary>
    public static void FocusOnCollision(ModelItem item1, ModelItem item2, 
        double viewAngleDegrees, double targetViewRatio)
    {
        var bounds1 = item1.BoundingBox();
        var bounds2 = item2.BoundingBox();
        
        // 计算碰撞中心（两个包围盒最近的点）
        Point3D collisionCenter = CalculateCollisionCenter(bounds1, bounds2);
        double maxSize = Math.Max(
            Math.Max(bounds1.Size.X, bounds1.Size.Y),
            Math.Max(bounds2.Size.X, bounds2.Size.Y)
        );
        
        double cameraDistance = CalculateCameraDistance(
            maxSize, viewAngleDegrees, 0.35);
        
        Document doc = Application.ActiveDocument;
        Vector3D viewDir = doc.FrontRightTopViewVector;
        Vector3D upVector = doc.FrontRightTopViewUpVector;
        
        Point3D cameraPos = new Point3D(
            collisionCenter.X - viewDir.X * cameraDistance,
            collisionCenter.Y - viewDir.Y * cameraDistance,
            collisionCenter.Z - viewDir.Z * cameraDistance
        );
        
        ApplyViewpoint(cameraPos, collisionCenter, upVector, useAlignDirection: true);
    }
    
    /// <summary>
    /// 聚焦到指定点和大小的区域（ZoomBox法）
    /// </summary>
    public static void FocusOnPosition(Point3D center, double size, 
        double marginRatio = 0.1)
    {
        Document doc = Application.ActiveDocument;
        
        // 创建目标包围盒
        double halfSize = size / 2.0;
        double margin = size * marginRatio;
        
        BoundingBox3D targetBox = new BoundingBox3D(
            new Point3D(center.X - halfSize - margin, center.Y - halfSize - margin, 
                center.Z - halfSize - margin),
            new Point3D(center.X + halfSize + margin, center.Y + halfSize + margin, 
                center.Z + halfSize + margin)
        );
        
        // 先设置视角方向
        Vector3D viewDir = doc.FrontRightTopViewVector;
        Vector3D upVector = doc.FrontRightTopViewUpVector;
        double distance = size * 2.0;
        
        Point3D cameraPos = new Point3D(
            center.X - viewDir.X * distance,
            center.Y - viewDir.Y * distance,
            center.Z - viewDir.Z * distance
        );
        
        ApplyViewpoint(cameraPos, center, upVector, useAlignDirection: false);
        
        // 使用ZoomBox确保可见
        doc.ActiveView.ZoomBox(targetBox);
    }
    
    // 私有辅助方法...
    private static double CalculateCameraDistance(double targetSize, double fovDegrees, 
        double ratio)
    {
        double fovRadians = fovDegrees * Math.PI / 180.0;
        return (targetSize / 2.0) / Math.Tan(fovRadians / 2.0) / ratio;
    }
    
    private static void ApplyViewpoint(Point3D cameraPos, Point3D target, 
        Vector3D upVector, bool useAlignDirection)
    {
        Document doc = Application.ActiveDocument;
        Viewpoint vp = doc.CurrentViewpoint.Value.CreateCopy();
        
        vp.Position = cameraPos;
        
        if (useAlignDirection)
        {
            Vector3D lookDir = new Vector3D(
                target.X - cameraPos.X,
                target.Y - cameraPos.Y,
                target.Z - cameraPos.Z);
            lookDir.Normalize();
            vp.AlignDirection(lookDir);
            vp.AlignUp(upVector);
        }
        else
        {
            vp.Rotation = new Rotation3D();
            vp.WorldUpVector = new UnitVector3D(upVector.X, upVector.Y, upVector.Z);
        }
        
        doc.CurrentViewpoint.CopyFrom(vp);
    }
}
```

### 13.7 视角控制最佳实践

1. **选择合适的聚焦方法**：
   - 单个对象精确聚焦 → 距离计算法
   - 路径或区域显示 → ZoomBox法
   - 碰撞点查看 → 碰撞中心计算 + 距离法

2. **使用模型标准视角向量**：
   - 不要硬编码向量值
   - 使用 `doc.FrontRightTopViewVector` 等API

3. **理解ZoomBox的副作用**：
   - ZoomBox会覆盖相机距离设置
   - ZoomBox后不要再调整Position来微调距离

4. **正确处理Rotation3D**：
   - 创建新视角时，先用 `new Rotation3D()` 清除旋转
   - 再使用 `AlignDirection` 设置精确方向

5. **保存和恢复视角**：
   ```csharp
   // 保存
   Viewpoint savedView = doc.CurrentViewpoint.Value.CreateCopy();
   
   // ... 修改视角 ...
   
   // 恢复
   doc.CurrentViewpoint.CopyFrom(savedView);
   ```

---

**文档版本**：2026.2  
**最后更新**：基于 ViewpointHelper 重构经验（聚焦功能实现）  
**适用版本**：Navisworks Manage 2026


## 15. Progress 进度条与窗口焦点管理 ⚠️ 重要

### 15.1 问题现象

在使用 `Application.BeginProgress` 创建进度条后，如果在 `finally` 块中关闭进度条并立即显示消息对话框，会出现以下问题：

1. **焦点丢失**：关闭进度条后，Navisworks 主窗口失去焦点，Windows 将焦点交给其他应用程序
2. **消息框被遮盖**：消息对话框显示在其他应用程序后面，用户需要点击任务栏才能看到
3. **用户体验差**：用户以为程序卡死或无响应

### 15.2 根本原因

Navisworks 的 `Progress` 是一个**应用程序级别的模态对话框**：

```
进度条关闭时：
1. Windows 焦点管理器需要决定下一个获得焦点的窗口
2. 如果 Navisworks 主窗口之前被进度条遮盖/禁用
3. 关闭后焦点可能没有正确回到主窗口
4. Windows 可能将焦点交给其他应用程序（如资源管理器、浏览器等）
```

### 15.3 错误做法

```csharp
// ❌ 错误：先关闭进度条，后显示消息框
try
{
    // 执行长时间操作...
    ShowMessage("操作完成");  // 先显示消息
}
finally
{
    Application.EndProgress(); // 后关闭进度条
}
// 结果：消息框显示在进度条后面，进度条关闭时焦点丢失
```

### 15.4 正确做法

**解决方案：在关闭进度条之前，先显式激活 Navisworks 主窗口**

```csharp
// ✅ 正确：先激活主窗口，再关闭进度条，最后显示消息框
using System.Runtime.InteropServices;

public class ClashDetectiveIntegration
{
    // Windows API: 设置前台窗口
    [DllImport("user32.dll")]
    private static extern bool SetForegroundWindow(IntPtr hWnd);

    public void LongRunningOperation()
    {
        Progress progress = Application.BeginProgress("正在处理...", "请稍候");
        
        try
        {
            // 执行长时间操作...
            RunCollisionDetection(progress);
        }
        finally
        {
            // 🔥 关键：先激活 Navisworks 主窗口
            try
            {
                var mainWindow = Autodesk.Navisworks.Api.Application.Gui.MainWindow;
                if (mainWindow != null)
                {
                    SetForegroundWindow(mainWindow.Handle);
                }
            }
            catch { /* 忽略激活失败 */ }
            
            // 然后关闭进度条
            Application.EndProgress();
        }

        // 最后显示消息对话框（在主窗口激活后）
        ShowCancellationMessage();
    }

    private void ShowCancellationMessage()
    {
        // 使用主窗口作为父窗口
        var mainWindow = System.Windows.Forms.Form.FromHandle(
            Autodesk.Navisworks.Api.Application.Gui.MainWindow.Handle);
        
        System.Windows.Forms.MessageBox.Show(
            mainWindow,  // 指定父窗口
            "操作已取消。",
            "取消",
            System.Windows.Forms.MessageBoxButtons.OK,
            System.Windows.Forms.MessageBoxIcon.Information);
    }
}
```

### 15.5 实际应用案例

**场景：碰撞检测取消后的处理**

```csharp
public bool CreateAllAnimationCollisionTests(...)
{
    // 重置取消标志
    _wasLastTestCanceled = false;
    
    Progress progress = Application.BeginProgress("碰撞检测数据分析中，请稍候...");
    bool wasCanceled = false;

    try
    {
        var result = RunClashDetectiveTestsAndSaveToDatabase(..., progress);
        wasCanceled = result.wasCanceled;

        if (wasCanceled)
        {
            // 设置取消标志
            _wasLastTestCanceled = true;
            
            // 清空结果
            ClearCurrentTestCache();
            
            // 恢复物体状态
            PathAnimationManager.GetInstance().RestoreAnimatedObjectState(animatedObject);
            
            // 重置动画完成标志
            ResetAnimationCompletedFlag(routeId);
        }
    }
    finally
    {
        // 🔥 关键：先激活 Navisworks 主窗口（避免关闭进度条后丢失焦点）
        try
        {
            var mainWindow = Autodesk.Navisworks.Api.Application.Gui.MainWindow;
            if (mainWindow != null)
            {
                SetForegroundWindow(mainWindow.Handle);
            }
        }
        catch (Exception ex)
        {
            LogManager.Debug($"[ClashDetective] 激活主窗口失败: {ex.Message}");
        }
        
        // 关闭进度条
        Application.EndProgress();
    }

    // 如果取消，在 finally 之后显示提示消息
    if (wasCanceled)
    {
        ShowCancellationMessage();
        return false;
    }

    return true;
}
```

### 15.6 关键要点总结

| 步骤 | 操作 | 目的 |
|------|------|------|
| 1 | `SetForegroundWindow(mainWindow.Handle)` | 显式激活 Navisworks 主窗口 |
| 2 | `Application.EndProgress()` | 关闭进度条 |
| 3 | `MessageBox.Show(mainWindow, ...)` | 显示消息对话框（使用父窗口） |

**为什么这个顺序有效？**

1. **激活主窗口**：在进度条关闭前，先让主窗口成为活动窗口
2. **关闭进度条**：进度条关闭时，焦点会回到已经激活的主窗口
3. **显示消息框**：消息框作为主窗口的子窗口，自然显示在最前面

### 15.7 常见错误

```csharp
// ❌ 错误1：先显示消息框，后关闭进度条
try { }
finally
{
    ShowMessage("完成");  // 消息框在进度条前显示
    Application.EndProgress();  // 关闭进度条时消息框也消失
}

// ❌ 错误2：只关闭进度条，不激活主窗口
try { }
finally
{
    Application.EndProgress();  // 焦点丢失
}
ShowMessage("完成");  // 消息框显示在其他程序后面

// ❌ 错误3：使用延时等待
finally
{
    Application.EndProgress();
    Thread.Sleep(100);  // 不必要的延时
    Application.DoEvents();
}
```

### 15.8 最佳实践

1. **始终使用 `SetForegroundWindow`** 在关闭进度条前激活主窗口
2. **指定父窗口** 在 `MessageBox.Show` 中使用 `mainWindow` 参数
3. **正确处理异常** 激活失败不应影响主要功能
4. **保持代码简洁** 不需要 Thread.Sleep 等延时操作
5. **测试验证** 在取消操作后验证焦点是否正确回到 Navisworks

---

*最后更新：2026-02-14 - 添加了 Progress 进度条焦点管理的实践经验*
