修复网格可视化性能问题

根本原因:新代码在循环中调用了 AddPoint() 方法,该方法每次都会触发 UpdateTotalLength(),导致:
算法复杂度:从 O(n) 变成 O(n² log n)
排序次数:12,480 个点 = 78,000,000 次排序
性能损失:从 76 毫秒 → 12.5 秒(130倍慢)
修复方案:直接使用 Points.Add() 绕过昂贵的 UpdateTotalLength() 调用
This commit is contained in:
tian 2026-01-16 20:35:33 +08:00
parent f4fda4e308
commit 07f8f5b2bf
6 changed files with 300 additions and 21 deletions

216
AGENTS.md Normal file
View File

@ -0,0 +1,216 @@
# AGENTS.md
本文件包含在 NavisworksTransport 仓库中工作的代理编码助手所需的构建/测试命令和代码风格指南。
## 构建命令
### 主要构建
```bash
./compile.bat
```
- 使用 Visual Studio 2022 的 MSBuild
- 构建 Release 配置的 x64 平台
- 目标框架:.NET Framework 4.8
### 测试命令
```bash
./run-unit-tests.bat
```
- 首先构建主项目
- 构建测试项目Release/x64
- 使用 VSTest 控制台运行单元测试
- 测试框架MSTest 3.0.4
### 手动测试执行
```bash
# 仅构建测试项目
MSBuild.exe NavisworksTransport.UnitTests.csproj /p:Configuration=Release /p:Platform=x64
# 运行特定测试(使用 Release 路径匹配构建输出)
VSTest.Console.exe "bin\x64\Release\NavisworksTransport.UnitTests.dll" /Framework:".NETFramework,Version=v4.8" /TestAdapterPath:"packages\MSTest.TestAdapter.3.0.4\build\net462"
```
## 项目结构
### 目标环境
- **平台**: Navisworks 2026 (Windows 10+)
- **框架**: .NET Framework 4.8
- **语言**: C# 7.3
- **架构**: x64
### 关键目录
- `src/Core/` - 主插件文件和业务逻辑
- `src/Commands/` - 命令模式实现
- `src/PathPlanning/` - A* 算法和网格地图生成
- `src/UI/WPF/` - WPF MVVM UI 组件
- `src/Utils/` - 工具类和助手
- `UnitTests/` - 测试文件
### 插件架构
- **MainPlugin.cs**: AddInPlugin - Ribbon UI + 停靠面板
- **PathClickToolPlugin.cs**: ToolPlugin - 3D 鼠标交互
- **PathPointRenderPlugin.cs**: RenderPlugin - 3D 可视化
### API 使用
- **Native API** (`Autodesk.Navisworks.Api`): 核心功能
- **COM API** (`Autodesk.Navisworks.ComApi`): 属性持久化, TimeLiner
- 在 MainPlugin 构造函数中初始化 `GlobalExceptionHandler`
## 代码风格指南
### 导入和命名空间
```csharp
// System 命名空间优先
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
// 第三方库
using RoyT.AStar;
// Navisworks API 命名空间
using Autodesk.Navisworks.ComApi;
using Autodesk.Navisworks.Api;
using Autodesk.Navisworks.Api.Plugins;
// 项目命名空间(按字母顺序)
using NavisworksTransport.Core;
using NavisworksTransport.Utils;
using NavisworksTransport.UI.WPF.ViewModels;
```
### 命名约定
- **类**: PascalCase (例如: `PathPlanningManager`)
- **方法**: PascalCase (例如: `GenerateGridMap`)
- **属性**: PascalCase (例如: `CellSize`)
- **字段**: camelCase 带下划线前缀 (例如: `_uiStateManager`)
- **常量**: PascalCase (例如: `MaxHeightDiff`)
- **接口**: PascalCase 带 'I' 前缀 (例如: `IPathPlanningCommand`)
- **枚举**: PascalCase (例如: `LogLevel`)
### 单位系统 - 极其重要
所有网格地图和路径规划计算**必须使用模型单位**,不要用米制单位。
```csharp
// ✅ 正确:在函数入口转换
public GridMap GenerateFromBIM(BoundingBox3D bounds, double cellSizeMeters, ...)
{
double factor = UnitsConverter.GetMetersToUnitsConversionFactor(Application.ActiveDocument.Units);
double cellSizeInModelUnits = cellSizeMeters * factor;
// 使用模型单位进行所有计算
}
// ❌ 错误:在计算中混用单位
private const double MAX_HEIGHT_DIFF = 0.35; // 米!
if (heightDiff > MAX_HEIGHT_DIFF) // Bug!
```
### 错误处理
- **防御性编程**: 检测并报告错误,不要隐藏它们
- **日志记录**: 使用 `LogManager` 进行所有错误报告
- **异常**: 不要仅仅为了吞噬异常而捕获它们
```csharp
// ✅ 正确:记录并重新抛出或适当处理
try
{
var result = SomeOperation();
return result;
}
catch (Exception ex)
{
LogManager.Error($"Operation failed: {ex.Message}");
throw; // 重新抛出或适当处理
}
// ❌ 错误:吞噬异常
try
{
var result = SomeOperation();
return result;
}
catch
{
return null; // 隐藏问题
}
```
### 线程安全和UI更新
- 使用 `UIStateManager` 进行线程安全的UI更新
- UI操作必须编组到主线程
- 使用异步事件触发避免死锁
```csharp
// ✅ 正确:异步事件触发
private void OnStatusChanged(string status)
{
Task.Run(() =>
{
try
{
StatusChanged?.Invoke(this, status);
}
catch (Exception ex)
{
LogManager.Error($"StatusChanged事件失败: {ex.Message}");
}
});
}
```
### 包管理
- 使用旧式 csproj 与 `<Reference Include>` 和 HintPath
- packages.config 用于手动包管理
- **不要** 使用 `dotnet add package`
- 包路径: `packages\{PackageId}.{Version}\lib\{TargetFramework}\{Assembly}.dll`
### 文档和注释
- 对公共 API 使用 XML 文档
- 业务逻辑可以使用中文注释
- 注释重点说明"为什么"而不是"什么"
### 性能考虑
- 缓存频繁访问的几何数据
- 使用空间索引进行碰撞检测
- 使用 `SmartDataBindingOptimizer` 优化 UI 更新
- 最小化跨线程操作
## 常见错误避免
1. **单位混淆**: 绝不要在计算中混用米制和模型单位
2. **线程违规**: 始终将UI操作编组到主线程
3. **异常吞噬**: 正确记录和处理错误
4. **内存泄漏**: 释放资源并取消事件订阅
5. **API兼容性**: 专门针对 Navisworks 2026不考虑向后兼容
## 验证
运行命令前,代理应该验证:
- 构建输出存在: `dir bin\x64\Release\NavisworksTransportPlugin.dll`
- 测试程序集存在: `dir bin\x64\Release\NavisworksTransport.UnitTests.dll`
- 依赖项在预期的包路径中
- MSBuild 和 VSTest 在预期位置可用
## 调试
- 调试日志位置: `"C:\ProgramData\Autodesk\Navisworks Manage 2026\plugins\NavisworksTransportPlugin\logs\debug.log"`
- 使用 `LogManager` 进行结构化日志记录
- 在 Navisworks 2026 环境中测试以进行完整集成测试

