# 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 Transform 相关 API 概念

**核心概念**：

- `ModelItem.Transform` - 返回设计文件中的原始变换，**只读属性**，**不反映override后的状态**
- `OverridePermanentTransform()` - 应用增量变换（相对于原始Transform累积）
- `ResetPermanentTransform()` - 清除所有增量变换，恢复到设计文件原始位置
- `ModelItem.BoundingBox()` - 返回**当前实际显示**的包围盒（反映override效果）

**⚠️ 关键理解**：

1. **`ModelItem.Transform` 永远返回原始值**，即使通过 `OverridePermanentTransform` 改变了物体位置
2. **Override 信息存储在别处**，不会修改 `ModelItem.Transform` 属性
3. **要获取实际位置，使用 `BoundingBox().Center`**，它反映override后的实际位置

### 11.2 Transform 操作的正确用法

```csharp
// ✅ 获取物体的原始Transform（设计文件中的位置）
Transform3D originalTransform = modelItem.Transform;

// ✅ 应用增量变换（累积变换）
var doc = Application.ActiveDocument;
var modelItems = new ModelItemCollection { modelItem };
doc.Models.OverridePermanentTransform(modelItems, newTransform, false);

// ✅ 重置到原始位置（清除所有增量变换）
doc.Models.ResetPermanentTransform(modelItems);
```

### 11.3 Transform 操作的关键区别

| API方法 | 作用 | 使用场景 | 注意事项 |
|---------|------|---------|---------|
| `ModelItem.Transform` | 获取原始变换 | 记录物体初始位置 | 只读属性，返回设计文件位置 |
| `OverridePermanentTransform()` | 应用增量变换 | 动画中移动物体 | 与现有变换累积，不是绝对位置 |
| `ResetPermanentTransform()` | 重置到原始位置 | 清除所有移动，恢复初始状态 | 忽略所有之前的变换 |

### 11.4 实际应用案例

**案例1：动画系统中的Transform管理**

```csharp
// 动画开始时记录原始位置
private Transform3D _originalTransform;

public void StartAnimation(ModelItem animatedObject)
{
    // 记录原始Transform
    _originalTransform = animatedObject.Transform;
    
    // 移动到路径起点（增量变换）
    var startTransform = Transform3D.CreateTranslation(startPosition);
    var modelItems = new ModelItemCollection { animatedObject };
    doc.Models.OverridePermanentTransform(modelItems, startTransform, false);
}

public void ResetAnimation()
{
    // 动画结束后，使用原始Transform恢复位置
    var modelItems = new ModelItemCollection { _animatedObject };
    doc.Models.OverridePermanentTransform(modelItems, _originalTransform, false);
}
```

**案例2：用户手动位置恢复**

```csharp
public void RestoreToOriginalPosition(ModelItem selectedObject)
{
    // 不需要记录Transform，直接重置到设计文件原始位置
    var doc = Application.ActiveDocument;
    var modelItems = new ModelItemCollection { selectedObject };
    
    // 清除所有增量变换，恢复到设计文件原始位置
    doc.Models.ResetPermanentTransform(modelItems);
}
```

### 11.5 常见Transform问题和解决方案

**问题1：获取不到实际位置**

```csharp
// ❌ 错误：以为 Transform 反映当前位置
var transform = item.Transform;  
// 问题：这永远返回原始Transform，即使物体已被移动

// ✅ 正确：使用 BoundingBox 获取实际位置
var actualCenter = item.BoundingBox().Center;  // 反映override后的实际位置
```

**问题2：动画结束后位置不准确**

```csharp
// ✅ 动画系统应该记录原始Transform并使用增量恢复
private Transform3D _originalTransform;

// 动画开始时
_originalTransform = animatedObject.Transform;

// 动画结束时恢复
doc.Models.OverridePermanentTransform(modelItems, _originalTransform, false);
```

**问题3：记录Transform但不使用**

```csharp
// ❌ 不必要：记录Transform但使用Reset
private Transform3D _originalTransform;
_originalTransform = selectedItem.Transform;  // 记录了但不使用
doc.Models.ResetPermanentTransform(modelItems);  // 直接重置

// ✅ 简化：直接重置，无需记录
doc.Models.ResetPermanentTransform(modelItems);
```

### 11.6 Transform 最佳实践

1. **选择合适的恢复方式**：
   - 动画系统：使用 `OverridePermanentTransform` + 原始Transform
   - 用户操作：使用 `ResetPermanentTransform` 直接重置

2. **避免不必要的Transform记录**：
   - 如果只需要恢复到设计文件原始位置，使用 `ResetPermanentTransform`
   - 只有需要恢复到特定中间状态时才记录Transform

