开始实现2.5D空间网格,进行到空间哈希成功,垂直扫描失败
This commit is contained in:
parent
4dc989926e
commit
ea809277c3
@ -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
|
||||
|
||||
@ -155,6 +155,8 @@
|
||||
<Compile Include="src\PathPlanning\ChannelHeightDetector.cs" />
|
||||
<Compile Include="src\PathPlanning\SlopeAnalyzer.cs" />
|
||||
<Compile Include="src\PathPlanning\OptimizedHeightCalculator.cs" />
|
||||
<Compile Include="src\PathPlanning\ChannelBasedGridBuilder.cs" />
|
||||
<Compile Include="src\PathPlanning\VerticalScanProcessor.cs" />
|
||||
|
||||
<!-- UI - Forms -->
|
||||
<Compile Include="src\UI\Forms\LogisticsPropertyEditDialog.cs">
|
||||
|
||||
144
QWEN.md
144
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,并注意处理缓存和刷新问题。
|
||||
* **MVVM 模式**: 新的 UI 开发强烈推荐使用 WPF 和 MVVM 模式,将 UI 逻辑 (`ViewModel`) 与 UI 呈现 (`View`) 分离。
|
||||
* **全局异常处理**: 使用 `GlobalExceptionHandler` 类来捕获和处理未处理的异常,提供用户友好的错误提示。
|
||||
* **日志记录**: 使用 `LogManager` 进行统一的日志记录,便于调试和问题追踪。
|
||||
* **安全执行**: 关键操作包装在 `GlobalExceptionHandler.SafeExecute` 方法中,以提高稳定性。
|
||||
* **COM API 使用**: 在需要与 Navisworks 属性系统深度交互时,谨慎使用 COM API,并注意处理缓存和刷新问题。
|
||||
|
||||
299
doc/design/Design_Plan.md
Normal file
299
doc/design/Design_Plan.md
Normal file
@ -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 视图模型与命令绑定。
|
||||
|
||||
22
doc/design/architecture/README.md
Normal file
22
doc/design/architecture/README.md
Normal file
@ -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/算法/序列化等支撑能力的依赖关系。
|
||||
- 如需在图中标注具体类名/文件路径,可在支撑能力块旁新增注释或节点。
|
||||
125
doc/design/architecture/system_architecture_functional_2026.md
Normal file
125
doc/design/architecture/system_architecture_functional_2026.md
Normal file
@ -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;
|
||||
|
||||
```
|
||||
BIN
doc/design/architecture/system_architecture_functional_2026.png
Normal file
BIN
doc/design/architecture/system_architecture_functional_2026.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 45 KiB |
593
doc/design/architecture/功能方案_详版.md
Normal file
593
doc/design/architecture/功能方案_详版.md
Normal file
@ -0,0 +1,593 @@
|
||||
3.3.3. 通道选择及导航功能方案(对标投标模板结构,含输入/输出数据详细规格)
|
||||
|
||||
3.3.3.1. 通道选择及路径点规划
|
||||
|
||||
3.3.3.1.1. 模型切分
|
||||
3.3.3.1.1.1. 功能要求
|
||||
|
||||
- 满足用户需求文档“模型切分”:支持全模型分层切分;对编组站区域楼层间上下贯穿模块可按业务需要忽略,确保视图聚焦物流相关区域;
|
||||
- 以“楼层/区域/子系统/自定义”等属性进行分层,支持预览、筛选和批量导出;
|
||||
- 支持人工选择集导出(非分层路径),用于快速生成局部审阅材料或成果文件;
|
||||
- 操作过程提供进度与状态反馈,长耗时任务可中止,防止卡死;
|
||||
- 导出时可配置NWD导出选项(纹理嵌入、对象特性导出开关),并提供风险提示;
|
||||
- 所有操作应留痕(时间、操作者、参数)以便审查与复盘。
|
||||
|
||||
3.3.3.1.1.2. 实现方案
|
||||
|
||||
- 设计思路:采用“策略+深度+预览”的交互范式,确保分层口径与业务关注范围一致。对“自定义分层”提供二级枚举选项(如楼层F1/F2、区域东/西、子系统强电/弱电等),保障灵活性;
|
||||
- 业务流程:
|
||||
1) 用户在“分层策略”和“遍历深度”处选择参数(必要时选择“自定义分层”的二级选项);
|
||||
2) 点击“预览”,系统遍历选择树,生成“分层预览列表”(包含分层名称、分层属性、是否保存);
|
||||
3) 通过“单独显示/显示所有”在三维视图中快速验证分层效果;
|
||||
4) 在预览列表中勾选需要导出的项,点击“分层保存”,弹出保存对话框,按勾选项批量导出NWD;
|
||||
5) 需要仅导出当前人工选择的范围时,使用“选择集保存”中的“保存当前选择项”;
|
||||
- 性能与稳健性:预览/导出均提供进度与状态文本,期间可“取消操作”。对“嵌入纹理数据”等高风险选项给出警告,建议在小模型上使用;
|
||||
- 与其他模块关系:分层口径可与“类别设置”的物流属性协同(先按属性筛选,再分层预览与导出),提升聚焦效率。
|
||||
|
||||
3.3.3.1.1.3. 输入数据(详细规格)
|
||||
|
||||
- 模型文件(必需):
|
||||
- 类型:文件;格式:NWD/NWF/NWC;编码:二进制;
|
||||
- 约束:文件可读且未损坏;Windows 路径长度≤260字符;有读权限;
|
||||
- 分层策略(必需):
|
||||
- 类型:enum{楼层, 区域, 子系统, 自定义};默认:楼层;
|
||||
- 二级选项(条件必需):
|
||||
- 类型:string;当策略=自定义时必填;示例:F1、F2、北区、强电等;长度≤64;禁止字符:/ \ : * ? " < > |;
|
||||
- 遍历深度(必需):
|
||||
- 类型: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可选校验;根元素<route>;
|
||||
- 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;根元素<route>;带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/列表):以业务语言列出调整建议与优先级。
|
||||
159
doc/design/architecture/功能方案模板.md
Normal file
159
doc/design/architecture/功能方案模板.md
Normal file
@ -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. 功能界面
|
||||
@ -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、对于自定义查找,用指定的分层属性,在每个节点的"分层信息“属性类别中查找。
|
||||
|
||||
@ -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插件集成和联动,运行时间线模拟并获取碰撞结果。 |
|
||||
| 路径规划分析 | 对多个路径运行的碰撞结果,进行分析,生成路径分析报告,选择最佳路径,提供调整建议。 |
|
||||
|
||||
|
||||
151
doc/working/astar_implementation_plan.md
Normal file
151
doc/working/astar_implementation_plan.md
Normal file
@ -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<HeightInterval> 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型通道中的不必要绕行
|
||||
|
||||
这个方案充分利用了现有代码的几何处理、属性管理等核心功能,只需要新增网格生成的改进算法,实现成本低且风险可控。
|
||||
159
doc/working/grid_generation_2_5d_upgrade_report.md
Normal file
159
doc/working/grid_generation_2_5d_upgrade_report.md
Normal file
@ -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参数支持
|
||||
- ✅ 实现了向后兼容的回退机制
|
||||
- ✅ 增加了详细的性能统计和日志记录
|
||||
- ✅ 统一了路径规划的实现逻辑
|
||||
- ✅ 保持了代码编译的正确性
|
||||
|
||||
修改已完成并验证,可以安全部署到测试环境进行进一步验证。
|
||||
746
doc/working/pathfinding_grid_improvement_plan.md
Normal file
746
doc/working/pathfinding_grid_improvement_plan.md
Normal file
@ -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<ModelItem> 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<Triangle3D> 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<Triangle3D> 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<Triangle3D> 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<Triangle3D> 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<ModelItem> 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<ModelItem>();
|
||||
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<ModelItem> FilterByHeight(List<ModelItem> 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<HeightInterval> ScanUpward(
|
||||
Point3D position,
|
||||
List<Triangle3D> 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<double>();
|
||||
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<Triangle3D> 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<HeightInterval> 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<int, List<Triangle3D>> _grid;
|
||||
|
||||
public List<Triangle3D> GetTrianglesNear(Point2D point)
|
||||
{
|
||||
int hash = GetGridHash(point);
|
||||
return _grid.ContainsKey(hash) ? _grid[hash] : new List<Triangle3D>();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. **缓存机制**
|
||||
```csharp
|
||||
// 缓存垂直扫描结果
|
||||
private Dictionary<Point2D, List<HeightInterval>> _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
|
||||
<PathPlanning>
|
||||
<GridGeneration mode="ChannelBased2_5D">
|
||||
<VehicleHeight>2.0</VehicleHeight>
|
||||
<GridSize>0.5</GridSize>
|
||||
<ScanPrecision>High</ScanPrecision>
|
||||
<EnableCache>true</EnableCache>
|
||||
<ParallelThreads>4</ParallelThreads>
|
||||
</GridGeneration>
|
||||
</PathPlanning>
|
||||
```
|
||||
|
||||
## 七、空间哈希优化详细设计
|
||||
|
||||
### TriangleSpatialHash 类实现
|
||||
|
||||
```csharp
|
||||
// 文件位置:src/PathPlanning/TriangleSpatialHash.cs
|
||||
public class TriangleSpatialHash
|
||||
{
|
||||
private Dictionary<int, List<Triangle3D>> spatialHash;
|
||||
private double cellSize;
|
||||
private int gridWidth = 10000; // 用于哈希计算
|
||||
|
||||
public void Build(List<Triangle3D> triangles, double gridSize)
|
||||
{
|
||||
cellSize = gridSize * 2; // 使用稍大的单元格减少重复
|
||||
spatialHash = new Dictionary<int, List<Triangle3D>>();
|
||||
|
||||
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<Triangle3D>();
|
||||
spatialHash[hash].Add(triangle);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public List<Triangle3D> 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<Triangle3D>();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 优化后的通道覆盖生成
|
||||
|
||||
```csharp
|
||||
private ChannelCoverage GeneratePreciseChannelCoverage(List<ModelItem> 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
|
||||
326
doc/working/spatial_hash_principle.md
Normal file
326
doc/working/spatial_hash_principle.md
Normal file
@ -0,0 +1,326 @@
|
||||
# 空间哈希原理与应用
|
||||
|
||||
## 一、核心问题
|
||||
|
||||
在3D空间中进行几何查询时,最直接的方法是遍历所有几何体,但这会导致巨大的计算量。
|
||||
|
||||
**示例场景**:
|
||||
- 10,000个查询点
|
||||
- 10,000个三角形
|
||||
- 暴力方法:1亿次相交测试
|
||||
|
||||
## 二、空间哈希基本原理
|
||||
|
||||
空间哈希是一种空间索引数据结构,通过将空间划分成规则网格,预先将几何体分类到对应的格子中,查询时只检查相关格子内的几何体。
|
||||
|
||||
### 2.1 核心思想
|
||||
|
||||
```
|
||||
空间划分 → 几何体分类 → 快速查询
|
||||
```
|
||||
|
||||
### 2.2 数据结构
|
||||
|
||||
```csharp
|
||||
Dictionary<int, List<Geometry>> 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<Geometry> 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<T> where T : IHasBounds
|
||||
{
|
||||
private Dictionary<int, List<T>> hash;
|
||||
private double cellSize;
|
||||
private int gridWidth;
|
||||
|
||||
public SpatialHash(double cellSize, int gridWidth = 10000)
|
||||
{
|
||||
this.cellSize = cellSize;
|
||||
this.gridWidth = gridWidth;
|
||||
hash = new Dictionary<int, List<T>>();
|
||||
}
|
||||
|
||||
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<T>();
|
||||
hash[key].Add(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public List<T> Query(Point2D point)
|
||||
{
|
||||
var grid = WorldToGrid(point);
|
||||
int key = GetHash(grid.X, grid.Y);
|
||||
return hash.ContainsKey(key) ? hash[key] : new List<T>();
|
||||
}
|
||||
|
||||
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<Triangle3D>(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倍,是路径规划等场景的理想选择。
|
||||
@ -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)
|
||||
{
|
||||
|
||||
@ -750,8 +750,9 @@ namespace NavisworksTransport
|
||||
/// <param name="vehicleSize">车辆尺寸(米)</param>
|
||||
/// <param name="safetyMargin">安全间隙(米)</param>
|
||||
/// <param name="gridSize">网格精度(米)</param>
|
||||
/// <param name="vehicleHeight">车辆高度(米)</param>
|
||||
/// <returns>规划结果</returns>
|
||||
public Task<PathRoute> AutoPlanPath(PathPoint startPoint, PathPoint endPoint, double vehicleSize = 1.0, double safetyMargin = 0.5, double gridSize = -1)
|
||||
public Task<PathRoute> 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默认设置保持一致
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -12,31 +12,98 @@ namespace NavisworksTransport.PathPlanning
|
||||
/// <summary>
|
||||
/// 自动路径查找器
|
||||
/// 基于A*算法的路径规划核心类
|
||||
/// 支持传统模式和2.5D高度约束的路径规划
|
||||
/// </summary>
|
||||
public class AutoPathFinder
|
||||
{
|
||||
/// <summary>
|
||||
/// 查找路径(支持精确高度计算)
|
||||
/// 查找路径(传统方法,保持向后兼容)
|
||||
/// </summary>
|
||||
/// <param name="start">起点(世界坐标)</param>
|
||||
/// <param name="end">终点(世界坐标)</param>
|
||||
/// <param name="gridMap">网格地图</param>
|
||||
/// <param name="channelItems">通道模型项集合(可选,用于精确高度计算)</param>
|
||||
/// <returns>路径点列表(世界坐标)</returns>
|
||||
public List<Point3D> FindPath(Point3D start, Point3D end, GridMap gridMap, IEnumerable<ModelItem> channelItems = null)
|
||||
public List<Point3D> 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找路径(支持通道数据和高度约束)
|
||||
/// </summary>
|
||||
/// <param name="start">起点(世界坐标)</param>
|
||||
/// <param name="end">终点(世界坐标)</param>
|
||||
/// <param name="gridMap">网格地图</param>
|
||||
/// <param name="channelCoverage">通道覆盖数据(可选,用于2.5D路径规划)</param>
|
||||
/// <param name="vehicleHeight">车辆高度(用于高度约束检查)</param>
|
||||
/// <returns>路径点列表(世界坐标)</returns>
|
||||
public List<Point3D> 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 2.5D路径规划(使用通道覆盖数据和高度约束)
|
||||
/// </summary>
|
||||
/// <param name="start">起点(世界坐标)</param>
|
||||
/// <param name="end">终点(世界坐标)</param>
|
||||
/// <param name="gridMap">网格地图</param>
|
||||
/// <param name="channelCoverage">通道覆盖数据</param>
|
||||
/// <param name="vehicleHeight">车辆高度</param>
|
||||
/// <returns>路径点列表(世界坐标)</returns>
|
||||
private List<Point3D> 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<Point3D>();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogManager.Error($"[2.5D路径规划] 路径查找失败: {ex.Message}");
|
||||
LogManager.Info("[2.5D路径规划] 回退到传统模式");
|
||||
return FindPathCore(start, end, gridMap);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找路径(传统方法,保持向后兼容)
|
||||
/// </summary>
|
||||
@ -156,7 +223,80 @@ namespace NavisworksTransport.PathPlanning
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将GridMap转换为RoyT.AStar的Grid格式
|
||||
/// 将网格地图转换为A*算法使用的格式(2.5D模式)
|
||||
/// </summary>
|
||||
/// <param name="gridMap">网格地图</param>
|
||||
/// <param name="channelCoverage">通道覆盖数据</param>
|
||||
/// <param name="vehicleHeight">车辆高度</param>
|
||||
/// <returns>A*算法网格</returns>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将GridMap转换为RoyT.AStar的Grid格式(传统模式)
|
||||
/// </summary>
|
||||
/// <param name="gridMap">网格地图</param>
|
||||
/// <returns>A*算法网格</returns>
|
||||
@ -686,5 +826,199 @@ namespace NavisworksTransport.PathPlanning
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查指定点在给定高度约束下是否可通行
|
||||
/// </summary>
|
||||
/// <param name="point">检查点</param>
|
||||
/// <param name="gridMap">网格地图</param>
|
||||
/// <param name="vehicleHeight">车辆高度</param>
|
||||
/// <returns>是否可通行</returns>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 使用2.5D高度区间信息对路径进行高度插值
|
||||
/// </summary>
|
||||
/// <param name="path">原始路径</param>
|
||||
/// <param name="gridMap">网格地图</param>
|
||||
/// <param name="vehicleHeight">车辆高度</param>
|
||||
/// <returns>插值后的路径</returns>
|
||||
private List<Point3D> InterpolatePathHeights2_5D(List<Point3D> path, GridMap gridMap, double vehicleHeight)
|
||||
{
|
||||
try
|
||||
{
|
||||
LogManager.Info($"[2.5D高度插值] 开始插值,路径点数: {path.Count}");
|
||||
|
||||
var interpolatedPath = new List<Point3D>();
|
||||
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; // 返回原始路径
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从网格中获取指定位置的最优高度
|
||||
/// </summary>
|
||||
/// <param name="point">世界坐标点</param>
|
||||
/// <param name="gridMap">网格地图</param>
|
||||
/// <param name="vehicleHeight">车辆高度</param>
|
||||
/// <returns>最优高度,如果无法确定则返回null</returns>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 执行A*算法的核心逻辑
|
||||
/// </summary>
|
||||
/// <param name="start">起点</param>
|
||||
/// <param name="end">终点</param>
|
||||
/// <param name="gridMap">网格地图</param>
|
||||
/// <param name="astarGrid">A*网格</param>
|
||||
/// <returns>路径点列表</returns>
|
||||
private List<Point3D> 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<Point3D>();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogManager.Error($"[A*执行] A*算法执行失败: {ex.Message}");
|
||||
throw new AutoPathPlanningException($"A*算法执行失败: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
727
src/PathPlanning/ChannelBasedGridBuilder.cs
Normal file
727
src/PathPlanning/ChannelBasedGridBuilder.cs
Normal file
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 基于通道的网格构建器
|
||||
/// 实现A*算法改进方案中的通道优先策略和2.5D垂直扫描
|
||||
/// </summary>
|
||||
public class ChannelBasedGridBuilder
|
||||
{
|
||||
private CategoryAttributeManager _categoryManager;
|
||||
private ChannelHeightDetector _heightDetector;
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数
|
||||
/// </summary>
|
||||
public ChannelBasedGridBuilder()
|
||||
{
|
||||
_heightDetector = new ChannelHeightDetector();
|
||||
LogManager.Info("[通道网格构建器] 初始化完成");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构建基于通道的精确覆盖网格
|
||||
/// </summary>
|
||||
/// <param name="gridSize">网格单元格大小</param>
|
||||
/// <param name="document">当前文档</param>
|
||||
/// <returns>通道覆盖信息</returns>
|
||||
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<ModelItem>();
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取所有通道物品
|
||||
/// </summary>
|
||||
/// <param name="allItems">所有模型物品</param>
|
||||
/// <returns>通道物品列表</returns>
|
||||
private List<ModelItem> GetChannelItems(List<ModelItem> 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<ModelItem>();
|
||||
|
||||
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<ModelItem>();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断物品是否为通道
|
||||
/// </summary>
|
||||
/// <param name="item">模型物品</param>
|
||||
/// <returns>是否为通道</returns>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将通道投影到网格
|
||||
/// 核心创新:智能投影策略 - 薄通道优先使用顶面提取,复杂通道使用射线扫描
|
||||
/// </summary>
|
||||
/// <param name="channel">通道物品</param>
|
||||
/// <param name="gridMap">目标网格地图</param>
|
||||
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($"[通道网格构建器] 使用垂直射线扫描(精确投影)");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 方法1:顶面提取投影(快速且精确)
|
||||
/// </summary>
|
||||
/// <param name="item">模型物品</param>
|
||||
/// <param name="gridMap">网格地图</param>
|
||||
/// <param name="triangles">三角形列表(可选,避免重复提取)</param>
|
||||
private void ProjectUsingTopFaces(ModelItem item, GridMap gridMap, List<Triangle3D> 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} 个三角形");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 方法2:垂直射线扫描(最精确)
|
||||
/// </summary>
|
||||
/// <param name="item">模型物品</param>
|
||||
/// <param name="gridMap">网格地图</param>
|
||||
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<Point2D>();
|
||||
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} 个网格单元");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 提取模型物品的三角形(复用ChannelHeightDetector的现有功能)
|
||||
/// </summary>
|
||||
/// <param name="modelItem">模型物品</param>
|
||||
/// <returns>三角形列表</returns>
|
||||
private List<Triangle3D> ExtractTrianglesFromModelItem(ModelItem modelItem)
|
||||
{
|
||||
try
|
||||
{
|
||||
LogManager.Info($"[通道网格构建器] 提取三角形: {modelItem.DisplayName}");
|
||||
|
||||
// 直接调用已有的三角形提取功能,复用ChannelHeightDetector的代码逻辑
|
||||
var allTriangles = new List<Triangle3D>();
|
||||
|
||||
// 创建几何提取回调
|
||||
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<Triangle3D>();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 递归提取ModelItem及其子项的几何数据
|
||||
/// </summary>
|
||||
private void ExtractTrianglesRecursive(ModelItem modelItem, GeometryExtractorCallback callback, List<Triangle3D> 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}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从单个有几何数据的ModelItem中提取三角形
|
||||
/// </summary>
|
||||
private void ExtractTrianglesFromGeometry(ModelItem modelItem, GeometryExtractorCallback callback, List<Triangle3D> 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}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取Fragment的变换矩阵
|
||||
/// </summary>
|
||||
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; // 使用静态属性创建单位矩阵
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 射线-三角形相交测试(复用ChannelHeightDetector的现有功能)
|
||||
/// </summary>
|
||||
/// <param name="rayOrigin">射线起点</param>
|
||||
/// <param name="rayDirection">射线方向</param>
|
||||
/// <param name="triangle">三角形</param>
|
||||
/// <param name="intersectionZ">交点Z坐标</param>
|
||||
/// <returns>是否相交</returns>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算两点之间的向量
|
||||
/// </summary>
|
||||
private Point3D SubtractPoints(Point3D a, Point3D b)
|
||||
{
|
||||
return new Point3D(a.X - b.X, a.Y - b.Y, a.Z - b.Z);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算两个向量的叉积
|
||||
/// </summary>
|
||||
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
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算两个向量的点积
|
||||
/// </summary>
|
||||
private double DotProduct(Point3D a, Point3D b)
|
||||
{
|
||||
return a.X * b.X + a.Y * b.Y + a.Z * b.Z;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算三角形法向量
|
||||
/// </summary>
|
||||
/// <param name="triangle">三角形</param>
|
||||
/// <returns>法向量</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 统计顶面数量
|
||||
/// </summary>
|
||||
/// <param name="triangles">三角形列表</param>
|
||||
/// <returns>顶面数量</returns>
|
||||
private int CountTopFaces(List<Triangle3D> triangles)
|
||||
{
|
||||
int count = 0;
|
||||
foreach (var triangle in triangles)
|
||||
{
|
||||
var normal = ComputeTriangleNormal(triangle);
|
||||
if (normal.Z > 0.7) // 朝上的面
|
||||
{
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将三角形光栅化到网格
|
||||
/// </summary>
|
||||
/// <param name="gridMap">网格地图</param>
|
||||
/// <param name="triangle">三角形</param>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查2D点是否在三角形内部
|
||||
/// </summary>
|
||||
/// <param name="point">测试点</param>
|
||||
/// <param name="triangle">三角形</param>
|
||||
/// <returns>是否在内部</returns>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置网格单元为通道类型
|
||||
/// </summary>
|
||||
/// <param name="gridMap">网格地图</param>
|
||||
/// <param name="gridPos">网格位置</param>
|
||||
/// <param name="relatedItem">关联的模型物品</param>
|
||||
private void SetCellAsChannel(GridMap gridMap, Point2D gridPos, ModelItem relatedItem)
|
||||
{
|
||||
// 使用GridMap提供的方法设置通道单元格
|
||||
gridMap.SetCellAsChannel(gridPos, ChannelType.Corridor, relatedItem);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算通道总边界
|
||||
/// </summary>
|
||||
/// <param name="channelItems">通道物品列表</param>
|
||||
/// <returns>总边界框</returns>
|
||||
private BoundingBox3D CalculateTotalBounds(List<ModelItem> 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));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建空的通道覆盖
|
||||
/// </summary>
|
||||
/// <returns>空的通道覆盖</returns>
|
||||
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<ModelItem>(),
|
||||
TotalBounds = emptyBounds
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 格式化边界框信息用于日志
|
||||
/// </summary>
|
||||
/// <param name="bounds">边界框</param>
|
||||
/// <returns>格式化字符串</returns>
|
||||
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}]";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@ -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<HeightInterval>(),
|
||||
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<HeightInterval>(),
|
||||
ChannelType = cellType == ElementType.Channel ? ChannelType.Corridor : ChannelType.Other,
|
||||
WorldPosition = worldPos
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置网格单元为通道类型 - A*改进方案新增方法
|
||||
/// </summary>
|
||||
/// <param name="gridPosition">网格坐标</param>
|
||||
/// <param name="channelType">通道类型</param>
|
||||
/// <param name="relatedItem">关联的模型物品</param>
|
||||
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<HeightInterval>(),
|
||||
ChannelType = channelType,
|
||||
WorldPosition = worldPos,
|
||||
RelatedModelItem = relatedItem
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 添加可通行高度区间到指定网格单元
|
||||
/// </summary>
|
||||
/// <param name="gridPosition">网格坐标</param>
|
||||
/// <param name="heightInterval">高度区间</param>
|
||||
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<HeightInterval>();
|
||||
}
|
||||
cell.PassableHeights.Add(heightInterval);
|
||||
Cells[gridPosition.X, gridPosition.Y] = cell;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查指定位置在指定高度是否可通行
|
||||
/// </summary>
|
||||
/// <param name="gridPosition">网格坐标</param>
|
||||
/// <param name="height">检查的高度</param>
|
||||
/// <returns>是否可通行</returns>
|
||||
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));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取所有通道类型的网格单元
|
||||
/// </summary>
|
||||
/// <returns>通道网格单元列表</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置通道模型项集合(用于精确高度计算)
|
||||
/// </summary>
|
||||
@ -445,6 +547,26 @@ namespace NavisworksTransport.PathPlanning
|
||||
/// </summary>
|
||||
public ModelItem RelatedModelItem { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否在通道内 - A*改进方案新增字段
|
||||
/// </summary>
|
||||
public bool IsInChannel { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 可通行高度区间列表 - 支持2.5D路径规划
|
||||
/// </summary>
|
||||
public List<HeightInterval> PassableHeights { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 通道类型 - 用于区分不同类型的通道
|
||||
/// </summary>
|
||||
public ChannelType ChannelType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 世界坐标位置 - 用于高度计算和空间查询
|
||||
/// </summary>
|
||||
public Point3D WorldPosition { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建障碍物单元格
|
||||
/// </summary>
|
||||
@ -455,7 +577,10 @@ namespace NavisworksTransport.PathPlanning
|
||||
{
|
||||
IsWalkable = false,
|
||||
Cost = double.MaxValue,
|
||||
CellType = ElementType.Obstacle
|
||||
CellType = ElementType.Obstacle,
|
||||
IsInChannel = false,
|
||||
PassableHeights = new List<HeightInterval>(),
|
||||
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<HeightInterval>(),
|
||||
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<HeightInterval>(),
|
||||
ChannelType = ChannelType.Other
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建通道单元格
|
||||
/// </summary>
|
||||
/// <param name="channelType">通道类型</param>
|
||||
/// <returns>通道单元格</returns>
|
||||
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<HeightInterval>(),
|
||||
ChannelType = channelType
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -530,6 +665,129 @@ namespace NavisworksTransport.PathPlanning
|
||||
Channel = 3
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 高度区间结构体 - 用于2.5D路径规划
|
||||
/// 表示某个网格位置的可通行高度范围
|
||||
/// </summary>
|
||||
public struct HeightInterval
|
||||
{
|
||||
/// <summary>
|
||||
/// 最小高度(米)
|
||||
/// </summary>
|
||||
public double MinZ { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 最大高度(米)
|
||||
/// </summary>
|
||||
public double MaxZ { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数
|
||||
/// </summary>
|
||||
/// <param name="minZ">最小高度</param>
|
||||
/// <param name="maxZ">最大高度</param>
|
||||
public HeightInterval(double minZ, double maxZ)
|
||||
{
|
||||
MinZ = minZ;
|
||||
MaxZ = maxZ;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查指定高度是否在此区间内
|
||||
/// </summary>
|
||||
/// <param name="height">要检查的高度</param>
|
||||
/// <returns>是否在区间内</returns>
|
||||
public bool Contains(double height)
|
||||
{
|
||||
return height >= MinZ && height <= MaxZ;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查指定高度范围是否与此区间有重叠
|
||||
/// </summary>
|
||||
/// <param name="minHeight">要检查的最小高度</param>
|
||||
/// <param name="maxHeight">要检查的最大高度</param>
|
||||
/// <returns>是否有重叠</returns>
|
||||
public bool Overlaps(double minHeight, double maxHeight)
|
||||
{
|
||||
return maxHeight >= MinZ && minHeight <= MaxZ;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取区间的高度跨度
|
||||
/// </summary>
|
||||
/// <returns>高度跨度</returns>
|
||||
public double GetSpan()
|
||||
{
|
||||
return MaxZ - MinZ;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取区间的中心高度
|
||||
/// </summary>
|
||||
/// <returns>中心高度</returns>
|
||||
public double GetCenter()
|
||||
{
|
||||
return (MinZ + MaxZ) / 2.0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 重写ToString方法
|
||||
/// </summary>
|
||||
/// <returns>字符串表示</returns>
|
||||
public override string ToString()
|
||||
{
|
||||
return $"[{MinZ:F2}m - {MaxZ:F2}m]";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通道覆盖信息
|
||||
/// 存储通道构建器生成的网格地图和相关数据
|
||||
/// </summary>
|
||||
public class ChannelCoverage
|
||||
{
|
||||
/// <summary>
|
||||
/// 网格地图
|
||||
/// </summary>
|
||||
public GridMap GridMap { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 通道物品列表
|
||||
/// </summary>
|
||||
public List<ModelItem> ChannelItems { get; set; } = new List<ModelItem>();
|
||||
|
||||
/// <summary>
|
||||
/// 总边界框
|
||||
/// </summary>
|
||||
public BoundingBox3D TotalBounds { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取统计信息
|
||||
/// </summary>
|
||||
/// <returns>统计信息字符串</returns>
|
||||
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}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 自动路径规划异常
|
||||
/// </summary>
|
||||
|
||||
@ -7,13 +7,32 @@ using NavisworksTransport.Core;
|
||||
|
||||
namespace NavisworksTransport.PathPlanning
|
||||
{
|
||||
/// <summary>
|
||||
/// 网格生成模式
|
||||
/// </summary>
|
||||
public enum GridGenerationMode
|
||||
{
|
||||
/// <summary>
|
||||
/// 边界框模式 - 传统的基于整个边界框的网格生成
|
||||
/// </summary>
|
||||
BoundingBox,
|
||||
|
||||
/// <summary>
|
||||
/// 通道基础2.5D模式 - 使用通道优先策略和垂直扫描的高性能模式
|
||||
/// </summary>
|
||||
ChannelBased2_5D
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 网格地图生成器
|
||||
/// 负责将BIM模型转换为路径规划可用的网格地图
|
||||
/// 支持传统边界框模式和新的通道基础2.5D模式
|
||||
/// </summary>
|
||||
public class GridMapGenerator
|
||||
{
|
||||
private readonly CategoryAttributeManager _categoryManager;
|
||||
private readonly ChannelBasedGridBuilder _channelBuilder;
|
||||
private readonly VerticalScanProcessor _verticalScanner;
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数
|
||||
@ -21,6 +40,8 @@ namespace NavisworksTransport.PathPlanning
|
||||
public GridMapGenerator()
|
||||
{
|
||||
_categoryManager = new CategoryAttributeManager();
|
||||
_channelBuilder = new ChannelBasedGridBuilder();
|
||||
_verticalScanner = new VerticalScanProcessor();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -33,8 +54,37 @@ namespace NavisworksTransport.PathPlanning
|
||||
/// <param name="safetyMargin">安全间隙(额外的膨胀间距,米)</param>
|
||||
/// <param name="planningStartPoint">规划起点(用于确定楼层)</param>
|
||||
/// <param name="planningEndPoint">规划终点(用于确定楼层)</param>
|
||||
/// <param name="mode">网格生成模式,默认为边界框模式以保持向后兼容</param>
|
||||
/// <param name="vehicleHeight">车辆高度(用于垂直通行检查)</param>
|
||||
/// <returns>生成的网格地图</returns>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 传统边界框模式的网格生成(原GenerateFromBIM方法内容)
|
||||
/// </summary>
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通道基础2.5D模式的网格生成(高性能模式)
|
||||
/// 使用通道优先策略和垂直扫描处理器
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 生成网格点坐标用于垂直扫描
|
||||
/// </summary>
|
||||
/// <param name="gridMap">网格地图</param>
|
||||
/// <returns>网格点坐标列表</returns>
|
||||
private List<Point3D> GenerateGridPoints(GridMap gridMap)
|
||||
{
|
||||
var points = new List<Point3D>();
|
||||
|
||||
// 只为通道网格生成扫描点,不扫描整个网格
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将高度区间信息集成到网格地图中
|
||||
/// </summary>
|
||||
/// <param name="gridMap">网格地图</param>
|
||||
/// <param name="heightIntervals">高度区间数据</param>
|
||||
private void IntegrateHeightIntervalsToGrid(GridMap gridMap, Dictionary<Point3D, List<HeightInterval>> 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<HeightInterval>();
|
||||
}
|
||||
|
||||
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}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证网格统计一致性,确保可通行单元格数量不超过预期
|
||||
/// </summary>
|
||||
/// <param name="gridMap">要验证的网格地图</param>
|
||||
/// <param name="stage">验证阶段描述</param>
|
||||
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})");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 进行楼层过滤,获取起终点所在楼层的所有模型项
|
||||
/// </summary>
|
||||
|
||||
402
src/PathPlanning/TriangleSpatialHash.cs
Normal file
402
src/PathPlanning/TriangleSpatialHash.cs
Normal file
@ -0,0 +1,402 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Autodesk.Navisworks.Api;
|
||||
using NavisworksTransport.Utils;
|
||||
|
||||
namespace NavisworksTransport.PathPlanning
|
||||
{
|
||||
/// <summary>
|
||||
/// 三角形空间哈希索引类
|
||||
/// 用于A*改进方案中的射线相交测试性能优化
|
||||
///
|
||||
/// 核心原理:
|
||||
/// - 将3D空间划分为2D哈希网格
|
||||
/// - 每个三角形根据其2D投影分配到对应的哈希格子中
|
||||
/// - 射线检测时只需检查相关格子内的三角形,大幅减少相交测试次数
|
||||
/// </summary>
|
||||
public class TriangleSpatialHash
|
||||
{
|
||||
private Dictionary<int, List<Triangle3D>> _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;
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数
|
||||
/// </summary>
|
||||
public TriangleSpatialHash()
|
||||
{
|
||||
_spatialHash = new Dictionary<int, List<Triangle3D>>();
|
||||
_gridWidth = 10000; // 用于哈希计算的网格宽度
|
||||
LogManager.Info("[空间哈希] 初始化完成");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构建空间哈希索引
|
||||
/// </summary>
|
||||
/// <param name="triangles">三角形列表</param>
|
||||
/// <param name="gridSize">网格单元大小</param>
|
||||
/// <param name="bounds">边界框(可选,自动计算)</param>
|
||||
public void Build(List<Triangle3D> 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} 个");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取指定2D位置附近的三角形
|
||||
/// </summary>
|
||||
/// <param name="x">世界坐标X</param>
|
||||
/// <param name="y">世界坐标Y</param>
|
||||
/// <param name="searchRadius">搜索半径(格子数量)</param>
|
||||
/// <returns>附近的三角形列表</returns>
|
||||
public List<Triangle3D> GetTrianglesAt(double x, double y, int searchRadius = 0)
|
||||
{
|
||||
var result = new List<Triangle3D>();
|
||||
|
||||
// 计算基准格子坐标
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取指定点的三角形(单个格子)
|
||||
/// </summary>
|
||||
/// <param name="point">世界坐标点</param>
|
||||
/// <returns>该位置的三角形列表</returns>
|
||||
public List<Triangle3D> GetTrianglesAt(Point3D point)
|
||||
{
|
||||
return GetTrianglesAt(point.X, point.Y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取指定区域内的所有三角形
|
||||
/// </summary>
|
||||
/// <param name="minX">最小X坐标</param>
|
||||
/// <param name="minY">最小Y坐标</param>
|
||||
/// <param name="maxX">最大X坐标</param>
|
||||
/// <param name="maxY">最大Y坐标</param>
|
||||
/// <returns>区域内的三角形列表</returns>
|
||||
public List<Triangle3D> GetTrianglesInRegion(double minX, double minY, double maxX, double maxY)
|
||||
{
|
||||
var result = new List<Triangle3D>();
|
||||
var addedHashes = new HashSet<int>(); // 避免重复添加
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取性能统计信息
|
||||
/// </summary>
|
||||
/// <returns>统计信息字符串</returns>
|
||||
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";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清除所有数据
|
||||
/// </summary>
|
||||
public void Clear()
|
||||
{
|
||||
_spatialHash.Clear();
|
||||
_totalTriangles = 0;
|
||||
_totalCells = 0;
|
||||
_averageTrianglesPerCell = 0;
|
||||
_maxTrianglesPerCell = 0;
|
||||
LogManager.Info("[空间哈希] 已清除所有数据");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查空间哈希是否为空
|
||||
/// </summary>
|
||||
/// <returns>是否为空</returns>
|
||||
public bool IsEmpty()
|
||||
{
|
||||
return _totalTriangles == 0 || !_spatialHash.Any();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取指定格子中的三角形数量
|
||||
/// </summary>
|
||||
/// <param name="x">世界坐标X</param>
|
||||
/// <param name="y">世界坐标Y</param>
|
||||
/// <returns>三角形数量</returns>
|
||||
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 私有方法
|
||||
|
||||
/// <summary>
|
||||
/// 将三角形分配到哈希格子
|
||||
/// </summary>
|
||||
/// <param name="triangle">三角形</param>
|
||||
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<Triangle3D>();
|
||||
}
|
||||
|
||||
_spatialHash[hash].Add(triangle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算哈希值
|
||||
/// </summary>
|
||||
/// <param name="gridX">格子X坐标</param>
|
||||
/// <param name="gridY">格子Y坐标</param>
|
||||
/// <returns>哈希值</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算三角形列表的边界框
|
||||
/// </summary>
|
||||
/// <param name="triangles">三角形列表</param>
|
||||
/// <returns>边界框</returns>
|
||||
private BoundingBox3D CalculateTrianglesBounds(List<Triangle3D> 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));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算统计信息
|
||||
/// </summary>
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 估算内存使用量(字节)
|
||||
/// </summary>
|
||||
/// <returns>内存使用量估算</returns>
|
||||
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 调试和可视化方法
|
||||
|
||||
/// <summary>
|
||||
/// 获取格子密度分布信息(用于调试)
|
||||
/// </summary>
|
||||
/// <returns>密度分布字典:密度区间 -> 格子数量</returns>
|
||||
public Dictionary<string, int> GetDensityDistribution()
|
||||
{
|
||||
var distribution = new Dictionary<string, int>
|
||||
{
|
||||
["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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证空间哈希的正确性(调试用)
|
||||
/// </summary>
|
||||
/// <param name="testPoint">测试点</param>
|
||||
/// <returns>验证结果信息</returns>
|
||||
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
|
||||
}
|
||||
}
|
||||
808
src/PathPlanning/VerticalScanProcessor.cs
Normal file
808
src/PathPlanning/VerticalScanProcessor.cs
Normal file
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 垂直扫描处理器
|
||||
/// 实现多级筛选优化的障碍物检测和2.5D高度区间计算
|
||||
/// 预期实现10,000-40,000倍性能提升
|
||||
/// </summary>
|
||||
public class VerticalScanProcessor
|
||||
{
|
||||
#region 私有字段
|
||||
|
||||
/// <summary>
|
||||
/// 空间哈希表,用于快速查找邻域内的模型项
|
||||
/// </summary>
|
||||
private readonly Dictionary<string, List<ModelItem>> _spatialHashMap;
|
||||
|
||||
/// <summary>
|
||||
/// 空间哈希的网格大小(米)
|
||||
/// </summary>
|
||||
private readonly double _spatialHashSize;
|
||||
|
||||
/// <summary>
|
||||
/// 并行处理的任务数量
|
||||
/// </summary>
|
||||
private readonly int _parallelDegree;
|
||||
|
||||
/// <summary>
|
||||
/// 高度筛选的容差值(米)
|
||||
/// </summary>
|
||||
private const double HEIGHT_TOLERANCE = 0.1;
|
||||
|
||||
/// <summary>
|
||||
/// 默认人行高度(米)- 用于未检测到通道时的默认高度
|
||||
/// </summary>
|
||||
private const double DEFAULT_WALKING_HEIGHT = 2.5;
|
||||
|
||||
/// <summary>
|
||||
/// 最小通行高度(米)
|
||||
/// </summary>
|
||||
private const double MIN_PASSABLE_HEIGHT = 1.8;
|
||||
|
||||
#endregion
|
||||
|
||||
#region 构造函数
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数
|
||||
/// </summary>
|
||||
/// <param name="spatialHashSize">空间哈希网格大小(米),默认为10米</param>
|
||||
/// <param name="parallelDegree">并行度,默认为CPU核心数的一半,避免过度并行导致崩溃</param>
|
||||
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<string, List<ModelItem>>();
|
||||
|
||||
LogManager.Info($"[垂直扫描处理器] 初始化完成,空间哈希大小: {_spatialHashSize}m, 并行度: {_parallelDegree} (最大并行度: {maxParallelism})");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 公共方法
|
||||
|
||||
/// <summary>
|
||||
/// 构建空间哈希索引
|
||||
/// 第一级筛选:邻域筛选
|
||||
/// </summary>
|
||||
/// <param name="modelItems">所有模型项</param>
|
||||
/// <param name="bounds">扫描边界</param>
|
||||
/// <param name="channelItems">已确定的通道元素列表(将被排除)</param>
|
||||
public void BuildSpatialHashIndex(IEnumerable<ModelItem> modelItems, BoundingBox3D bounds, IEnumerable<ModelItem> channelItems = null)
|
||||
{
|
||||
LogManager.Info("【垂直扫描处理器】 开始构建空间哈希索引");
|
||||
var startTime = DateTime.Now;
|
||||
|
||||
_spatialHashMap.Clear();
|
||||
|
||||
// 创建通道元素的HashSet以便快速查找
|
||||
var channelItemsSet = new HashSet<ModelItem>(channelItems ?? new List<ModelItem>());
|
||||
|
||||
// 统计变量
|
||||
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<ModelItem>();
|
||||
}
|
||||
_spatialHashMap[key].Add(item);
|
||||
totalHashEntries++;
|
||||
}
|
||||
}
|
||||
|
||||
var elapsed = (DateTime.Now - startTime).TotalMilliseconds;
|
||||
LogManager.Info($"【垂直扫描处理器】 空间哈希索引构建完成,耗时: {elapsed:F1}ms");
|
||||
LogManager.Info($"【垂直扫描处理器】 最终统计 - 参与元素: {itemsWithBounds.Count}, 哈希桶: {_spatialHashMap.Count}, 总哈希条目: {totalHashEntries}");
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 并行扫描网格点的2.5D高度区间
|
||||
/// </summary>
|
||||
/// <param name="gridPoints">网格点列表</param>
|
||||
/// <param name="scanHeight">扫描高度(从地面向上的距离)</param>
|
||||
/// <param name="vehicleHeight">车辆高度(用于通行检查)</param>
|
||||
/// <returns>每个网格点的高度区间列表</returns>
|
||||
public Dictionary<Point3D, List<HeightInterval>> ParallelScanHeightIntervals(
|
||||
IEnumerable<Point3D> gridPoints,
|
||||
double scanHeight = 20.0,
|
||||
double vehicleHeight = 3.0)
|
||||
{
|
||||
LogManager.Info($"[垂直扫描处理器] 开始并行扫描高度区间,扫描高度: {scanHeight}m, 车辆高度: {vehicleHeight}m");
|
||||
var startTime = DateTime.Now;
|
||||
|
||||
var results = new ConcurrentDictionary<Point3D, List<HeightInterval>>();
|
||||
var pointsList = gridPoints?.ToList() ?? new List<Point3D>();
|
||||
|
||||
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<HeightInterval>();
|
||||
}
|
||||
}
|
||||
catch (OutOfMemoryException)
|
||||
{
|
||||
LogManager.Error($"[垂直扫描处理器] 内存不足,跳过点 {gridPoint}");
|
||||
results[gridPoint] = new List<HeightInterval>();
|
||||
throw; // 内存不足需要重新抛出
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogManager.Error($"[垂直扫描处理器] 扫描点 {gridPoint} 失败: {ex.Message}");
|
||||
results[gridPoint] = new List<HeightInterval>();
|
||||
}
|
||||
});
|
||||
}
|
||||
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<HeightInterval>();
|
||||
}
|
||||
}
|
||||
catch (Exception serialEx)
|
||||
{
|
||||
LogManager.Error($"[垂直扫描处理器] 串行扫描点 {gridPoint} 也失败: {serialEx.Message}");
|
||||
results[gridPoint] = new List<HeightInterval>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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<Point3D, List<HeightInterval>>(results);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 扫描单个垂直线上的通行区间
|
||||
/// </summary>
|
||||
/// <param name="basePoint">基础点(X,Y坐标)</param>
|
||||
/// <param name="scanHeight">扫描高度</param>
|
||||
/// <param name="vehicleHeight">车辆高度</param>
|
||||
/// <returns>可通行的高度区间列表</returns>
|
||||
public List<HeightInterval> 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 私有方法 - 多级筛选实现
|
||||
|
||||
/// <summary>
|
||||
/// 第一级筛选:从空间哈希中获取候选模型项(邻域筛选)
|
||||
/// </summary>
|
||||
/// <param name="point">查询点</param>
|
||||
/// <returns>候选模型项列表</returns>
|
||||
private List<ModelItem> GetCandidateItemsFromSpatialHash(Point3D point)
|
||||
{
|
||||
var candidates = new HashSet<ModelItem>();
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 第二级筛选:高度筛选
|
||||
/// </summary>
|
||||
/// <param name="items">候选模型项</param>
|
||||
/// <param name="minZ">最小Z坐标</param>
|
||||
/// <param name="maxZ">最大Z坐标</param>
|
||||
/// <returns>高度筛选后的模型项</returns>
|
||||
private List<ModelItem> HeightFiltering(List<ModelItem> items, double minZ, double maxZ)
|
||||
{
|
||||
var filtered = new ConcurrentBag<ModelItem>();
|
||||
|
||||
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<ModelItem>())
|
||||
{
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 第三级筛选:精确几何相交测试(空间哈希优化)
|
||||
/// </summary>
|
||||
/// <param name="items">高度筛选后的模型项</param>
|
||||
/// <param name="basePoint">基础点</param>
|
||||
/// <param name="scanHeight">扫描高度</param>
|
||||
/// <returns>相交测试结果</returns>
|
||||
private List<IntersectionResult> PerformIntersectionTests(List<ModelItem> items, Point3D basePoint, double scanHeight)
|
||||
{
|
||||
var results = new ConcurrentBag<IntersectionResult>();
|
||||
|
||||
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<ModelItem>())
|
||||
{
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算可通行区间
|
||||
/// </summary>
|
||||
/// <param name="intersectionResults">相交测试结果</param>
|
||||
/// <param name="baseZ">基础Z坐标</param>
|
||||
/// <param name="scanHeight">扫描高度</param>
|
||||
/// <param name="vehicleHeight">车辆高度</param>
|
||||
/// <returns>可通行区间列表</returns>
|
||||
private List<HeightInterval> CalculatePassableIntervals(
|
||||
List<IntersectionResult> intersectionResults,
|
||||
double baseZ,
|
||||
double scanHeight,
|
||||
double vehicleHeight)
|
||||
{
|
||||
// 收集所有障碍物的高度范围
|
||||
var obstacles = new List<HeightInterval>();
|
||||
var floors = new List<HeightInterval>();
|
||||
|
||||
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<HeightInterval>();
|
||||
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 私有辅助方法
|
||||
|
||||
/// <summary>
|
||||
/// 检查边界框是否在指定范围内
|
||||
/// </summary>
|
||||
/// <param name="bbox">要检查的边界框</param>
|
||||
/// <param name="bounds">范围边界框</param>
|
||||
/// <returns>是否在范围内</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取边界框的所有空间哈希键
|
||||
/// </summary>
|
||||
/// <param name="bbox">边界框</param>
|
||||
/// <returns>空间哈希键列表</returns>
|
||||
private List<string> GetSpatialHashKeys(BoundingBox3D bbox)
|
||||
{
|
||||
var keys = new List<string>();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取点的空间哈希键(包括相邻网格)
|
||||
/// </summary>
|
||||
/// <param name="point">查询点</param>
|
||||
/// <returns>空间哈希键列表</returns>
|
||||
private List<string> GetSpatialHashKeysForPoint(Point3D point)
|
||||
{
|
||||
var keys = new List<string>();
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试垂直线与模型项的相交
|
||||
/// </summary>
|
||||
/// <param name="item">模型项</param>
|
||||
/// <param name="basePoint">基础点</param>
|
||||
/// <param name="scanHeight">扫描高度</param>
|
||||
/// <returns>相交数据,如果不相交则返回null</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查模型项是否类似地面/楼板
|
||||
/// </summary>
|
||||
/// <param name="item">模型项</param>
|
||||
/// <param name="intersectionData">相交数据</param>
|
||||
/// <returns>是否为地面类型</returns>
|
||||
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厚度认为是楼板
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 合并重叠的区间
|
||||
/// </summary>
|
||||
/// <param name="intervals">区间列表</param>
|
||||
/// <returns>合并后的区间列表</returns>
|
||||
private List<HeightInterval> MergeOverlappingIntervals(List<HeightInterval> intervals)
|
||||
{
|
||||
if (!intervals.Any())
|
||||
return new List<HeightInterval>();
|
||||
|
||||
var sortedIntervals = intervals.OrderBy(i => i.MinZ).ToList();
|
||||
var merged = new List<HeightInterval>();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查指定的ModelItem是否为通道集合中任意一个通道的子节点
|
||||
/// </summary>
|
||||
/// <param name="item">要检查的ModelItem</param>
|
||||
/// <param name="channelItemsSet">通道集合</param>
|
||||
/// <returns>如果是通道的子节点则返回true</returns>
|
||||
private bool IsChildOfChannelItems(ModelItem item, HashSet<ModelItem> 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 内部数据结构
|
||||
|
||||
/// <summary>
|
||||
/// 相交数据结构
|
||||
/// </summary>
|
||||
private class IntersectionData
|
||||
{
|
||||
public double MinZ { get; set; }
|
||||
public double MaxZ { get; set; }
|
||||
public Point3D IntersectionPoint { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 相交测试结果
|
||||
/// </summary>
|
||||
private class IntersectionResult
|
||||
{
|
||||
public ModelItem ModelItem { get; set; }
|
||||
public IntersectionData IntersectionData { get; set; }
|
||||
public bool IsObstacle { get; set; }
|
||||
public bool IsPassable { get; set; }
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user