View File

@ -3781,7 +3781,7 @@ namespace NavisworksTransport
Notes = $"GridType:{cell.CellType}, LayerZ={layerZ:F2}, IsWalkable={layer.IsWalkable}"
};
targetRoute.AddPoint(gridPoint);
targetRoute.Points.Add(gridPoint);
totalVisualized++;
}
}
@ -3800,7 +3800,7 @@ namespace NavisworksTransport
Index = totalVisualized,
Notes = $"GridType:Unknown, NoLayers"
};
unknownRoute.AddPoint(gridPoint);
unknownRoute.Points.Add(gridPoint);
unknownCells++;
totalVisualized++;
}

View File

@ -385,6 +385,9 @@ namespace NavisworksTransport
// 静态实例,用于外部访问
private static PathPointRenderPlugin _instance;
// 防抖机制:记录上次刷新时间(使用高精度计数器)
private long _lastRefreshTicks = 0;
/// <summary>
/// 构造函数
@ -2462,6 +2465,19 @@ namespace NavisworksTransport
{
try
{
// 防抖机制最小间隔50ms避免过于频繁的Navisworks重绘
// 使用高精度Ticks避免毫秒精度不足的问题
long currentTicks = DateTime.Now.Ticks;
long elapsedTicks = currentTicks - _lastRefreshTicks;
long intervalMs = elapsedTicks / TimeSpan.TicksPerMillisecond;
if (intervalMs < 50)
{
LogManager.Debug($"[RequestViewRefresh] 防抖触发,忽略刷新请求 (间隔: {intervalMs:F1}ms)");
return;
}
_lastRefreshTicks = currentTicks;
if (Application.ActiveDocument?.ActiveView != null)
{
Application.ActiveDocument.ActiveView.RequestDelayedRedraw(ViewRedrawRequests.Render);
@ -2557,8 +2573,6 @@ namespace NavisworksTransport
return CalculateDistance(point, closestPoint);
}
private static DateTime _lastRefreshTime = DateTime.MinValue;
#endregion
}

View File

@ -39,6 +39,30 @@ namespace NavisworksTransport.PathPlanning
/// 完成百分比
/// </summary>
public double CompletionPercentage { get; set; } = 100.0;
/// <summary>
/// 路径查找是否成功
/// </summary>
public bool IsSuccess => PathPoints.Count >= 2;
/// <summary>
/// 失败消息
/// </summary>
public string ErrorMessage { get; set; }
/// <summary>
/// 创建失败结果
/// </summary>
public static PathFindingResult Failure(string errorMessage)
{
return new PathFindingResult
{
PathPoints = new List<Point3D>(),
IsComplete = false,
CompletionPercentage = 0.0,
ErrorMessage = errorMessage
};
}
}
/// <summary>
@ -454,16 +478,16 @@ namespace NavisworksTransport.PathPlanning
bool startOnObstacle = (startNode == null);
bool endOnObstacle = (endNode == null);
// 如果起点在障碍物上,直接报错
// 如果起点在障碍物上,返回失败结果(正常业务流程,不记录错误)
if (startOnObstacle && endOnObstacle)
{
LogManager.Error($"[A*执行-3D] 起点网格({startGridPos.X},{startGridPos.Y})和终点网格({endGridPos.X},{endGridPos.Y})都在不可通行区域");
throw new AutoPathPlanningException("起点和终点都在不可通行区域,无法生成路径");
LogManager.Info($"[A*执行-3D] 起点网格({startGridPos.X},{startGridPos.Y})和终点网格({endGridPos.X},{endGridPos.Y})都在不可通行区域");
return PathFindingResult.Failure("起点和终点都在不可通行区域,请选择可通行的位置");
}
else if (startOnObstacle)
{
LogManager.Error($"[A*执行-3D] 起点网格({startGridPos.X},{startGridPos.Y})在不可通行区域");
throw new AutoPathPlanningException("起点在不可通行区域,无法生成路径");
LogManager.Info($"[A*执行-3D] 起点网格({startGridPos.X},{startGridPos.Y})在不可通行区域");
return PathFindingResult.Failure("起点在不可通行区域,请选择可通行的位置");
}
// 如果终点不可达寻找最接近终点的可达节点类似2.5D的ClosestApproach逻辑
@ -499,8 +523,8 @@ namespace NavisworksTransport.PathPlanning
}
else
{
LogManager.Error($"[A*执行-3D] 无法找到任何可达节点(图中没有任何节点)");
throw new AutoPathPlanningException("无法找到任何可达节点,网格地图可能完全被障碍物覆盖");
LogManager.Warning($"[A*执行-3D] 无法找到任何可达节点(图中没有任何节点)");
return PathFindingResult.Failure("无法找到任何可达节点,网格地图可能完全被障碍物覆盖");
}
}
@ -515,8 +539,8 @@ namespace NavisworksTransport.PathPlanning
if (astarPath.Edges.Count == 0)
{
LogManager.Error($"[A*执行-3D] 未找到路径,起点({startInfo.X},{startInfo.Y})到终点({endInfo.X},{endInfo.Y})之间被障碍物完全阻断");
throw new AutoPathPlanningException("起点和终点之间被障碍物完全阻断,无法找到可行路径");
LogManager.Warning($"[A*执行-3D] 未找到路径,起点({startInfo.X},{startInfo.Y})到终点({endInfo.X},{endInfo.Y})之间被障碍物完全阻断");
return PathFindingResult.Failure("起点和终点之间被障碍物完全阻断,无法找到可行路径");
}
// 转换为世界坐标路径
@ -731,8 +755,8 @@ namespace NavisworksTransport.PathPlanning
if (pathResult == null || !pathResult.PathPoints.Any())
{
LogManager.Error("[2.5D路径规划] 未找到有效路径");
throw new AutoPathPlanningException("未找到有效路径,起点和终点之间可能被障碍物阻断");
LogManager.Warning("[2.5D路径规划] 未找到有效路径");
return PathFindingResult.Failure("未找到有效路径,起点和终点之间可能被障碍物阻断");
}
LogManager.Info($"[2.5D路径规划] A*算法找到原始路径,包含 {pathResult.PathPoints.Count} 个点");
@ -2401,8 +2425,8 @@ namespace NavisworksTransport.PathPlanning
}
else
{
LogManager.Error("[A*执行] 未找到路径");
throw new AutoPathPlanningException("A*算法未找到路径,起点和终点之间被障碍物阻断");
LogManager.Warning("[A*执行] 未找到路径");
return PathFindingResult.Failure("A*算法未找到路径,起点和终点之间被障碍物阻断");
}
}
catch (Exception ex)