3. **理解增量vs绝对变换**：
   - `OverridePermanentTransform` 是增量的，会与现有变换叠加
   - `ResetPermanentTransform` 是绝对的，清除所有变换

4. **线程安全**：
   - 所有Transform操作都必须在主UI线程中执行
   - 使用 `Dispatcher.Invoke` 确保线程安全

### 11.7 旋转操作的关键限制和解决方案 ⚠️ 重要

基于实际测试验证的关键发现（2025-12-15）。

#### 11.7.1 旋转中心的API限制

**⚠️ 核心限制：Navisworks API的旋转总是绕世界原点(0,0,0)进行**

```csharp
// ❌ 错误理解：以为旋转绕物体中心
var rotation = new Transform3D(new Rotation3D(new UnitVector3D(0, 0, 1), angle));
doc.Models.OverridePermanentTransform(modelItems, rotation, false);
// 实际效果：物体绕世界原点(0,0,0)"公转"，不是绕自己"自转"

// 🔍 实际测试验证：
// 物体在 (-2.499, -1.640, 0.500) 位置
// 旋转45度后移动到 (-0.608, -2.927, 0.500)
// 验证公式：x' = x*cos(45°) - y*sin(45°) = -0.607 ✓
//          y' = x*sin(45°) + y*cos(45°) = -2.927 ✓
// 证明：旋转中心是世界原点(0,0,0)，不是物体中心
```

**验证代码**：

```csharp
// ✅ 测试代码：证明旋转绕世界原点
var initialCenter = item.BoundingBox().Center;  // (-2.499, -1.640, 0.500)

// 应用45度旋转
var rotation = new Transform3D(new Rotation3D(new UnitVector3D(0, 0, 1), Math.PI/4));
doc.Models.OverridePermanentTransform(modelItems, rotation, false);

var afterCenter = item.BoundingBox().Center;    // (-0.608, -2.927, 0.500)

// 计算期望位置（绕原点旋转）
double cos45 = Math.Cos(Math.PI/4);
double sin45 = Math.Sin(Math.PI/4);
double expectedX = initialCenter.X * cos45 - initialCenter.Y * sin45;  // -0.607
double expectedY = initialCenter.X * sin45 + initialCenter.Y * cos45;  // -2.927

// 验证：实际位置 = 期望位置（绕原点旋转）✓
```

#### 11.7.2 Transform3DComponents 的行为

**关键理解：`Transform3DComponents.Combine()` 的变换顺序**

```csharp
// Transform3DComponents.Combine() 应用顺序：
// 1. Scale（缩放）
// 2. Rotation（旋转，绕原点）
// 3. Translation（平移）

// ❌ 错误：直接设置rotation和translation
var components = identity.Factor();
components.Rotation = new Rotation3D(new UnitVector3D(0, 0, 1), deltaYaw);
components.Translation = deltaPos;  // 平移在旋转之后应用
var transform = components.Combine();

// 问题：物体先绕原点旋转（产生位置偏移），然后平移
// 结果：物体"公转"到错误位置
```

#### 11.7.3 正确实现"绕物体中心旋转"

**解决方案：手动计算旋转导致的位置偏移并补偿**

```csharp
// ✅ 正确方法：计算补偿平移量
private void UpdateObjectPosition(Point3D newPosition, double newYaw)
{
    var doc = Application.ActiveDocument;
    var modelItems = new ModelItemCollection { _animatedObject };
    
    // 计算旋转和平移增量
    var deltaPos = new Vector3D(
        newPosition.X - _currentPosition.X,
        newPosition.Y - _currentPosition.Y,
        newPosition.Z - _currentPosition.Z
    );
    
    Transform3D incrementalTransform;
    
    if (!double.IsNaN(newYaw))
    {
        double deltaYaw = newYaw - _currentYaw;
        
        // 🎯 关键：计算绕当前位置旋转的等效变换
        // 1. 如果绕原点旋转deltaYaw，当前位置会移到哪里？
        double cos = Math.Cos(deltaYaw);
        double sin = Math.Sin(deltaYaw);
        double rotatedX = _currentPosition.X * cos - _currentPosition.Y * sin;
        double rotatedY = _currentPosition.X * sin + _currentPosition.Y * cos;
        
        // 2. 我们希望物体绕自己旋转，位置移动到newPosition
        // 所以需要的平移 = newPosition - (旋转后的位置)
        var compensatedTranslation = new Vector3D(
            newPosition.X - rotatedX,      // 补偿X方向的偏移
            newPosition.Y - rotatedY,      // 补偿Y方向的偏移
            newPosition.Z - _currentPosition.Z  // Z保持增量
        );
        
        // 3. 组合：先旋转（绕原点），再平移（补偿+目标位置）
        var identity = Transform3D.CreateTranslation(new Vector3D(0, 0, 0));
        var components = identity.Factor();
        components.Rotation = new Rotation3D(new UnitVector3D(0, 0, 1), deltaYaw);
        components.Translation = compensatedTranslation;  // 关键：使用补偿后的平移
        
        incrementalTransform = components.Combine();
        _currentYaw = newYaw;
    }
    else
    {
        // 纯平移：直接使用增量
        incrementalTransform = Transform3D.CreateTranslation(deltaPos);
    }
    
    // 应用增量变换
    doc.Models.OverridePermanentTransform(modelItems, incrementalTransform, false);
    _currentPosition = newPosition;
}
```

