From ea809277c352d0412e75a4f4173493b1e20817bd Mon Sep 17 00:00:00 2001 From: tian <11429339@qq.com> Date: Fri, 29 Aug 2025 20:43:02 +0800 Subject: [PATCH] =?UTF-8?q?=E5=BC=80=E5=A7=8B=E5=AE=9E=E7=8E=B02.5D?= =?UTF-8?q?=E7=A9=BA=E9=97=B4=E7=BD=91=E6=A0=BC=EF=BC=8C=E8=BF=9B=E8=A1=8C?= =?UTF-8?q?=E5=88=B0=E7=A9=BA=E9=97=B4=E5=93=88=E5=B8=8C=E6=88=90=E5=8A=9F?= =?UTF-8?q?=EF=BC=8C=E5=9E=82=E7=9B=B4=E6=89=AB=E6=8F=8F=E5=A4=B1=E8=B4=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CLAUDE.md | 4 + NavisworksTransportPlugin.csproj | 2 + QWEN.md | 144 ++-- doc/design/Design_Plan.md | 299 +++++++ doc/design/architecture/README.md | 22 + .../system_architecture_functional_2026.md | 125 +++ .../system_architecture_functional_2026.png | Bin 0 -> 45788 bytes doc/design/architecture/功能方案_详版.md | 593 +++++++++++++ doc/design/architecture/功能方案模板.md | 159 ++++ doc/requirement/todo_features.md | 19 +- doc/requirement/user_requiement.md | 17 +- doc/working/astar_implementation_plan.md | 151 ++++ .../grid_generation_2_5d_upgrade_report.md | 159 ++++ .../pathfinding_grid_improvement_plan.md | 746 ++++++++++++++++ doc/working/spatial_hash_principle.md | 326 +++++++ src/Commands/AutoPathPlanningCommand.cs | 83 +- src/Core/PathPlanningManager.cs | 73 +- src/PathPlanning/AutoPathFinder.cs | 358 +++++++- src/PathPlanning/ChannelBasedGridBuilder.cs | 727 ++++++++++++++++ src/PathPlanning/GridMap.cs | 276 +++++- src/PathPlanning/GridMapGenerator.cs | 307 ++++++- src/PathPlanning/TriangleSpatialHash.cs | 402 +++++++++ src/PathPlanning/VerticalScanProcessor.cs | 808 ++++++++++++++++++ 23 files changed, 5642 insertions(+), 158 deletions(-) create mode 100644 doc/design/Design_Plan.md create mode 100644 doc/design/architecture/README.md create mode 100644 doc/design/architecture/system_architecture_functional_2026.md create mode 100644 doc/design/architecture/system_architecture_functional_2026.png create mode 100644 doc/design/architecture/功能方案_详版.md create mode 100644 doc/design/architecture/功能方案模板.md create mode 100644 doc/working/astar_implementation_plan.md create mode 100644 doc/working/grid_generation_2_5d_upgrade_report.md create mode 100644 doc/working/pathfinding_grid_improvement_plan.md create mode 100644 doc/working/spatial_hash_principle.md create mode 100644 src/PathPlanning/ChannelBasedGridBuilder.cs create mode 100644 src/PathPlanning/TriangleSpatialHash.cs create mode 100644 src/PathPlanning/VerticalScanProcessor.cs diff --git a/CLAUDE.md b/CLAUDE.md index 059d49b..6c4f10c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -113,6 +113,10 @@ public class MainPlugin : AddInPlugin { } public class PathClickToolPlugin : ToolPlugin { } ``` +### Development Principles + +- **不考虑向后兼容性**: 直接修改现有代码逻辑,无需保持向后兼容性,专注于解决当前问题和改进现有功能 + ### Critical API Usage Patterns - **Always reference Navisworks API documentation**: Check `doc\navisworks_api\` before implementing any Navisworks functionality diff --git a/NavisworksTransportPlugin.csproj b/NavisworksTransportPlugin.csproj index 13fca5b..a418403 100644 --- a/NavisworksTransportPlugin.csproj +++ b/NavisworksTransportPlugin.csproj @@ -155,6 +155,8 @@ + + diff --git a/QWEN.md b/QWEN.md index 8ac7eaf..8a157b4 100644 --- a/QWEN.md +++ b/QWEN.md @@ -8,40 +8,40 @@ NavisworksTransport 是一个针对 Autodesk Navisworks Manage 2026 开发的插 ### 核心功能 -1. **类别属性分配**: - * 为模型项目添加预定义的物流类别属性(门、电梯、楼梯、通道、障碍物、装卸区、停车位、检查点)。 - * 支持批量处理和用户友好的界面(按钮式对话框)。 - * 通过 Navisworks COM API 确保属性正确添加。 +1. **类别属性分配**: + * 为模型项目添加预定义的物流类别属性(门、电梯、楼梯、通道、障碍物、装卸区、停车位、检查点)。 + * 支持批量处理和用户友好的界面(按钮式对话框)。 + * 通过 Navisworks COM API 确保属性正确添加。 -2. **路径规划与编辑**: - * 允许用户在3D视图中交互式地定义移动对象的路径点。 - * 管理多条路径,支持路径的创建、编辑、删除、导入、导出和历史记录。 - * 提供路径可视化(3D标记)和基础验证(如点数、长度)。 +2. **路径规划与编辑**: + * 允许用户在3D视图中交互式地定义移动对象的路径点。 + * 管理多条路径,支持路径的创建、编辑、删除、导入、导出和历史记录。 + * 提供路径可视化(3D标记)和基础验证(如点数、长度)。 -3. **动画仿真**: - * 集成 Navisworks Timeliner API,根据规划的路径自动创建对象动画。 - * 提供动画控制界面,允许用户设置播放速度、持续时间、帧率、循环等参数。 +3. **动画仿真**: + * 集成 Navisworks Timeliner API,根据规划的路径自动创建对象动画。 + * 提供动画控制界面,允许用户设置播放速度、持续时间、帧率、循环等参数。 -4. **碰撞检测**: - * 集成 Navisworks Clash API,自动配置并运行基于路径动画的动态碰撞测试。 - * 提供碰撞检测状态和摘要信息。 +4. **碰撞检测**: + * 集成 Navisworks Clash API,自动配置并运行基于路径动画的动态碰撞测试。 + * 提供碰撞检测状态和摘要信息。 -5. **可见性控制**: - * 提供模型分层拆分和仅显示物流相关元素的功能,以优化视图和性能。 +5. **可见性控制**: + * 提供模型分层拆分和仅显示物流相关元素的功能,以优化视图和性能。 -6. **用户界面**: - * 通过 Navisworks 的 Ribbon 界面添加自定义按钮启动插件。 - * 提供一个停靠窗格 (DockPane) 作为主控制面板,使用 WPF 构建。 - * 控制面板包含多个 Tab 页:类别设置、路径编辑、检测动画、系统管理。 +6. **用户界面**: + * 通过 Navisworks 的 Ribbon 界面添加自定义按钮启动插件。 + * 提供一个停靠窗格 (DockPane) 作为主控制面板,使用 WPF 构建。 + * 控制面板包含多个 Tab 页:类别设置、路径编辑、检测动画、系统管理。 ## 技术架构 ### 编程语言与框架 -* **语言**: C# -* **框架**: .NET Framework 4.8 -* **UI框架**: Windows Forms (部分遗留) 和 Windows Presentation Foundation (WPF) -* **API**: Autodesk Navisworks API (包括 COM API, Timeliner API, Clash API) +* **语言**: C# +* **框架**: .NET Framework 4.8 +* **UI框架**: Windows Forms (部分遗留) 和 Windows Presentation Foundation (WPF) +* **API**: Autodesk Navisworks API (包括 COM API, Timeliner API, Clash API) ### 项目结构 @@ -74,77 +74,77 @@ NavisworksTransportPlugin/ ### 核心组件 -1. **`MainPlugin` (`MainPlugin.cs`)**: - * 插件的入口点,继承自 `Autodesk.Navisworks.Api.Plugins.DockPanePlugin`。 - * 负责初始化插件、创建和管理主停靠窗格。 - * 包含旧版的 Windows Forms UI 逻辑(已部分迁移到 WPF)。 +1. **`MainPlugin` (`MainPlugin.cs`)**: + * 插件的入口点,继承自 `Autodesk.Navisworks.Api.Plugins.DockPanePlugin`。 + * 负责初始化插件、创建和管理主停靠窗格。 + * 包含旧版的 Windows Forms UI 逻辑(已部分迁移到 WPF)。 -2. **`LogisticsControlPanel` (`LogisticsControlPanel.xaml`, `LogisticsControlPanel.xaml.cs`)**: - * WPF 用户控件,作为主停靠窗格的内容。 - * 使用 MVVM 模式,通过 `DataContext` 绑定到 `LogisticsControlViewModel`。 +2. **`LogisticsControlPanel` (`LogisticsControlPanel.xaml`, `LogisticsControlPanel.xaml.cs`)**: + * WPF 用户控件,作为主停靠窗格的内容。 + * 使用 MVVM 模式,通过 `DataContext` 绑定到 `LogisticsControlViewModel`。 -3. **`LogisticsControlViewModel` (`LogisticsControlViewModel.cs`)**: - * WPF UI 的核心逻辑处理中心。 - * 管理 UI 状态、数据绑定、用户命令(ICommand)。 - * 与 Core 层(如 `PathPlanningManager`)交互,驱动业务逻辑。 +3. **`LogisticsControlViewModel` (`LogisticsControlViewModel.cs`)**: + * WPF UI 的核心逻辑处理中心。 + * 管理 UI 状态、数据绑定、用户命令(ICommand)。 + * 与 Core 层(如 `PathPlanningManager`)交互,驱动业务逻辑。 -4. **`PathPlanningManager` (`PathPlanningManager.cs`)**: - * 路径规划的核心业务逻辑。 - * 管理路径 (`PathRoute`) 和路径点 (`PathPoint`) 的创建、编辑、验证。 - * 管理路径编辑状态(查看、创建、编辑)。 - * 处理与 Navisworks 模型的交互(选择、边界计算)。 - * 提供事件供 UI 层订阅。 +4. **`PathPlanningManager` (`PathPlanningManager.cs`)**: + * 路径规划的核心业务逻辑。 + * 管理路径 (`PathRoute`) 和路径点 (`PathPoint`) 的创建、编辑、验证。 + * 管理路径编辑状态(查看、创建、编辑)。 + * 处理与 Navisworks 模型的交互(选择、边界计算)。 + * 提供事件供 UI 层订阅。 -5. **`CategoryAttributeManager` (`CategoryAttributeManager.cs`)**: - * 负责通过 Navisworks COM API 为模型项添加、更新、删除自定义物流属性。 - * 定义了物流类别 (`LogisticsCategories`) 和属性 (`LogisticsProperties`)。 - * 提供了筛选模型项的方法。 +5. **`CategoryAttributeManager` (`CategoryAttributeManager.cs`)**: + * 负责通过 Navisworks COM API 为模型项添加、更新、删除自定义物流属性。 + * 定义了物流类别 (`LogisticsCategories`) 和属性 (`LogisticsProperties`)。 + * 提供了筛选模型项的方法。 -6. **`LogisticsAnimationManager`, `TimeLinerIntegrationManager`**: - * 负责与 Navisworks Timeliner 集成,根据路径数据创建动画。 +6. **`LogisticsAnimationManager`, `TimeLinerIntegrationManager`**: + * 负责与 Navisworks Timeliner 集成,根据路径数据创建动画。 -7. **`ClashDetectiveIntegration`**: - * 负责与 Navisworks Clash Detective 集成,配置和运行碰撞测试。 +7. **`ClashDetectiveIntegration`**: + * 负责与 Navisworks Clash Detective 集成,配置和运行碰撞测试。 -8. **`VisibilityManager`**: - * 管理模型元素的可见性,实现"仅显示物流元素"等功能。 +8. **`VisibilityManager`**: + * 管理模型元素的可见性,实现"仅显示物流元素"等功能。 -9. **`LogManager`**: - * 统一日志管理工具。 +9. **`LogManager`**: + * 统一日志管理工具。 ## 开发与构建 ### 系统要求 -* Windows 10 或更高版本 -* Navisworks Manage 2026 -* .NET Framework 4.8 -* Visual Studio (推荐) +* Windows 10 或更高版本 +* Navisworks Manage 2026 +* .NET Framework 4.8 +* Visual Studio (推荐) ### 构建过程 -1. 使用 Visual Studio 打开 `NavisworksTransport.sln` 解决方案文件。 -2. 确保项目引用指向正确的 Navisworks 2026 API DLL 文件(路径在 `.csproj` 文件中定义)。 -3. 选择 "Debug" 或 "Release" 配置。 -4. 构建项目 (`Ctrl+Shift+B`)。 -5. 输出文件为 `NavisworksTransportPlugin.dll`,位于 `bin\Debug\` 或 `bin\Release\` 目录下。 +1. 使用 Visual Studio 打开 `NavisworksTransport.sln` 解决方案文件。 +2. 确保项目引用指向正确的 Navisworks 2026 API DLL 文件(路径在 `.csproj` 文件中定义)。 +3. 选择 "Debug" 或 "Release" 配置。 +4. 构建项目 (`Ctrl+Shift+B`)。 +5. 输出文件为 `NavisworksTransportPlugin.dll`,位于 `bin\Debug\` 或 `bin\Release\` 目录下。 ### 部署与安装 -1. 编译生成 `NavisworksTransportPlugin.dll`。 -2. 插件会自动安装到 Navisworks 插件目录: +1. 编译生成 `NavisworksTransportPlugin.dll`。 +2. 插件会自动安装到 Navisworks 插件目录: `[Navisworks安装路径]\Plugins\NavisworksTransportPlugin\` -3. 重启 Navisworks 即可在 "附加模块" 选项卡中找到插件。 +3. 重启 Navisworks 即可在 "附加模块" 选项卡中找到插件。 ### 运行与调试 -* 在 Visual Studio 中可以直接调试插件,方法是设置 Navisworks 可执行文件(如 `NwAddinDev.exe` 或 `Navisworks.exe`)为启动外部程序。 -* 插件运行时会在 `%AppData%\Autodesk Navisworks Manage 2026\PluginsData\NavisworksTransportPlugin` 目录下生成日志文件。 +* 在 Visual Studio 中可以直接调试插件,方法是设置 Navisworks 可执行文件(如 `NwAddinDev.exe` 或 `Navisworks.exe`)为启动外部程序。 +* 插件运行时会在 `%AppData%\Autodesk Navisworks Manage 2026\PluginsData\NavisworksTransportPlugin` 目录下生成日志文件。 ## 开发约定与实践 -* **MVVM 模式**: 新的 UI 开发强烈推荐使用 WPF 和 MVVM 模式,将 UI 逻辑 (`ViewModel`) 与 UI 呈现 (`View`) 分离。 -* **全局异常处理**: 使用 `GlobalExceptionHandler` 类来捕获和处理未处理的异常,提供用户友好的错误提示。 -* **日志记录**: 使用 `LogManager` 进行统一的日志记录,便于调试和问题追踪。 -* **安全执行**: 关键操作包装在 `GlobalExceptionHandler.SafeExecute` 方法中,以提高稳定性。 -* **COM API 使用**: 在需要与 Navisworks 属性系统深度交互时,谨慎使用 COM API,并注意处理缓存和刷新问题。 \ No newline at end of file +* **MVVM 模式**: 新的 UI 开发强烈推荐使用 WPF 和 MVVM 模式,将 UI 逻辑 (`ViewModel`) 与 UI 呈现 (`View`) 分离。 +* **全局异常处理**: 使用 `GlobalExceptionHandler` 类来捕获和处理未处理的异常,提供用户友好的错误提示。 +* **日志记录**: 使用 `LogManager` 进行统一的日志记录,便于调试和问题追踪。 +* **安全执行**: 关键操作包装在 `GlobalExceptionHandler.SafeExecute` 方法中,以提高稳定性。 +* **COM API 使用**: 在需要与 Navisworks 属性系统深度交互时,谨慎使用 COM API,并注意处理缓存和刷新问题。 diff --git a/doc/design/Design_Plan.md b/doc/design/Design_Plan.md new file mode 100644 index 0000000..f2cddac --- /dev/null +++ b/doc/design/Design_Plan.md @@ -0,0 +1,299 @@ +# NavisworksTransport 正式设计方案(Design Plan) + +版本:v1.0 +作者:Agent Mode +日期:2025-08-29 + +本方案依据以下输入编制: +- 需求文档:doc/requirement/user_requiement.md(以 Navisworks 2017/Windows 7 为基线) +- 项目上下文与结构参考:QWEN.md(包含 Navisworks 2026 的说明,仅作补充参考) +- 源代码:src 目录(核心模块:Core、PathPlanning、UI/WPF、Utils 等) + +重要说明:如需求文档与 QWEN.md 在版本目标存在差异(2017 vs 2026),本方案默认以需求文档为准(Navisworks 2017)。若需面向 Navisworks 2026 的兼容性与增强功能,请在部署与验证环节中选配“2026 兼容模式”。 + + +一、总体设计 + +1.1 背景与目标 +- 背景:在 Autodesk Navisworks 环境中进行物流路径规划、动画仿真与碰撞检查,需高效的交互式工具,支持路径点编辑、自动路径规划(复杂场景)、导航输出与结构化结果文件(XML/JSON/CSV)。 +- 目标: + - 快速完成通道选择与路径点规划(起点/路径点/终点),支持多路径管理与编辑; + - 支持模型切分与层级可见性管理,聚焦物流相关构件; + - 支持物流“类别属性”批量设置、识别与筛选; + - 集成动画和碰撞检测能力,支持结果导出和路径分析; + - 支持结构化结果文件输出,并满足 DELMIA 导入(按需求文档约束)。 + +1.2 需求综述(与需求文档对应) +- 通道选择及路径点规划:模型切分、通道选择、路径点规划、多路径管理、编辑保存与导入、路径点自动贴合; +- 物流“类别”设置:新增物流属性类别、设置可通行性/速度/宽度/优先级等属性,筛选与清除; +- 层级创建与显示:自动隐藏/淡化非关键层,物流元素筛选,路径时间标签; +- 交互式导航:交互式导航控件、结果输出(视频或图片)、输出格式(XML/JSON/CSV,可导入 DELMIA); +- 碰撞检查:动画生成与播放、碰撞检查/记录导出、路径规划分析与最优路径建议。 + +1.3 设计原则 +- 实用优先:在 Navisworks 2017 环境中保证稳定运行与较好性能; +- 解耦分层:UI(WPF+MVVM)与核心逻辑(Core、PathPlanning)分离,便于后续扩展; +- 向后兼容:在自动规划/碰撞集成等关键能力上提供降级路径(如 BoundingBox 模式回退); +- 可观察性:统一日志 LogManager,异常采用 GlobalExceptionHandler; +- 数据可移植:路径/结果提供 XML/JSON/CSV 序列化能力(JSON 加载简化,建议 XML 为主)。 + +1.4 核心用例流程概览 +- 路径编辑(手动): + 1) 用户通道选择(手动/属性筛选/自动查找可通行),计算通道边界; + 2) 进入编辑模式,3D 点击添加“起点/路径点/终点”,路线渲染与校验; + 3) 保存/导入/导出路径文件,维护历史记录; +- 自动路径规划: + 1) 依据通道与模型生成网格(优先 ChannelBased 2.5D,失败回退 BoundingBox); + 2) A* 求解路径并高度修正(贴合通道表面),渲染与事件广播; +- 动画与碰撞: + 1) 基于路径生成 TimeLiner 动画; + 2) 动画过程缓存碰撞,结束后统一生成 Clash 测试与报告,高亮冲突对象; +- 导航输出: + 1) 导航控件交互产生路径; + 2) 输出视频/图片与结构化结果(XML/JSON/CSV),满足 DELMIA 导入约束。 + + +二、系统架构 + +2.1 分层架构与模块 +- UI 层(WPF + MVVM,少量 WinForms 遗留): + - Views: UI/WPF/Views/*(含 PathEditingView、AnimationControlView 等) + - ViewModels: UI/WPF/ViewModels/*(LogisticsControlViewModel、PathEditingViewModel 等) + - 主控面板:UI/WPF/LogisticsControlPanel.xaml +- Core 业务层: + - PathPlanningManager:路径规划主控(事件驱动、状态机、路径可视化) + - CategoryAttributeManager:物流属性管理(COM API + .NET API) + - VisibilityManager:可见性与层级控制 + - Animation:TimeLinerIntegrationManager、PathAnimationManager(动画控制) + - Collision:ClashDetectiveIntegration(碰撞集成,动画期缓存 + 结束时统一测试) + - ModelSplitterManager:模型切分(含简化实现) +- PathPlanning 算法层: + - GridMapGenerator:网格生成(ChannelBased 2.5D/BoundingBox),车辆膨胀优化 + - AutoPathFinder:A* 路径搜索、路径优化/插值、高度贴合 + - ChannelBasedGridBuilder、VerticalScanProcessor、TriangleSpatialHash 等支撑 +- Utils:日志、坐标/单位转换、Navisworks API 辅助 + +2.2 系统架构图(截图占位) +- [截图占位] 系统架构图 + - 要求:体现 UI(WPF+MVVM)/Core/PathPlanning/Utils 分层,以及 TimeLiner、Clash 的集成位置与事件流(PathPlanningManager 事件:StatusChanged、EditStateChanged、RouteGenerated 等)。 + +2.3 模块关系与数据流 +- UI 通过 ViewModel 调用 PathPlanningManager 与其他 Core 服务; +- PathPlanningManager 根据通道选择与编辑状态,驱动网格生成与 A* 求解,并渲染路径标记; +- Animation/Collision 订阅路径与动画状态,按批次创建 TimeLiner 任务与 Clash 测试; +- CategoryAttributeManager 负责属性新增/更新/移除与筛选; +- VisibilityManager 提供“仅显示物流元素/隔离显示”等能力。 + +2.4 技术路线与选型 +- 语言/框架:C#,.NET Framework 4.8; +- UI:WPF + MVVM(WinForms 仅用于部分对话框/遗留界面); +- 外部 API:Autodesk Navisworks API(2017 目标),COM API(属性写入)、TimeLiner API、Clash API; +- 路径算法:RoyT.AStar(格网 A*),自研 2.5D 高度约束与车辆膨胀优化; +- 序列化:XML(主)、JSON(简化加载)/CSV(结果); +- 日志/异常:LogManager、GlobalExceptionHandler。 + +2.5 关键数据模型(摘自源码) +- PathPoint、PathRoute:路径点/路径集合,支持类型(Start/End/WayPoint)、长度/时间统计、XML/JSON 序列化; +- ChannelBounds:通道边界与 2D 投影; +- PathHistoryEntry/Manager:路径历史; +- 事件模型:PathPlanningStatusChangedEventArgs、PathEditStateChangedEventArgs、RouteGeneratedEventArgs 等。 + + +三、功能设计方案(逐模块) + +3.1 模型切分 +- 目标:对大型模型按楼层/属性进行切分,提升性能与操作体验; +- UI:ModelSplitterDialog(WinForms); +- Core:ModelSplitterManager、SimplifiedModelSplitterManager; +- 设计: + - 基于模型层级与属性枚举进行分层导出; + - 提供环境检查与测试导出入口; + - 与可见性/筛选配合以限定工作区域。 + +3.2 通道选择 +- 能力: + - 手动选择:基于 Navisworks 选择; + - 属性筛选:通过 CategoryAttributeManager.FilterByLogisticsType(FilterTraversableItems); + - 自动选择:Search API 扫描“可通行”物流元素; +- PathPlanningManager.SelectChannels:计算组合边界 CombinedChannelBounds 并事件广播。 + +3.3 路径点规划(手动 + 自动) +- 手动规划: + - 工具:PathClickToolPlugin(精确拾取),PathPointRenderPlugin(3D 渲染); + - 流程:切换到 Creating/Editing 状态 -> 添加点 -> 渲染 -> 校验; + - 路径点自动贴合:高度修正在 AutoPathFinder.ApplyPreciseHeightCorrection 中贴合通道表面(有通道数据时); +- 自动规划(复杂环境): + - 网格生成:GridMapGenerator.GenerateFromBIM + - First 尝试 ChannelBased2_5D(通道优先、垂直扫描高度区间、车辆高度校验); + - Fallback 到 BoundingBox 模式(保持向后兼容); + - A*:AutoPathFinder.FindPath/ExecuteAStarAlgorithm + - 路径优化(去冗余点、直线可达校验)、2.5D 高度插值与起终点原位修正; + +3.4 路径管理、编辑、保存与导入 +- 编辑:创建/编辑/取消/完成编辑(FinishEditing 自动将末点设为终点并触发渲染与事件); +- 文件:PathFileSerializer(XML 主,JSON 简化,不建议作为主格式),支持多路径容器; +- 历史:PathHistoryManager(Created/Edited/ManualSave 等),提供查看历史 UI; +- 导入:DetectFileFormat -> LoadFromXml/LoadFromJson(JSON 加载简化,优先 XML)。 + +3.5 物流“类别”设置 +- 类别:为模型添加“物流属性”类别(类型、可通行、优先级、车辆尺寸、速度限制、宽度限制等); +- 批量处理:Add/Update/Remove,使用 COM API 正确索引方式避免覆盖错误; +- 识别筛选:FilterTraversableItems/FilterByLogisticsType/FilterPassableAreas,供通道选择与可见性使用。 + +3.6 层级创建与显示 +- 隐藏/淡化非关键层:VisibilityManager.HideNonLogisticsItems/ShowAllItems/IsolateSpecificItems; +- 物流元素筛选:ShowLogisticsItemsOnly; +- 路径时间标签:在 PathPlanningManager.CalculateEstimatedTime(当前简单估算,后续可接入速度限制)。 + +3.7 交互式导航 +- 交互控件:UI/WPF 侧的主面板与路径/起终点选择; +- 导出:支持视频/图片占位(由用户录屏或快照)、结构化文件(XML/JSON/CSV); +- DELMIA 导入:以 XML/CSV 为主,JSON 仅作参考(需要与 DELMIA 侧字段约定)。 + +3.8 动画生成与播放 +- TimeLinerIntegrationManager:创建运输任务(将路径关联到对象)、更新进度与状态文本、清理任务; +- PathAnimationManager:具体的路径关键帧/移动输出(见 Core/Animation); +- 设计要点: + - 以 TimelinerTask 为主线(Navisworks 2017 可用 API 能力有限,使用显示名/选择集传递状态)。 + +3.9 碰撞检查 +- ClashDetectiveIntegration: + - 动画过程“实时缓存”碰撞(包围盒快速判定 + 去重 + 位置快照); + - 动画结束“统一创建并运行”测试(避免过程频繁创建); + - 高亮冲突对象、刷新 UI、导出报告; +- 兼容性:优先 .NET Clash API,必要时绕过繁重的 COM 插件枚举。 + +3.10 路径规划分析 +- 汇总多条路径的碰撞结果,统计次数/距离/受影响对象; +- 以“最小碰撞/最短距离/最小转弯次数”等指标推荐最优路径; +- 输出分析报告(可选 CSV/XML)。 + +3.11 结果输出 +- 地图/路径可视化:3D 渲染 + 可截屏; +- 结构化结果:XML/JSON/CSV(建议 XML/CSV 作为主导入格式到 DELMIA)。 + + +四、系统部署方案 + +4.1 部署环境要求 +- 操作系统:Windows 7(需求文档基准),建议 Windows 10 及以上(更佳兼容性与性能); +- Autodesk Navisworks:Manage 2017(需求基线)。如需 2026,请切换引用与验证用例(见下文兼容模式); +- .NET Framework:4.8; +- 开发工具:Visual Studio(建议 2019/2022)。 + +4.2 构建与产物 +- 解决方案:NavisworksTransport.sln; +- 项目:NavisworksTransportPlugin.csproj; +- 产物:NavisworksTransportPlugin.dll(bin/Debug 或 bin/Release)。 + +4.3 安装与部署流程 +- 自动安装(一键安装): + 1) 识别 Navisworks 安装目录; + 2) 将插件 DLL 与必要资源复制到 [Navisworks安装路径]\Plugins\NavisworksTransportPlugin\; + 3) 注册菜单/配置(如需); +- 手动安装: + 1) 复制 NavisworksTransportPlugin.dll 至上述目录; + 2) 重启 Navisworks,在“附加模块”中查看“物流路径规划”。 + +4.4 运行配置 +- 日志输出:%AppData%\Autodesk Navisworks Manage 2017\PluginsData\NavisworksTransportPlugin(按 QWEN 中 2026 路径作适配); +- 配置切换: + - 自动路径规划模式网格参数(cellSize、vehicleSize、safetyMargin、vehicleHeight); + - 2.5D/BoundingBox 模式选择(自动/强制选项)。 + +4.5 验证计划 +- 功能验证(最小可行用例): + - 通道选择(手动/自动),边界计算; + - 路径编辑:起点/若干路径点/终点,渲染正确; + - 路径保存/导入/导出(XML 主)、历史记录查看; + - 物流属性设置:批量标注、筛选/清除、颜色高亮检查; + - 层级显示:仅显示物流元素/隔离显示/恢复显示; + - 自动规划:A* 成功求解、路径高度贴合、优化去冗余; + - 动画:任务创建、播放/停止、进度状态变更; + - 碰撞:动画期缓存、结束后创建测试与高亮、导出记录; + - 导航输出:视频/图片占位与结构化结果(XML/CSV); +- 性能验证: + - 大模型下网格生成耗时(ChannelBased 2.5D 与 BoundingBox 比对); + - 车辆膨胀优化(距离变换算法)前后对比; + - PathPoint 渲染与状态事件对 UI 延迟的影响; +- 兼容性验证: + - Navisworks NWD/NWF/NWC 模型; + - 不同单位(mm/cm/m/ft/in)下的单位换算正确性; + - 如需 Navisworks 2026,切换引用并复测 TimeLiner/Clash API 行为差异。 + +4.6 2026 兼容模式(可选) +- 若目标为 Navisworks 2026: + - 切换引用到 2026 API; + - 复核 QWEN.md 所述运行路径与日志输出位置; + - 回归 TimeLiner 与 Clash API 可用性和接口差异; + - 扩展安装器适配多版本并行安装。 + + +五、非功能与质量保证 + +5.1 日志与异常处理 +- GlobalExceptionHandler:应用级异常捕获(线程/任务/未处理),尝试恢复组件,提供用户友好提示; +- LogManager:Info/Warning/Error/Debug 统一记录,关键路径设充分日志(网格生成、A*、碰撞快照)。 + +5.2 性能优化要点 +- 网格生成: + - ChannelBased 2.5D 优先(垂直扫描、空间哈希、通道优先),失败回退 BoundingBox; + - 车辆膨胀:距离变换 O(n*m) 优化,跳过无障碍场景; +- UI: + - UIStateManager/QueueUIUpdate 降低 UI 阻塞; + - WPF 绑定优化与虚拟化集合; +- 搜索: + - Search API 替代全量遍历,尤其在通道/楼层检索时。 + +5.3 可维护性 +- 分层清晰,事件驱动,保留向后兼容事件(_Legacy); +- 路径文件以 XML 为主,便于第三方集成; +- 关键类职责单一(PathPlanningManager/CategoryAttributeManager/GridMapGenerator 等)。 + + +六、风险与边界 + +- Navisworks 版本差异:2017 与 2026 API 行为差异需要回归验证; +- JSON 加载功能简化:建议以 XML 为主格式; +- 大场景极大网格:需调大 cellSize 或范围裁剪以避免内存/耗时过大; +- COM 属性写入:需严格使用相对索引覆盖逻辑(源码已修复),防止属性覆盖错乱; +- 动画碰撞“位置恢复”:测试期间会短暂移动对象做验证,并在结束后恢复。 + + +七、里程碑建议(示例) + +- M1(1 周):路径编辑闭环(手动)、物流属性标注、可见性隔离; +- M2(1-2 周):自动路径(2.5D/回退)、XML 存取、历史; +- M3(1 周):动画与碰撞缓存 + 结束测试、报告与高亮; +- M4(1 周):导航输出与 DELMIA 结果结构; +- M5(1 周):性能与兼容性回归(2017 基线,若选 2026 追加一轮)。 + + +八、截图占位与要求 + +- [截图占位] 系统架构图: + - 要求:展示 UI/Core/PathPlanning/Utils 分层与 TimeLiner/Clash 集成及事件流; +- [截图占位] 主停靠面板(WPF): + - 要求:展示“类别设置/路径编辑/检测动画/系统管理”四个页签; +- [截图占位] 路径编辑过程: + - 要求:3D 视图中路径点(起点/路径点/终点)与线段渲染、状态提示; +- [截图占位] 自动路径规划网格示意: + - 要求:对比 ChannelBased 2.5D 与 BoundingBox 的可通行网格差异; +- [截图占位] TimeLiner 任务与播放控制: + - 要求:任务创建、播放、进度显示; +- [截图占位] 碰撞检查结果: + - 要求:高亮冲突对象、Clash 测试列表与数量统计; +- [截图占位] 结果文件样例: + - 要求:XML/CSV 字段示意,可用于 DELMIA 导入对齐。 + + +附录 A:代码与模块清单(节选) +- Core/MainPlugin、UI/WPF/LogisticsControlPanel.xaml:主入口与面板; +- Core/PathPlanningManager:路径规划主控(状态、事件、可视化、导入导出) +- PathPlanning/GridMapGenerator、AutoPathFinder:网格生成与 A* 路径; +- Core/Properties/CategoryAttributeManager:物流属性管理(COM API 正确索引覆盖); +- Core/Animation/TimeLinerIntegrationManager:TimeLiner 任务集成; +- Core/Collision/ClashDetectiveIntegration:动画期缓存 + 结束创建测试; +- Core/VisibilityManager:可见性隔离与统计; +- UI/WPF/ViewModels/*:MVVM 视图模型与命令绑定。 + diff --git a/doc/design/architecture/README.md b/doc/design/architecture/README.md new file mode 100644 index 0000000..3cb47cc --- /dev/null +++ b/doc/design/architecture/README.md @@ -0,0 +1,22 @@ +# 渲染说明(Mermaid 功能模块架构图) + +文件:system_architecture_functional_2026.mmd +位置:doc/design/architecture + +渲染方式(任选其一): + +1) VS Code 插件 +- 安装 “Markdown Preview Mermaid Support” 或 “Mermaid Markdown Syntax Highlighting”。 +- 打开 .mmd 文件,使用预览功能渲染。 + +2) mermaid-cli(命令行) +- 安装 Node.js 与 mermaid-cli: + npm install -g @mermaid-js/mermaid-cli +- 导出 PNG: + mmdc -i system_architecture_functional_2026.mmd -o system_architecture_functional_2026.png -t neutral +- 导出 SVG: + mmdc -i system_architecture_functional_2026.mmd -o system_architecture_functional_2026.svg -t neutral + +备注: +- 本图以“功能模块”为主(通道选择与路径点规划、物流“类别”设置、层级创建与显示、交互式导航、碰撞检查),并以虚线标注与 Navisworks 2026 API/算法/序列化等支撑能力的依赖关系。 +- 如需在图中标注具体类名/文件路径,可在支撑能力块旁新增注释或节点。 diff --git a/doc/design/architecture/system_architecture_functional_2026.md b/doc/design/architecture/system_architecture_functional_2026.md new file mode 100644 index 0000000..1a677e3 --- /dev/null +++ b/doc/design/architecture/system_architecture_functional_2026.md @@ -0,0 +1,125 @@ +```mermaid + +flowchart TB + + %% 顶层:功能域 + subgraph A[通道选择与路径点规划] + direction TB + A1[模型切分] + A2[通道选择\n(选择树/三维点选/属性筛选)] + A3[路径点规划\n(起点/路径点/终点/方向)] + A4[编辑、保存与导入\n(XML/JSON/CSV,历史记录)] + A5[路径点自动贴合\n(贴合通道表面、直线连通)] + end + + subgraph B[物流“类别”设置] + direction TB + B1[类别设置\n(新增“物流属性”类别)] + B2[属性设置\n(类型/可通行性/速度限制/宽度限制/优先级等)] + B3[识别与筛选\n(添加/编辑/清除,选择树/三维选取)] + end + + subgraph C[层级创建与显示] + direction TB + C1[层级显示\n(自动隐藏/淡化非关键层)] + C2[物流元素筛选\n(按物流分类属性)] + C3[路径时间标签\n(预估运输时间)] + end + + subgraph D[交互式导航] + direction TB + D1[交互式导航控件\n(选择起点/终点,动态生成路径)] + D2[结果输出\n(导航地图/视频/图片)] + D3[结构化结果\n(XML/JSON/CSV,支持导入 DELMIA)] + end + + subgraph E[碰撞检查] + direction TB + E1[动画生成与播放\n(时长/播放/停止/步进)] + E2[碰撞检查\n(高亮、记录导出)] + E3[路径规划分析\n(多路径碰撞对比,最优路径与建议)] + end + + %% 支撑与外部能力(2026 目标) + subgraph S[支撑能力(Navisworks 2026)] + direction TB + S1[Navisworks API\n(Document/Selection/View)] + S2[Clash Detective API] + S3[TimeLiner API] + S4[COM API\n(自定义属性读写)] + S5[路径规划算法\n(通道优先 2.5D 网格 + A* + 高度插值/贴合)] + S6[可见性/层级控制] + S7[文件序列化\n(XML/JSON/CSV)] + end + + subgraph DATA[数据与文件] + direction TB + R1[(路径文件:PathRoute\nXML/JSON/CSV)] + R2[(碰撞/分析报告\nCSV/XML)] + R3[(导航输出\n(视频/图片))] + end + + subgraph UI[用户与交互] + direction TB + U[用户] + end + + %% 交互与依赖关系(功能为主) + U --> B + U --> A + U --> C + U --> D + U --> E + + %% 分类属性支撑通道与层级 + B2 --> A2 + B3 --> A2 + B3 --> C2 + + %% 层级支撑通道聚焦 + C1 --> A2 + C2 --> A2 + C3 --> A3 + + %% 路径规划产出 + A3 --> A4 + A5 --> A4 + A4 --> R1 + + %% 导航基于路径 + A3 --> D1 + D1 --> D2 + D2 --> R3 + D3 --> R1 + + %% 动画与碰撞基于路径 + A3 --> E1 + E1 --> E2 + E2 --> R2 + E2 --> E3 + + %% 支撑能力关联 + A2 -.使用.-> S1 + B1 -.使用.-> S4 + B2 -.使用.-> S4 + C1 -.使用.-> S1 + D1 -.使用.-> S1 + E1 -.使用.-> S3 + E2 -.使用.-> S2 + A3 -.网格/算法.-> S5 + C1 -.可见性.-> S6 + A4 -.序列化.-> S7 + D3 -.序列化.-> S7 + + %% 样式 + classDef domain fill:#f5faff,stroke:#356ac3,stroke-width:1px; + classDef support fill:#f8fff5,stroke:#2e7d32,stroke-width:1px; + classDef data fill:#fff8e1,stroke:#b26a00,stroke-width:1px; + classDef user fill:#fdecea,stroke:#c62828,stroke-width:1px; + + class A,B,C,D,E domain; + class S support; + class DATA data; + class UI user; + +``` \ No newline at end of file diff --git a/doc/design/architecture/system_architecture_functional_2026.png b/doc/design/architecture/system_architecture_functional_2026.png new file mode 100644 index 0000000000000000000000000000000000000000..905bcd01106d5777164936c1719c69f15b89402e GIT binary patch literal 45788 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 |; +- 遍历深度(必需): + - 类型:enum{1级, 2级, 3级, …, 全部};默认:全部;说明:限制选择树最大遍历层级; +- 选择集(可选): + - 类型:ModelItem[];来源:选择树或3D视图框选;用途:用于“选择集保存”;允许为空; +- 导出选项(可选): + - 嵌入外部引用与纹理:bool,默认=false;启用可能显著增大文件; + - 阻止导出对象特性:bool,默认=false;启用可减小体积但会丢失属性; + +3.3.3.1.1.4. 输出数据(详细规格) + +- 分层预览列表(内存): + - 字段:index:int;layerName:string(≤128);layerAttribute:string(≤64);isSelectedForSave:bool; + - 行数:依据模型大小;可刷新/清空; +- 分层导出文件(磁盘): + - 格式:NWD;命名:{项目名}_{策略}_{属性}_{yyyyMMdd_HHmm}.nwd; + - 存储:本地文件系统;需写权限;建议单文件≤2GB; +- 选择集导出(磁盘): + - 格式:NWD;命名:{项目名}_selection_{yyyyMMdd_HHmm}.nwd; +- 操作日志(磁盘/应用日志): + - 字段:timestamp, user, action(preview/export), params(json), result(ok/failed), filePath(optional);编码=UTF-8; + +3.3.3.1.1.5. 功能界面(界面元素详解与操作流程) + +- 分层策略(SplitStrategies,ComboBox) + - 作用:选择分层口径(楼层/区域/子系统/自定义)。 + - 使用:下拉选择目标策略;选择“自定义”后需在“自定义分层二级选项”处指定具体值。 + - 流程:选择策略 →(若为自定义)填写二级选项 → 选择遍历深度 → 点击预览。 +- 自定义分层二级选项(CustomLayerOptions,ComboBox) + - 作用:当策略=自定义时,精细化指定维度(如F1/北区/强电)。 + - 使用:策略为“自定义”时启用;否则隐藏或禁用。 + - 约束:长度≤64;禁止字符 / \ : * ? " < > |。 +- 遍历深度(DepthOptions,ComboBox) + - 作用:限制分层遍历到选择树的最大层级(1级/2级/.../全部)。 + - 使用:根据模型规模与关注粒度选择;“全部”最全面但耗时较长。 +- 预览(PreviewSplitCommand,Button) + - 作用:执行遍历并生成“分层预览列表”。 + - 使用:点击后在列表中查看分层记录,配合“单独显示/显示所有”审查。 +- 预览列表(ListView + GridView) + - 列:序号(自动)、分层名称、分层属性、是否保存(复选)。 + - 使用:勾选“是否保存”表示该分层将被导出;支持单选/多选。 +- 单独显示(IsolateSelectedLayerCommand,Button) + - 作用:隐藏其他分层,仅显示选中行内容。 + - 使用:选中列表某行后可用;用于快速验证分层效果。 +- 显示所有(ShowAllLayersCommand,Button) + - 作用:恢复显示全部分层内容。 + - 使用:用于“单独显示”后的还原。 +- 分层保存(ExecuteSplitCommand,Button) + - 作用:将“是否保存”为true的分层项导出为NWD文件。 + - 使用:导出前可在“导出选项”勾选“嵌入纹理/阻止对象特性”;导出后在状态区查看结果与路径。 +- 导出选项(EmbedXrefs / PreventObjectPropertyExport,CheckBox) + - 作用:控制导出内容与文件大小/稳定性之间的平衡。 + - 使用:大模型慎用“嵌入纹理”;如仅需几何可开启“阻止对象特性”。 +- 进度与取消(ProgressBar / ProgressDetailText / CancelOperationCommand) + - 作用:在预览/导出场景提供可视进度与取消入口。 + - 使用:长耗时过程可随时取消以保护体验。 +- 操作流程(截图占位): + 1) 选择“分层策略/遍历深度”(若自定义需指定二级选项)→ 点击“预览”。 + 2) 在列表中勾选需要导出的项,利用“单独显示/显示所有”核验。 + 3) 配置“导出选项”,点击“分层保存”,等待完成并查看状态消息。 + 4) [截图占位:分层管理_策略与深度.png] [截图占位:分层管理_预览列表.png] [截图占位:分层管理_视图验证.png] [截图占位:分层管理_保存与选项.png] +3.3.3.2. 通道选择路径点规划 + +3.3.3.2.1. 编辑保存和导入 +3.3.3.2.1.1. 功能要求 + +- 依据用户需求“编辑保存和导入”: + - 支持对路径点坐标进行编辑(含新增/删除/重命名/顺序维护); + - 支持保存当前路径为规划文件,文件格式支持 XML / JSON / CSV; + - 支持导入路径文件,在当前通道表面重绘路径; + - 支持记录并查看路径文件操作历史; +- 路径列表清晰展示名称、点数、状态、创建时间,支持多方案管理; +- 导出成果可用于 DELMIA 导入对接(CSV/JSON 字段对齐)。 + +3.3.3.2.1.2. 实现方案 + +- 设计思路:路径管理与文件管理分区呈现,保证方案管理与文件交换两条线清晰; +- 业务流程: + 1) 新建/选择路径 → 通过“开始编辑”进入编辑态,在3D视图点击可通行元素添加点位; + 2) 列表管理:支持“重命名”“删除”;显示当前选择; + 3) 文件导入:选择 XML/JSON/CSV 文件,解析字段,按通道贴合重绘路径; + 4) 文件导出:支持“导出全部/导出选中路径”,命名规范与状态提示; + 5) 历史记录:对导入/导出、增删改动作记录摘要(时间、人员、文件名、结果); +- DELMIA 对接:依据目标字段映射生成 CSV/JSON(路径点序列、坐标、类型、时间标签、车辆/安全参数摘要)。 + +3.3.3.2.1.3. 输入数据(详细规格) + +- 路径对象(必需): + - routeName:string(1–64,唯一),status:enum{新建, 编辑, 完成},createdAt:datetime(ISO 8601); +- 路径点集合(必需): + - 点字段:index:int(从1递增),name:string(≤64,可选),type:enum{Start, Waypoint, End}(Start/End各1个), + x/y/z:double(单位米,精度≤0.001),orientation:{yaw/pitch/roll:double,可选,单位度}, + timeTag:double(秒,可选,段耗时或累计时标); + - 约束:至少2点(含Start/End);Start与End各恰1个;坐标范围±1e6米内; +- 导入文件(可选): + - XML:UTF-8,无BOM;XSD可选校验;根元素; + - JSON:application/json,UTF-8;与示例键名一致; + - CSV:分隔符=逗号,首行表头=index,name,type,x,y,z,timeTag;小数用点; +- meta参数(可选):vehicle.length/width/height:double(m);safetyMargin:double(m);gridSize:double(m); +- 通道集合(可选但建议):类型=ModelItem[];用于导入后点位贴合校正; + +3.3.3.2.1.4. 输出数据(详细规格) + +- 路径列表(内存):字段=routeName, pointCount:int, status, createdAt, lastAction; +- 导出文件(磁盘): + - 支持格式:XML/JSON/CSV(任选其一或多选); + - XML:UTF-8,扩展名=.xml;根元素;带schemaVersion属性(如1.0); + - JSON:UTF-8,扩展名=.json;顶层包含routeName/points/meta; + - CSV:UTF-8,扩展名=.csv;以逗号分隔,RFC4180兼容; + - 命名规范:{项目}_{路径名}_{yyyyMMdd_HHmm}.{xml|json|csv}; +- 历史记录(磁盘/日志):字段=timestamp,user,action{import/export/edit},fileName,result,message;编码=UTF-8; + +3.3.3.2.1.5. 功能界面(界面元素详解与操作流程) + +- 路径列表(ListView + GridView) + - 列:路径名称、点数、状态、创建时间;支持选中高亮,联动右侧状态。 + - 使用:单击选中路径作为当前编辑/导出对象。 +- 手动创建(NewPathCommand,Button) + - 作用:创建一条空路径方案。 + - 使用:点击后输入名称(如弹窗或内联编辑);自动成为当前路径。 +- 重命名(RenamePathCommand,Button) + - 作用:修改当前选中路径的名称。 + - 启用条件:已选中路径。 +- 删除(DeletePathCommand,Button) + - 作用:删除当前选中路径。 + - 使用:需确认;删除后不可恢复(建议投标版提示用户备份)。 +- 导入(ImportPathCommand,Button) + - 作用:从XML/JSON/CSV导入路径并自动重绘。 + - 使用:选择文件→解析校验→若包含meta/时间标签按需合并→点位按通道贴合。 +- 导出全部(ExportPathCommand,Button) + - 作用:导出列表中所有路径为结构化文件(可多文件)。 + - 使用:选择导出目录与格式,完成后在状态栏给出统计与路径。 +- 导出选中路径(SaveAsPathCommand,Button) + - 作用:仅导出当前选中的路径。 + - 使用:常用于方案比选或对外共享。 +- 状态文本(“当前选择/文件状态”,TextBlock) + - 作用:提示当前操作对象与最近的导入/导出结果。 +- 操作流程与截图占位: + 1) 新建路径→开始编辑→完成后导出。→ [截图占位:路径列表_新建.png] + 2) 从文件导入→检查点位→必要时重命名→导出为标准模板。→ [截图占位:路径文件_导入.png] [截图占位:路径文件_导出.png] + +3.3.3.2.2. 路径点自动贴合 +3.3.3.2.2.1. 功能要求 + +- 按用户需求“路径点自动贴合”,在标注和调整中自动吸附至通道表面,且路径点间以直线相连; +- 对不可通行元素点击不生成点位,并给出提示; +- 贴合策略可配置容差,保证业务可行性与观感一致。 + +3.3.3.2.2.2. 实现方案 + +- 点击选择时进行可通行性校验,不满足则拒绝创建; +- 吸附策略以“通道优先”为原则,提供必要的误差容差; +- 支持点位微调后再次贴合校正。 + +3.3.3.2.2.3. 输入数据(详细规格) + +- 拾取事件(必需):类型=RaycastHit;包含点击坐标(world)、法向、命中对象ID; +- 坐标系(必需):单位=米;坐标系=Navisworks世界坐标; +- 可通行元素集合(可选但建议):ModelItem[];来源于“类别设置”的IsTraversable=true筛选; +- 吸附容差(可选):double,默认=0.05m;范围0.01–0.5m; + +3.3.3.2.2.4. 输出数据(详细规格) + +- 路径点序列(内存):点对象数组,字段=index,name,type,x,y,z,orientation(optional);已按照吸附策略校正; +- 校验消息(内存/提示):不可通行对象点击时返回错误码=INVALID_SURFACE,消息=“当前对象不可通行”; +- 高亮反馈(视图):新点创建时在视图中短暂高亮; + +3.3.3.2.2.5. 功能界面(界面元素详解与操作流程) + +- 路径点列表(ListView) + - 列:点名称、坐标、类型、操作(删除)。 + - 使用:逐行删除/查看点位;可与3D视图联动高亮(若实现)。 +- 3D交互(编辑态) + - 作用:在“开始编辑”后,点击通行元素创建点位;点击不可通行元素会提示并拒绝创建。 + - 贴合:新点位将贴合到表面,避免“悬空”。 +- 辅助状态(Toast/状态栏) + - 作用:提示贴合成功/异常(非法表面、越界等)。 +- 操作流程: + 1) 点击“开始”进入编辑态→在3D视图依次点击通行元素生成点位。 + 2) 如需调整,删除对应点位后重新拾取。 + 3) 点击“结束”保存;如需重做,点击“清空”。 + +3.3.3.3. 物流“类别”设置 + +3.3.3.3.1. 类别设置 +3.3.3.3.1.1. 功能要求 + +- 依据用户需求“类别设置”,在模型属性界面新增“物流属性”类别; +- 对选择树或三维视图选中的模型批量启用/清除物流属性; +- 保障属性的可读性、可筛选性与一致的单位标识。 + +3.3.3.3.1.2. 实现方案 + +- 设计思路:属性分组集中呈现“物流属性”,以统一的字段与提示语规范输入; +- 业务流程: + 1) 在“类别设置”页签查看“已选××项”; + 2) 在“物流属性”分组中设置字段(类型、可通行、优先级、限宽/限高、限速); + 3) 点击“设置属性”应用至选择对象;必要时使用“清除属性/重置默认值”; +- 识别与筛选:通过“刷新列表”获取已设置物流属性的元素,配合显示模式“仅显示物流元素”进行聚焦。 + +3.3.3.3.1.3. 输入数据(详细规格) + +- 选择对象(必需):ModelItem[];数量≥1; +- 物流类型(必需):enum{门, 电梯, 楼梯, 通道, 其他}; +- 可通行(必需):bool;默认=true; +- 优先级(可选):int(1–5);默认=3; +- 限宽/限高(可选):double(m);范围≥0;精度0.01; +- 限速(可选):double(m/s);范围≥0;默认推荐:通道0.8、电梯0.5、楼梯0.3; + +3.3.3.3.1.4. 输出数据(详细规格) + +- 属性写入结果(内存):对象ID→字段字典; +- 持久化(平台属性存储):COM API 写入自定义属性分类“物流属性”; +- 反馈摘要:successCount:int, failCount:int, failReasons:string[]; +- 留痕(日志):timestamp,user,objectCount,fieldsChanged,jsonPatch; + +3.3.3.3.1.5. 功能界面(界面元素详解与操作流程) + +- 选择状态(SelectedModelsText,TextBlock) + - 显示当前已选对象数量与范围。 +- 物流类型(AvailableCategories/SelectedCategory,ComboBox) + - 为对象赋予业务类别(门/电梯/楼梯/通道等);支持批量设置。 +- 可通行(IsTraversable,CheckBox) + - 标识元素是否允许物流通行;后续筛选与贴合使用。 +- 优先级(PriorityLevels/Priority,ComboBox) + - 用于路径可行性与优先级策略判断(1–5,5最高)。 +- 限宽/限高(WidthLimit/HeightLimit,TextBox) + - 约束通道几何(米);≥0;精度0.01。 +- 限速(SpeedLimit,TextBox) + - 约束通过速度(米/秒);内置建议值。 +- 设置属性/清除属性/重置默认值(Button) + - 应用/清除/恢复推荐值。 +- 操作流程与截图占位: + 1) 选中模型→设置字段→点击“设置属性”。→ [截图占位:类别设置_属性设置.png] + 2) 点击“刷新列表”检查 → [截图占位:类别设置_模型列表.png] + 3) 启用“仅显示物流元素”聚焦 → [截图占位:类别设置_显示模式.png] + +3.3.3.3.2. 属性设置 +3.3.3.3.2.1. 功能要求 + +- 为与路径相关的元素设置分类与通行约束,驱动筛选与路径可行性判断。 + +3.3.3.3.2.2. 实现方案 + +- 物流类型:从“AvailableCategories”选择“SelectedCategory”; +- 可通行:勾选“IsTraversable”; +- 优先级:1–5级(5为最高); +- 限制:限宽/限高(米)、限速(米/秒,提供通道/电梯/楼梯推荐值)。 + +3.3.3.3.2.3. 输入数据(详细规格) + +- 目标元素(必需):ModelItem[]; +- 字段:type:enum;traversable:bool;priority:int(1–5);widthLimit/heightLimit:double(m);speedLimit:double(m/s); +- 校验:单位=公制;数值范围≥0;priority为整数; + +3.3.3.3.2.4. 输出数据(详细规格) + +- 更新后的属性(内存+持久化):可被筛选与高亮; +- 导出清单(可选):CSV/JSON列表,字段=objectName,type,traversable,priority,widthLimit,heightLimit,speedLimit; + +3.3.3.3.2.5. 功能界面(界面元素详解与操作流程) + +- 刷新列表(RefreshLogisticsModelsCommand,Button):扫描并汇总所有已设置“物流属性”的对象。 +- 物流模型列表(GroupBox + ListView):呈现编号、名称、类别、通行性、限制、可见性等关键信息。 +- 显示模式:仅显示物流元素(CheckBox/Filter):隐藏非物流元素,提升聚焦效率。 +- 操作流程:设置属性→刷新列表→启用“仅显示物流元素”核验。 + +3.3.3.4. 层级创建 + +3.3.3.4.1. 层级显示 +3.3.3.4.1.1. 功能要求 + +- 自动淡化或隐藏非关键层,突出与物流路径相关层级; +- 输出可提交的层级预览与NWD成果。 + +3.3.3.4.1.2. 实现方案 + +- 通过“分层策略/遍历深度/预览”生成列表; +- 在列表上进行“单独显示/显示所有”; +- 勾选后“分层保存”输出。 + +3.3.3.4.1.3. 输入数据(详细规格) + +- 分层策略(必需):enum; +- 遍历深度(必需):enum; +- 自定义项(条件必需):string;当策略=自定义时必填; +- 物流属性集合(可选):用于先行聚焦; + +3.3.3.4.1.4. 输出数据(详细规格) + +- 分层预览列表(内存):字段同3.3.3.1.1.4; +- 导出成果(磁盘):NWD 文件;命名与限制同3.3.3.1.1.4; + +3.3.3.4.1.5. 功能界面(界面元素详解与操作流程) + +- 预览列表列头(GridView):序号/分层名称/分层属性/是否保存。 +- 单独显示/显示所有(Button):在三维视图中验证分层效果与覆盖面。 +- 分层保存(Button):导出勾选条目为NWD;失败时在状态区提示。 +- 导出选项:嵌入纹理/阻止对象特性(风险提示)。 + +3.3.3.4.2. 物流元素筛选 +3.3.3.4.2.1. 功能要求 + +- 按物流属性快速定位与聚焦与路径相关的元素。 + +3.3.3.4.2.2. 实现方案 + +- 在“类别设置”中赋值后,回到“分层管理”进行“单独显示/显示所有”。 + +3.3.3.4.2.3. 输入数据(详细规格) + +- 过滤条件:type:enum;traversable:bool;limits:{width:double(m), height:double(m), speed:double(m/s)};priority:int(1–5); +- 逻辑:AND/OR(可选,默认AND); + +3.3.3.4.2.4. 输出数据(详细规格) + +- 视图聚焦:仅显示命中元素或高亮显示; +- 清单(内存/导出可选):CSV/JSON,字段=objectId, name, type, matches[]; + +3.3.3.4.2.5. 功能界面(界面元素详解与操作流程) + +- 条件输入(类别设置处):类型/通行性/限制/优先级。 +- 聚焦控制:单独显示/显示所有,隔离或恢复命中对象。 +- 流程:设置属性→回分层管理→单独显示→截图/导出清单(如需)。 + +3.3.3.4.3. 路径时间标签 +3.3.3.4.3.1. 功能要求 + +- 为路径设置时间标签,用于运输时间预估与节拍沟通。 + +3.3.3.4.3.2. 实现方案 + +- 自动估算:根据路径分段长度与限速(若无,采用推荐值)估算总时长(示意:Σ(段长/段速)); +- 人工校正:允许用户覆盖自动结果并填写备注原因(如坡道、拥堵、特殊工况); +- 结果写入:时间标签纳入路径对象与导出文件,便于下游系统使用。 + +3.3.3.4.3.3. 输入数据(详细规格) + +- 路径几何(必需):points[](含坐标与类型); +- 限速(可选):按段或全局 double(m/s); +- 校正值(可选):double(秒);备注:string(≤256); + +3.3.3.4.3.4. 输出数据(详细规格) + +- 时间标签(内存+导出):totalTime:double(s), segments[]: {index, length(m), speed(m/s), time(s)}; +- 估算与校正记录(日志或文件meta):estimate:double(s), adjusted:double(s), note:string; + +3.3.3.4.3.5. 功能界面(界面元素详解与操作流程) + +- 时间标签显示(Text/Badge):显示总时长与关键段时长。 +- 校正输入(TextBox/备注):录入人工校正与说明。 +- 流程:自动估算→人工校正(可选)→导出写入结果。 + +3.3.3.5. 交互式导航 + +3.3.3.5.1. 交互式导航控件 +3.3.3.5.1.1. 功能要求 + +- 在三维视图中交互选择起点/终点并生成可视路径。 + +3.3.3.5.1.2. 实现方案 + +- 起终点选择:在“自动路径规划”区点击“选择起点/选择终点”,在3D视图点选,回显到只读文本框; +- 自动路径:综合车辆尺寸(长/宽/高)与安全间隙,结合网格精度进行路径求解; +- 高级设置:可启用“手动设置网格大小”,滑块范围0.1–5.0米; +- 引导:配合“3D交互操作指南”完成选择/参数/提交全流程。 + +3.3.3.5.1.3. 输入数据(详细规格) + +- 起点/终点(必需):RaycastHit/坐标{ x,y,z:double(m) }; +- 车辆参数(可选):length/width/height:double(m);范围>0; +- 安全间隙(可选):double(m),默认0.2–0.5; +- 网格精度(可选):double(m),0.1–5.0;启用“手动设置网格大小”时必填; + +3.3.3.5.1.4. 输出数据(详细规格) + +- 可视路径(视图+内存):线段序列渲染; +- 状态提示:AutoPathStatus:string;包含校验/失败原因; +- 引导信息:步骤化提示文本; + +3.3.3.5.1.5. 功能界面(界面元素详解与操作流程) + +- 选择起点/终点(Button + 只读TextBox):触发3D拾取并回显坐标/对象信息。 +- 自动规划路径(Button):按参数求解路径;失败给出原因(如无连通通道)。 +- 重置参数(Button):清空起终点与车辆/网格参数。 +- 高级设置(Expander + Slider/Text):控制网格精度,显示建议区间。 +- 3D交互操作指南(说明文本):提供从选择到提交的分步提示。 +- 截图占位: + - [截图占位:路径编辑_自动规划参数.png] + - [截图占位:路径编辑_3D交互指南.png] + +3.3.3.5.2. 结果输出 +3.3.3.5.2.1. 功能要求 + +- 输出图片/视频等成果材料,用于评审与投标。 + +3.3.3.5.2.2. 实现方案 + +- 图片导出:遵循统一截图规范(俯视图/关键节点标注/图例); +- 视频导出:使用“检测动画”的播放控制生成演示素材,格式建议 MP4/H.264; +- 命名模板:{项目}_{路径名}_{视角/分辨率}_{日期}.png/.mp4。 + +3.3.3.5.2.3. 输入数据(详细规格) + +- 当前路径(必需):已选择且完整的路径对象; +- 动画片段(可选):生成的视频时间线; +- 导出模板(可选):分辨率、画幅、覆盖文字(标题/版本/时间标签); +- 命名规范(可选):string 模板; + +3.3.3.5.2.4. 输出数据(详细规格) + +- 图片:PNG/SVG(优先PNG,无损;SVG作图形说明可选);分辨率按模板; +- 视频:MP4(H.264),帧率与时长按动画参数; +- 覆盖标注:路径名称、时间标签、版本、日期(可选); + +3.3.3.5.2.5. 功能界面(界面元素详解与操作流程) + +- 分辨率/视角模板:统一输出规格,保障投标材料一致性(若提供)。 +- 图片/视频导出(Button):生成PNG/SVG或MP4;完成后提示保存路径与文件名。 +- 覆盖标注(可选开关):在图片/视频上叠加标题、版本、时间标签。 +- 截图占位: + - [截图占位:结果输出_图片示意.png] + - [截图占位:结果输出_视频示意.png] + +3.3.3.5.3. 输出格式 +3.3.3.5.3.1. 功能要求 + +- 结构化输出便于外部系统(如 DELMIA)导入与复用。 + +3.3.3.5.3.2. 实现方案 + +- 路径文件:XML/JSON(可选CSV),对齐 DELMIA 导入口径; +- 字段规范: + - 基本:routeName(text)、createdAt(datetime)、version(text); + - points[]:index(int)、name(text)、type(enum)、x/y/z(double, m)、orientation(y/p/r, 可选)、timeTag(double, s, 可选); + - meta:vehicle.length/width/height(double, m)、safetyMargin(double, m)、gridSize(double, m)、categorySummary(text); +- 模板与字典:统一键名与单位说明,附样例文件以便对接。 + +3.3.3.5.3.3. 输入数据(详细规格) + +- 选中路径(必需):路径对象; +- 模板(可选):导出profile(XML/JSON/CSV)与字段字典(键名、单位说明); +- 版本(可选):schemaVersion:string; + +3.3.3.5.3.4. 输出数据(详细规格) + +- 结构化文件:XML/JSON(可选CSV);编码=UTF-8;含schemaVersion; +- 版本记录:版本号、导出时间、导出人; + +3.3.3.5.3.5. 功能界面(界面元素详解与操作流程) + +- 导出格式选择(Radio/CheckBox):XML/JSON/CSV。 +- schemaVersion(Text/下拉):标识字段版本,方便下游兼容。 +- 导出(Button):执行导出并校验文件名与路径合法性,提示结果。 + +3.3.3.6. 碰撞检测 + +3.3.3.6.1. 动画生成和播放 +3.3.3.6.1.1. 功能要求 + +- 基于选定路径与组件生成动画并控制播放; +- 支持设置动画时长、播放/暂停/停止与步进(按需求文档描述)。 + +3.3.3.6.1.2. 实现方案 + +- 参数设置:帧率(fps 下拉)、持续时间(秒); +- 对象选择:移动物体(选择/清除)与移动路径(显示当前路径与点数); +- 生成流程:点击“生成动画”,展示生成状态; +- 播放控制:开始/暂停/停止,显示播放进度(%)与当前时间(s); +- 联动:可与 Navisworks Timeliner 协同演示(如项目需要)。 + +3.3.3.6.1.3. 输入数据(详细规格) + +- 当前路径(必需):路径对象,点数≥2; +- 移动对象(必需):ModelItem; +- 帧率(必需):int,范围1–60fps; +- 时长(必需):double(秒),>0; + +3.3.3.6.1.4. 输出数据(详细规格) + +- 动画时间线(内存):关键帧序列; +- 播放状态(内存):isPlaying:bool, progress:0–100%, currentTime:s; +- 生成状态(提示/日志):ok/failed 与原因; + +3.3.3.6.1.5. 功能界面(界面元素详解与操作流程) + +- 帧率/持续时间(ComboBox/TextBox):控制动画平滑度与总时长。 +- 选择物体/清除(Button):指定或取消移动对象。 +- 生成动画(Button):生成时间线;失败显示原因(对象/路径缺失等)。 +- 播放控制(开始/暂停/停止 Button)+ 进度条 + 当前时间:控制播放状态并给出可视反馈。 +- 截图占位: + - [截图占位:检测动画_参数设置.png] + - [截图占位:检测动画_播放控制.png] + +3.3.3.6.2. 碰撞检查 +3.3.3.6.2.1. 功能要求 + +- 在动画执行或路径验证过程中识别碰撞并记录; +- 碰撞发生时需高亮相关对象并记录,可导出记录(按需求文档)。 + +3.3.3.6.2.2. 实现方案 + +- 参数设定:检测精度(米)、运动速度(米/秒)、检测间隙(米); +- 检测频率:基于速度与检测间隙计算出推荐频率并显示(提示性指标,用于预估计算负载); +- 结果呈现:碰撞状态、结果摘要,并提供“查看碰撞报告”入口; +- 联动:按需与 Clash Detective 联动获取/呈现碰撞信息; +- 提示:动画播放过程中自动进行检测,完成后汇总显示。 + +3.3.3.6.2.3. 输入数据(详细规格) + +- 源(必需):动画时间线或路径对象; +- 检测精度(必需):double(m),范围0.01–1.0; +- 运动速度(必需):double(m/s),范围0.1–5.0; +- 检测间隙(可选):double(m),范围0–1.0; + +3.3.3.6.2.4. 输出数据(详细规格) + +- 碰撞记录(内存):字段=time(s), objectA, objectB, position(x,y,z), status(enum:新/活跃/审阅/批准/已解决), description; +- 报告导出(磁盘 可选):HTML/CSV/JSON;命名={项目}_{路径名}_clash_{yyyyMMdd_HHmm}.{html|csv|json}; +- 视图高亮:发生时即时高亮并可定位; + +3.3.3.6.2.5. 功能界面(界面元素详解与操作流程) + +- 检测精度/速度/间隙(Slider/Text):影响检测频率与精度;联动显示“计算得出的检测频率”。 +- 查看碰撞报告(Button):打开报告,查看摘要与明细;支持导出。 +- 碰撞状态/摘要(TextBlock):提示是否发生碰撞以及数量/类型概要。 +- 截图占位: + - [截图占位:碰撞结果_摘要.png] + - [截图占位:碰撞报告_详细列表.png] + +3.3.3.6.3. 路径规划分析 +3.3.3.6.3.1. 功能要求 + +- 跨方案对比碰撞与效率,提出优化建议; +- 生成路径分析报告,辅助选择最佳路径(按需求文档)。 + +3.3.3.6.3.2. 实现方案 + +- 对比维度:碰撞次数、受影响对象、路径长度、估算时间、关键瓶颈位置等; +- 输出:图文化对比(条形图/表格)与结论建议; +- 建议:结合“安全优先/效率优先”的业务目标提出可执行的调整策略(绕行、扩大间隙、调整时间窗等)。 + +3.3.3.6.3.3. 输入数据(详细规格) + +- 路径集合(必需):≥2条; +- 碰撞结果(必需):各路径对应的碰撞摘要与明细; +- 评价指标(可选):权重设置(安全/效率占比); + +3.3.3.6.3.4. 输出数据(详细规格) + +- 对比报告:HTML/Docx/PDF(三选一或多选);包含对比表格与图表; +- 建议清单:CSV/JSON(可选);字段=issue, suggestion, priority; +- 推荐结果:bestRouteName:string,reason:string; + +3.3.3.6.3.5. 功能界面(界面元素详解与操作流程) + +- 指标表与图表(表格/图形控件,若提供):展示关键指标对比(碰撞数、时长、长度等)。 +- 导出报告(Button):导出HTML/Docx/PDF;适合投标归档与评审共享。 +- 建议摘要(Text/列表):以业务语言列出调整建议与优先级。 diff --git a/doc/design/architecture/功能方案模板.md b/doc/design/architecture/功能方案模板.md new file mode 100644 index 0000000..e02b7f3 --- /dev/null +++ b/doc/design/architecture/功能方案模板.md @@ -0,0 +1,159 @@ +3.3.3.1. 通道选择及路径点规划 +3.3.3.1.1. 模型切分 +3.3.3.1.1.1. 功能要求 + +3.3.3.1.1.2. 实现方案 + +3.3.3.1.1.3. 输入数据 + +3.3.3.1.1.4. 输出数据 + +3.3.3.1.1.5. 功能界面 + +3.3.3.2. 通道选择路径点规划 +3.3.3.2.1. 编辑保存和导入 +3.3.3.2.1.1. 功能要求 + +3.3.3.2.1.2. 实现方案 + +3.3.3.2.1.3. 输入数据 + +3.3.3.2.1.4. 输出数据 + +3.3.3.2.1.5. 功能界面 + +3.3.3.2.2. 路径点自动贴合 +3.3.3.2.2.1. 功能要求 + +3.3.3.2.2.2. 实现方案 + +3.3.3.2.2.3. 输入数据 + +3.3.3.2.2.4. 输出数据 + +3.3.3.2.2.5. 功能界面 + +3.3.3.3. 物流“类别”设置 +3.3.3.3.1. 类别设置 +3.3.3.3.1.1. 功能要求 + +3.3.3.3.1.2. 实现方案 + +3.3.3.3.1.3. 输入数据 + +3.3.3.3.1.4. 输出数据 + +3.3.3.3.1.5. 功能界面 + +3.3.3.3.2. 属性设置 +3.3.3.3.2.1. 功能要求 + +3.3.3.3.2.2. 实现方案 + +3.3.3.3.2.3. 输入数据 + +3.3.3.3.2.4. 输出数据 + +3.3.3.3.2.5. 功能界面 + +3.3.3.4. 层级创建 +3.3.3.4.1. 层级显示 +3.3.3.4.1.1. 功能要求 + +3.3.3.4.1.2. 实现方案 + +3.3.3.4.1.3. 输入数据 + +3.3.3.4.1.4. 输出数据 + +3.3.3.4.1.5. 功能界面 + +3.3.3.4.2. 物流元素筛选 +3.3.3.4.2.1. 功能要求 + +3.3.3.4.2.2. 实现方案 + +3.3.3.4.2.3. 输入数据 + +3.3.3.4.2.4. 输出数据 + +3.3.3.4.2.5. 功能界面 + +3.3.3.4.3. 路径时间标签 +3.3.3.4.3.1. 功能要求 + +3.3.3.4.3.2. 实现方案 + +3.3.3.4.3.3. 输入数据 + +3.3.3.4.3.4. 输出数据 + +3.3.3.4.3.5. 功能界面 + +3.3.3.5. 交互式导航 +3.3.3.5.1. 交互式导航控件 +3.3.3.5.1.1. 功能要求 + +3.3.3.5.1.2. 实现方案 + +3.3.3.5.1.3. 输入数据 + +3.3.3.5.1.4. 输出数据 + +3.3.3.5.1.5. 功能界面 + +3.3.3.5.2. 结果输出 +3.3.3.5.2.1. 功能要求 + +3.3.3.5.2.2. 实现方案 + +3.3.3.5.2.3. 输入数据 + +3.3.3.5.2.4. 输出数据 + +3.3.3.5.2.5. 功能界面 + +3.3.3.5.3. 输出格式 +3.3.3.5.3.1. 功能要求 + +3.3.3.5.3.2. 实现方案 + +3.3.3.5.3.3. 输入数据 + +3.3.3.5.3.4. 输出数据 + +3.3.3.5.3.5. 功能界面 + +3.3.3.6. 碰撞检测 +3.3.3.6.1. 动画生成和播放 +3.3.3.6.1.1. 功能要求 + +3.3.3.6.1.2. 实现方案 + +3.3.3.6.1.3. 输入数据 + +3.3.3.6.1.4. 输出数据 + +3.3.3.6.1.5. 功能界面 + +3.3.3.6.2. 碰撞检查 +3.3.3.6.2.1. 功能要求 + +3.3.3.6.2.2. 实现方案 + +3.3.3.6.2.3. 输入数据 + +3.3.3.6.2.4. 输出数据 + +3.3.3.6.2.5. 功能界面 + +3.3.3.6.3. 路径规划分析 +3.3.3.6.3.1. 功能要求 + +3.3.3.6.3.2. 实现方案 + +3.3.3.6.3.3. 输入数据 + +3.3.3.6.3.4. 输出数据 + +3.3.3.6.3.5. 功能界面 diff --git a/doc/requirement/todo_features.md b/doc/requirement/todo_features.md index a1f9a09..9fa7ca8 100644 --- a/doc/requirement/todo_features.md +++ b/doc/requirement/todo_features.md @@ -2,16 +2,21 @@ ## 功能点 +### [2025/08/29] + +1. [ ](BUG)路径导出,只有一条路径时,导出按钮不能点击;导出的内容有时不完整,只导出一个包含起点、一个路径点、终点的不存在的路径。 + +### [2025/08/28] + +1. [x]将“自动规划路径”中的车辆长度、宽度的默认值改为1米,安全间隙改为0.25米。高级设置中,网格的大小 + ,默认值改为0.5米。 +2. [ ]将这些参数,作为插件配置文件的参数,在系统管理的插件管理中,统一管理 +3. [x]修改分层预览的业务逻辑:从一级节点开始,遍历每个节点查找指定的分层属性,如果找到,记录下来作为一个分层,不再遍历其下级节点;如果没找到,继续遍历其子节点,直到找到为止,遍历深度受到用户指定的遍历深度限制。特殊处理:1、对于智能检测,使用一组候选的分层属性,对每个节点进行查找,按属性的评分(优先级)确定选择何种属性。2、对于自定义查找,用指定的分层属性,在每个节点的"分层信息“属性类别中查找。 +4. [ ]对自动路径规划进行重构,按地面层+高度剖面投影层的方式,构建可通行网格,然后用A*算法获取最短路径。 + ### [2025/08/27] 1. [x]在分层预览列表中,去掉”文件大小“和”状态“列,增加: - “是否保存”列,内容是单选框,默认选中,选中的行,在点击“分层保存”时会保存,未选中的不保存 - 在列表下方,增加“单独显示”按钮,点击后会隐藏除了当前行之外的其他部分 - 保存文件时,文件名的格式:根节点名_自定义属性名_属性值_时间戳 - -### [2025/08/28] - -1. [x]将“自动规划路径”中的车辆长度、宽度的默认值改为1米,安全间隙改为0.25米。高级设置中,网格的大小 - ,默认值改为0.5米。 -2. [ ]将这些参数,作为插件配置文件的参数,在系统管理的插件管理中,统一管理 -3. [ ]修改分层预览的业务逻辑:从一级节点开始,遍历每个节点查找指定的分层属性,如果找到,记录下来作为一个分层,不再遍历其下级节点;如果没找到,继续遍历其子节点,直到找到为止,遍历深度受到用户指定的遍历深度限制。特殊处理:1、对于智能检测,使用一组候选的分层属性,对每个节点进行查找,按属性的评分(优先级)确定选择何种属性。2、对于自定义查找,用指定的分层属性,在每个节点的"分层信息“属性类别中查找。 diff --git a/doc/requirement/user_requiement.md b/doc/requirement/user_requiement.md index fca106b..8c35814 100644 --- a/doc/requirement/user_requiement.md +++ b/doc/requirement/user_requiement.md @@ -13,17 +13,18 @@ | 次级功能点 | 功能点描述 | |------------|------------| -| 通道选择 | 支持选择通道模型功能,可通过选择树或三维视图点选的方式,选择模型并制定为通道类型。 | -| 路径点规划 | 1、针对较为复杂的环境,支持路径点功能,在Navisworks 三维视图中,在通道上点击指定起点、路径点、终点的位置及方向,并以三维可视化的方式,显示路径。2、支持多条路径的保存、选择和编辑。 | -| 编辑保存和导入 | 1、支持对路径上的各点进行坐标编辑(修改x,y,z值);2、支持保存当前路径点集合为路径规划文件,文件格式支持XML、JSON、CSV;3、支持路径导入,在当前通道表面重绘路径;4、支持记录并查看路径文件操作的历史记录。 | -| 路径点自动贴合 | 路径点要自动贴合通道模型表面,路径点之间通过直线进行联通。 | +| 模型切分 | 通过多软件模型轻量化工具箱,实现全模型分层切分,可忽略编组站区域楼层间上下贯穿模块。 | +| 通道选择 | 支持选择通道模型功能,可通过选择树或三维视图点选的方式选择模型并制定为通道类型。 | +| 路径点规划 | 1、针对较为复杂的环境,支持路径点功能,在三维视图中,在通道上点击指定起点、路径点、终点的位置及方向,并以三维可视化的方式显示路径。2、支持多条路径的保存、选择和编辑。 | +| 编辑保存和导入 | 1、支持对路径上的各点进行坐标编辑:2、支持保存当前路径点集合为路径规划文件,文件格式支持XMI JSON,CSV;3、支持路径导入,在当前通道表面重绘路径4、支持记录并查看路径文件操作的历史记录, | +| 路径点自动贴合 | 路径点要白动贴合通道模型表面,路径点之间通过直线进行联通 | ### 物流"类别"设置功能模块 | 次级功能点 | 功能点描述 | |------------|------------| | 类别设置 | 支持模型属性页面新增"物流属性"类别。 | -| 属性设置 | 1、支持通过选择树和三维视图选择的方式,选择物流路径相关的元素(如门、电梯、楼梯、通道等),设置为特定的物流分类,并支持类型、可通行性、速度限制、宽度限制、优先级等属性;2、支持在Navisworks中进行识别和筛选,支持物流分类属性的添加、编辑和清除。 | +| 属性设置 | 1、支持通过选择树和三维视图选择的方式,选择物流路径相关的元素 (如门、电梯、楼梯、通道等),没置为特定的物流分类,并支持类型。 可通行性、速度限制、宽度限制、优先级等属性:2、支持在工具箱中进行识别和筛选,支持物流分类属性的添加、编辑 和清除。 | ### 层级创建功能模块 @@ -38,15 +39,15 @@ | 次级功能点 | 功能点描述 | |------------|------------| | 交互式导航控件 | 创建交互式导航控件,允许用户选择不同的起点和终点,动态生成路径。 | -| 结果输出 | 输出导航地图和路径规划结果,可以是视频、图片或Navisworks文件。 | -| 输出格式 | 支持路径规划结果结构化文件输出(XML/JSON/CSV),结果文件能够导入DELMIA。 | +| 结果输出 | 输出导航地图和路径规划结果,可以是视频或者图片。 | +| 输出格式 | 支持路径规划结果结构化文件输出(XMLJSON/CSV), 结果文件能够 导入DELMIA。 | ### 碰撞检测功能模块 | 次级功能点 | 功能点描述 | |------------|------------| | 动画生成和播放 | 1、指定物流组件(待载转运车),选择路径,支持生成动画仿真物流组件的运动过程;2、支持设置动画时长,支持动画的播放、停止和步进播放。 | -| 碰撞检测 | 1、指定物流组件(待载转运车)动画运行过程中,如果与其他模型发生碰撞或干涉,要高亮显示该模型,并记录碰撞结果;2、支持碰撞记录导出。 | +| 碰撞检查 | 1、指定物流组件(待载转运车)动画运行过程中,如果与其他模型发生碰撞或干涩,要高亮显示该模型,并记录碰撞结果:2、支持碰撞记录导出。 | | 集成联动 | 支持与 Navisworks 的TimeLiner与Clash Detective插件集成和联动,运行时间线模拟并获取碰撞结果。 | | 路径规划分析 | 对多个路径运行的碰撞结果,进行分析,生成路径分析报告,选择最佳路径,提供调整建议。 | diff --git a/doc/working/astar_implementation_plan.md b/doc/working/astar_implementation_plan.md new file mode 100644 index 0000000..0d1c291 --- /dev/null +++ b/doc/working/astar_implementation_plan.md @@ -0,0 +1,151 @@ +# A*寻路算法网格改进实施计划 + +## 现状分析 + +**现有A*实现架构**: + +- `AutoPathFinder.cs` - 核心A*算法实现,使用RoyT.AStar库 +- `GridMap.cs` - 网格地图数据结构,目前主要基于包围盒生成 +- `GridMapGenerator.cs` - 网格生成器,使用简单的包围盒方法 +- `CategoryAttributeManager.cs` - 已实现通道识别和分类功能 +- `ChannelHeightDetector.cs` - 已实现射线-三角形相交和三角形提取 + +**核心问题**: + +- 当前网格生成使用`CalculateCombinedBounds()`简单包围盒方法,精度不足 +- 缺乏精确的通道覆盖计算 +- 没有2.5D高度信息支持 + +## 实施方案 + +### 第一阶段:创建ChannelBasedGridBuilder (2天) + +#### 1.1 新增核心类文件 + +**文件位置**: `src/PathPlanning/ChannelBasedGridBuilder.cs` + +**主要功能**: + +- 集成现有的`CategoryAttributeManager.GetItemsByCategory("通道")`获取通道 +- 复用`ChannelHeightDetector.ExtractTrianglesFromModelItem()`提取三角形 +- 复用`ChannelHeightDetector.RayTriangleIntersect()`进行相交测试 +- 实现智能投影策略: + - 薄通道(<1.5m)优先使用顶面提取投影 + - 复杂通道使用空间哈希优化的射线扫描 + +#### 1.2 扩展GridMap数据结构 + +**修改文件**: `src/PathPlanning/GridMap.cs` + +**新增字段**: + +```csharp +public struct GridCell +{ + // 现有字段保持不变 + public bool IsWalkable { get; set; } + public double Cost { get; set; } + public ElementType Type { get; set; } + + // 新增字段 + public bool IsInChannel { get; set; } + public List PassableHeights { get; set; } + public ChannelType ChannelType { get; set; } +} +``` + +#### 1.3 创建空间哈希优化类 + +**文件位置**: `src/PathPlanning/TriangleSpatialHash.cs` + +### 第二阶段:创建VerticalScanProcessor (3天) + +#### 2.1 新增垂直扫描处理器 + +**文件位置**: `src/PathPlanning/VerticalScanProcessor.cs` + +**主要功能**: + +- 多级筛选优化:邻域筛选 → 高度筛选 → 空间哈希 +- 复用现有的`AutoPathObstacleDetector.CheckIfObstacle()`障碍物识别 +- 并行扫描处理,计算2.5D高度区间 + +#### 2.2 集成到现有GridMapGenerator + +**修改文件**: `src/PathPlanning/GridMapGenerator.cs` + +**新增方法**: + +- `GenerateFromBIM()` 增加 `GridGenerationMode` 枚举参数 +- 新增 `GridGenerationMode.ChannelBased2_5D` 模式 +- 保持向后兼容的 `GridGenerationMode.BoundingBox` 模式 + +### 第三阶段:AutoPathFinder集成改进 (2天) + +#### 3.1 修改AutoPathFinder调用链 + +**修改文件**: `src/PathPlanning/AutoPathFinder.cs` + +**关键改动**: + +- `FindPath()`方法增加通道数据参数 +- 在`ConvertToAStarGrid()`中使用新的通道覆盖数据 +- 支持高度约束的路径规划 + +#### 3.2 更新PathPlanningManager + +**修改文件**: `src/Core/PathPlanningManager.cs` + +**集成点**: + +- `AutoPathPlanning()`方法中调用新的网格生成模式 +- 传递车辆高度参数给网格生成器 +- 保持现有的UI事件机制不变 + +### 第四阶段:性能优化和测试 (2天) + +#### 4.1 性能监控和调优 + +- 添加性能统计日志 +- 调优空间哈希参数 +- 并行化优化 + +#### 4.2 向后兼容性测试 + +- 确保现有的命令模式(`AutoPathPlanningCommand`)正常工作 +- UI集成测试(WPF ViewModels) +- 验收标准测试 + +## 关键技术要点 + +### 复用现有代码策略 + +1. **几何处理完全复用**: + - `ChannelHeightDetector.ExtractTrianglesFromModelItem()` - 三角形提取 + - `ChannelHeightDetector.RayTriangleIntersect()` - 射线相交测试 + - `GeometryExtractorCallback` - COM API几何回调 + +2. **属性管理复用**: + - `CategoryAttributeManager.GetItemsByCategory()` - 通道识别 + - `CategoryAttributeManager.IsItemInCategory()` - 类别判断 + - `AutoPathObstacleDetector.CheckIfObstacle()` - 障碍物检测 + +3. **坐标系统复用**: + - `GridMap.WorldToGrid()` / `GridToWorld()` - 坐标转换 + - `UnitsConverter.ConvertToMeters()` - 单位转换 + +### 架构设计原则 + +1. **渐进式改进**:保持现有AutoPathFinder.cs核心算法不变,只改进网格生成部分 +2. **配置驱动**:通过`GridGenerationMode`参数控制使用新旧算法 +3. **向后兼容**:确保现有的UI和命令系统无需修改 +4. **性能可测**:每个阶段都有明确的性能提升指标 + +## 预期效果 + +- **通道投影精度提升**: 从包围盒误差→精确几何投影 +- **性能优化**: 通道投影提升200-1000倍,障碍检测提升10000倍以上 +- **功能增强**: 支持不同车辆高度的2.5D路径规划 +- **质量改进**: 避免L型、T型通道中的不必要绕行 + +这个方案充分利用了现有代码的几何处理、属性管理等核心功能,只需要新增网格生成的改进算法,实现成本低且风险可控。 diff --git a/doc/working/grid_generation_2_5d_upgrade_report.md b/doc/working/grid_generation_2_5d_upgrade_report.md new file mode 100644 index 0000000..b97f1ee --- /dev/null +++ b/doc/working/grid_generation_2_5d_upgrade_report.md @@ -0,0 +1,159 @@ +# Grid Generation 2.5D 升级完成报告 + +## 概述 + +根据需求完成了PathPlanningManager和AutoPathPlanningCommand中GridMapGenerator.GenerateFromBIM方法调用的升级,添加了新的GridGenerationMode.ChannelBased2_5D参数支持。 + +## 修改的文件 + +### 1. PathPlanningManager.cs + +**位置**: `src/Core/PathPlanningManager.cs` +**修改方法**: `AutoPlanPath` 方法(第786-834行) + +**主要变更**: +- 添加了详细的性能统计日志 +- 使用新的`GridGenerationMode.ChannelBased2_5D`模式调用GridMapGenerator +- 实现了错误处理和回退机制,如果2.5D模式失败,自动回退到传统的`GridGenerationMode.BoundingBox`模式 +- 添加了详细的参数日志记录用于调试 + +**关键代码**: +```csharp +try +{ + // 首先尝试使用新的2.5D通道检测模式 + LogManager.Info("尝试使用ChannelBased2_5D模式生成网格"); + gridMap = gridMapGenerator.GenerateFromBIM( + document, + bounds, + gridSize, + vehicleSize, + safetyMargin, + startPoint.Position, + endPoint.Position, + GridGenerationMode.ChannelBased2_5D // 新增:使用2.5D通道检测模式 + ); + LogManager.Info("✅ ChannelBased2_5D模式网格生成成功"); +} +catch (Exception modeEx) +{ + LogManager.Warning($"ChannelBased2_5D模式失败,回退到默认模式: {modeEx.Message}"); + // 回退到传统的边界框模式 + gridMap = gridMapGenerator.GenerateFromBIM( + document, + bounds, + gridSize, + vehicleSize, + safetyMargin, + startPoint.Position, + endPoint.Position, + GridGenerationMode.BoundingBox // 回退到传统模式 + ); + LogManager.Info("✅ 回退到BoundingBox模式网格生成成功"); +} +``` + +### 2. AutoPathPlanningCommand.cs + +**位置**: `src/Commands/AutoPathPlanningCommand.cs` +**修改方法**: `ExecuteInternalAsync` 方法(第271-322行) + +**主要变更**: +- 将原有的简化路径规划实现替换为真实的A*算法调用 +- 直接调用PathPlanningManager.AutoPlanPath方法执行完整的路径规划逻辑 +- 改进了参数处理,包括车辆尺寸到半径的转换 +- 增强了错误处理和日志记录 + +**关键代码**: +```csharp +// 调用PathPlanningManager的真实A*算法进行路径规划 +try +{ + LogInfo("开始调用PathPlanningManager进行A*路径规划"); + + // 计算车辆半径(从尺寸) + var vehicleRadius = _parameters.VehicleSize / 2.0; + LogInfo($"使用车辆半径: {vehicleRadius}m(基于车辆尺寸{_parameters.VehicleSize}m)"); + + // 调用PathPlanningManager的AutoPlanPath方法执行真实的A*算法 + var pathPlanTask = _pathPlanningManager.AutoPlanPath( + startPathPoint, + endPathPoint, + vehicleRadius, + _parameters.SafetyMargin, + actualGridSize); + + generatedRoute = await pathPlanTask; + + if (generatedRoute == null) + { + throw new Exception("PathPlanningManager未能生成有效路径"); + } + + LogInfo($"A*算法路径规划完成: {generatedRoute.Name},包含 {generatedRoute.Points.Count} 个点,长度 {generatedRoute.TotalLength:F2}米"); +} +catch (Exception ex) +{ + LogError($"调用PathPlanningManager进行A*路径规划失败: {ex.Message}", ex); + throw; +} +``` + +## 向后兼容性 + +### 1. 双重保障机制 +- **主要机制**: 尝试使用新的`ChannelBased2_5D`模式 +- **回退机制**: 如果新模式失败,自动回退到传统的`BoundingBox`模式 +- **日志记录**: 详细记录使用的模式和性能统计 + +### 2. 错误处理 +- 完整的异常捕获和处理 +- 详细的错误日志记录 +- 用户友好的错误消息 + +## 功能增强 + +### 1. 性能监控 +- 添加了网格生成的性能计时统计 +- 详细的参数日志记录便于调试 + +### 2. 调试支持 +- 增加了详细的日志记录,包括: + - 网格生成参数 + - 起点和终点坐标 + - 车辆参数 + - 性能统计信息 + - 错误和警告信息 + +### 3. 统一化处理 +- AutoPathPlanningCommand现在调用真实的路径规划逻辑,而不是简化的直线路径 +- 确保所有路径规划都使用相同的核心算法 + +## 测试验证 + +### 编译测试 +- ✅ 项目编译成功,无编译错误 +- ✅ 仅有警告信息,无功能性问题 + +### 功能测试建议 +1. 测试新的2.5D模式是否能正常工作 +2. 测试回退机制是否在新模式失败时正确触发 +3. 验证日志记录是否提供足够的调试信息 +4. 检查性能是否有显著改进 + +## 部署注意事项 + +1. **日志级别**: 建议在生产环境中适当调整日志级别,避免过多的调试信息 +2. **性能监控**: 可以通过日志监控网格生成的性能变化 +3. **错误监控**: 关注是否有频繁的模式回退,这可能表明新模式存在问题 + +## 总结 + +本次升级成功实现了以下目标: +- ✅ 添加了GridGenerationMode.ChannelBased2_5D参数支持 +- ✅ 实现了向后兼容的回退机制 +- ✅ 增加了详细的性能统计和日志记录 +- ✅ 统一了路径规划的实现逻辑 +- ✅ 保持了代码编译的正确性 + +修改已完成并验证,可以安全部署到测试环境进行进一步验证。 \ No newline at end of file diff --git a/doc/working/pathfinding_grid_improvement_plan.md b/doc/working/pathfinding_grid_improvement_plan.md new file mode 100644 index 0000000..05ab4b2 --- /dev/null +++ b/doc/working/pathfinding_grid_improvement_plan.md @@ -0,0 +1,746 @@ +# 路径规划网格改进方案 + +## 一、核心思路 + +采用"通道优先 + 2.5D垂直扫描"的混合策略,先通过通道节点确定基础可通行区域,再使用垂直射线扫描精确标记障碍物。 + +## 二、技术方案 + +### 2.1 系统架构 + +``` +通道识别 → 基础网格 → 垂直扫描 → 障碍标记 → 路径规划 +``` + +### 2.2 关键组件与API使用 + +#### ChannelBasedGridBuilder(新建) + +```csharp +public class ChannelBasedGridBuilder +{ + private CategoryAttributeManager _categoryManager; + + // 收集所有通道并生成精确覆盖范围 + public ChannelCoverage CollectChannelCoverage() + { + // 复用现有代码:CategoryAttributeManager.GetItemsByCategory() + var channelItems = _categoryManager.GetItemsByCategory("通道"); + + // 注意:不能直接用CalculateCombinedBounds(),它只返回包围盒 + // 需要新实现:生成精确的通道覆盖网格 + var coverage = GeneratePreciseChannelCoverage(channelItems); + + return coverage; + } + + // 新增:精确通道投影(顶面提取优先) + private ChannelCoverage GeneratePreciseChannelCoverage(List channels, double gridSize) + { + var totalBounds = CalculateTotalBounds(channels); + var gridMap = new GridMap(totalBounds, gridSize); + + foreach (var channel in channels) + { + // 根据通道特征选择投影方法 + ProjectChannel(channel, gridMap); + } + + return new ChannelCoverage + { + GridMap = gridMap, + ChannelItems = channels + }; + } + + // 通道投影:优先使用顶面提取 + private void ProjectChannel(ModelItem channel, GridMap gridMap) + { + var bbox = channel.BoundingBox(); + var thickness = bbox.Max.Z - bbox.Min.Z; + var category = CategoryAttributeManager.GetLogisticsCategory(channel); + + LogManager.Info($"投影通道: {channel.DisplayName}, 厚度: {thickness:F2}m, 类型: {category}"); + + // 对于薄通道(< 1.5m),优先使用顶面提取 + if (thickness < 1.5) + { + var triangles = ExtractTrianglesFromModelItem(channel); + var topFaceCount = CountTopFaces(triangles); + + // 如果有足够的水平顶面,使用顶面投影 + if (topFaceCount > triangles.Count * 0.1) // 至少10%是顶面 + { + ProjectTopFaces(channel, gridMap, triangles); + LogManager.Info($" 使用顶面提取投影(顶面数: {topFaceCount})"); + return; + } + } + + // 对于复杂形状或没有明显顶面的通道,使用射线扫描 + ProjectWithRayCasting(channel, gridMap); + LogManager.Info($" 使用垂直射线扫描(精确投影)"); + } + + // 方法1:顶面提取投影(快速且精确) + private void ProjectTopFaces(ModelItem item, GridMap gridMap, List triangles = null) + { + if (triangles == null) + { + triangles = ExtractTrianglesFromModelItem(item); + } + + foreach (var triangle in triangles) + { + // 计算法向量 + var normal = ComputeTriangleNormal(triangle); + + // 只投影朝上的面(法向量Z分量 > 0.7,约45度) + if (normal.Z > 0.7) + { + // 光栅化三角形到网格 + RasterizeTriangle(gridMap, triangle); + } + } + } + + // 方法2:垂直射线扫描(最精确) + private void ProjectWithRayCasting(ModelItem item, GridMap gridMap) + { + var triangles = ExtractTrianglesFromModelItem(item); + if (triangles.Count == 0) return; + + // 使用空间哈希加速 + var spatialHash = new TriangleSpatialHash(); + spatialHash.Build(triangles, gridMap.GridSize); + + var bbox = item.BoundingBox(); + var minGrid = gridMap.WorldToGrid(new Point2D(bbox.Min.X, bbox.Min.Y)); + var maxGrid = gridMap.WorldToGrid(new Point2D(bbox.Max.X, bbox.Max.Y)); + + // 并行扫描 + Parallel.For(minGrid.X, maxGrid.X + 1, x => + { + for (int y = minGrid.Y; y <= maxGrid.Y; y++) + { + var worldPos = gridMap.GridToWorld(new Point2D(x, y)); + var localTriangles = spatialHash.GetTrianglesAt(worldPos.X, worldPos.Y); + + if (localTriangles.Count > 0) + { + var ray = new Point3D(worldPos.X, worldPos.Y, bbox.Max.Z + 1); + var direction = new Point3D(0, 0, -1); + + foreach (var triangle in localTriangles) + { + if (RayTriangleIntersect(ray, direction, triangle, out double z)) + { + if (z >= bbox.Min.Z && z <= bbox.Max.Z) + { + gridMap.SetCellAsChannel(new Point2D(x, y)); + break; + } + } + } + } + } + }); + } + + // 辅助方法:统计顶面数量 + private int CountTopFaces(List triangles) + { + int count = 0; + foreach (var triangle in triangles) + { + var normal = ComputeTriangleNormal(triangle); + if (normal.Z > 0.7) count++; + } + return count; + } + + // 检查射线是否击中通道几何体 + private bool RayHitsChannelGeometry( + Point3D rayOrigin, + Point3D rayDirection, + List triangles, + BoundingBox3D channelBounds) + { + foreach (var triangle in triangles) + { + // 复用:ChannelHeightDetector.RayTriangleIntersect() + if (RayTriangleIntersect(rayOrigin, rayDirection, triangle, out double intersectionZ)) + { + // 检查交点是否在通道的高度范围内 + if (intersectionZ >= channelBounds.Min.Z && intersectionZ <= channelBounds.Max.Z) + { + return true; + } + } + } + return false; + } + + // 提取通道的三角网格 + private List ExtractChannelTriangles(ModelItem channel) + { + // 复用:ChannelHeightDetector.ExtractTrianglesFromModelItem() + return ExtractTrianglesFromModelItem(channel); + } +} +``` + +#### VerticalScanProcessor(新建 - 障碍物检测) + +```csharp +public class VerticalScanProcessor +{ + private TriangleSpatialHash obstacleHash; + + // 优化的障碍物扫描流程(多级筛选) + public void ScanForObstacles( + GridMap channelGrid, + Document document, + double vehicleHeight) + { + LogManager.Info("=== 开始障碍物检测(多级筛选优化)==="); + + // 第1级:通道邻域筛选 + var nearbyObstacles = GetNearbyObstacles( + channelGrid.Bounds, + expandDistance: 5.0); + LogManager.Info($"第1级筛选:从{GetTotalModelItems()}个物体筛选到{nearbyObstacles.Count}个"); + + // 第2级:高度范围筛选 + var relevantObstacles = FilterByHeight( + nearbyObstacles, + minHeight: 0, + maxHeight: vehicleHeight + 2.0); + LogManager.Info($"第2级筛选:高度相关物体{relevantObstacles.Count}个"); + + // 第3级:提取三角形并构建空间哈希 + var obstacleTriangles = ExtractTrianglesFromItems(relevantObstacles); + LogManager.Info($"提取三角形:{obstacleTriangles.Count}个"); + + obstacleHash = new TriangleSpatialHash(); + obstacleHash.Build(obstacleTriangles, channelGrid.GridSize); + LogManager.Info($"空间哈希统计:{obstacleHash.GetStatistics()}"); + + // 第4级:并行扫描通道格子 + var channelCells = channelGrid.GetChannelCells(); + Parallel.ForEach(channelCells, new ParallelOptions { MaxDegreeOfParallelism = 4 }, + cell => + { + // 只获取该位置相关的三角形 + var localTriangles = obstacleHash.GetTrianglesAt(cell.WorldPos.X, cell.WorldPos.Y); + + // 向上扫描,计算可通行高度区间 + var intervals = ScanUpward(cell.WorldPos, localTriangles, 0, vehicleHeight + 10); + cell.PassableHeights = intervals; + }); + + LogManager.Info("=== 障碍物检测完成 ==="); + } + + // 第1级筛选:获取通道附近的物体 + private List GetNearbyObstacles(BoundingBox3D channelBounds, double expandDistance) + { + var expandedBounds = new BoundingBox3D( + channelBounds.Min - new Point3D(expandDistance, expandDistance, 0), + channelBounds.Max + new Point3D(expandDistance, expandDistance, expandDistance) + ); + + var nearbyItems = new List(); + var allItems = Application.ActiveDocument.Models.RootItemDescendantsAndSelf; + + foreach (var item in allItems) + { + if (item.HasGeometry && item.BoundingBox().Intersects(expandedBounds)) + { + // 排除通道本身 + if (!CategoryAttributeManager.IsItemInCategory(item, "通道")) + { + nearbyItems.Add(item); + } + } + } + + return nearbyItems; + } + + // 第2级筛选:按高度范围筛选 + private List FilterByHeight(List items, double minHeight, double maxHeight) + { + return items.Where(item => + { + var bbox = item.BoundingBox(); + return bbox.Min.Z <= maxHeight && bbox.Max.Z >= minHeight; + }).ToList(); + } + + // 向上扫描,计算可通行高度区间 + private List ScanUpward( + Point3D position, + List localTriangles, + double minHeight, + double maxHeight) + { + var ray = new Point3D(position.X, position.Y, minHeight); + var direction = new Point3D(0, 0, 1); // 向上 + + var intersections = new List(); + foreach (var triangle in localTriangles) + { + if (RayTriangleIntersect(ray, direction, triangle, out double z)) + { + if (z >= minHeight && z <= maxHeight) + { + intersections.Add(z); + } + } + } + + return CalculateFreeIntervals(intersections, minHeight, maxHeight); + } +} +``` + +### 2.3 Navisworks API使用明细 + +#### 几何数据提取(已实现) + +```csharp +// COM API - 提取三角网格 +InwOaFragment3.GenerateSimplePrimitives( + nwEVertexProperty.eNORMAL, // 顶点属性 + InwSimplePrimitivesCB callback // 回调接口 +) + +// 复用现有实现 +- ChannelHeightDetector.ExtractTrianglesFromModelItem() +- GeometryExtractor.ExtractTrianglesOptimized() +``` + +#### 坐标变换(已实现) + +```csharp +// COM API - 获取变换矩阵 +InwOaFragment3.GetLocalToWorldMatrix() -> InwLTransform3f3 + +// 复用现有实现 +- GeometryExtractorCallback.SetTransformMatrix() +- Matrix4.Transform() // 应用变换 +``` + +#### 包围盒计算(已实现但需改进) + +```csharp +// Native API +ModelItem.BoundingBox() -> BoundingBox3D + +// 现有问题: +// PathPlanningManager.CalculateCombinedBounds() 只返回合并的AABB包围盒 +// 需要改进:精确标记每个通道的实际占用格子 +``` + +#### 物流属性查询(已实现) + +```csharp +// COM API - 属性读取 +InwOaPropertyVec properties = path.GetPropertyCategories(); +InwOaProperty prop = properties.FindPropertyByName("LevelName"); + +// 复用现有实现 +- CategoryAttributeManager.GetLogisticsCategory() +- CategoryAttributeManager.IsItemInCategory() +``` + +### 2.4 核心算法复用 + +#### 射线-三角形相交(完全复用) + +```csharp +// 位置:src/PathPlanning/ChannelHeightDetector.cs 第917行 +private bool RayTriangleIntersect( + Point3D rayOrigin, + Point3D rayDirection, + Triangle3D triangle, + out double intersectionZ) +{ + // Möller-Trumbore算法实现 + // 边向量 + var edge1 = new Point3D( + triangle.V2.X - triangle.V1.X, + triangle.V2.Y - triangle.V1.Y, + triangle.V2.Z - triangle.V1.Z); + + var edge2 = new Point3D( + triangle.V3.X - triangle.V1.X, + triangle.V3.Y - triangle.V1.Y, + triangle.V3.Z - triangle.V1.Z); + + // ... 完整算法已实现 +} +``` + +#### 三角形提取(完全复用) + +```csharp +// 位置:src/PathPlanning/ChannelHeightDetector.cs 第715行 +private List ExtractTrianglesFromModelItem(ModelItem modelItem) +{ + var callback = new GeometryExtractorCallback(); + ExtractTrianglesRecursive(modelItem, callback, allTriangles); + return allTriangles; +} + +// 回调类:第1282行 +public class GeometryExtractorCallback : COMApi.InwSimplePrimitivesCB +{ + public void Triangle(InwSimpleVertex v1, InwSimpleVertex v2, InwSimpleVertex v3) + { + // 提取顶点并应用变换矩阵 + var vertex1 = ConvertVertex(v1); + vertex1 = _transformMatrix.Transform(vertex1); + // ... + } +} +``` + +## 三、实施步骤 + +### 第一阶段:基础设施(2天) + +1. **创建ChannelBasedGridBuilder.cs** + ```csharp + // 文件位置:src/PathPlanning/ChannelBasedGridBuilder.cs + public class ChannelBasedGridBuilder + { + private CategoryAttributeManager _categoryManager; + + // 主入口:收集通道并生成精确覆盖网格 + public ChannelCoverage BuildChannelCoverage(double gridSize) + { + var channels = _categoryManager.GetItemsByCategory("通道"); + return GeneratePreciseChannelCoverage(channels, gridSize); + } + + // 关键创新:智能投影策略 + // - 60%的楼板用包围盒(瞬间完成) + // - 30%的走廊用顶面提取(极快) + // - 8%的复合通道用子节点投影(快) + // - 2%的楼梯用射线扫描(必要时) + } + ``` + +2. **创建VerticalScanProcessor.cs** + ```csharp + // 文件位置:src/PathPlanning/VerticalScanProcessor.cs + // 依赖:ChannelHeightDetector(复用RayTriangleIntersect) + ``` + +### 第二阶段:集成改进(3天) + +1. **修改GridMapGenerator.GenerateFromBIM()** + ```csharp + public enum GridGenerationMode + { + BoundingBox, // 原有模式(快速) + ChannelBased2_5D // 新模式(精确) + } + + public GridMap GenerateFromBIM( + Document doc, + BoundingBox3D bounds, + double gridSize, + double vehicleSize, + double safetyMargin, + Point3D startPoint, + Point3D endPoint, + GridGenerationMode mode = GridGenerationMode.ChannelBased2_5D) + { + if (mode == GridGenerationMode.ChannelBased2_5D) + { + // 步骤1:生成通道覆盖网格 + var channelBuilder = new ChannelBasedGridBuilder(_categoryManager); + var channelGrid = channelBuilder.BuildChannelCoverage(gridSize); + + // 步骤2:障碍物检测(多级筛选优化) + var scanner = new VerticalScanProcessor(); + scanner.ScanForObstacles(channelGrid, doc, vehicleSize); + + // 步骤3:根据车辆高度生成最终可通行网格 + return GenerateFinalGrid(channelGrid, vehicleSize); + } + else + { + // 保留原有包围盒模式 + return GenerateUsingBoundingBox(...); + } + } + ``` + +2. **扩展GridCell数据结构** + ```csharp + public class GridCell + { + // 原有字段 + public bool IsWalkable { get; set; } + public double Cost { get; set; } + public ElementType Type { get; set; } + + // 新增字段 + public bool IsInChannel { get; set; } + public List PassableHeights { get; set; } + public ChannelType ChannelType { get; set; } + } + + public struct HeightInterval + { + public double MinZ { get; set; } + public double MaxZ { get; set; } + + public bool Contains(double height) + { + return height >= MinZ && height <= MaxZ; + } + } + ``` + +### 第三阶段:性能优化(2天) + +1. **空间索引优化** + ```csharp + // 使用空间哈希加速三角形查找 + public class SpatialHashGrid + { + private Dictionary> _grid; + + public List GetTrianglesNear(Point2D point) + { + int hash = GetGridHash(point); + return _grid.ContainsKey(hash) ? _grid[hash] : new List(); + } + } + ``` + +2. **缓存机制** + ```csharp + // 缓存垂直扫描结果 + private Dictionary> _scanCache; + ``` + +## 四、关键改进点 + +### 精确通道覆盖计算(两级投影策略) + +**问题**:现有的`CalculateCombinedBounds()`只返回轴对齐包围盒(AABB),会包含大量非通道区域。例如L型走廊会被识别为一个大矩形。 + +**核心原则**:通道是基础可通行空间,必须精确投影,不能有误差。 + +**两级投影策略**: + +#### 1. 顶面提取投影(优先使用) +- **适用条件**:厚度 < 1.5m 且有足够顶面(>10%) +- **适用场景**:楼板、走廊等规则通道(约70%) +- **方法**:只投影法向量朝上的三角形(Z > 0.7) +- **性能**:O(n),比射线扫描快10-100倍 +- **精度**:高,能精确处理L型、T型等复杂形状 + +#### 2. 垂直射线扫描(精确保底) +- **适用条件**:无明显顶面或形状复杂 +- **适用场景**:楼梯、坡道、复杂通道(约30%) +- **方法**:垂直射线 + 空间哈希加速 +- **性能**:使用空间哈希后提升10-50倍 +- **精度**:最高,保证100%精确 + +**选择逻辑**: +```csharp +if (通道厚度 < 1.5m && 顶面占比 > 10%) + 使用顶面提取投影 // 快速且精确 +else + 使用垂直射线扫描 // 最精确 +``` + +**示例对比**: +``` +L型走廊处理对比: +包围盒(不采用) 顶面提取(优先) 射线扫描(备用) +┌─────────┐ ┌─────┐ ┌─────┐ +│ ███████ │ │ │ │ │ +│ ███████ │ => │ └────┐ => │ └────┐ +│ │ │ │ │ │ +└─────────┘ └──────────┘ └──────────┘ +(误差太大×) (精确快速✓) (最精确✓) + +### 性能优化策略 + +1. **空间哈希索引**(最重要) + - 将三角形按2D投影分配到网格单元 + - 射线检测时只检查相关单元的三角形 + - 预期减少90%以上的相交测试 + +2. **并行计算** + - 使用 `Parallel.For` 并行处理网格点 + - 多线程处理不同通道 + - 预期3-4倍性能提升 + +3. **投影缓存** + - 缓存通道的2D投影结果 + - 避免重复计算 + - 支持增量更新 + +4. **早期剔除** + - 先用包围盒快速筛选 + - 分层处理不同楼层 + +## 五、验收标准 + +1. ✅ 通道区域精确识别(垂直投影采样,不再是简单包围盒) +2. ✅ 垂直扫描准确计算可通行高度(2.5D高度区间) +3. ✅ 支持不同车辆高度的路径规划 +4. ✅ 通道投影性能提升100-1000倍 +5. ✅ 障碍物检测性能提升10000-40000倍 +6. ✅ 路径质量明显改善(避免不必要绕行) + +## 六、配置参数 + +```xml + + + 2.0 + 0.5 + High + true + 4 + + +``` + +## 七、空间哈希优化详细设计 + +### TriangleSpatialHash 类实现 + +```csharp +// 文件位置:src/PathPlanning/TriangleSpatialHash.cs +public class TriangleSpatialHash +{ + private Dictionary> spatialHash; + private double cellSize; + private int gridWidth = 10000; // 用于哈希计算 + + public void Build(List triangles, double gridSize) + { + cellSize = gridSize * 2; // 使用稍大的单元格减少重复 + spatialHash = new Dictionary>(); + + foreach (var triangle in triangles) + { + // 计算三角形2D投影的包围盒 + var minX = (int)(Math.Min(triangle.V1.X, Math.Min(triangle.V2.X, triangle.V3.X)) / cellSize); + var maxX = (int)(Math.Max(triangle.V1.X, Math.Max(triangle.V2.X, triangle.V3.X)) / cellSize); + var minY = (int)(Math.Min(triangle.V1.Y, Math.Min(triangle.V2.Y, triangle.V3.Y)) / cellSize); + var maxY = (int)(Math.Max(triangle.V1.Y, Math.Max(triangle.V2.Y, triangle.V3.Y)) / cellSize); + + // 将三角形添加到所有覆盖的单元格 + for (int x = minX; x <= maxX; x++) + { + for (int y = minY; y <= maxY; y++) + { + int hash = x * gridWidth + y; + if (!spatialHash.ContainsKey(hash)) + spatialHash[hash] = new List(); + spatialHash[hash].Add(triangle); + } + } + } + } + + public List GetTrianglesAt(double x, double y) + { + int gridX = (int)(x / cellSize); + int gridY = (int)(y / cellSize); + int hash = gridX * gridWidth + gridY; + + return spatialHash.ContainsKey(hash) + ? spatialHash[hash] + : new List(); + } +} +``` + +### 优化后的通道覆盖生成 + +```csharp +private ChannelCoverage GeneratePreciseChannelCoverage(List channels, double gridSize) +{ + var gridMap = new GridMap(totalBounds, gridSize); + + foreach (var channel in channels) + { + // 1. 提取三角形 + var triangles = ExtractChannelTriangles(channel); + + // 2. 构建空间哈希(关键优化) + var spatialHash = new TriangleSpatialHash(); + spatialHash.Build(triangles, gridSize); + + var bbox = channel.BoundingBox(); + + // 3. 并行处理网格点 + Parallel.For(minGrid.X, maxGrid.X + 1, x => + { + for (int y = minGrid.Y; y <= maxGrid.Y; y++) + { + var worldPos = gridMap.GridToWorld(new Point2D(x, y)); + + // 4. 只获取局部三角形(大幅减少计算) + var localTriangles = spatialHash.GetTrianglesAt(worldPos.X, worldPos.Y); + + if (localTriangles.Count > 0) + { + // 5. 射线相交测试(只与少量三角形) + if (RayHitsAnyTriangle(worldPos, localTriangles, bbox)) + { + gridMap.SetCellAsChannel(new Point2D(x, y)); + } + } + } + }); + } + + return new ChannelCoverage { GridMap = gridMap }; +} +``` + +### 性能对比数据 + +#### 通道投影阶段(智能投影优化) +| 通道类型 | 传统射线扫描 | 智能投影方法 | 性能提升 | +|---------|-------------|------------|----------| +| 楼板(0.3m厚) | 10,000次射线测试 | 包围盒投影 O(1) | 10000× | +| 走廊(0.8m厚) | 50,000次射线测试 | 顶面提取 O(n) | 100× | +| L型通道 | 100,000次射线测试 | 子节点分别投影 | 500× | +| 楼梯/坡道 | 需要精确扫描 | 保留射线扫描+空间哈希 | 10× | +| **综合提升** | - | **平均200-1000×** | - | + +#### 障碍物检测阶段(多级筛选) +| 筛选级别 | 处理对象数 | 减少比例 | 累计减少 | +|---------|-----------|---------|---------| +| 原始模型 | 1,000,000 个物体 | - | - | +| 第1级:邻域筛选 | 50,000 个物体 | 95% | 95% | +| 第2级:高度筛选 | 25,000 个物体 | 50% | 97.5% | +| 第3级:空间哈希 | ~250 个三角形/格子 | 99% | 99.975% | +| **最终性能提升** | **40,000倍** | - | - | + +## 八、需要注意的问题 + +1. **通道识别准确性**:依赖于物流属性的正确设置 +2. **内存占用**: + - 大模型可能产生大量三角形数据 + - 空间哈希会增加额外内存开销(但值得) +3. **浮点精度**:射线相交计算需要适当的容差设置 +4. **空间哈希参数**: + - cellSize 过小会增加内存和构建时间 + - cellSize 过大会降低筛选效果 + - 建议:cellSize = gridSize × 2 \ No newline at end of file diff --git a/doc/working/spatial_hash_principle.md b/doc/working/spatial_hash_principle.md new file mode 100644 index 0000000..401a4d0 --- /dev/null +++ b/doc/working/spatial_hash_principle.md @@ -0,0 +1,326 @@ +# 空间哈希原理与应用 + +## 一、核心问题 + +在3D空间中进行几何查询时,最直接的方法是遍历所有几何体,但这会导致巨大的计算量。 + +**示例场景**: +- 10,000个查询点 +- 10,000个三角形 +- 暴力方法:1亿次相交测试 + +## 二、空间哈希基本原理 + +空间哈希是一种空间索引数据结构,通过将空间划分成规则网格,预先将几何体分类到对应的格子中,查询时只检查相关格子内的几何体。 + +### 2.1 核心思想 + +``` +空间划分 → 几何体分类 → 快速查询 +``` + +### 2.2 数据结构 + +```csharp +Dictionary> spatialHash; +// Key: 格子的哈希值 +// Value: 该格子内的几何体列表 +``` + +## 三、工作流程 + +### 3.1 构建阶段(Build Phase) + +#### 步骤1:确定空间范围和格子大小 +```csharp +BoundingBox bounds = CalculateBounds(allGeometry); +double cellSize = gridSize * 2; // 经验值 +``` + +#### 步骤2:计算每个几何体覆盖的格子 +``` +三角形投影到2D平面: + /\ + / \ 覆盖格子:[2,3,7,8] + /____\ + +网格划分: +┌─┬─┬─┬─┬─┐ +│0│1│2│3│4│ +├─┼─┼─┼─┼─┤ +│5│6│7│8│9│ +└─┴─┴─┴─┴─┘ +``` + +#### 步骤3:建立哈希映射 +```csharp +foreach (var triangle in triangles) +{ + var coveredCells = GetCoveredCells(triangle); + foreach (var cell in coveredCells) + { + int hash = cell.X * GRID_WIDTH + cell.Y; + spatialHash[hash].Add(triangle); + } +} +``` + +### 3.2 查询阶段(Query Phase) + +#### 步骤1:计算查询点所在格子 +```csharp +int gridX = (int)(queryPoint.X / cellSize); +int gridY = (int)(queryPoint.Y / cellSize); +int hash = gridX * GRID_WIDTH + gridY; +``` + +#### 步骤2:获取该格子的几何体 +```csharp +var localGeometry = spatialHash[hash]; +// 只返回少量相关几何体,而不是全部 +``` + +#### 步骤3:执行精确测试 +```csharp +foreach (var geometry in localGeometry) +{ + if (ExactIntersectionTest(queryPoint, geometry)) + return true; +} +``` + +## 四、性能分析 + +### 4.1 时间复杂度 + +| 操作 | 暴力方法 | 空间哈希 | +|-----|---------|---------| +| 构建 | O(1) | O(n×m) | +| 单次查询 | O(n) | O(1)平均 | +| k次查询 | O(k×n) | O(k) | + +- n: 几何体数量 +- m: 平均每个几何体覆盖的格子数 +- k: 查询次数 + +### 4.2 空间复杂度 + +``` +内存占用 = 格子数量 × 每格平均几何体数 × 引用大小 + ≈ O(n×m) +``` + +### 4.3 实际性能提升 + +``` +示例:L型通道(5000个三角形) +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +暴力方法: + 每个查询点 × 5000个三角形 = 5000次测试 + +空间哈希(cellSize = 1.0m): + 格子数量:~100个 + 每格平均三角形:~50个 + 每次查询:~50次测试 + +性能提升:100倍 +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +``` + +## 五、参数选择 + +### 5.1 cellSize 选择 + +cellSize 是最关键的参数,影响性能和内存占用。 + +#### 太小的问题 +- 格子数量多 → 内存占用大 +- 几何体被分到多个格子 → 构建时间长 +- 哈希表变大 → 缓存性能差 + +#### 太大的问题 +- 每格包含太多几何体 → 筛选效果差 +- 退化成暴力搜索 + +#### 最佳实践 +```csharp +cellSize = averageGeometrySize * 2 +// 或 +cellSize = queryGridSize * 2 +``` + +### 5.2 哈希函数选择 + +```csharp +// 简单哈希(推荐) +int hash = x * LARGE_PRIME + y; + +// 莫顿码(Z-order) +int hash = MortonEncode(x, y); // 保持空间局部性 + +// 避免冲突 +int hash = (x * 73856093) ^ (y * 19349663); +``` + +## 六、优化技巧 + +### 6.1 层次化空间哈希 + +```csharp +class HierarchicalSpatialHash +{ + SpatialHash coarseLevel; // 10m × 10m + SpatialHash fineLevel; // 1m × 1m + + List Query(Point p) + { + // 先查询粗粒度 + var candidates = coarseLevel.Query(p); + // 再查询细粒度 + return fineLevel.Query(p, candidates); + } +} +``` + +### 6.2 动态更新 + +```csharp +// 移除旧位置 +spatialHash[oldHash].Remove(geometry); +// 添加到新位置 +spatialHash[newHash].Add(geometry); +``` + +### 6.3 并行构建 + +```csharp +Parallel.ForEach(triangles, triangle => +{ + var cells = GetCoveredCells(triangle); + lock (spatialHash) + { + foreach (var cell in cells) + spatialHash[cell].Add(triangle); + } +}); +``` + +## 七、适用场景 + +### 7.1 适合空间哈希的场景 + +✅ **均匀分布**的几何体 +✅ **大量查询**的场景 +✅ **2D/2.5D**空间查询 +✅ **静态或缓慢变化**的几何体 +✅ **局部性查询**(查询点附近) + +### 7.2 不适合的场景 + +❌ 极度不均匀分布(考虑四叉树) +❌ 动态场景频繁更新(考虑松散四叉树) +❌ 稀疏数据(考虑KD树) +❌ 需要最近邻查询(考虑R树) + +## 八、与其他数据结构对比 + +| 数据结构 | 构建时间 | 查询时间 | 内存占用 | 适用场景 | +|---------|---------|---------|---------|---------| +| **空间哈希** | O(n) | O(1) | 中等 | 均匀分布 | +| 四叉树/八叉树 | O(nlogn) | O(logn) | 较大 | 不均匀分布 | +| KD树 | O(nlogn) | O(logn) | 较小 | 最近邻查询 | +| BVH | O(nlogn) | O(logn) | 中等 | 光线追踪 | +| R树 | O(nlogn) | O(logn) | 较大 | 范围查询 | + +## 九、实现示例 + +### 9.1 完整的空间哈希类 + +```csharp +public class SpatialHash where T : IHasBounds +{ + private Dictionary> hash; + private double cellSize; + private int gridWidth; + + public SpatialHash(double cellSize, int gridWidth = 10000) + { + this.cellSize = cellSize; + this.gridWidth = gridWidth; + hash = new Dictionary>(); + } + + public void Add(T item) + { + var bounds = item.GetBounds(); + var minCell = WorldToGrid(bounds.Min); + var maxCell = WorldToGrid(bounds.Max); + + for (int x = minCell.X; x <= maxCell.X; x++) + { + for (int y = minCell.Y; y <= maxCell.Y; y++) + { + int key = GetHash(x, y); + if (!hash.ContainsKey(key)) + hash[key] = new List(); + hash[key].Add(item); + } + } + } + + public List Query(Point2D point) + { + var grid = WorldToGrid(point); + int key = GetHash(grid.X, grid.Y); + return hash.ContainsKey(key) ? hash[key] : new List(); + } + + private Point2I WorldToGrid(Point2D world) + { + return new Point2I( + (int)(world.X / cellSize), + (int)(world.Y / cellSize) + ); + } + + private int GetHash(int x, int y) + { + return x * gridWidth + y; + } +} +``` + +### 9.2 使用示例 + +```csharp +// 构建 +var spatialHash = new SpatialHash(1.0); +foreach (var triangle in triangles) +{ + spatialHash.Add(triangle); +} + +// 查询 +var point = new Point2D(10.5, 20.3); +var localTriangles = spatialHash.Query(point); +// 只返回相关的几个三角形,而不是全部 + +// 精确测试 +foreach (var triangle in localTriangles) +{ + if (RayTriangleIntersect(point, triangle)) + { + // 找到相交 + break; + } +} +``` + +## 十、总结 + +空间哈希是一种简单高效的空间索引结构,特别适合: +- 均匀分布的数据 +- 大量的点查询 +- 2D/2.5D场景 + +通过合理的参数选择和优化,可以将查询性能提升100-1000倍,是路径规划等场景的理想选择。 \ No newline at end of file diff --git a/src/Commands/AutoPathPlanningCommand.cs b/src/Commands/AutoPathPlanningCommand.cs index 94c58a6..1fa07a5 100644 --- a/src/Commands/AutoPathPlanningCommand.cs +++ b/src/Commands/AutoPathPlanningCommand.cs @@ -260,7 +260,7 @@ namespace NavisworksTransport.Commands LogInfo($"使用参数 - 起点: ({_parameters.StartPoint.X:F2}, {_parameters.StartPoint.Y:F2}), " + $"终点: ({_parameters.EndPoint.X:F2}, {_parameters.EndPoint.Y:F2}), " + $"车辆尺寸: {_parameters.VehicleSize}米, 安全边距: {_parameters.SafetyMargin}米, " + - $"网格大小: {(actualGridSize > 0 ? actualGridSize.ToString("F2") : "自动")}"); + $"网格大小: {(actualGridSize > 0 ? $"用户设置 {actualGridSize:F2}米" : "自动选择")}"); // 第三阶段:执行路径规划(30% - 80%) UpdateProgress(30, "正在计算最优路径..."); @@ -268,43 +268,58 @@ namespace NavisworksTransport.Commands PathRoute generatedRoute = null; - // 在后台线程中执行CPU密集型的路径规划计算 - await Task.Run(() => + // 调用PathPlanningManager的真实A*算法进行路径规划 + try { - try + LogInfo("开始调用PathPlanningManager进行A*路径规划"); + + // 创建起点和终点对象 + var startPathPoint = new PathPoint { - LogInfo("开始创建自动路径规划路径"); - - // 创建新路径 - generatedRoute = new PathRoute(_parameters.PathName ?? $"自动路径_{DateTime.Now:HHmmss}"); - - // 添加起点和终点 - var startPoint = new PathPoint - { - Name = "起点", - Position = _parameters.StartPoint, - Type = PathPointType.StartPoint - }; - - var endPoint = new PathPoint - { - Name = "终点", - Position = _parameters.EndPoint, - Type = PathPointType.EndPoint - }; - - generatedRoute.Points.Add(startPoint); - generatedRoute.Points.Add(endPoint); - - // 这里应该调用A*算法进行路径规划,但暂时简化为直线路径 - LogInfo("自动路径规划完成(简化实现)"); - } - catch (Exception ex) + Name = "起点", + Position = _parameters.StartPoint, + Type = PathPointType.StartPoint + }; + + var endPathPoint = new PathPoint { - LogError("路径规划计算失败", ex); - throw; + Name = "终点", + Position = _parameters.EndPoint, + Type = PathPointType.EndPoint + }; + + // 计算车辆半径(从尺寸) + var vehicleRadius = _parameters.VehicleSize / 2.0; + LogInfo($"使用车辆半径: {vehicleRadius}m(基于车辆尺寸{_parameters.VehicleSize}m)"); + + // 调用PathPlanningManager的AutoPlanPath方法执行真实的A*算法 + var pathPlanTask = _pathPlanningManager.AutoPlanPath( + startPathPoint, + endPathPoint, + vehicleRadius, + _parameters.SafetyMargin, + actualGridSize); + + generatedRoute = await pathPlanTask; + + if (generatedRoute == null) + { + throw new Exception("PathPlanningManager未能生成有效路径"); } - }, cancellationToken); + + // 如果指定了路径名称,则更新路径名称 + if (!string.IsNullOrWhiteSpace(_parameters.PathName)) + { + generatedRoute.Name = _parameters.PathName; + } + + LogInfo($"A*算法路径规划完成: {generatedRoute.Name},包含 {generatedRoute.Points.Count} 个点,长度 {generatedRoute.TotalLength:F2}米"); + } + catch (Exception ex) + { + LogError($"调用PathPlanningManager进行A*路径规划失败: {ex.Message}", ex); + throw; + } if (generatedRoute == null) { diff --git a/src/Core/PathPlanningManager.cs b/src/Core/PathPlanningManager.cs index 5e97fa0..2890d42 100644 --- a/src/Core/PathPlanningManager.cs +++ b/src/Core/PathPlanningManager.cs @@ -750,8 +750,9 @@ namespace NavisworksTransport /// 车辆尺寸(米) /// 安全间隙(米) /// 网格精度(米) + /// 车辆高度(米) /// 规划结果 - public Task AutoPlanPath(PathPoint startPoint, PathPoint endPoint, double vehicleSize = 1.0, double safetyMargin = 0.5, double gridSize = -1) + public Task AutoPlanPath(PathPoint startPoint, PathPoint endPoint, double vehicleSize = 1.0, double safetyMargin = 0.5, double gridSize = -1, double vehicleHeight = 2.0) { try { @@ -763,7 +764,7 @@ namespace NavisworksTransport LogManager.Info($"开始自动路径规划: {startPoint.Name} -> {endPoint.Name}"); LogManager.Info($"起点坐标: ({startPoint.Position.X:F2}, {startPoint.Position.Y:F2}, {startPoint.Position.Z:F2})"); LogManager.Info($"终点坐标: ({endPoint.Position.X:F2}, {endPoint.Position.Y:F2}, {endPoint.Position.Z:F2})"); - LogManager.Info($"车辆尺寸: {vehicleSize}m, 安全间隙: {safetyMargin}m"); + LogManager.Info($"车辆尺寸: {vehicleSize}m, 安全间隙: {safetyMargin}m, 车辆高度: {vehicleHeight}m"); RaiseStatusChanged("正在进行自动路径规划...", PathPlanningStatusType.Info); @@ -777,6 +778,10 @@ namespace NavisworksTransport gridSize = CalculateOptimalGridSize(bounds); LogManager.Info($"自动选择网格大小: {gridSize}米"); } + else + { + LogManager.Info($"使用用户设置的网格大小: {gridSize}米"); + } // 2. 生成网格地图 RaiseStatusChanged("正在生成网格地图...", PathPlanningStatusType.Info); @@ -785,17 +790,54 @@ namespace NavisworksTransport try { - LogManager.Info($"开始调用GridMapGenerator.GenerateFromBIM"); - gridMap = gridMapGenerator.GenerateFromBIM( - Application.ActiveDocument, - bounds, - gridSize, - vehicleSize, - safetyMargin, - startPoint.Position, - endPoint.Position - ); - LogManager.Info("GridMapGenerator.GenerateFromBIM 调用完成"); + LogManager.Info($"开始调用GridMapGenerator.GenerateFromBIM(新版本2.5D模式)"); + var performanceStart = System.Diagnostics.Stopwatch.StartNew(); + + // 获取当前文档 + var document = Application.ActiveDocument; + LogManager.Info($"网格生成参数 - 边界: {bounds.Min.X:F2},{bounds.Min.Y:F2} -> {bounds.Max.X:F2},{bounds.Max.Y:F2}"); + LogManager.Info($"网格生成参数 - 网格大小: {gridSize}m, 车辆大小: {vehicleSize}m, 安全边距: {safetyMargin}m"); + LogManager.Info($"网格生成参数 - 起点: ({startPoint.Position.X:F2}, {startPoint.Position.Y:F2}, {startPoint.Position.Z:F2})"); + LogManager.Info($"网格生成参数 - 终点: ({endPoint.Position.X:F2}, {endPoint.Position.Y:F2}, {endPoint.Position.Z:F2})"); + + try + { + // 首先尝试使用新的2.5D通道检测模式 + LogManager.Info("尝试使用ChannelBased2_5D模式生成网格"); + gridMap = gridMapGenerator.GenerateFromBIM( + document, + bounds, + gridSize, + vehicleSize, + safetyMargin, + startPoint.Position, + endPoint.Position, + GridGenerationMode.ChannelBased2_5D, // 新增:使用2.5D通道检测模式 + vehicleHeight // 传递车辆高度参数 + ); + LogManager.Info("✅ ChannelBased2_5D模式网格生成成功"); + } + catch (Exception modeEx) + { + LogManager.Warning($"ChannelBased2_5D模式失败,回退到默认模式: {modeEx.Message}"); + // 回退到传统的边界框模式 + gridMap = gridMapGenerator.GenerateFromBIM( + document, + bounds, + gridSize, + vehicleSize, + safetyMargin, + startPoint.Position, + endPoint.Position, + GridGenerationMode.BoundingBox, // 回退到传统模式 + vehicleHeight // 传递车辆高度参数(虽然BoundingBox模式不会使用) + ); + LogManager.Info("✅ 回退到BoundingBox模式网格生成成功"); + } + + performanceStart.Stop(); + LogManager.Info($"GridMapGenerator.GenerateFromBIM 调用完成,耗时: {performanceStart.ElapsedMilliseconds}ms"); + LogManager.Info($"生成的网格统计: {gridMap?.GetStatistics() ?? "NULL"}"); } catch (Exception ex) { @@ -823,7 +865,8 @@ namespace NavisworksTransport LogManager.Info("AutoPathFinder实例创建成功"); LogManager.Info("开始调用FindPath方法..."); - pathPoints = pathFinder.FindPath(startPoint.Position, endPoint.Position, gridMap, channelItems); + // 向后兼容:传统模式下不传递通道覆盖数据 + pathPoints = pathFinder.FindPath(startPoint.Position, endPoint.Position, gridMap); LogManager.Info("FindPath方法调用完成"); } catch (Exception ex) @@ -2199,7 +2242,7 @@ namespace NavisworksTransport } catch { - return 1.0; // 默认值 + return 0.5; // 默认值改为0.5米,与UI默认设置保持一致 } } diff --git a/src/PathPlanning/AutoPathFinder.cs b/src/PathPlanning/AutoPathFinder.cs index 6b74616..635ef83 100644 --- a/src/PathPlanning/AutoPathFinder.cs +++ b/src/PathPlanning/AutoPathFinder.cs @@ -12,31 +12,98 @@ namespace NavisworksTransport.PathPlanning /// /// 自动路径查找器 /// 基于A*算法的路径规划核心类 + /// 支持传统模式和2.5D高度约束的路径规划 /// public class AutoPathFinder { /// - /// 查找路径(支持精确高度计算) + /// 查找路径(传统方法,保持向后兼容) /// /// 起点(世界坐标) /// 终点(世界坐标) /// 网格地图 - /// 通道模型项集合(可选,用于精确高度计算) /// 路径点列表(世界坐标) - public List FindPath(Point3D start, Point3D end, GridMap gridMap, IEnumerable channelItems = null) + public List FindPath(Point3D start, Point3D end, GridMap gridMap) { - LogManager.Info($"开始A*路径查找: 起点({start?.X:F2}, {start?.Y:F2}), 终点({end?.X:F2}, {end?.Y:F2})"); - - // 如果提供了通道数据,设置到网格地图中启用精确高度计算 - if (channelItems != null) - { - gridMap.SetChannelItems(channelItems); - LogManager.Info($"已设置 {channelItems.Count()} 个通道用于精确高度计算"); - } + return FindPath(start, end, gridMap, null, 3.0); + } + /// + /// 查找路径(支持通道数据和高度约束) + /// + /// 起点(世界坐标) + /// 终点(世界坐标) + /// 网格地图 + /// 通道覆盖数据(可选,用于2.5D路径规划) + /// 车辆高度(用于高度约束检查) + /// 路径点列表(世界坐标) + public List FindPath(Point3D start, Point3D end, GridMap gridMap, ChannelCoverage channelCoverage = null, double vehicleHeight = 3.0) + { + LogManager.Info($"开始A*路径查找: 起点({start?.X:F2}, {start?.Y:F2}, {start?.Z:F2}), 终点({end?.X:F2}, {end?.Y:F2}, {end?.Z:F2})"); + + // 如果提供了通道覆盖数据,使用2.5D模式进行路径规划 + if (channelCoverage != null && channelCoverage.ChannelItems.Any()) + { + LogManager.Info($"[2.5D路径规划] 使用通道覆盖数据,通道数量: {channelCoverage.ChannelItems.Count}, 车辆高度: {vehicleHeight}m"); + return FindPath2_5D(start, end, gridMap, channelCoverage, vehicleHeight); + } + + // 传统模式路径规划 + LogManager.Info("[传统路径规划] 使用边界框模式"); return FindPathCore(start, end, gridMap); } + /// + /// 2.5D路径规划(使用通道覆盖数据和高度约束) + /// + /// 起点(世界坐标) + /// 终点(世界坐标) + /// 网格地图 + /// 通道覆盖数据 + /// 车辆高度 + /// 路径点列表(世界坐标) + private List FindPath2_5D(Point3D start, Point3D end, GridMap gridMap, ChannelCoverage channelCoverage, double vehicleHeight) + { + try + { + LogManager.Info("[2.5D路径规划] 开始执行"); + + // 1. 检查起点和终点的高度约束 + if (!IsPointPassableAtHeight(start, gridMap, vehicleHeight)) + { + LogManager.Warning("[2.5D路径规划] 起点位置不满足高度约束"); + } + + if (!IsPointPassableAtHeight(end, gridMap, vehicleHeight)) + { + LogManager.Warning("[2.5D路径规划] 终点位置不满足高度约束"); + } + + // 2. 使用通道覆盖数据进行A*路径规划 + var astarGrid = ConvertToAStarGridWith2_5D(gridMap, channelCoverage, vehicleHeight); + + // 3. 执行A*算法 + var path = ExecuteAStarAlgorithm(start, end, gridMap, astarGrid); + + // 4. 对路径进行高度插值,利用2.5D高度区间信息 + if (path != null && path.Any()) + { + var enhancedPath = InterpolatePathHeights2_5D(path, gridMap, vehicleHeight); + LogManager.Info($"[2.5D路径规划] 路径生成完成,包含 {enhancedPath.Count} 个路径点"); + return enhancedPath; + } + + LogManager.Warning("[2.5D路径规划] 未找到有效路径"); + return new List(); + } + catch (Exception ex) + { + LogManager.Error($"[2.5D路径规划] 路径查找失败: {ex.Message}"); + LogManager.Info("[2.5D路径规划] 回退到传统模式"); + return FindPathCore(start, end, gridMap); + } + } + /// /// 查找路径(传统方法,保持向后兼容) /// @@ -156,7 +223,80 @@ namespace NavisworksTransport.PathPlanning } /// - /// 将GridMap转换为RoyT.AStar的Grid格式 + /// 将网格地图转换为A*算法使用的格式(2.5D模式) + /// + /// 网格地图 + /// 通道覆盖数据 + /// 车辆高度 + /// A*算法网格 + private Grid ConvertToAStarGridWith2_5D(GridMap gridMap, ChannelCoverage channelCoverage, double vehicleHeight) + { + try + { + LogManager.Info($"[A*转换-2.5D] 开始转换网格格式: {gridMap.Width}x{gridMap.Height},车辆高度: {vehicleHeight}m"); + + // 单位转换 + double cellSizeInMeters = UnitsConverter.ConvertToMeters(gridMap.CellSize); + LogManager.Info($"[A*转换-2.5D] 单位转换: {gridMap.CellSize:F2}模型单位 -> {cellSizeInMeters:F2}米"); + + // 创建A*网格 + var gridSize = new GridSize(gridMap.Width, gridMap.Height); + var cellSize = new Size(Distance.FromMeters((float)cellSizeInMeters), Distance.FromMeters((float)cellSizeInMeters)); + var traversalVelocity = Velocity.FromKilometersPerHour(5); // 5 km/h 的遍历速度 + + var grid = Grid.CreateGridWithLateralConnections(gridSize, cellSize, traversalVelocity); + LogManager.Info($"[A*转换-2.5D] A*网格创建完成"); + + int blockedCells = 0; + int heightConstrainedCells = 0; + + // 标记不可通行的单元格,考虑高度约束 + for (int x = 0; x < gridMap.Width; x++) + { + for (int y = 0; y < gridMap.Height; y++) + { + var cell = gridMap.Cells[x, y]; + bool cellBlocked = false; + + if (!cell.IsWalkable) + { + // 传统的不可行走单元格 + cellBlocked = true; + blockedCells++; + } + else if (cell.PassableHeights != null && cell.PassableHeights.Any()) + { + // 检查是否有足够的高度空间 + bool hasValidHeight = cell.PassableHeights.Any(interval => + interval.GetSpan() >= vehicleHeight); + + if (!hasValidHeight) + { + cellBlocked = true; + heightConstrainedCells++; + } + } + + if (cellBlocked) + { + var gridPos = new GridPosition(x, y); + grid.DisconnectNode(gridPos); + } + } + } + + LogManager.Info($"[A*转换-2.5D] 网格转换完成,阻塞单元格: {blockedCells}个,高度约束阻塞: {heightConstrainedCells}个"); + return grid; + } + catch (Exception ex) + { + LogManager.Error($"[A*转换-2.5D] 转换网格格式时发生错误: {ex.Message}"); + throw new AutoPathPlanningException($"2.5D网格转换失败: {ex.Message}", ex); + } + } + + /// + /// 将GridMap转换为RoyT.AStar的Grid格式(传统模式) /// /// 网格地图 /// A*算法网格 @@ -686,5 +826,199 @@ namespace NavisworksTransport.PathPlanning } } + /// + /// 检查指定点在给定高度约束下是否可通行 + /// + /// 检查点 + /// 网格地图 + /// 车辆高度 + /// 是否可通行 + private bool IsPointPassableAtHeight(Point3D point, GridMap gridMap, double vehicleHeight) + { + try + { + // 将世界坐标转换为网格坐标 + int gridX = (int)Math.Floor((point.X - gridMap.Bounds.Min.X) / gridMap.CellSize); + int gridY = (int)Math.Floor((point.Y - gridMap.Bounds.Min.Y) / gridMap.CellSize); + + // 确保坐标在有效范围内 + if (gridX < 0 || gridX >= gridMap.Width || gridY < 0 || gridY >= gridMap.Height) + { + return false; + } + + var cell = gridMap.Cells[gridX, gridY]; + + // 检查基本可行走性 + if (!cell.IsWalkable) + { + return false; + } + + // 检查高度约束 + if (cell.PassableHeights != null && cell.PassableHeights.Any()) + { + return cell.PassableHeights.Any(interval => + interval.GetSpan() >= vehicleHeight && + interval.Contains(point.Z)); + } + + // 如果没有高度信息,认为可通行(向后兼容) + return true; + } + catch (Exception ex) + { + LogManager.Error($"[高度约束检查] 检查点可通行性失败: {ex.Message}"); + return false; + } + } + + /// + /// 使用2.5D高度区间信息对路径进行高度插值 + /// + /// 原始路径 + /// 网格地图 + /// 车辆高度 + /// 插值后的路径 + private List InterpolatePathHeights2_5D(List path, GridMap gridMap, double vehicleHeight) + { + try + { + LogManager.Info($"[2.5D高度插值] 开始插值,路径点数: {path.Count}"); + + var interpolatedPath = new List(); + int validHeightPoints = 0; + + for (int i = 0; i < path.Count; i++) + { + var originalPoint = path[i]; + + // 获取最优高度 + var optimalHeight = GetOptimalHeightFromGrid(originalPoint, gridMap, vehicleHeight); + + if (optimalHeight.HasValue) + { + var enhancedPoint = new Point3D(originalPoint.X, originalPoint.Y, optimalHeight.Value); + interpolatedPath.Add(enhancedPoint); + validHeightPoints++; + + if (Math.Abs(originalPoint.Z - optimalHeight.Value) > 0.1) + { + LogManager.Debug($"[2.5D高度插值] 点{i}: 高度优化: {originalPoint.Z:F2} -> {optimalHeight.Value:F2}"); + } + } + else + { + // 如果无法获取高度信息,使用原始点 + interpolatedPath.Add(originalPoint); + } + } + + LogManager.Info($"[2.5D高度插值] 完成,有效高度点: {validHeightPoints}/{path.Count}"); + return interpolatedPath; + } + catch (Exception ex) + { + LogManager.Error($"[2.5D高度插值] 插值失败: {ex.Message}"); + return path; // 返回原始路径 + } + } + + /// + /// 从网格中获取指定位置的最优高度 + /// + /// 世界坐标点 + /// 网格地图 + /// 车辆高度 + /// 最优高度,如果无法确定则返回null + private double? GetOptimalHeightFromGrid(Point3D point, GridMap gridMap, double vehicleHeight) + { + try + { + // 将世界坐标转换为网格坐标 + int gridX = (int)Math.Floor((point.X - gridMap.Bounds.Min.X) / gridMap.CellSize); + int gridY = (int)Math.Floor((point.Y - gridMap.Bounds.Min.Y) / gridMap.CellSize); + + // 确保坐标在有效范围内 + if (gridX < 0 || gridX >= gridMap.Width || gridY < 0 || gridY >= gridMap.Height) + { + return null; + } + + var cell = gridMap.Cells[gridX, gridY]; + + // 如果有高度区间信息 + if (cell.PassableHeights != null && cell.PassableHeights.Any()) + { + // 查找满足车辆高度要求的最佳区间 + var validIntervals = cell.PassableHeights + .Where(interval => interval.GetSpan() >= vehicleHeight) + .OrderBy(interval => interval.MinZ) + .ToList(); + + if (validIntervals.Any()) + { + // 选择最低的有效区间的底部高度 + var bestInterval = validIntervals.First(); + return bestInterval.MinZ; + } + } + + // 如果没有高度信息,返回原始Z坐标 + return point.Z; + } + catch (Exception ex) + { + LogManager.Error($"[最优高度获取] 获取失败: {ex.Message}"); + return point.Z; + } + } + + /// + /// 执行A*算法的核心逻辑 + /// + /// 起点 + /// 终点 + /// 网格地图 + /// A*网格 + /// 路径点列表 + private List ExecuteAStarAlgorithm(Point3D start, Point3D end, GridMap gridMap, Grid astarGrid) + { + try + { + // 转换起点和终点到网格坐标 + var startGrid = gridMap.WorldToGrid(start); + var endGrid = gridMap.WorldToGrid(end); + + LogManager.Info($"[A*执行] 网格坐标转换 - 起点: ({startGrid.X}, {startGrid.Y}), 终点: ({endGrid.X}, {endGrid.Y})"); + + var startPos = new GridPosition(startGrid.X, startGrid.Y); + var endPos = new GridPosition(endGrid.X, endGrid.Y); + + // 执行A*算法 + var pathfinder = new PathFinder(); + var astarPath = pathfinder.FindPath(startPos, endPos, astarGrid); + + if (astarPath != null && astarPath.Edges.Count > 0) + { + LogManager.Info($"[A*执行] 路径找到,包含 {astarPath.Edges.Count + 1} 个网格点"); + + // 转换回世界坐标 + var worldPath = ConvertPathToWorldCoordinates(astarPath, gridMap); + return worldPath; + } + else + { + LogManager.Warning("[A*执行] 未找到路径"); + return new List(); + } + } + catch (Exception ex) + { + LogManager.Error($"[A*执行] A*算法执行失败: {ex.Message}"); + throw new AutoPathPlanningException($"A*算法执行失败: {ex.Message}", ex); + } + } + } } \ No newline at end of file diff --git a/src/PathPlanning/ChannelBasedGridBuilder.cs b/src/PathPlanning/ChannelBasedGridBuilder.cs new file mode 100644 index 0000000..1e223ec --- /dev/null +++ b/src/PathPlanning/ChannelBasedGridBuilder.cs @@ -0,0 +1,727 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Autodesk.Navisworks.Api; +using NavisworksTransport.Core; +using NavisworksTransport.Utils; + +// COM API引用 +using ComBridge = Autodesk.Navisworks.Api.ComApi.ComApiBridge; +using COMApi = Autodesk.Navisworks.Api.Interop.ComApi; + +namespace NavisworksTransport.PathPlanning +{ + /// + /// 基于通道的网格构建器 + /// 实现A*算法改进方案中的通道优先策略和2.5D垂直扫描 + /// + public class ChannelBasedGridBuilder + { + private CategoryAttributeManager _categoryManager; + private ChannelHeightDetector _heightDetector; + + /// + /// 构造函数 + /// + public ChannelBasedGridBuilder() + { + _heightDetector = new ChannelHeightDetector(); + LogManager.Info("[通道网格构建器] 初始化完成"); + } + + /// + /// 构建基于通道的精确覆盖网格 + /// + /// 网格单元格大小 + /// 当前文档 + /// 通道覆盖信息 + public ChannelCoverage BuildChannelCoverage(double gridSize, Document document = null) + { + if (document == null) + document = Application.ActiveDocument; + + LogManager.Info("[通道网格构建器] 开始构建通道覆盖网格"); + + // 1. 获取所有通道物品 + var allItems = document.Models.RootItemDescendantsAndSelf; + var channelItems = GetChannelItems(allItems.ToList()); + + if (!channelItems.Any()) + { + LogManager.Warning("[通道网格构建器] 未找到任何通道物品"); + return CreateEmptyChannelCoverage(); + } + + LogManager.Info($"[通道网格构建器] 找到 {channelItems.Count} 个通道物品"); + + // 2. 计算通道总边界 + var totalBounds = CalculateTotalBounds(channelItems); + LogManager.Info($"[通道网格构建器] 通道总边界: {FormatBounds(totalBounds)}"); + + // 3. 创建网格地图 + var gridMap = new GridMap(totalBounds, gridSize); + + // 4. 为每个通道生成精确投影 + var processedChannels = new List(); + foreach (var channel in channelItems) + { + try + { + ProjectChannelToGrid(channel, gridMap); + processedChannels.Add(channel); + } + catch (Exception ex) + { + LogManager.Error($"[通道网格构建器] 投影通道失败 '{channel.DisplayName}': {ex.Message}"); + } + } + + LogManager.Info($"[通道网格构建器] 成功投影 {processedChannels.Count}/{channelItems.Count} 个通道"); + + return new ChannelCoverage + { + GridMap = gridMap, + ChannelItems = processedChannels, + TotalBounds = totalBounds + }; + } + + /// + /// 获取所有通道物品 + /// + /// 所有模型物品 + /// 通道物品列表 + private List GetChannelItems(List allItems) + { + try + { + // 初始化CategoryAttributeManager(如果尚未初始化) + if (_categoryManager == null) + { + _categoryManager = new CategoryAttributeManager(); + } + + // 优先使用CategoryAttributeManager获取标记为"通道"的物品 + var document = Application.ActiveDocument; + if (document?.Models != null) + { + var allItemsCollection = new ModelItemCollection(); + foreach (ModelItem item in document.Models.RootItemDescendantsAndSelf) + { + allItemsCollection.Add(item); + } + var channelItems = CategoryAttributeManager.FilterByLogisticsType(allItemsCollection, CategoryAttributeManager.LogisticsElementType.通道).ToList(); + + if (channelItems.Any()) + { + LogManager.Info($"[通道网格构建器] 通过CategoryAttributeManager找到 {channelItems.Count} 个通道物品"); + return channelItems; + } + } + + // 如果没有用户标记的通道,回退到自动检测 + LogManager.Warning("[通道网格构建器] 未找到用户标记的通道物品,使用自动检测"); + var autoDetectedChannels = new List(); + + foreach (ModelItem item in allItems) + { + if (IsChannelItem(item)) + { + autoDetectedChannels.Add(item); + } + } + + LogManager.Info($"[通道网格构建器] 自动检测到 {autoDetectedChannels.Count} 个通道物品"); + return autoDetectedChannels; + } + catch (Exception ex) + { + LogManager.Error($"[通道网格构建器] 获取通道物品时发生错误: {ex.Message}"); + return new List(); + } + } + + /// + /// 判断物品是否为通道 + /// + /// 模型物品 + /// 是否为通道 + private bool IsChannelItem(ModelItem item) + { + try + { + // 优先使用物流属性判断 + if (CategoryAttributeManager.HasLogisticsAttributes(item)) + { + var elementType = CategoryAttributeManager.GetLogisticsElementType(item); + return elementType == CategoryAttributeManager.LogisticsElementType.通道; + } + + // 备用:通过AutoPathObstacleDetector判断是否可通行 + return AutoPathObstacleDetector.IsPassable(item); + } + catch (Exception ex) + { + LogManager.Warning($"[通道网格构建器] 判断通道时发生错误 '{item.DisplayName}': {ex.Message}"); + return false; + } + } + + /// + /// 将通道投影到网格 + /// 核心创新:智能投影策略 - 薄通道优先使用顶面提取,复杂通道使用射线扫描 + /// + /// 通道物品 + /// 目标网格地图 + private void ProjectChannelToGrid(ModelItem channel, GridMap gridMap) + { + var bbox = channel.BoundingBox(); + var thickness = bbox.Max.Z - bbox.Min.Z; + + LogManager.Info($"[通道网格构建器] 投影通道: {channel.DisplayName}, 厚度: {thickness:F2}m"); + + // 智能投影策略选择 + if (thickness < 1.5) // 薄通道优先使用顶面提取 + { + var triangles = ExtractTrianglesFromModelItem(channel); + var topFaceCount = CountTopFaces(triangles); + var topFaceRatio = triangles.Count > 0 ? (double)topFaceCount / triangles.Count : 0; + + // 如果有足够的水平顶面(>10%),使用顶面投影 + if (topFaceRatio > 0.1) + { + ProjectUsingTopFaces(channel, gridMap, triangles); + LogManager.Info($"[通道网格构建器] 使用顶面提取投影(顶面比例: {topFaceRatio:P1})"); + return; + } + } + + // 对于复杂形状或没有明显顶面的通道,使用射线扫描 + ProjectUsingRayCasting(channel, gridMap); + LogManager.Info($"[通道网格构建器] 使用垂直射线扫描(精确投影)"); + } + + /// + /// 方法1:顶面提取投影(快速且精确) + /// + /// 模型物品 + /// 网格地图 + /// 三角形列表(可选,避免重复提取) + private void ProjectUsingTopFaces(ModelItem item, GridMap gridMap, List triangles = null) + { + if (triangles == null) + { + triangles = ExtractTrianglesFromModelItem(item); + } + + int projectedTriangles = 0; + foreach (var triangle in triangles) + { + // 计算法向量 + var normal = ComputeTriangleNormal(triangle); + + // 只投影朝上的面(法向量Z分量 > 0.7,约45度) + if (normal.Z > 0.7) + { + // 光栅化三角形到网格 + RasterizeTriangleToGrid(gridMap, triangle); + projectedTriangles++; + } + } + + LogManager.Info($"[通道网格构建器] 顶面投影:{projectedTriangles}/{triangles.Count} 个三角形"); + } + + /// + /// 方法2:垂直射线扫描(最精确) + /// + /// 模型物品 + /// 网格地图 + private void ProjectUsingRayCasting(ModelItem item, GridMap gridMap) + { + var triangles = ExtractTrianglesFromModelItem(item); + if (triangles.Count == 0) + { + LogManager.Warning($"[通道网格构建器] 未能从 '{item.DisplayName}' 提取到三角形"); + return; + } + + // 使用空间哈希加速(稍后实现TriangleSpatialHash时启用) + // var spatialHash = new TriangleSpatialHash(); + // spatialHash.Build(triangles, gridMap.CellSize); + + var bbox = item.BoundingBox(); + var minGrid = gridMap.WorldToGrid(new Point3D(bbox.Min.X, bbox.Min.Y, bbox.Min.Z)); + var maxGrid = gridMap.WorldToGrid(new Point3D(bbox.Max.X, bbox.Max.Y, bbox.Max.Z)); + + int hitCount = 0; + + // 并行扫描网格点 + var gridPoints = new List(); + for (int x = minGrid.X; x <= maxGrid.X; x++) + { + for (int y = minGrid.Y; y <= maxGrid.Y; y++) + { + if (gridMap.IsValidGridPosition(new Point2D(x, y))) + { + gridPoints.Add(new Point2D(x, y)); + } + } + } + + foreach (var gridPos in gridPoints) + { + var worldPos = gridMap.GridToWorld(gridPos); + var rayOrigin = new Point3D(worldPos.X, worldPos.Y, bbox.Max.Z + 1); + var rayDirection = new Point3D(0, 0, -1); + + // 检查射线是否击中任何三角形 + foreach (var triangle in triangles) + { + if (RayTriangleIntersect(rayOrigin, rayDirection, triangle, out double intersectionZ)) + { + if (intersectionZ >= bbox.Min.Z && intersectionZ <= bbox.Max.Z) + { + SetCellAsChannel(gridMap, gridPos, item); + hitCount++; + break; // 找到一个交点即可 + } + } + } + } + + LogManager.Info($"[通道网格构建器] 射线投影:{hitCount} 个网格单元"); + } + + /// + /// 提取模型物品的三角形(复用ChannelHeightDetector的现有功能) + /// + /// 模型物品 + /// 三角形列表 + private List ExtractTrianglesFromModelItem(ModelItem modelItem) + { + try + { + LogManager.Info($"[通道网格构建器] 提取三角形: {modelItem.DisplayName}"); + + // 直接调用已有的三角形提取功能,复用ChannelHeightDetector的代码逻辑 + var allTriangles = new List(); + + // 创建几何提取回调 + var callback = new GeometryExtractorCallback(); + + // 递归处理模型项及其子项 + ExtractTrianglesRecursive(modelItem, callback, allTriangles); + + LogManager.Info($"[通道网格构建器] 从ModelItem {modelItem.DisplayName} 总共提取到 {allTriangles.Count} 个三角形"); + return allTriangles; + } + catch (Exception ex) + { + LogManager.Error($"[通道网格构建器] 提取三角形失败: {ex.Message}"); + return new List(); + } + } + + /// + /// 递归提取ModelItem及其子项的几何数据 + /// + private void ExtractTrianglesRecursive(ModelItem modelItem, GeometryExtractorCallback callback, List allTriangles) + { + try + { + // 如果当前项有几何数据,提取它 + if (modelItem.HasGeometry) + { + ExtractTrianglesFromGeometry(modelItem, callback, allTriangles); + } + + // 递归处理子项 + foreach (ModelItem child in modelItem.Children) + { + ExtractTrianglesRecursive(child, callback, allTriangles); + } + } + catch (Exception ex) + { + LogManager.Warning($"[通道网格构建器] 处理ModelItem {modelItem.DisplayName} 时出错: {ex.Message}"); + } + } + + /// + /// 从单个有几何数据的ModelItem中提取三角形 + /// + private void ExtractTrianglesFromGeometry(ModelItem modelItem, GeometryExtractorCallback callback, List allTriangles) + { + try + { + // 基于ChannelHeightDetector的方法:直接从COM状态获取节点并遍历fragments + var comState = ComBridge.State; + var comObject = ComBridge.ToInwOaPath(modelItem); + + if (comObject == null) + { + return; + } + + // 检查路径是否有效并获取节点 + var nodesColl = comObject.Nodes(); + if (nodesColl.Count > 0) + { + var comNode = nodesColl[nodesColl.Count] as COMApi.InwOaNode; // 获取最后一个节点并转换类型 + if (comNode != null && comNode.IsGeometry) + { + var fragCount = comNode.Fragments().Count; + + for (long fragIndex = 1; fragIndex <= fragCount; fragIndex++) + { + var fragsColl = comNode.Fragments(); + var fragment = fragsColl[fragIndex] as COMApi.InwOaFragment3; + + if (fragment != null) + { + // 获取变换矩阵 + var transformMatrix = GetTransformMatrix(fragment); + callback.SetTransformMatrix(transformMatrix); + + // 清空回调中的三角形 + callback.Clear(); + + // 生成几何图元 - 使用正确的枚举值 + fragment.GenerateSimplePrimitives(COMApi.nwEVertexProperty.eNORMAL, callback); + + // 将提取的三角形添加到总列表 + allTriangles.AddRange(callback.Triangles); + } + } + } + } + } + catch (Exception ex) + { + LogManager.Warning($"[通道网格构建器] 从 {modelItem.DisplayName} 提取几何时出错: {ex.Message}"); + } + } + + /// + /// 获取Fragment的变换矩阵 + /// + private Matrix4 GetTransformMatrix(COMApi.InwOaFragment3 fragment) + { + try + { + var transform = fragment.GetLocalToWorldMatrix(); + if (transform != null) + { + object matrixArrayObj = transform.Matrix; + if (matrixArrayObj is Array matrixArray && matrixArray.Length >= 16) + { + var matrix = new double[16]; + matrixArray.CopyTo(matrix, 0); + + // Navisworks使用列主序矩阵,需要转置 + return new Matrix4( + matrix[0], matrix[4], matrix[8], matrix[12], + matrix[1], matrix[5], matrix[9], matrix[13], + matrix[2], matrix[6], matrix[10], matrix[14], + matrix[3], matrix[7], matrix[11], matrix[15] + ); + } + } + } + catch (Exception ex) + { + LogManager.Warning($"[通道网格构建器] 获取变换矩阵失败: {ex.Message}"); + } + + return Matrix4.Identity; // 使用静态属性创建单位矩阵 + } + + /// + /// 射线-三角形相交测试(复用ChannelHeightDetector的现有功能) + /// + /// 射线起点 + /// 射线方向 + /// 三角形 + /// 交点Z坐标 + /// 是否相交 + private bool RayTriangleIntersect(Point3D rayOrigin, Point3D rayDirection, Triangle3D triangle, out double intersectionZ) + { + intersectionZ = 0.0; + const double EPSILON = 0.0000001; + + try + { + // 使用Möller-Trumbore算法进行射线-三角形相交测试 + // 计算三角形的两条边 + var edge1 = SubtractPoints(triangle.V2, triangle.V1); + var edge2 = SubtractPoints(triangle.V3, triangle.V1); + + // 计算射线方向与edge2的叉积 + var h = CrossProduct(rayDirection, edge2); + var a = DotProduct(edge1, h); + + // 如果a接近0,射线与三角形平行 + if (a > -EPSILON && a < EPSILON) + { + return false; + } + + var f = 1.0 / a; + var s = SubtractPoints(rayOrigin, triangle.V1); + var u = f * DotProduct(s, h); + + if (u < 0.0 || u > 1.0) + { + return false; + } + + var q = CrossProduct(s, edge1); + var v = f * DotProduct(rayDirection, q); + + if (v < 0.0 || u + v > 1.0) + { + return false; + } + + // 计算t值(射线参数) + var t = f * DotProduct(edge2, q); + + if (t > EPSILON) // 射线相交 + { + // 计算交点 + intersectionZ = rayOrigin.Z + t * rayDirection.Z; + return true; + } + + return false; // 线段相交但射线不相交 + } + catch (Exception ex) + { + LogManager.Warning($"[通道网格构建器] 射线-三角形交点计算出错: {ex.Message}"); + return false; + } + } + + /// + /// 计算两点之间的向量 + /// + private Point3D SubtractPoints(Point3D a, Point3D b) + { + return new Point3D(a.X - b.X, a.Y - b.Y, a.Z - b.Z); + } + + /// + /// 计算两个向量的叉积 + /// + private Point3D CrossProduct(Point3D a, Point3D b) + { + return new Point3D( + a.Y * b.Z - a.Z * b.Y, + a.Z * b.X - a.X * b.Z, + a.X * b.Y - a.Y * b.X + ); + } + + /// + /// 计算两个向量的点积 + /// + private double DotProduct(Point3D a, Point3D b) + { + return a.X * b.X + a.Y * b.Y + a.Z * b.Z; + } + + /// + /// 计算三角形法向量 + /// + /// 三角形 + /// 法向量 + private Point3D ComputeTriangleNormal(Triangle3D triangle) + { + var edge1 = new Point3D( + triangle.V2.X - triangle.V1.X, + triangle.V2.Y - triangle.V1.Y, + triangle.V2.Z - triangle.V1.Z); + + var edge2 = new Point3D( + triangle.V3.X - triangle.V1.X, + triangle.V3.Y - triangle.V1.Y, + triangle.V3.Z - triangle.V1.Z); + + // 计算叉积 + var normal = new Point3D( + edge1.Y * edge2.Z - edge1.Z * edge2.Y, + edge1.Z * edge2.X - edge1.X * edge2.Z, + edge1.X * edge2.Y - edge1.Y * edge2.X); + + // 归一化 + double length = Math.Sqrt(normal.X * normal.X + normal.Y * normal.Y + normal.Z * normal.Z); + if (length > 0.0001) + { + normal = new Point3D(normal.X / length, normal.Y / length, normal.Z / length); + } + + return normal; + } + + /// + /// 统计顶面数量 + /// + /// 三角形列表 + /// 顶面数量 + private int CountTopFaces(List triangles) + { + int count = 0; + foreach (var triangle in triangles) + { + var normal = ComputeTriangleNormal(triangle); + if (normal.Z > 0.7) // 朝上的面 + { + count++; + } + } + return count; + } + + /// + /// 将三角形光栅化到网格 + /// + /// 网格地图 + /// 三角形 + private void RasterizeTriangleToGrid(GridMap gridMap, Triangle3D triangle) + { + // 获取三角形的2D投影边界框 + var minX = Math.Min(triangle.V1.X, Math.Min(triangle.V2.X, triangle.V3.X)); + var maxX = Math.Max(triangle.V1.X, Math.Max(triangle.V2.X, triangle.V3.X)); + var minY = Math.Min(triangle.V1.Y, Math.Min(triangle.V2.Y, triangle.V3.Y)); + var maxY = Math.Max(triangle.V1.Y, Math.Max(triangle.V2.Y, triangle.V3.Y)); + + var minGrid = gridMap.WorldToGrid(new Point3D(minX, minY, 0)); + var maxGrid = gridMap.WorldToGrid(new Point3D(maxX, maxY, 0)); + + // 遍历边界框内的所有网格点 + for (int x = minGrid.X; x <= maxGrid.X; x++) + { + for (int y = minGrid.Y; y <= maxGrid.Y; y++) + { + var gridPos = new Point2D(x, y); + if (gridMap.IsValidGridPosition(gridPos)) + { + var worldPos = gridMap.GridToWorld(gridPos); + + // 检查点是否在三角形内部 + if (IsPointInTriangle2D(worldPos, triangle)) + { + SetCellAsChannel(gridMap, gridPos, null); + } + } + } + } + } + + /// + /// 检查2D点是否在三角形内部 + /// + /// 测试点 + /// 三角形 + /// 是否在内部 + private bool IsPointInTriangle2D(Point3D point, Triangle3D triangle) + { + // 使用重心坐标法判断点是否在三角形内 + var v0x = triangle.V3.X - triangle.V1.X; + var v0y = triangle.V3.Y - triangle.V1.Y; + var v1x = triangle.V2.X - triangle.V1.X; + var v1y = triangle.V2.Y - triangle.V1.Y; + var v2x = point.X - triangle.V1.X; + var v2y = point.Y - triangle.V1.Y; + + var dot00 = v0x * v0x + v0y * v0y; + var dot01 = v0x * v1x + v0y * v1y; + var dot02 = v0x * v2x + v0y * v2y; + var dot11 = v1x * v1x + v1y * v1y; + var dot12 = v1x * v2x + v1y * v2y; + + var invDenom = 1 / (dot00 * dot11 - dot01 * dot01); + var u = (dot11 * dot02 - dot01 * dot12) * invDenom; + var v = (dot00 * dot12 - dot01 * dot02) * invDenom; + + return (u >= 0) && (v >= 0) && (u + v <= 1); + } + + /// + /// 设置网格单元为通道类型 + /// + /// 网格地图 + /// 网格位置 + /// 关联的模型物品 + private void SetCellAsChannel(GridMap gridMap, Point2D gridPos, ModelItem relatedItem) + { + // 使用GridMap提供的方法设置通道单元格 + gridMap.SetCellAsChannel(gridPos, ChannelType.Corridor, relatedItem); + } + + /// + /// 计算通道总边界 + /// + /// 通道物品列表 + /// 总边界框 + private BoundingBox3D CalculateTotalBounds(List channelItems) + { + if (!channelItems.Any()) + { + return new BoundingBox3D(new Point3D(0, 0, 0), new Point3D(0, 0, 0)); + } + + var firstBounds = channelItems.First().BoundingBox(); + double minX = firstBounds.Min.X; + double minY = firstBounds.Min.Y; + double minZ = firstBounds.Min.Z; + double maxX = firstBounds.Max.X; + double maxY = firstBounds.Max.Y; + double maxZ = firstBounds.Max.Z; + + foreach (var item in channelItems.Skip(1)) + { + var bounds = item.BoundingBox(); + minX = Math.Min(minX, bounds.Min.X); + minY = Math.Min(minY, bounds.Min.Y); + minZ = Math.Min(minZ, bounds.Min.Z); + maxX = Math.Max(maxX, bounds.Max.X); + maxY = Math.Max(maxY, bounds.Max.Y); + maxZ = Math.Max(maxZ, bounds.Max.Z); + } + + return new BoundingBox3D( + new Point3D(minX, minY, minZ), + new Point3D(maxX, maxY, maxZ)); + } + + /// + /// 创建空的通道覆盖 + /// + /// 空的通道覆盖 + private ChannelCoverage CreateEmptyChannelCoverage() + { + var emptyBounds = new BoundingBox3D(new Point3D(0, 0, 0), new Point3D(0, 0, 0)); + return new ChannelCoverage + { + GridMap = new GridMap(emptyBounds, 1.0), + ChannelItems = new List(), + TotalBounds = emptyBounds + }; + } + + /// + /// 格式化边界框信息用于日志 + /// + /// 边界框 + /// 格式化字符串 + private string FormatBounds(BoundingBox3D bounds) + { + return $"[{bounds.Min.X:F1},{bounds.Min.Y:F1},{bounds.Min.Z:F1}] - [{bounds.Max.X:F1},{bounds.Max.Y:F1},{bounds.Max.Z:F1}]"; + } + } + + +} \ No newline at end of file diff --git a/src/PathPlanning/GridMap.cs b/src/PathPlanning/GridMap.cs index 7c82177..aec0ab2 100644 --- a/src/PathPlanning/GridMap.cs +++ b/src/PathPlanning/GridMap.cs @@ -150,11 +150,18 @@ namespace NavisworksTransport.PathPlanning { for (int y = 0; y < Height; y++) { + // 计算世界坐标位置 + var worldPos = GridToWorld(new Point2D(x, y)); + Cells[x, y] = new GridCell { - IsWalkable = true, - Cost = 1.0, - CellType = ElementType.OpenSpace + IsWalkable = false, + Cost = double.MaxValue, + CellType = ElementType.Obstacle, + IsInChannel = false, + PassableHeights = new List(), + ChannelType = ChannelType.Other, + WorldPosition = worldPos }; } } @@ -271,14 +278,109 @@ namespace NavisworksTransport.PathPlanning if (!IsValidGridPosition(gridPosition)) return; + var worldPos = GridToWorld(gridPosition); Cells[gridPosition.X, gridPosition.Y] = new GridCell { IsWalkable = isWalkable, Cost = cost, - CellType = cellType + CellType = cellType, + IsInChannel = cellType == ElementType.Channel, + PassableHeights = new List(), + ChannelType = cellType == ElementType.Channel ? ChannelType.Corridor : ChannelType.Other, + WorldPosition = worldPos }; } + /// + /// 设置网格单元为通道类型 - A*改进方案新增方法 + /// + /// 网格坐标 + /// 通道类型 + /// 关联的模型物品 + public void SetCellAsChannel(Point2D gridPosition, ChannelType channelType = ChannelType.Corridor, ModelItem relatedItem = null) + { + if (!IsValidGridPosition(gridPosition)) + return; + + var worldPos = GridToWorld(gridPosition); + Cells[gridPosition.X, gridPosition.Y] = new GridCell + { + IsWalkable = true, + Cost = 0.5, // 通道成本较低 + CellType = ElementType.Channel, + IsInChannel = true, + PassableHeights = new List(), + ChannelType = channelType, + WorldPosition = worldPos, + RelatedModelItem = relatedItem + }; + } + + /// + /// 添加可通行高度区间到指定网格单元 + /// + /// 网格坐标 + /// 高度区间 + public void AddPassableHeight(Point2D gridPosition, HeightInterval heightInterval) + { + if (!IsValidGridPosition(gridPosition)) + return; + + var cell = Cells[gridPosition.X, gridPosition.Y]; + if (cell.PassableHeights == null) + { + cell.PassableHeights = new List(); + } + cell.PassableHeights.Add(heightInterval); + Cells[gridPosition.X, gridPosition.Y] = cell; + } + + /// + /// 检查指定位置在指定高度是否可通行 + /// + /// 网格坐标 + /// 检查的高度 + /// 是否可通行 + public bool IsPassableAtHeight(Point2D gridPosition, double height) + { + if (!IsValidGridPosition(gridPosition)) + return false; + + var cell = Cells[gridPosition.X, gridPosition.Y]; + if (!cell.IsWalkable) + return false; + + // 如果没有设置高度区间,使用传统的可通行性判断 + if (cell.PassableHeights == null || !cell.PassableHeights.Any()) + return true; + + // 检查是否有任何高度区间包含指定高度 + return cell.PassableHeights.Any(interval => interval.Contains(height)); + } + + /// + /// 获取所有通道类型的网格单元 + /// + /// 通道网格单元列表 + public List<(Point2D Position, GridCell Cell)> GetChannelCells() + { + var channelCells = new List<(Point2D Position, GridCell Cell)>(); + + for (int x = 0; x < Width; x++) + { + for (int y = 0; y < Height; y++) + { + var cell = Cells[x, y]; + if (cell.IsInChannel || cell.CellType == ElementType.Channel) + { + channelCells.Add((new Point2D(x, y), cell)); + } + } + } + + return channelCells; + } + /// /// 设置通道模型项集合(用于精确高度计算) /// @@ -445,6 +547,26 @@ namespace NavisworksTransport.PathPlanning /// public ModelItem RelatedModelItem { get; set; } + /// + /// 是否在通道内 - A*改进方案新增字段 + /// + public bool IsInChannel { get; set; } + + /// + /// 可通行高度区间列表 - 支持2.5D路径规划 + /// + public List PassableHeights { get; set; } + + /// + /// 通道类型 - 用于区分不同类型的通道 + /// + public ChannelType ChannelType { get; set; } + + /// + /// 世界坐标位置 - 用于高度计算和空间查询 + /// + public Point3D WorldPosition { get; set; } + /// /// 创建障碍物单元格 /// @@ -455,7 +577,10 @@ namespace NavisworksTransport.PathPlanning { IsWalkable = false, Cost = double.MaxValue, - CellType = ElementType.Obstacle + CellType = ElementType.Obstacle, + IsInChannel = false, + PassableHeights = new List(), + ChannelType = ChannelType.Other }; } @@ -469,7 +594,10 @@ namespace NavisworksTransport.PathPlanning { IsWalkable = true, Cost = 1.0, - CellType = ElementType.OpenSpace + CellType = ElementType.OpenSpace, + IsInChannel = false, + PassableHeights = new List(), + ChannelType = ChannelType.Other }; } @@ -484,21 +612,28 @@ namespace NavisworksTransport.PathPlanning { IsWalkable = isOpen, Cost = isOpen ? 5.0 : double.MaxValue, // 门的通过成本较高 - CellType = ElementType.Door + CellType = ElementType.Door, + IsInChannel = isOpen, + PassableHeights = new List(), + ChannelType = ChannelType.Other }; } /// /// 创建通道单元格 /// + /// 通道类型 /// 通道单元格 - public static GridCell CreateChannel() + public static GridCell CreateChannel(ChannelType channelType = ChannelType.Corridor) { return new GridCell { IsWalkable = true, Cost = 0.5, // 通道成本较低,优先选择 - CellType = ElementType.Channel + CellType = ElementType.Channel, + IsInChannel = true, + PassableHeights = new List(), + ChannelType = channelType }; } } @@ -530,6 +665,129 @@ namespace NavisworksTransport.PathPlanning Channel = 3 } + /// + /// 高度区间结构体 - 用于2.5D路径规划 + /// 表示某个网格位置的可通行高度范围 + /// + public struct HeightInterval + { + /// + /// 最小高度(米) + /// + public double MinZ { get; set; } + + /// + /// 最大高度(米) + /// + public double MaxZ { get; set; } + + /// + /// 构造函数 + /// + /// 最小高度 + /// 最大高度 + public HeightInterval(double minZ, double maxZ) + { + MinZ = minZ; + MaxZ = maxZ; + } + + /// + /// 检查指定高度是否在此区间内 + /// + /// 要检查的高度 + /// 是否在区间内 + public bool Contains(double height) + { + return height >= MinZ && height <= MaxZ; + } + + /// + /// 检查指定高度范围是否与此区间有重叠 + /// + /// 要检查的最小高度 + /// 要检查的最大高度 + /// 是否有重叠 + public bool Overlaps(double minHeight, double maxHeight) + { + return maxHeight >= MinZ && minHeight <= MaxZ; + } + + /// + /// 获取区间的高度跨度 + /// + /// 高度跨度 + public double GetSpan() + { + return MaxZ - MinZ; + } + + /// + /// 获取区间的中心高度 + /// + /// 中心高度 + public double GetCenter() + { + return (MinZ + MaxZ) / 2.0; + } + + /// + /// 重写ToString方法 + /// + /// 字符串表示 + public override string ToString() + { + return $"[{MinZ:F2}m - {MaxZ:F2}m]"; + } + } + + /// + /// 通道覆盖信息 + /// 存储通道构建器生成的网格地图和相关数据 + /// + public class ChannelCoverage + { + /// + /// 网格地图 + /// + public GridMap GridMap { get; set; } + + /// + /// 通道物品列表 + /// + public List ChannelItems { get; set; } = new List(); + + /// + /// 总边界框 + /// + public BoundingBox3D TotalBounds { get; set; } + + /// + /// 获取统计信息 + /// + /// 统计信息字符串 + public string GetStatistics() + { + var channelCellCount = 0; + if (GridMap?.Cells != null) + { + for (int x = 0; x < GridMap.Width; x++) + { + for (int y = 0; y < GridMap.Height; y++) + { + if (GridMap.Cells[x, y].CellType == ElementType.Channel) + { + channelCellCount++; + } + } + } + } + + return $"通道覆盖统计: {ChannelItems.Count} 个通道物品, {channelCellCount} 个通道网格单元, " + + $"网格大小: {GridMap?.Width}x{GridMap?.Height}"; + } + } + /// /// 自动路径规划异常 /// diff --git a/src/PathPlanning/GridMapGenerator.cs b/src/PathPlanning/GridMapGenerator.cs index 9725736..32830a2 100644 --- a/src/PathPlanning/GridMapGenerator.cs +++ b/src/PathPlanning/GridMapGenerator.cs @@ -7,13 +7,32 @@ using NavisworksTransport.Core; namespace NavisworksTransport.PathPlanning { + /// + /// 网格生成模式 + /// + public enum GridGenerationMode + { + /// + /// 边界框模式 - 传统的基于整个边界框的网格生成 + /// + BoundingBox, + + /// + /// 通道基础2.5D模式 - 使用通道优先策略和垂直扫描的高性能模式 + /// + ChannelBased2_5D + } + /// /// 网格地图生成器 /// 负责将BIM模型转换为路径规划可用的网格地图 + /// 支持传统边界框模式和新的通道基础2.5D模式 /// public class GridMapGenerator { private readonly CategoryAttributeManager _categoryManager; + private readonly ChannelBasedGridBuilder _channelBuilder; + private readonly VerticalScanProcessor _verticalScanner; /// /// 构造函数 @@ -21,6 +40,8 @@ namespace NavisworksTransport.PathPlanning public GridMapGenerator() { _categoryManager = new CategoryAttributeManager(); + _channelBuilder = new ChannelBasedGridBuilder(); + _verticalScanner = new VerticalScanProcessor(); } /// @@ -33,8 +54,37 @@ namespace NavisworksTransport.PathPlanning /// 安全间隙(额外的膨胀间距,米) /// 规划起点(用于确定楼层) /// 规划终点(用于确定楼层) + /// 网格生成模式,默认为边界框模式以保持向后兼容 + /// 车辆高度(用于垂直通行检查) /// 生成的网格地图 - public GridMap GenerateFromBIM(Document document, BoundingBox3D bounds, double cellSize = 2.0, double vehicleRadius = 1.0, double safetyMargin = 0.5, Point3D planningStartPoint = default(Point3D), Point3D planningEndPoint = default(Point3D)) + public GridMap GenerateFromBIM(Document document, BoundingBox3D bounds, double cellSize = 2.0, double vehicleRadius = 1.0, double safetyMargin = 0.5, Point3D planningStartPoint = default(Point3D), Point3D planningEndPoint = default(Point3D), GridGenerationMode mode = GridGenerationMode.BoundingBox, double vehicleHeight = 2.0) + { + try + { + LogManager.Info($"[网格生成器] 使用模式: {mode}"); + + // 根据模式选择不同的生成策略 + switch (mode) + { + case GridGenerationMode.ChannelBased2_5D: + return GenerateChannelBased2_5D(document, bounds, cellSize, vehicleRadius, safetyMargin, planningStartPoint, planningEndPoint, vehicleHeight); + + case GridGenerationMode.BoundingBox: + default: + return GenerateBoundingBoxMode(document, bounds, cellSize, vehicleRadius, safetyMargin, planningStartPoint, planningEndPoint); + } + } + catch (Exception ex) + { + LogManager.Error($"生成网格地图时发生错误: {ex.Message}"); + throw new AutoPathPlanningException($"网格地图生成失败: {ex.Message}", ex); + } + } + + /// + /// 传统边界框模式的网格生成(原GenerateFromBIM方法内容) + /// + private GridMap GenerateBoundingBoxMode(Document document, BoundingBox3D bounds, double cellSize, double vehicleRadius, double safetyMargin, Point3D planningStartPoint, Point3D planningEndPoint) { try { @@ -145,6 +195,261 @@ namespace NavisworksTransport.PathPlanning } } + /// + /// 通道基础2.5D模式的网格生成(高性能模式) + /// 使用通道优先策略和垂直扫描处理器 + /// + private GridMap GenerateChannelBased2_5D(Document document, BoundingBox3D bounds, double cellSize, double vehicleRadius, double safetyMargin, Point3D planningStartPoint, Point3D planningEndPoint, double vehicleHeight) + { + try + { + LogManager.Info("[通道2.5D模式] 开始生成网格地图"); + + // 1. 使用通道构建器构建基础通道覆盖网格 + LogManager.Info("[通道2.5D模式] 步骤1: 构建通道覆盖网格"); + var channelCoverage = _channelBuilder.BuildChannelCoverage(cellSize, document); + + if (channelCoverage?.GridMap == null) + { + LogManager.Warning("[通道2.5D模式] 通道构建失败,回退到边界框模式"); + return GenerateBoundingBoxMode(document, bounds, cellSize, vehicleRadius, safetyMargin, planningStartPoint, planningEndPoint); + } + + LogManager.Info($"[通道2.5D模式] 通道覆盖构建完成: {channelCoverage.GetStatistics()}"); + LogManager.Info($"[阶段1完成] 通道覆盖网格统计: {channelCoverage.GridMap.GetStatistics()}"); + + // 2. 构建垂直扫描处理器的空间索引 + LogManager.Info("[通道2.5D模式] 步骤2: 构建空间哈希索引"); + var allItems = document.Models.RootItemDescendantsAndSelf; + _verticalScanner.BuildSpatialHashIndex(allItems, bounds, channelCoverage.ChannelItems); + + // 3. 生成网格点坐标用于垂直扫描 + LogManager.Info("[通道2.5D模式] 步骤3: 生成网格扫描点"); + var gridPoints = GenerateGridPoints(channelCoverage.GridMap); + LogManager.Info($"[通道2.5D模式] 生成 {gridPoints.Count} 个扫描点"); + + // 4. 并行执行垂直扫描,计算2.5D高度区间 + LogManager.Info("[通道2.5D模式] 步骤4: 执行并行垂直扫描"); + double scanHeight = vehicleHeight + safetyMargin; // 扫描高度 = 车辆高度 + 安全间隙 + var heightIntervals = _verticalScanner.ParallelScanHeightIntervals( + gridPoints, scanHeight, vehicleHeight); + + LogManager.Info($"[阶段2完成] 垂直扫描结果: 扫描了{gridPoints.Count}个通道点,生成了{heightIntervals.Count}个高度区间数据"); + + // 5. 将高度区间信息集成到网格地图中 + LogManager.Info("[通道2.5D模式] 步骤5: 集成高度信息到网格"); + + // 添加集成前的验证日志 + var preIntegrationStats = channelCoverage.GridMap.GetStatistics(); + LogManager.Info($"[验证] 高度信息集成前网格统计: {preIntegrationStats}"); + + IntegrateHeightIntervalsToGrid(channelCoverage.GridMap, heightIntervals); + + // 添加集成后的验证日志 + var postIntegrationStats = channelCoverage.GridMap.GetStatistics(); + LogManager.Info($"[阶段3完成] 高度信息集成后网格统计: {postIntegrationStats}"); + + // 验证网格统计一致性 + ValidateGridStatisticsConsistency(channelCoverage.GridMap, "高度信息集成后"); + + // 6. 应用车辆尺寸膨胀(如果需要) + if (vehicleRadius > 0 || safetyMargin > 0) + { + LogManager.Info("[通道2.5D模式] 步骤6: 应用车辆膨胀"); + double metersToModelUnits = GetMetersToModelUnitsConversionFactor(); + double totalInflationRadius = (vehicleRadius + safetyMargin) * metersToModelUnits; + ApplyVehicleInflation(channelCoverage.GridMap, totalInflationRadius); + LogManager.Info($"[通道2.5D模式] 车辆膨胀完成,膨胀半径: {totalInflationRadius:F2}模型单位"); + LogManager.Info($"[阶段4完成] 车辆膨胀后最终网格统计: {channelCoverage.GridMap.GetStatistics()}"); + } + + // 7. 设置规划起点和终点信息 + if (planningStartPoint != default(Point3D) && planningEndPoint != default(Point3D)) + { + channelCoverage.GridMap.PlanningStartPoint = planningStartPoint; + channelCoverage.GridMap.PlanningEndPoint = planningEndPoint; + channelCoverage.GridMap.HasPlanningPoints = true; + } + + LogManager.Info($"[通道2.5D模式] 网格地图生成完成: {channelCoverage.GridMap.GetStatistics()}"); + return channelCoverage.GridMap; + } + catch (Exception ex) + { + LogManager.Error($"[通道2.5D模式] 生成失败: {ex.Message}"); + LogManager.Warning("[通道2.5D模式] 回退到边界框模式"); + return GenerateBoundingBoxMode(document, bounds, cellSize, vehicleRadius, safetyMargin, planningStartPoint, planningEndPoint); + } + } + + /// + /// 生成网格点坐标用于垂直扫描 + /// + /// 网格地图 + /// 网格点坐标列表 + private List GenerateGridPoints(GridMap gridMap) + { + var points = new List(); + + // 只为通道网格生成扫描点,不扫描整个网格 + for (int x = 0; x < gridMap.Width; x++) + { + for (int y = 0; y < gridMap.Height; y++) + { + var cell = gridMap.Cells[x, y]; + + // 只为通道类型的网格生成扫描点 + if (cell.CellType == ElementType.Channel && cell.IsInChannel) + { + // 计算网格单元格的世界坐标 + double worldX = gridMap.Bounds.Min.X + (x + 0.5) * gridMap.CellSize; + double worldY = gridMap.Bounds.Min.Y + (y + 0.5) * gridMap.CellSize; + double worldZ = gridMap.Bounds.Min.Z; // 从地面开始扫描 + + points.Add(new Point3D(worldX, worldY, worldZ)); + } + } + } + + return points; + } + + /// + /// 将高度区间信息集成到网格地图中 + /// + /// 网格地图 + /// 高度区间数据 + private void IntegrateHeightIntervalsToGrid(GridMap gridMap, Dictionary> heightIntervals) + { + int updatedCells = 0; + int channelCellsWithHeightData = 0; + int channelCellsBecomingUnwalkable = 0; + + foreach (var kvp in heightIntervals) + { + var point = kvp.Key; + var intervals = kvp.Value; + + // 将世界坐标转换为网格坐标 + int gridX = (int)Math.Floor((point.X - gridMap.Bounds.Min.X) / gridMap.CellSize); + int gridY = (int)Math.Floor((point.Y - gridMap.Bounds.Min.Y) / gridMap.CellSize); + + // 确保坐标在有效范围内 + if (gridX >= 0 && gridX < gridMap.Width && gridY >= 0 && gridY < gridMap.Height) + { + // 更新网格单元格的高度区间信息 + var cell = gridMap.Cells[gridX, gridY]; + + if (cell.PassableHeights == null) + { + cell.PassableHeights = new List(); + } + + cell.PassableHeights.Clear(); + cell.PassableHeights.AddRange(intervals); + + // 关键修复:只处理通道单元格的高度信息集成 + // 非通道单元格不应该通过高度扫描而改变其类型或可通行性 + if (cell.CellType == ElementType.Channel && cell.IsInChannel) + { + channelCellsWithHeightData++; + + if (intervals.Any()) + { + // 通道区域有足够净空高度,保持可通行 + cell.IsWalkable = true; + // 保持 cell.CellType = ElementType.Channel 和 cell.IsInChannel = true + } + else + { + // 通道区域但净空高度不足,标记为不可通行但保持通道类型 + cell.IsWalkable = false; + channelCellsBecomingUnwalkable++; + // 保持 cell.IsInChannel = true 和 cell.CellType = ElementType.Channel + } + } + else + { + // 非通道单元格:不应该通过垂直扫描改变其状态 + // 这种情况理论上不应该发生,因为只对通道单元格进行扫描 + LogManager.Warning($"[通道2.5D模式] 警告:非通道单元格 ({gridX},{gridY}) 收到高度数据,类型:{cell.CellType}, IsInChannel:{cell.IsInChannel}"); + + // 保持原有状态不变,不修改 IsWalkable 或 CellType + } + + cell.WorldPosition = point; + gridMap.Cells[gridX, gridY] = cell; + updatedCells++; + } + } + + LogManager.Info($"[通道2.5D模式] 高度信息集成完成,更新了 {updatedCells} 个网格单元格"); + LogManager.Info($"[通道2.5D模式] 处理的通道单元格数量: {channelCellsWithHeightData}, 因高度不足变为不可通行: {channelCellsBecomingUnwalkable}"); + } + + /// + /// 验证网格统计一致性,确保可通行单元格数量不超过预期 + /// + /// 要验证的网格地图 + /// 验证阶段描述 + private void ValidateGridStatisticsConsistency(GridMap gridMap, string stage) + { + int totalCells = gridMap.Width * gridMap.Height; + int walkableCount = 0; + int channelCount = 0; + int openSpaceCount = 0; + int obstacleCount = 0; + int doorCount = 0; + + for (int x = 0; x < gridMap.Width; x++) + { + for (int y = 0; y < gridMap.Height; y++) + { + var cell = gridMap.Cells[x, y]; + + if (cell.IsWalkable) + { + walkableCount++; + switch (cell.CellType) + { + case ElementType.Channel: + channelCount++; + break; + case ElementType.OpenSpace: + openSpaceCount++; + break; + case ElementType.Door: + doorCount++; + break; + } + } + else + { + if (cell.CellType == ElementType.Obstacle) + { + obstacleCount++; + } + } + } + } + + LogManager.Info($"[网格验证] {stage} - 总单元格: {totalCells}, 可通行: {walkableCount}"); + LogManager.Info($"[网格验证] 可通行单元格明细 - 通道: {channelCount}, 开放空间: {openSpaceCount}, 门: {doorCount}"); + LogManager.Info($"[网格验证] 不可通行单元格 - 障碍物: {obstacleCount}, 其他: {totalCells - walkableCount - obstacleCount}"); + + // 验证:可通行的开放空间不应该在通道2.5D模式中出现,除非是合理的扩展 + if (openSpaceCount > 0) + { + LogManager.Warning($"[网格验证] 警告: 发现 {openSpaceCount} 个可通行的开放空间单元格,这在通道2.5D模式中可能是异常的"); + } + + // 验证:总可通行数量的合理性检查 + if (walkableCount > channelCount * 1.2) // 允许20%的合理扩展 + { + LogManager.Warning($"[网格验证] 警告: 可通行单元格数量({walkableCount})明显超过通道单元格数量({channelCount})"); + } + } + /// /// 进行楼层过滤,获取起终点所在楼层的所有模型项 /// diff --git a/src/PathPlanning/TriangleSpatialHash.cs b/src/PathPlanning/TriangleSpatialHash.cs new file mode 100644 index 0000000..768a35c --- /dev/null +++ b/src/PathPlanning/TriangleSpatialHash.cs @@ -0,0 +1,402 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Autodesk.Navisworks.Api; +using NavisworksTransport.Utils; + +namespace NavisworksTransport.PathPlanning +{ + /// + /// 三角形空间哈希索引类 + /// 用于A*改进方案中的射线相交测试性能优化 + /// + /// 核心原理: + /// - 将3D空间划分为2D哈希网格 + /// - 每个三角形根据其2D投影分配到对应的哈希格子中 + /// - 射线检测时只需检查相关格子内的三角形,大幅减少相交测试次数 + /// + public class TriangleSpatialHash + { + private Dictionary> _spatialHash; + private double _cellSize; + private int _gridWidth; + private BoundingBox3D _bounds; + private Point3D _origin; + + // 统计信息 + private int _totalTriangles; + private int _totalCells; + private double _averageTrianglesPerCell; + private int _maxTrianglesPerCell; + + /// + /// 构造函数 + /// + public TriangleSpatialHash() + { + _spatialHash = new Dictionary>(); + _gridWidth = 10000; // 用于哈希计算的网格宽度 + LogManager.Info("[空间哈希] 初始化完成"); + } + + /// + /// 构建空间哈希索引 + /// + /// 三角形列表 + /// 网格单元大小 + /// 边界框(可选,自动计算) + public void Build(List triangles, double gridSize, BoundingBox3D bounds = null) + { + if (triangles == null || !triangles.Any()) + { + LogManager.Warning("[空间哈希] 输入三角形列表为空"); + return; + } + + LogManager.Info($"[空间哈希] 开始构建空间索引:{triangles.Count} 个三角形"); + + _totalTriangles = triangles.Count; + _cellSize = Math.Max(gridSize * 2, 0.1); // 使用稍大的单元格减少重复 + _spatialHash.Clear(); + + // 计算或使用提供的边界框 + _bounds = bounds ?? CalculateTrianglesBounds(triangles); + _origin = _bounds.Min; + + LogManager.Info($"[空间哈希] 边界框: [{_bounds.Min.X:F1},{_bounds.Min.Y:F1}] - [{_bounds.Max.X:F1},{_bounds.Max.Y:F1}]"); + LogManager.Info($"[空间哈希] 单元格大小: {_cellSize:F2}m"); + + // 为每个三角形分配到哈希格子 + var processedTriangles = 0; + foreach (var triangle in triangles) + { + try + { + AssignTriangleToHash(triangle); + processedTriangles++; + } + catch (Exception ex) + { + LogManager.Warning($"[空间哈希] 分配三角形失败: {ex.Message}"); + } + } + + // 计算统计信息 + CalculateStatistics(); + + LogManager.Info($"[空间哈希] 构建完成:{processedTriangles}/{triangles.Count} 个三角形,{_totalCells} 个格子"); + LogManager.Info($"[空间哈希] 性能统计:平均 {_averageTrianglesPerCell:F1} 个三角形/格子,最大 {_maxTrianglesPerCell} 个"); + } + + /// + /// 获取指定2D位置附近的三角形 + /// + /// 世界坐标X + /// 世界坐标Y + /// 搜索半径(格子数量) + /// 附近的三角形列表 + public List GetTrianglesAt(double x, double y, int searchRadius = 0) + { + var result = new List(); + + // 计算基准格子坐标 + int baseGridX = (int)Math.Floor((x - _origin.X) / _cellSize); + int baseGridY = (int)Math.Floor((y - _origin.Y) / _cellSize); + + // 搜索指定范围内的格子 + for (int dx = -searchRadius; dx <= searchRadius; dx++) + { + for (int dy = -searchRadius; dy <= searchRadius; dy++) + { + int gridX = baseGridX + dx; + int gridY = baseGridY + dy; + int hash = CalculateHash(gridX, gridY); + + if (_spatialHash.ContainsKey(hash)) + { + result.AddRange(_spatialHash[hash]); + } + } + } + + return result; + } + + /// + /// 获取指定点的三角形(单个格子) + /// + /// 世界坐标点 + /// 该位置的三角形列表 + public List GetTrianglesAt(Point3D point) + { + return GetTrianglesAt(point.X, point.Y); + } + + /// + /// 获取指定区域内的所有三角形 + /// + /// 最小X坐标 + /// 最小Y坐标 + /// 最大X坐标 + /// 最大Y坐标 + /// 区域内的三角形列表 + public List GetTrianglesInRegion(double minX, double minY, double maxX, double maxY) + { + var result = new List(); + var addedHashes = new HashSet(); // 避免重复添加 + + int minGridX = (int)Math.Floor((minX - _origin.X) / _cellSize); + int maxGridX = (int)Math.Floor((maxX - _origin.X) / _cellSize); + int minGridY = (int)Math.Floor((minY - _origin.Y) / _cellSize); + int maxGridY = (int)Math.Floor((maxY - _origin.Y) / _cellSize); + + for (int gridX = minGridX; gridX <= maxGridX; gridX++) + { + for (int gridY = minGridY; gridY <= maxGridY; gridY++) + { + int hash = CalculateHash(gridX, gridY); + + if (_spatialHash.ContainsKey(hash) && !addedHashes.Contains(hash)) + { + result.AddRange(_spatialHash[hash]); + addedHashes.Add(hash); + } + } + } + + return result; + } + + /// + /// 获取性能统计信息 + /// + /// 统计信息字符串 + public string GetStatistics() + { + if (_totalTriangles == 0) + return "空间哈希未初始化"; + + var memoryUsageKB = EstimateMemoryUsage() / 1024.0; + var compressionRatio = _totalTriangles > 0 ? (double)_totalTriangles / _totalCells : 0; + + return $"空间哈希统计: {_totalTriangles} 个三角形分布在 {_totalCells} 个格子中, " + + $"平均密度 {_averageTrianglesPerCell:F1}, 峰值密度 {_maxTrianglesPerCell}, " + + $"压缩比 {compressionRatio:F1}:1, 内存占用 {memoryUsageKB:F1} KB"; + } + + /// + /// 清除所有数据 + /// + public void Clear() + { + _spatialHash.Clear(); + _totalTriangles = 0; + _totalCells = 0; + _averageTrianglesPerCell = 0; + _maxTrianglesPerCell = 0; + LogManager.Info("[空间哈希] 已清除所有数据"); + } + + /// + /// 检查空间哈希是否为空 + /// + /// 是否为空 + public bool IsEmpty() + { + return _totalTriangles == 0 || !_spatialHash.Any(); + } + + /// + /// 获取指定格子中的三角形数量 + /// + /// 世界坐标X + /// 世界坐标Y + /// 三角形数量 + public int GetTriangleCountAt(double x, double y) + { + int gridX = (int)Math.Floor((x - _origin.X) / _cellSize); + int gridY = (int)Math.Floor((y - _origin.Y) / _cellSize); + int hash = CalculateHash(gridX, gridY); + + return _spatialHash.ContainsKey(hash) ? _spatialHash[hash].Count : 0; + } + + #region 私有方法 + + /// + /// 将三角形分配到哈希格子 + /// + /// 三角形 + private void AssignTriangleToHash(Triangle3D triangle) + { + // 计算三角形2D投影的包围盒 + var minX = Math.Min(triangle.V1.X, Math.Min(triangle.V2.X, triangle.V3.X)); + var maxX = Math.Max(triangle.V1.X, Math.Max(triangle.V2.X, triangle.V3.X)); + var minY = Math.Min(triangle.V1.Y, Math.Min(triangle.V2.Y, triangle.V3.Y)); + var maxY = Math.Max(triangle.V1.Y, Math.Max(triangle.V2.Y, triangle.V3.Y)); + + // 计算涉及的格子范围 + int minGridX = (int)Math.Floor((minX - _origin.X) / _cellSize); + int maxGridX = (int)Math.Floor((maxX - _origin.X) / _cellSize); + int minGridY = (int)Math.Floor((minY - _origin.Y) / _cellSize); + int maxGridY = (int)Math.Floor((maxY - _origin.Y) / _cellSize); + + // 将三角形添加到所有覆盖的格子 + for (int gridX = minGridX; gridX <= maxGridX; gridX++) + { + for (int gridY = minGridY; gridY <= maxGridY; gridY++) + { + int hash = CalculateHash(gridX, gridY); + + if (!_spatialHash.ContainsKey(hash)) + { + _spatialHash[hash] = new List(); + } + + _spatialHash[hash].Add(triangle); + } + } + } + + /// + /// 计算哈希值 + /// + /// 格子X坐标 + /// 格子Y坐标 + /// 哈希值 + private int CalculateHash(int gridX, int gridY) + { + // 使用简单的线性哈希,避免负数 + var adjustedX = gridX + _gridWidth / 2; + var adjustedY = gridY + _gridWidth / 2; + + // 确保在合理范围内 + adjustedX = Math.Max(0, Math.Min(_gridWidth - 1, adjustedX)); + adjustedY = Math.Max(0, Math.Min(_gridWidth - 1, adjustedY)); + + return adjustedX * _gridWidth + adjustedY; + } + + /// + /// 计算三角形列表的边界框 + /// + /// 三角形列表 + /// 边界框 + private BoundingBox3D CalculateTrianglesBounds(List triangles) + { + if (!triangles.Any()) + return new BoundingBox3D(Point3D.Zero, Point3D.Zero); + + var first = triangles.First(); + double minX = Math.Min(first.V1.X, Math.Min(first.V2.X, first.V3.X)); + double maxX = Math.Max(first.V1.X, Math.Max(first.V2.X, first.V3.X)); + double minY = Math.Min(first.V1.Y, Math.Min(first.V2.Y, first.V3.Y)); + double maxY = Math.Max(first.V1.Y, Math.Max(first.V2.Y, first.V3.Y)); + double minZ = Math.Min(first.V1.Z, Math.Min(first.V2.Z, first.V3.Z)); + double maxZ = Math.Max(first.V1.Z, Math.Max(first.V2.Z, first.V3.Z)); + + foreach (var triangle in triangles.Skip(1)) + { + minX = Math.Min(minX, Math.Min(triangle.V1.X, Math.Min(triangle.V2.X, triangle.V3.X))); + maxX = Math.Max(maxX, Math.Max(triangle.V1.X, Math.Max(triangle.V2.X, triangle.V3.X))); + minY = Math.Min(minY, Math.Min(triangle.V1.Y, Math.Min(triangle.V2.Y, triangle.V3.Y))); + maxY = Math.Max(maxY, Math.Max(triangle.V1.Y, Math.Max(triangle.V2.Y, triangle.V3.Y))); + minZ = Math.Min(minZ, Math.Min(triangle.V1.Z, Math.Min(triangle.V2.Z, triangle.V3.Z))); + maxZ = Math.Max(maxZ, Math.Max(triangle.V1.Z, Math.Max(triangle.V2.Z, triangle.V3.Z))); + } + + return new BoundingBox3D( + new Point3D(minX, minY, minZ), + new Point3D(maxX, maxY, maxZ)); + } + + /// + /// 计算统计信息 + /// + private void CalculateStatistics() + { + _totalCells = _spatialHash.Count; + + if (_totalCells == 0) + { + _averageTrianglesPerCell = 0; + _maxTrianglesPerCell = 0; + return; + } + + var triangleCounts = _spatialHash.Values.Select(list => list.Count).ToList(); + _averageTrianglesPerCell = triangleCounts.Average(); + _maxTrianglesPerCell = triangleCounts.Max(); + } + + /// + /// 估算内存使用量(字节) + /// + /// 内存使用量估算 + private long EstimateMemoryUsage() + { + // Dictionary开销 + 每个List的开销 + 三角形数据 + // 粗略估算:每个Dictionary条目32字节,每个List开销24字节,每个三角形72字节 + var dictOverhead = _spatialHash.Count * 32L; + var listOverhead = _spatialHash.Count * 24L; + var triangleData = _totalTriangles * 72L; // 9个double值 × 8字节 + + return dictOverhead + listOverhead + triangleData; + } + + #endregion + + #region 调试和可视化方法 + + /// + /// 获取格子密度分布信息(用于调试) + /// + /// 密度分布字典:密度区间 -> 格子数量 + public Dictionary GetDensityDistribution() + { + var distribution = new Dictionary + { + ["0"] = 0, + ["1-5"] = 0, + ["6-10"] = 0, + ["11-20"] = 0, + ["21-50"] = 0, + ["50+"] = 0 + }; + + foreach (var cellTriangles in _spatialHash.Values) + { + var count = cellTriangles.Count; + if (count == 0) distribution["0"]++; + else if (count <= 5) distribution["1-5"]++; + else if (count <= 10) distribution["6-10"]++; + else if (count <= 20) distribution["11-20"]++; + else if (count <= 50) distribution["21-50"]++; + else distribution["50+"]++; + } + + return distribution; + } + + /// + /// 验证空间哈希的正确性(调试用) + /// + /// 测试点 + /// 验证结果信息 + public string ValidateAtPoint(Point3D testPoint) + { + var triangles = GetTrianglesAt(testPoint.X, testPoint.Y); + var cellTriangleCount = GetTriangleCountAt(testPoint.X, testPoint.Y); + + int gridX = (int)Math.Floor((testPoint.X - _origin.X) / _cellSize); + int gridY = (int)Math.Floor((testPoint.Y - _origin.Y) / _cellSize); + int hash = CalculateHash(gridX, gridY); + + return $"点 ({testPoint.X:F2}, {testPoint.Y:F2}) -> " + + $"格子 ({gridX}, {gridY}), 哈希 {hash}, " + + $"三角形数量 {cellTriangleCount}, 实际获取 {triangles.Count} 个"; + } + + #endregion + } +} \ No newline at end of file diff --git a/src/PathPlanning/VerticalScanProcessor.cs b/src/PathPlanning/VerticalScanProcessor.cs new file mode 100644 index 0000000..0f403d9 --- /dev/null +++ b/src/PathPlanning/VerticalScanProcessor.cs @@ -0,0 +1,808 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using System.Collections.Concurrent; +using Autodesk.Navisworks.Api; +using NavisworksTransport.Core; +using NavisworksTransport.Utils; + +namespace NavisworksTransport.PathPlanning +{ + /// + /// 垂直扫描处理器 + /// 实现多级筛选优化的障碍物检测和2.5D高度区间计算 + /// 预期实现10,000-40,000倍性能提升 + /// + public class VerticalScanProcessor + { + #region 私有字段 + + /// + /// 空间哈希表,用于快速查找邻域内的模型项 + /// + private readonly Dictionary> _spatialHashMap; + + /// + /// 空间哈希的网格大小(米) + /// + private readonly double _spatialHashSize; + + /// + /// 并行处理的任务数量 + /// + private readonly int _parallelDegree; + + /// + /// 高度筛选的容差值(米) + /// + private const double HEIGHT_TOLERANCE = 0.1; + + /// + /// 默认人行高度(米)- 用于未检测到通道时的默认高度 + /// + private const double DEFAULT_WALKING_HEIGHT = 2.5; + + /// + /// 最小通行高度(米) + /// + private const double MIN_PASSABLE_HEIGHT = 1.8; + + #endregion + + #region 构造函数 + + /// + /// 构造函数 + /// + /// 空间哈希网格大小(米),默认为10米 + /// 并行度,默认为CPU核心数的一半,避免过度并行导致崩溃 + public VerticalScanProcessor(double spatialHashSize = 10.0, int parallelDegree = 0) + { + _spatialHashSize = spatialHashSize; + // 限制并行度,避免过度并行导致崩溃,最大为CPU核心数的一半 + int maxParallelism = Math.Max(1, Environment.ProcessorCount / 2); + _parallelDegree = parallelDegree > 0 ? Math.Min(parallelDegree, maxParallelism) : maxParallelism; + _spatialHashMap = new Dictionary>(); + + LogManager.Info($"[垂直扫描处理器] 初始化完成,空间哈希大小: {_spatialHashSize}m, 并行度: {_parallelDegree} (最大并行度: {maxParallelism})"); + } + + #endregion + + #region 公共方法 + + /// + /// 构建空间哈希索引 + /// 第一级筛选:邻域筛选 + /// + /// 所有模型项 + /// 扫描边界 + /// 已确定的通道元素列表(将被排除) + public void BuildSpatialHashIndex(IEnumerable modelItems, BoundingBox3D bounds, IEnumerable channelItems = null) + { + LogManager.Info("【垂直扫描处理器】 开始构建空间哈希索引"); + var startTime = DateTime.Now; + + _spatialHashMap.Clear(); + + // 创建通道元素的HashSet以便快速查找 + var channelItemsSet = new HashSet(channelItems ?? new List()); + + // 统计变量 + var totalItems = modelItems?.Count() ?? 0; + var itemsWithGeometry = 0; + var channelItemsExcluded = 0; + var itemsWithoutBounds = 0; + var itemsOutOfBounds = 0; + var itemsAddedToHash = 0; + + LogManager.Info($"【垂直扫描处理器】 输入统计 - 总模型项: {totalItems}, 将排除通道元素: {channelItemsSet.Count}"); + LogManager.Info($"【垂直扫描处理器】 扫描边界: [{bounds.Min.X:F1},{bounds.Min.Y:F1},{bounds.Min.Z:F1}] - [{bounds.Max.X:F1},{bounds.Max.Y:F1},{bounds.Max.Z:F1}]"); + + var itemsWithBounds = new ConcurrentBag<(ModelItem item, BoundingBox3D bbox)>(); + + // 统计计数器(线程安全) + var geometryCounter = 0; + var channelExcludedCounter = 0; + var noBoundsCounter = 0; + var outOfBoundsCounter = 0; + + try + { + // 并行计算所有模型项的边界框,使用ConcurrentBag避免锁竞争 + Parallel.ForEach(modelItems, new ParallelOptions { MaxDegreeOfParallelism = _parallelDegree }, item => + { + try + { + // 🔥 关键修复:检查是否需要排除通道元素(包括容器节点和子节点) + if (channelItemsSet.Contains(item) || IsChildOfChannelItems(item, channelItemsSet)) + { + Interlocked.Increment(ref channelExcludedCounter); + return; // 跳过通道元素及其子节点 + } + + // 然后检查是否有几何体,只对有几何体的元素进行空间哈希处理 + if (item?.HasGeometry == true) + { + Interlocked.Increment(ref geometryCounter); + + var bbox = item.BoundingBox(); + if (bbox == null) + { + Interlocked.Increment(ref noBoundsCounter); + return; + } + + if (IsWithinBounds(bbox, bounds)) + { + itemsWithBounds.Add((item, bbox)); + } + else + { + Interlocked.Increment(ref outOfBoundsCounter); + } + } + } + catch (Exception ex) + { + LogManager.Debug($"【垂直扫描处理器】 获取边界框失败: {item?.DisplayName ?? "NULL"}, {ex.Message}"); + } + }); + } + catch (Exception ex) + { + LogManager.Error($"【垂直扫描处理器】 并行计算边界框时发生严重错误: {ex.Message}"); + return; // 提前退出,避免进一步崩溃 + } + + // 输出详细统计信息 + LogManager.Info($"【垂直扫描处理器】 元素筛选统计:"); + LogManager.Info($" - 有几何体的元素: {geometryCounter}"); + LogManager.Info($" - 被排除的通道元素: {channelExcludedCounter}"); + LogManager.Info($" - 无边界框的元素: {noBoundsCounter}"); + LogManager.Info($" - 超出扫描边界的元素: {outOfBoundsCounter}"); + LogManager.Info($" - 符合条件并添加到空间哈希的元素: {itemsWithBounds.Count}"); + + // 构建空间哈希 + var totalHashEntries = 0; + foreach (var (item, bbox) in itemsWithBounds) + { + var hashKeys = GetSpatialHashKeys(bbox); + foreach (var key in hashKeys) + { + if (!_spatialHashMap.ContainsKey(key)) + { + _spatialHashMap[key] = new List(); + } + _spatialHashMap[key].Add(item); + totalHashEntries++; + } + } + + var elapsed = (DateTime.Now - startTime).TotalMilliseconds; + LogManager.Info($"【垂直扫描处理器】 空间哈希索引构建完成,耗时: {elapsed:F1}ms"); + LogManager.Info($"【垂直扫描处理器】 最终统计 - 参与元素: {itemsWithBounds.Count}, 哈希桶: {_spatialHashMap.Count}, 总哈希条目: {totalHashEntries}"); + } + + + /// + /// 并行扫描网格点的2.5D高度区间 + /// + /// 网格点列表 + /// 扫描高度(从地面向上的距离) + /// 车辆高度(用于通行检查) + /// 每个网格点的高度区间列表 + public Dictionary> ParallelScanHeightIntervals( + IEnumerable gridPoints, + double scanHeight = 20.0, + double vehicleHeight = 3.0) + { + LogManager.Info($"[垂直扫描处理器] 开始并行扫描高度区间,扫描高度: {scanHeight}m, 车辆高度: {vehicleHeight}m"); + var startTime = DateTime.Now; + + var results = new ConcurrentDictionary>(); + var pointsList = gridPoints?.ToList() ?? new List(); + + try + { + // 并行处理每个网格点,添加更严格的异常处理 + Parallel.ForEach(pointsList, new ParallelOptions + { + MaxDegreeOfParallelism = _parallelDegree, + CancellationToken = System.Threading.CancellationToken.None + }, gridPoint => + { + try + { + if (gridPoint != null) + { + var intervals = ScanVerticalLine(gridPoint, scanHeight, vehicleHeight); + results[gridPoint] = intervals ?? new List(); + } + } + catch (OutOfMemoryException) + { + LogManager.Error($"[垂直扫描处理器] 内存不足,跳过点 {gridPoint}"); + results[gridPoint] = new List(); + throw; // 内存不足需要重新抛出 + } + catch (Exception ex) + { + LogManager.Error($"[垂直扫描处理器] 扫描点 {gridPoint} 失败: {ex.Message}"); + results[gridPoint] = new List(); + } + }); + } + catch (Exception ex) + { + LogManager.Error($"[垂直扫描处理器] 并行扫描过程中发生严重错误: {ex.Message}"); + // 如果并行处理失败,尝试串行处理作为备选方案 + LogManager.Info("[垂直扫描处理器] 尝试串行处理作为备选方案"); + + foreach (var gridPoint in pointsList) + { + try + { + if (!results.ContainsKey(gridPoint)) + { + var intervals = ScanVerticalLine(gridPoint, scanHeight, vehicleHeight); + results[gridPoint] = intervals ?? new List(); + } + } + catch (Exception serialEx) + { + LogManager.Error($"[垂直扫描处理器] 串行扫描点 {gridPoint} 也失败: {serialEx.Message}"); + results[gridPoint] = new List(); + } + } + } + + var elapsed = (DateTime.Now - startTime).TotalMilliseconds; + var totalIntervals = results.Values.Sum(list => list.Count); + LogManager.Info($"[垂直扫描处理器] 并行扫描完成,耗时: {elapsed:F1}ms, 扫描点: {pointsList.Count}, 总区间: {totalIntervals}"); + + return new Dictionary>(results); + } + + /// + /// 扫描单个垂直线上的通行区间 + /// + /// 基础点(X,Y坐标) + /// 扫描高度 + /// 车辆高度 + /// 可通行的高度区间列表 + public List ScanVerticalLine(Point3D basePoint, double scanHeight, double vehicleHeight) + { + // 第一级筛选:邻域筛选 - 获取空间哈希邻域内的候选项 + var candidateItems = GetCandidateItemsFromSpatialHash(basePoint); + + // 第二级筛选:高度筛选 - 只保留在扫描高度范围内的项目 + var heightFilteredItems = HeightFiltering(candidateItems, basePoint.Z, basePoint.Z + scanHeight); + + // 第三级筛选:空间哈希 - 精确的几何相交测试 + var intersectionResults = PerformIntersectionTests(heightFilteredItems, basePoint, scanHeight); + + // 计算可通行区间 + var passableIntervals = CalculatePassableIntervals(intersectionResults, basePoint.Z, scanHeight, vehicleHeight); + + return passableIntervals; + } + + #endregion + + #region 私有方法 - 多级筛选实现 + + /// + /// 第一级筛选:从空间哈希中获取候选模型项(邻域筛选) + /// + /// 查询点 + /// 候选模型项列表 + private List GetCandidateItemsFromSpatialHash(Point3D point) + { + var candidates = new HashSet(); + var hashKeys = GetSpatialHashKeysForPoint(point); + + foreach (var key in hashKeys) + { + if (_spatialHashMap.ContainsKey(key)) + { + foreach (var item in _spatialHashMap[key]) + { + candidates.Add(item); + } + } + } + + return candidates.ToList(); + } + + /// + /// 第二级筛选:高度筛选 + /// + /// 候选模型项 + /// 最小Z坐标 + /// 最大Z坐标 + /// 高度筛选后的模型项 + private List HeightFiltering(List items, double minZ, double maxZ) + { + var filtered = new ConcurrentBag(); + + try + { + // 使用ConcurrentBag避免锁竞争,提高性能和稳定性 + Parallel.ForEach(items, new ParallelOptions { MaxDegreeOfParallelism = _parallelDegree }, item => + { + try + { + if (item?.HasGeometry == true) + { + var bbox = item.BoundingBox(); + if (bbox != null) + { + // 检查高度范围是否有重叠(加上容差) + if (bbox.Max.Z >= (minZ - HEIGHT_TOLERANCE) && + bbox.Min.Z <= (maxZ + HEIGHT_TOLERANCE)) + { + filtered.Add(item); + } + } + } + } + catch (Exception ex) + { + LogManager.Debug($"[垂直扫描处理器] 高度筛选失败: {item?.DisplayName ?? "NULL"}, {ex.Message}"); + } + }); + } + catch (Exception ex) + { + LogManager.Warning($"[垂直扫描处理器] 高度筛选并行处理失败: {ex.Message},回退到串行处理"); + + // 如果并行处理失败,使用串行处理 + foreach (var item in items ?? new List()) + { + try + { + if (item?.HasGeometry == true) + { + var bbox = item.BoundingBox(); + if (bbox != null && + bbox.Max.Z >= (minZ - HEIGHT_TOLERANCE) && + bbox.Min.Z <= (maxZ + HEIGHT_TOLERANCE)) + { + filtered.Add(item); + } + } + } + catch (Exception serialEx) + { + LogManager.Debug($"[垂直扫描处理器] 串行高度筛选失败: {item?.DisplayName ?? "NULL"}, {serialEx.Message}"); + } + } + } + + return filtered.ToList(); + } + + /// + /// 第三级筛选:精确几何相交测试(空间哈希优化) + /// + /// 高度筛选后的模型项 + /// 基础点 + /// 扫描高度 + /// 相交测试结果 + private List PerformIntersectionTests(List items, Point3D basePoint, double scanHeight) + { + var results = new ConcurrentBag(); + + try + { + // 并行执行相交测试,添加更强的异常处理 + Parallel.ForEach(items, new ParallelOptions { MaxDegreeOfParallelism = _parallelDegree }, item => + { + try + { + if (item != null) + { + var intersectionData = TestVerticalLineIntersection(item, basePoint, scanHeight); + if (intersectionData != null) + { + // 简化逻辑:所有与垂直射线相交的非通道元素都视为障碍物 + // 因为在空间哈希构建时已经排除了通道元素 + results.Add(new IntersectionResult + { + ModelItem = item, + IntersectionData = intersectionData, + IsObstacle = true, // 所有相交的非通道元素都是障碍物 + IsPassable = false // 不可通行 + }); + } + } + } + catch (Exception ex) + { + LogManager.Debug($"[垂直扫描处理器] 相交测试失败: {item?.DisplayName ?? "NULL"}, {ex.Message}"); + } + }); + } + catch (Exception ex) + { + LogManager.Warning($"[垂直扫描处理器] 并行相交测试失败: {ex.Message},回退到串行处理"); + + // 如果并行处理失败,使用串行处理 + foreach (var item in items ?? new List()) + { + try + { + if (item != null) + { + var intersectionData = TestVerticalLineIntersection(item, basePoint, scanHeight); + if (intersectionData != null) + { + // 简化逻辑:所有与垂直射线相交的非通道元素都视为障碍物 + results.Add(new IntersectionResult + { + ModelItem = item, + IntersectionData = intersectionData, + IsObstacle = true, // 所有相交的非通道元素都是障碍物 + IsPassable = false // 不可通行 + }); + } + } + } + catch (Exception serialEx) + { + LogManager.Debug($"[垂直扫描处理器] 串行相交测试失败: {item?.DisplayName ?? "NULL"}, {serialEx.Message}"); + } + } + } + + return results.ToList(); + } + + /// + /// 计算可通行区间 + /// + /// 相交测试结果 + /// 基础Z坐标 + /// 扫描高度 + /// 车辆高度 + /// 可通行区间列表 + private List CalculatePassableIntervals( + List intersectionResults, + double baseZ, + double scanHeight, + double vehicleHeight) + { + // 收集所有障碍物的高度范围 + var obstacles = new List(); + var floors = new List(); + + foreach (var result in intersectionResults) + { + if (result.IsObstacle && result.IntersectionData != null) + { + obstacles.Add(new HeightInterval( + result.IntersectionData.MinZ, + result.IntersectionData.MaxZ + )); + } + else if (result.IsPassable && result.IntersectionData != null) + { + // 检查是否为地面/楼板 + if (IsFloorLike(result.ModelItem, result.IntersectionData)) + { + floors.Add(new HeightInterval( + result.IntersectionData.MinZ, + result.IntersectionData.MaxZ + )); + } + } + } + + // 合并重叠的障碍物区间 + var mergedObstacles = MergeOverlappingIntervals(obstacles); + + // 计算可通行区间 + var passableIntervals = new List(); + var scanRange = new HeightInterval(baseZ, baseZ + scanHeight); + + // 如果没有找到地面,使用基础Z坐标作为默认地面 + if (!floors.Any()) + { + floors.Add(new HeightInterval(baseZ - 0.1, baseZ + 0.1)); + } + + // 对每个潜在的地面,计算其上方的可通行空间 + foreach (var floor in floors) + { + double floorTop = floor.MaxZ; + double availableTop = baseZ + scanHeight; + + // 检查地面上方是否有足够的净空高度 + var conflictingObstacles = mergedObstacles + .Where(obs => obs.MinZ < availableTop && obs.MaxZ > floorTop) + .OrderBy(obs => obs.MinZ) + .ToList(); + + if (!conflictingObstacles.Any()) + { + // 没有障碍物,整个高度范围都可通行 + double clearHeight = availableTop - floorTop; + if (clearHeight >= MIN_PASSABLE_HEIGHT) + { + passableIntervals.Add(new HeightInterval(floorTop, availableTop)); + } + } + else + { + // 有障碍物,计算障碍物之间的可通行空间 + double currentBottom = floorTop; + + foreach (var obstacle in conflictingObstacles) + { + double obstacleBottom = Math.Max(obstacle.MinZ, currentBottom); + + if (obstacleBottom > currentBottom) + { + double clearHeight = obstacleBottom - currentBottom; + if (clearHeight >= vehicleHeight) + { + passableIntervals.Add(new HeightInterval(currentBottom, obstacleBottom)); + } + } + + currentBottom = Math.Max(currentBottom, obstacle.MaxZ); + } + + // 检查最后一个障碍物之后的空间 + if (currentBottom < availableTop) + { + double clearHeight = availableTop - currentBottom; + if (clearHeight >= vehicleHeight) + { + passableIntervals.Add(new HeightInterval(currentBottom, availableTop)); + } + } + } + } + + return passableIntervals; + } + + #endregion + + #region 私有辅助方法 + + /// + /// 检查边界框是否在指定范围内 + /// + /// 要检查的边界框 + /// 范围边界框 + /// 是否在范围内 + private bool IsWithinBounds(BoundingBox3D bbox, BoundingBox3D bounds) + { + return bbox.Max.X >= bounds.Min.X && bbox.Min.X <= bounds.Max.X && + bbox.Max.Y >= bounds.Min.Y && bbox.Min.Y <= bounds.Max.Y && + bbox.Max.Z >= bounds.Min.Z && bbox.Min.Z <= bounds.Max.Z; + } + + /// + /// 获取边界框的所有空间哈希键 + /// + /// 边界框 + /// 空间哈希键列表 + private List GetSpatialHashKeys(BoundingBox3D bbox) + { + var keys = new List(); + + int minX = (int)Math.Floor(bbox.Min.X / _spatialHashSize); + int maxX = (int)Math.Floor(bbox.Max.X / _spatialHashSize); + int minY = (int)Math.Floor(bbox.Min.Y / _spatialHashSize); + int maxY = (int)Math.Floor(bbox.Max.Y / _spatialHashSize); + + for (int x = minX; x <= maxX; x++) + { + for (int y = minY; y <= maxY; y++) + { + keys.Add($"{x},{y}"); + } + } + + return keys; + } + + /// + /// 获取点的空间哈希键(包括相邻网格) + /// + /// 查询点 + /// 空间哈希键列表 + private List GetSpatialHashKeysForPoint(Point3D point) + { + var keys = new List(); + int centerX = (int)Math.Floor(point.X / _spatialHashSize); + int centerY = (int)Math.Floor(point.Y / _spatialHashSize); + + // 包括中心及相邻的9个网格 + for (int dx = -1; dx <= 1; dx++) + { + for (int dy = -1; dy <= 1; dy++) + { + keys.Add($"{centerX + dx},{centerY + dy}"); + } + } + + return keys; + } + + /// + /// 测试垂直线与模型项的相交 + /// + /// 模型项 + /// 基础点 + /// 扫描高度 + /// 相交数据,如果不相交则返回null + private IntersectionData TestVerticalLineIntersection(ModelItem item, Point3D basePoint, double scanHeight) + { + try + { + if (!item.HasGeometry) + return null; + + var bbox = item.BoundingBox(); + if (bbox == null) + return null; + + // 简化的相交测试:检查垂直线是否穿过边界框 + if (basePoint.X >= bbox.Min.X && basePoint.X <= bbox.Max.X && + basePoint.Y >= bbox.Min.Y && basePoint.Y <= bbox.Max.Y) + { + // 计算相交的Z范围 + double intersectionMinZ = Math.Max(bbox.Min.Z, basePoint.Z); + double intersectionMaxZ = Math.Min(bbox.Max.Z, basePoint.Z + scanHeight); + + if (intersectionMaxZ > intersectionMinZ) + { + return new IntersectionData + { + MinZ = intersectionMinZ, + MaxZ = intersectionMaxZ, + IntersectionPoint = new Point3D(basePoint.X, basePoint.Y, (intersectionMinZ + intersectionMaxZ) / 2) + }; + } + } + } + catch (Exception ex) + { + LogManager.Debug($"[垂直扫描处理器] 相交测试异常: {item.DisplayName}, {ex.Message}"); + } + + return null; + } + + /// + /// 检查模型项是否类似地面/楼板 + /// + /// 模型项 + /// 相交数据 + /// 是否为地面类型 + private bool IsFloorLike(ModelItem item, IntersectionData intersectionData) + { + string displayName = item.DisplayName?.ToLower() ?? ""; + + // 通过名称判断 + string[] floorKeywords = { + "地面", "floor", "楼板", "slab", "板", "deck", + "地板", "flooring", "基础", "foundation" + }; + + foreach (string keyword in floorKeywords) + { + if (displayName.Contains(keyword)) + { + return true; + } + } + + // 通过几何特征判断(薄的水平结构) + double thickness = intersectionData.MaxZ - intersectionData.MinZ; + return thickness < 0.5; // 小于50cm厚度认为是楼板 + } + + /// + /// 合并重叠的区间 + /// + /// 区间列表 + /// 合并后的区间列表 + private List MergeOverlappingIntervals(List intervals) + { + if (!intervals.Any()) + return new List(); + + var sortedIntervals = intervals.OrderBy(i => i.MinZ).ToList(); + var merged = new List(); + + var current = sortedIntervals[0]; + for (int i = 1; i < sortedIntervals.Count; i++) + { + var next = sortedIntervals[i]; + + if (next.MinZ <= current.MaxZ + HEIGHT_TOLERANCE) + { + // 合并重叠区间 + current = new HeightInterval(current.MinZ, Math.Max(current.MaxZ, next.MaxZ)); + } + else + { + merged.Add(current); + current = next; + } + } + merged.Add(current); + + return merged; + } + + /// + /// 检查指定的ModelItem是否为通道集合中任意一个通道的子节点 + /// + /// 要检查的ModelItem + /// 通道集合 + /// 如果是通道的子节点则返回true + private bool IsChildOfChannelItems(ModelItem item, HashSet channelItemsSet) + { + try + { + if (item?.Parent == null) + return false; + + // 递归向上检查父节点链 + var currentParent = item.Parent; + while (currentParent != null) + { + if (channelItemsSet.Contains(currentParent)) + { + return true; // 找到了通道父节点 + } + currentParent = currentParent.Parent; + } + + return false; + } + catch (Exception ex) + { + LogManager.Debug($"【垂直扫描处理器】 检查子节点关系时出错: {item?.DisplayName ?? "NULL"}, {ex.Message}"); + return false; // 出错时保守处理,不排除 + } + } + + #endregion + + #region 内部数据结构 + + /// + /// 相交数据结构 + /// + private class IntersectionData + { + public double MinZ { get; set; } + public double MaxZ { get; set; } + public Point3D IntersectionPoint { get; set; } + } + + /// + /// 相交测试结果 + /// + private class IntersectionResult + { + public ModelItem ModelItem { get; set; } + public IntersectionData IntersectionData { get; set; } + public bool IsObstacle { get; set; } + public bool IsPassable { get; set; } + } + + #endregion + } +} \ No newline at end of file