View File

@ -1024,7 +1024,24 @@ namespace NavisworksTransport.PathPlanning
/// </summary>
public class AutoPathPlanningException : Exception
{
public AutoPathPlanningException(string message) : base(message) { }
public AutoPathPlanningException(string message, Exception innerException) : base(message, innerException) { }
public bool IsBusinessError { get; }
public AutoPathPlanningException(string message, bool isBusinessError = false) : base(message)
{
IsBusinessError = isBusinessError;
}
public AutoPathPlanningException(string message, Exception innerException, bool isBusinessError = false) : base(message, innerException)
{
IsBusinessError = isBusinessError;
}
}
/// <summary>
/// 路径规划业务异常(用户选择导致的正常失败)
/// </summary>
public class PathPlanningBusinessException : AutoPathPlanningException
{
public PathPlanningBusinessException(string message) : base(message, true) { }
}
}

View File

@ -3210,13 +3210,21 @@ namespace NavisworksTransport.UI.WPF.ViewModels
/// </summary>
private async void OnPathPlanningError(object sender, PathPlanningErrorOccurredEventArgs e)
{
LogManager.Info($"*** OnPathPlanningError被调用Sender: {sender?.GetType().Name}, Error: {e?.ErrorMessage} ***");
LogManager.Info($"*** OnPathPlanningError被调用Sender: {sender?.GetType().Name}, Error: {e?.ErrorMessage}, Level: {e?.ErrorLevel} ***");
try
{
await SafeExecuteAsync(async () =>
{
LogManager.Error($"自动路径规划失败: {e.ErrorMessage}");
// 根据错误级别决定日志记录方式
if (e.ErrorLevel == PathPlanningErrorLevel.Warning)
{
LogManager.Warning($"自动路径规划失败: {e.ErrorMessage}");
}
else
{
LogManager.Error($"自动路径规划失败: {e.ErrorMessage}");
}
// 使用UIStateManager确保UI更新在主线程执行
await _uiStateManager.ExecuteUIUpdateAsync(() =>