NavisworksTransport/doc/design/2026/NavisworksAPI使用方法.md

91 KiB
Raw Blame History

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.cohttps://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 正确的遍历方式

// ✅ 正确:获取所有模型项
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 获取子节点

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

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

1.3 遍历祖先节点

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

2. 搜索和查询

2.1 使用LINQ查询

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

2.2 使用Search类

// ✅ 正确使用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 迭代遍历(性能对比)

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

3. 选择操作

3.1 操作当前选择

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

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

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

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

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

4. 可见性控制

4.1 隐藏和显示

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

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

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

5. 文件导出

5.1 基本文件保存

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

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

5.2 ExportToNwd API

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

document.ExportToNwd(saveFilePath, exportOptions);

6. 性能最佳实践

6.1 避免的做法

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

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

6.2 推荐的做法

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

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

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

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

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 线程)中执行

// ❌ 错误:在后台线程中调用 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 方法通过后台线程调用时崩溃

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

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

// ✅ 修复代码:线程安全
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 线程安全检查和诊断

// ✅ 检查当前线程状态
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 最佳实践模式

// ✅ 推荐模式:安全的异步 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 操作的正确用法

// ✅ 获取物体的原始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管理

// 动画开始时记录原始位置
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用户手动位置恢复

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

11.5 常见Transform问题和解决方案

问题1获取不到实际位置

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

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

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

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

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

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

问题3记录Transform但不使用

// ❌ 不必要记录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)进行

// ❌ 错误理解:以为旋转绕物体中心
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),不是物体中心

验证代码

// ✅ 测试代码:证明旋转绕世界原点
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() 的变换顺序

// 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 正确实现"绕物体中心旋转"

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

// ✅ 正确方法:计算补偿平移量
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必须与第一帧匹配

// ❌ 错误初始化为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 避免第一帧产生旋转增量
调试验证 测试物体远离原点的情况 原点附近可能掩盖问题

调试技巧

// ✅ 测试旋转中心的方法
// 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

// ✅ 通过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

// ✅ 使用预定义属性分类和属性名进行搜索
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);

属性分类和属性名常量

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

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

12.2 COM API 属性访问方法(基于 AutoUserPropsExample.cs

获取和遍历属性

// ✅ 获取选中对象的属性节点
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}");
        }
    }
}

添加自定义属性

// ✅ 创建新的自定义属性
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", "自定义值");

修改现有自定义属性

// ✅ 正确的修改方法基于实际修复经验和官方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");

关键要点(基于实际修复经验):

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

删除自定义属性

// ✅ 删除指定的自定义属性分类
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 属性操作最佳实践

// ✅ 推荐模式结合两种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 常见属性操作示例

// ✅ 检查项目是否有特定属性分类
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编辑

// ✅ 可以修改:隐藏状态
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示例的确认

// 官方示例中的这些操作证实了这些属性是可修改的
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. 基础标识属性(只读)

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

2. 结构关系属性(只读)

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

3. 几何和材质属性(只读)

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

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

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

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

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

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

物流插件的实际应用策略

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

// 物流分类可见性管理
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. 使用自定义属性存储业务数据

// 存储物流分类信息
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. 只读属性用于查询和智能分析

// 基于只读属性进行智能空间分析
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. 综合应用示例:物流节点标记

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操作自定义属性时的关键要点。

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

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

错误做法

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

正确做法

// ✅ 正确:动态查找现有属性的索引
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:用户定义的显示名称,由我们控制

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

ClassName:系统生成的内部标识符,我们无法控制

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

修复要点

// ❌ 错误:同时匹配两个条件会导致找不到现有属性
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基索引
// ✅ 正确的索引转换
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);
}

🛠️ 完整的修复模板

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
  • 是否同时匹配了ClassUserNameClassName
  • 是否正确进行了索引转换0基→1基
  • 是否在主UI线程中执行COM API调用
  • 日志中是否显示"未找到现有属性分类"但实际存在

🎯 修复验证方法

测试步骤

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

日志验证

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

💡 经验总结

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

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

13.1 缓存刷新的必要性

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

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

13.2 安全的缓存刷新方法

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

// ✅ 最安全的缓存刷新方法空集合的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 危险的缓存刷新方法

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

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

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

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

13.4 何时需要缓存刷新

必须刷新的操作

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

可选刷新的操作

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

13.5 实践模式:操作后刷新

// ✅ 推荐模式:操作 + 刷新 + 验证
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返回不一致
  • 连续相同操作的结果不同

诊断代码

// ✅ 验证缓存同步状态
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 多线程环境下的缓存刷新

// ✅ 线程安全的缓存刷新
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容差 = 识别重复的最大距离

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

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

    // 创建临时测试以获取真实碰撞结果
    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模型几何体各种状态信息的接口包括颜色、变换、透明度、包围盒和几何体结构等重要属性。

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 标记自动多边形光标

使用方式:

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. 但这是为了"保护"这些节点不被隐藏,而不是为了隐藏它们
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.

基础示例

/// <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 提供了原生的进度条显示功能,可以在耗时操作中向用户展示进度和允许取消操作。

核心方法

// 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 带标题的进度条(推荐用法)

/// <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 支持用户取消操作

/// <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用法

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

/// <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

/// <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. 合理的更新频率

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

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

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

2. 异常处理中确保关闭

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

3. 取消操作的资源清理

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

4. 线程安全考虑

// ✅ 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开销
调试信息 不保留历史 可追溯问题
适用场景 耗时操作的用户交互 问题诊断和审计

推荐方案:同时使用两者

// 进度条:给用户看,提供交互
progress.Update((double)i / total);

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

14.8 完整使用模式总结

推荐的标准模板

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();
    }
}

常见陷阱和避免方法

// ❌ 陷阱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. 每次文档操作都有两个事件:
    • 先清空ModelsCount=0
    • 再加载新ModelsCount>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.Equals() 和 GetHashCode() 方法,避免哈希冲突和跨运行时不一致的问题