开始实现2.5D空间网格,进行到空间哈希成功,垂直扫描失败

This commit is contained in:
tian 2025-08-29 20:43:02 +08:00
parent 4dc989926e
commit ea809277c3
23 changed files with 5642 additions and 158 deletions

View File

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

View File

@ -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
View File

@ -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
View 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 环境中保证稳定运行与较好性能;
- 解耦分层UIWPF+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可见性与层级控制
- AnimationTimeLinerIntegrationManager、PathAnimationManager动画控制
- CollisionClashDetectiveIntegration碰撞集成动画期缓存 + 结束时统一测试)
- ModelSplitterManager模型切分含简化实现
- PathPlanning 算法层:
- GridMapGenerator网格生成ChannelBased 2.5D/BoundingBox车辆膨胀优化
- AutoPathFinderA* 路径搜索、路径优化/插值、高度贴合
- ChannelBasedGridBuilder、VerticalScanProcessor、TriangleSpatialHash 等支撑
- Utils日志、坐标/单位转换、Navisworks API 辅助
2.2 系统架构图(截图占位)
- [截图占位] 系统架构图
- 要求:体现 UIWPF+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
- UIWPF + MVVMWinForms 仅用于部分对话框/遗留界面);
- 外部 APIAutodesk Navisworks API2017 目标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 模型切分
- 目标:对大型模型按楼层/属性进行切分,提升性能与操作体验;
- UIModelSplitterDialogWinForms
- CoreModelSplitterManager、SimplifiedModelSplitterManager
- 设计:
- 基于模型层级与属性枚举进行分层导出;
- 提供环境检查与测试导出入口;
- 与可见性/筛选配合以限定工作区域。
3.2 通道选择
- 能力:
- 手动选择:基于 Navisworks 选择;
- 属性筛选:通过 CategoryAttributeManager.FilterByLogisticsType(FilterTraversableItems)
- 自动选择Search API 扫描“可通行”物流元素;
- PathPlanningManager.SelectChannels计算组合边界 CombinedChannelBounds 并事件广播。
3.3 路径点规划(手动 + 自动)
- 手动规划:
- 工具PathClickToolPlugin精确拾取PathPointRenderPlugin3D 渲染);
- 流程:切换到 Creating/Editing 状态 -> 添加点 -> 渲染 -> 校验;
- 路径点自动贴合:高度修正在 AutoPathFinder.ApplyPreciseHeightCorrection 中贴合通道表面(有通道数据时);
- 自动规划(复杂环境):
- 网格生成GridMapGenerator.GenerateFromBIM
- First 尝试 ChannelBased2_5D通道优先、垂直扫描高度区间、车辆高度校验
- Fallback 到 BoundingBox 模式(保持向后兼容);
- A*AutoPathFinder.FindPath/ExecuteAStarAlgorithm
- 路径优化去冗余点、直线可达校验、2.5D 高度插值与起终点原位修正;
3.4 路径管理、编辑、保存与导入
- 编辑:创建/编辑/取消/完成编辑FinishEditing 自动将末点设为终点并触发渲染与事件);
- 文件PathFileSerializerXML 主JSON 简化,不建议作为主格式),支持多路径容器;
- 历史PathHistoryManagerCreated/Edited/ManualSave 等),提供查看历史 UI
- 导入DetectFileFormat -> LoadFromXml/LoadFromJsonJSON 加载简化,优先 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 NavisworksManage 2017需求基线。如需 2026请切换引用与验证用例见下文兼容模式
- .NET Framework4.8
- 开发工具Visual Studio建议 2019/2022
4.2 构建与产物
- 解决方案NavisworksTransport.sln
- 项目NavisworksTransportPlugin.csproj
- 产物NavisworksTransportPlugin.dllbin/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应用级异常捕获线程/任务/未处理),尝试恢复组件,提供用户友好提示;
- LogManagerInfo/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 属性写入:需严格使用相对索引覆盖逻辑(源码已修复),防止属性覆盖错乱;
- 动画碰撞“位置恢复”:测试期间会短暂移动对象做验证,并在结束后恢复。
七、里程碑建议(示例)
- M11 周):路径编辑闭环(手动)、物流属性标注、可见性隔离;
- M21-2 周自动路径2.5D/回退、XML 存取、历史;
- M31 周):动画与碰撞缓存 + 结束测试、报告与高亮;
- M41 周):导航输出与 DELMIA 结果结构;
- M51 周性能与兼容性回归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/TimeLinerIntegrationManagerTimeLiner 任务集成;
- Core/Collision/ClashDetectiveIntegration动画期缓存 + 结束创建测试;
- Core/VisibilityManager可见性隔离与统计
- UI/WPF/ViewModels/*MVVM 视图模型与命令绑定。

View 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/算法/序列化等支撑能力的依赖关系。
- 如需在图中标注具体类名/文件路径,可在支撑能力块旁新增注释或节点。

View File

@ -0,0 +1,125 @@
```mermaid
flowchart TB
%% 顶层:功能域
subgraph A[通道选择与路径点规划]
direction TB
A1[模型切分]
A2[通道选择\n选择树/三维点选/属性筛选)]
A3[路径点规划\n起点/路径点/终点/方向)]
A4[编辑、保存与导入\nXML/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[结构化结果\nXML/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\nDocument/Selection/View]
S2[Clash Detective API]
S3[TimeLiner API]
S4[COM API\n自定义属性读写]
S5[路径规划算法\n通道优先 2.5D 网格 + A* + 高度插值/贴合)]
S6[可见性/层级控制]
S7[文件序列化\nXML/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;
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

View 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:intlayerName: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. 功能界面(界面元素详解与操作流程)
- 分层策略SplitStrategiesComboBox
- 作用:选择分层口径(楼层/区域/子系统/自定义)。
- 使用:下拉选择目标策略;选择“自定义”后需在“自定义分层二级选项”处指定具体值。
- 流程:选择策略 →(若为自定义)填写二级选项 → 选择遍历深度 → 点击预览。
- 自定义分层二级选项CustomLayerOptionsComboBox
- 作用:当策略=自定义时精细化指定维度如F1/北区/强电)。
- 使用:策略为“自定义”时启用;否则隐藏或禁用。
- 约束长度≤64禁止字符 / \ : * ? " < > |。
- 遍历深度DepthOptionsComboBox
- 作用限制分层遍历到选择树的最大层级1级/2级/.../全部)。
- 使用:根据模型规模与关注粒度选择;“全部”最全面但耗时较长。
- 预览PreviewSplitCommandButton
- 作用:执行遍历并生成“分层预览列表”。
- 使用:点击后在列表中查看分层记录,配合“单独显示/显示所有”审查。
- 预览列表ListView + GridView
- 列:序号(自动)、分层名称、分层属性、是否保存(复选)。
- 使用:勾选“是否保存”表示该分层将被导出;支持单选/多选。
- 单独显示IsolateSelectedLayerCommandButton
- 作用:隐藏其他分层,仅显示选中行内容。
- 使用:选中列表某行后可用;用于快速验证分层效果。
- 显示所有ShowAllLayersCommandButton
- 作用:恢复显示全部分层内容。
- 使用:用于“单独显示”后的还原。
- 分层保存ExecuteSplitCommandButton
- 作用将“是否保存”为true的分层项导出为NWD文件。
- 使用:导出前可在“导出选项”勾选“嵌入纹理/阻止对象特性”;导出后在状态区查看结果与路径。
- 导出选项EmbedXrefs / PreventObjectPropertyExportCheckBox
- 作用:控制导出内容与文件大小/稳定性之间的平衡。
- 使用:大模型慎用“嵌入纹理”;如仅需几何可开启“阻止对象特性”。
- 进度与取消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(164唯一)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/EndStart与End各恰1个坐标范围±1e6米内
- 导入文件(可选):
- XMLUTF-8无BOMXSD可选校验根元素<route>
- JSONapplication/jsonUTF-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任选其一或多选
- XMLUTF-8扩展名=.xml根元素<route>带schemaVersion属性如1.0
- JSONUTF-8扩展名=.json顶层包含routeName/points/meta
- CSVUTF-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
- 列:路径名称、点数、状态、创建时间;支持选中高亮,联动右侧状态。
- 使用:单击选中路径作为当前编辑/导出对象。
- 手动创建NewPathCommandButton
- 作用:创建一条空路径方案。
- 使用:点击后输入名称(如弹窗或内联编辑);自动成为当前路径。
- 重命名RenamePathCommandButton
- 作用:修改当前选中路径的名称。
- 启用条件:已选中路径。
- 删除DeletePathCommandButton
- 作用:删除当前选中路径。
- 使用:需确认;删除后不可恢复(建议投标版提示用户备份)。
- 导入ImportPathCommandButton
- 作用从XML/JSON/CSV导入路径并自动重绘。
- 使用选择文件→解析校验→若包含meta/时间标签按需合并→点位按通道贴合。
- 导出全部ExportPathCommandButton
- 作用:导出列表中所有路径为结构化文件(可多文件)。
- 使用:选择导出目录与格式,完成后在状态栏给出统计与路径。
- 导出选中路径SaveAsPathCommandButton
- 作用:仅导出当前选中的路径。
- 使用:常用于方案比选或对外共享。
- 状态文本(“当前选择/文件状态”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.010.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(15);默认=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. 功能界面(界面元素详解与操作流程)
- 选择状态SelectedModelsTextTextBlock
- 显示当前已选对象数量与范围。
- 物流类型AvailableCategories/SelectedCategoryComboBox
- 为对象赋予业务类别(门/电梯/楼梯/通道等);支持批量设置。
- 可通行IsTraversableCheckBox
- 标识元素是否允许物流通行;后续筛选与贴合使用。
- 优先级PriorityLevels/PriorityComboBox
- 用于路径可行性与优先级策略判断155最高
- 限宽/限高WidthLimit/HeightLimitTextBox
- 约束通道几何≥0精度0.01。
- 限速SpeedLimitTextBox
- 约束通过速度(米/秒);内置建议值。
- 设置属性/清除属性/重置默认值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”
- 优先级15级5为最高
- 限制:限宽/限高(米)、限速(米/秒,提供通道/电梯/楼梯推荐值)。
3.3.3.3.2.3. 输入数据(详细规格)
- 目标元素必需ModelItem[]
- 字段type:enumtraversable:boolpriority:int(15)widthLimit/heightLimit:double(m)speedLimit:double(m/s)
- 校验:单位=公制数值范围≥0priority为整数
3.3.3.3.2.4. 输出数据(详细规格)
- 更新后的属性(内存+持久化):可被筛选与高亮;
- 导出清单可选CSV/JSON列表字段=objectName,type,traversable,priority,widthLimit,heightLimit,speedLimit
3.3.3.3.2.5. 功能界面(界面元素详解与操作流程)
- 刷新列表RefreshLogisticsModelsCommandButton扫描并汇总所有已设置“物流属性”的对象。
- 物流模型列表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:enumtraversable:boollimits:{width:double(m), height:double(m), speed:double(m/s)}priority:int(15)
- 逻辑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)}
- 估算与校正记录日志或文件metaestimate: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.15.0米;
- 引导配合“3D交互操作指南”完成选择/参数/提交全流程。
3.3.3.5.1.3. 输入数据(详细规格)
- 起点/终点必需RaycastHit/坐标{ x,y,z:double(m) }
- 车辆参数可选length/width/height:double(m);范围>0
- 安全间隙可选double(m)默认0.20.5
- 网格精度可选double(m)0.15.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, 可选)
- metavehicle.length/width/height(double, m)、safetyMargin(double, m)、gridSize(double, m)、categorySummary(text)
- 模板与字典:统一键名与单位说明,附样例文件以便对接。
3.3.3.5.3.3. 输入数据(详细规格)
- 选中路径(必需):路径对象;
- 模板可选导出profileXML/JSON/CSV与字段字典键名、单位说明
- 版本可选schemaVersion:string
3.3.3.5.3.4. 输出数据(详细规格)
- 结构化文件XML/JSON可选CSV编码=UTF-8含schemaVersion
- 版本记录:版本号、导出时间、导出人;
3.3.3.5.3.5. 功能界面(界面元素详解与操作流程)
- 导出格式选择Radio/CheckBoxXML/JSON/CSV。
- schemaVersionText/下拉):标识字段版本,方便下游兼容。
- 导出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范围160fps
- 时长必需double(秒)>0
3.3.3.6.1.4. 输出数据(详细规格)
- 动画时间线(内存):关键帧序列;
- 播放状态内存isPlaying:bool, progress:0100%, 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.011.0
- 运动速度必需double(m/s)范围0.15.0
- 检测间隙可选double(m)范围01.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:stringreason:string
3.3.3.6.3.5. 功能界面(界面元素详解与操作流程)
- 指标表与图表(表格/图形控件,若提供):展示关键指标对比(碰撞数、时长、长度等)。
- 导出报告Button导出HTML/Docx/PDF适合投标归档与评审共享。
- 建议摘要Text/列表):以业务语言列出调整建议与优先级。

View 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. 功能界面

View File

@ -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、对于自定义查找用指定的分层属性在每个节点的"分层信息“属性类别中查找。

View File

@ -13,17 +13,18 @@
| 次级功能点 | 功能点描述 |
|------------|------------|
| 通道选择 | 支持选择通道模型功能,可通过选择树或三维视图点选的方式,选择模型并制定为通道类型。 |
| 路径点规划 | 1、针对较为复杂的环境支持路径点功能在Navisworks 三维视图中在通道上点击指定起点、路径点、终点的位置及方向并以三维可视化的方式显示路径。2、支持多条路径的保存、选择和编辑。 |
| 编辑保存和导入 | 1、支持对路径上的各点进行坐标编辑修改x,y,z值2、支持保存当前路径点集合为路径规划文件文件格式支持XML、JSON、CSV3、支持路径导入在当前通道表面重绘路径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插件集成和联动运行时间线模拟并获取碰撞结果。 |
| 路径规划分析 | 对多个路径运行的碰撞结果,进行分析,生成路径分析报告,选择最佳路径,提供调整建议。 |

View 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型通道中的不必要绕行
这个方案充分利用了现有代码的几何处理、属性管理等核心功能,只需要新增网格生成的改进算法,实现成本低且风险可控。

View 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参数支持
- ✅ 实现了向后兼容的回退机制
- ✅ 增加了详细的性能统计和日志记录
- ✅ 统一了路径规划的实现逻辑
- ✅ 保持了代码编译的正确性
修改已完成并验证,可以安全部署到测试环境进行进一步验证。

View 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

View 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倍是路径规划等场景的理想选择。

View File

@ -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)
{

View File

@ -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默认设置保持一致
}
}

View File

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

View 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}]";
}
}
}

View File

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

View File

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

View 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
}
}

View 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
}
}