**原理说明**：

```
API限制：
  旋转 → 物体绕(0,0,0)旋转 → 位置从P1偏移到P2
  
我们需要的效果：
  旋转 → 物体绕自己旋转 → 位置从P1移动到P_target
  
解决方案：
  补偿平移 = P_target - P2
  最终变换 = Rotation(deltaYaw) + Translation(P_target - P2)
  
结果：
  物体先绕原点旋转到P2，然后平移到P_target
  看起来像是绕自己旋转并移动到目标位置
```

#### 11.7.4 初始化问题

**⚠️ 重要：初始化yaw必须与第一帧匹配**

```csharp
// ❌ 错误：初始化为0
_currentYaw = 0.0;
// 第一帧调用UpdateObjectPosition时：
// deltaYaw = firstFrame.YawRadians - 0.0  // 产生大的旋转增量
// 导致物体从起点"公转"飞走

// ✅ 正确：初始化为第一帧的yaw
if (_animationFrames != null && _animationFrames.Count > 0)
{
    _currentYaw = _animationFrames[0].YawRadians;  // 使deltaYaw=0
    
    // 第一次调用UpdateObjectPosition
    var firstFrame = _animationFrames[0];
    UpdateObjectPosition(firstFrame.Position, firstFrame.YawRadians);
    // 此时：deltaYaw = firstFrame.YawRadians - firstFrame.YawRadians = 0
    // 结果：只有平移，没有旋转偏移
}
```

#### 11.7.5 相关API限制说明

Autodesk官方论坛已确认的限制（Issue NW-53280）：

- **无法设置旋转中心点**：API不提供指定旋转中心的方法
- **UI的Override Transform功能**：也是通过计算补偿实现的
- **建议的解决方案**：手动计算T(center) × R × T(-center)的等效变换

#### 11.7.6 旋转操作最佳实践

| 场景 | 方法 | 注意事项 |
|------|------|---------|
| 简单旋转（原地） | 使用位置补偿公式 | 必须计算旋转导致的偏移 |
| 旋转+移动 | 组合补偿平移和目标平移 | 理解Combine()的变换顺序 |
| 动画初始化 | `_currentYaw = firstFrame.YawRadians` | 避免第一帧产生旋转增量 |
| 调试验证 | 测试物体远离原点的情况 | 原点附近可能掩盖问题 |

**调试技巧**：

```csharp
// ✅ 测试旋转中心的方法
// 1. 将物体移动到远离原点的位置（如(-5, -5, 0)）
// 2. 应用旋转
// 3. 检查物体是否"公转"（位置大幅移动）还是"自转"（位置基本不变）
// 4. 如果发现"公转"，说明没有正确补偿

// ✅ 验证补偿计算的公式
double expectedX_afterRotation = currentX * cos(angle) - currentY * sin(angle);
double expectedY_afterRotation = currentX * sin(angle) + currentY * cos(angle);
var compensationX = targetX - expectedX_afterRotation;
var compensationY = targetY - expectedY_afterRotation;

LogManager.Debug($"旋转前: ({currentX}, {currentY})");
LogManager.Debug($"绕原点旋转后: ({expectedX_afterRotation}, {expectedY_afterRotation})");
LogManager.Debug($"目标位置: ({targetX}, {targetY})");
LogManager.Debug($"需要补偿: ({compensationX}, {compensationY})");
```

## 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）
  2. 状态转换清晰：
    - 无模型→无模型：初始清理
    - 无模型→有模型：文档加载完成 ✅ 这是我们需要初始化的时机
    - 有模型→无模型：文档关闭/切换 ✅ 这是我们需要清理的时机
  3. 时间间隔很短：事件成对出现，间隔仅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
  - 用于显示的几何体
  - 用于显示的外观

  2. 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用来唯一标识特定实例
- 但这里的"实例"指的是场景图中的节点实例，不是几何体的不同部分

  3. 你的地板问题分析

  真正的问题：

  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

  4. 解决方案建议

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

  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() 方法，避免哈希冲突和跨运行时不一致的问题

