diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..3e1f4e1 --- /dev/null +++ b/AGENTS.md @@ -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 与 `` 和 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 环境中测试以进行完整集成测试 diff --git a/src/Core/PathPlanningManager.cs b/src/Core/PathPlanningManager.cs index 2197f62..c36db7f 100644 --- a/src/Core/PathPlanningManager.cs +++ b/src/Core/PathPlanningManager.cs @@ -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++; } diff --git a/src/Core/PathPointRenderPlugin.cs b/src/Core/PathPointRenderPlugin.cs index d162ce1..f6cbdc5 100644 --- a/src/Core/PathPointRenderPlugin.cs +++ b/src/Core/PathPointRenderPlugin.cs @@ -385,6 +385,9 @@ namespace NavisworksTransport // 静态实例,用于外部访问 private static PathPointRenderPlugin _instance; + + // 防抖机制:记录上次刷新时间(使用高精度计数器) + private long _lastRefreshTicks = 0; /// /// 构造函数 @@ -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 } diff --git a/src/PathPlanning/AutoPathFinder.cs b/src/PathPlanning/AutoPathFinder.cs index cb57679..e362eee 100644 --- a/src/PathPlanning/AutoPathFinder.cs +++ b/src/PathPlanning/AutoPathFinder.cs @@ -39,6 +39,30 @@ namespace NavisworksTransport.PathPlanning /// 完成百分比 /// public double CompletionPercentage { get; set; } = 100.0; + + /// + /// 路径查找是否成功 + /// + public bool IsSuccess => PathPoints.Count >= 2; + + /// + /// 失败消息 + /// + public string ErrorMessage { get; set; } + + /// + /// 创建失败结果 + /// + public static PathFindingResult Failure(string errorMessage) + { + return new PathFindingResult + { + PathPoints = new List(), + IsComplete = false, + CompletionPercentage = 0.0, + ErrorMessage = errorMessage + }; + } } /// @@ -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) diff --git a/src/PathPlanning/GridMap.cs b/src/PathPlanning/GridMap.cs index 0cc29ef..5738fc6 100644 --- a/src/PathPlanning/GridMap.cs +++ b/src/PathPlanning/GridMap.cs @@ -1024,7 +1024,24 @@ namespace NavisworksTransport.PathPlanning /// 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; + } + } + + /// + /// 路径规划业务异常(用户选择导致的正常失败) + /// + public class PathPlanningBusinessException : AutoPathPlanningException + { + public PathPlanningBusinessException(string message) : base(message, true) { } } } diff --git a/src/UI/WPF/ViewModels/PathEditingViewModel.cs b/src/UI/WPF/ViewModels/PathEditingViewModel.cs index 540c701..b9c4c28 100644 --- a/src/UI/WPF/ViewModels/PathEditingViewModel.cs +++ b/src/UI/WPF/ViewModels/PathEditingViewModel.cs @@ -3210,13 +3210,21 @@ namespace NavisworksTransport.UI.WPF.ViewModels /// 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(() =>